JAVA / METHODS
Pass by value and what it means for object references
Predict whether a method changes the caller's variable or the object it points to, and use return values when a new object must reach the caller.
What you will learn
- Tell apart reassigning a parameter from mutating the object it refers to
- Explain why a swap method can never exchange the caller's two variables
- Return a new value when the caller must see a different object
- Read array and StringBuilder parameters as copied handles to one shared object
Understanding Pass by value and what it means for object references
Java has exactly one rule for arguments: the value of the argument expression is copied into the parameter variable. For an int, that value is the number itself. For any object type the variable never holds the object, it holds a reference to it, and it is that reference that gets copied. So after the call there are two variables, the caller's and the parameter, holding the same reference and therefore pointing at the same object.
That single rule produces the two behaviours beginners think contradict each other. An assignment to the parameter name, such as b = new Box(99), writes into the method's own copy of the reference and is thrown away when the method returns. An operation that goes through the reference, such as b.value = 99 or list.add(x) or data[0] = 0, reaches the one object both variables share, so the caller sees the change immediately.
A useful mental model is that a parameter is a fresh sticky note with the same street address copied onto it. Scribbling a new address on your note does not move the house; walking to the address and repainting the house is visible to everyone holding that address. This is why a method can never rebind a caller's variable, why swap is impossible to write, and why returning a value is the only way to hand a different object back. It also explains why methods that receive an immutable object such as String, Integer or LocalDate can never affect the caller at all: there is no repaint operation available.
public class PassByValue {
static class Box {
int value;
Box(int value) { this.value = value; }
}
static void reassign(Box b) {
b = new Box(99); // rebinds only this method's copy of the reference
System.out.println("inside reassign, b.value = " + b.value);
}
static void mutate(Box b) {
b.value = 99; // follows the reference to the shared object
}
static void bump(int n) {
n = n + 1; // n is a copy of the number
}
public static void main(String[] args) {
Box box = new Box(1);
reassign(box);
System.out.println("after reassign, box.value = " + box.value);
mutate(box);
System.out.println("after mutate, box.value = " + box.value);
int count = 5;
bump(count);
System.out.println("after bump, count = " + count);
}
}A Java method always receives a copy of the argument, so for objects it gets a second reference to the same object: it can change that object, but it can never change which object the caller's variable points to.
Worked examples
The swap that cannot work
Shows that exchanging two parameter variables has no effect on the caller's variables, whatever the type.
public class SwapAttempt {
static void swap(String a, String b) {
String tmp = a;
a = b;
b = tmp;
System.out.println("inside swap: a=" + a + " b=" + b);
}
public static void main(String[] args) {
String x = "left";
String y = "right";
swap(x, y);
System.out.println("after swap: x=" + x + " y=" + y);
}
}Example explained
Line 1String tmp = a; copies a reference into a third local variable, so three names now point at two strings.
Line 2a = b; and b = tmp; rewrite swap's own parameter variables, which is why the print inside really does show them exchanged.
Line 3x and y were only read to produce the copies passed in; nothing copies values back out at return, so they are untouched.
Line 4The same code fails for int, for Box, for everything: the obstacle is the copying of arguments, not the kind of object.
Array contents versus the array variable
Demonstrates that an array parameter is a copied reference, so element writes are visible but replacing the array is not.
import java.util.Arrays;
public class ArrayParams {
static void replaceArray(int[] data) {
data = new int[] {7, 7, 7};
}
static void fillWithZeros(int[] data) {
for (int i = 0; i < data.length; i++) {
data[i] = 0;
}
}
public static void main(String[] args) {
int[] numbers = {1, 2, 3};
replaceArray(numbers);
System.out.println("after replaceArray: " + Arrays.toString(numbers));
fillWithZeros(numbers);
System.out.println("after fillWithZeros: " + Arrays.toString(numbers));
}
}Example explained
Line 1replaceArray points its copy of the reference at a brand new array; numbers in main still refers to the original one.
Line 2fillWithZeros never assigns to data itself, it writes through it with data[i] = 0, reaching the array main created.
Line 3An int[] parameter is a copied reference even though the elements are primitives: the array itself is an object.
StringBuilder changes, String cannot
Contrasts a mutable object argument with an immutable one to show why only one of them appears to be modified by the method.
public class TextParams {
static void appendToBuilder(StringBuilder sb) {
sb.append("-done");
}
static void appendToString(String s) {
s = s + "-done";
}
public static void main(String[] args) {
StringBuilder builder = new StringBuilder("task");
appendToBuilder(builder);
System.out.println(builder);
String text = "task";
appendToString(text);
System.out.println(text);
}
}Example explained
Line 1sb.append("-done") mutates the StringBuilder object, and builder in main refers to that very object, so the change is shared.
Line 2s = s + "-done" creates a third String and rebinds only the local parameter s; the original characters are never altered.
Line 3String has no mutating method at all, so no method taking a String can change the caller's text; returning the new String is the only option.
Important notes
Pass by value copies the reference, never the object: nothing in Java deep copies an argument for you, so two variables keep sharing one object until you copy it explicitly.
Declaring a parameter final only forbids assigning to it; the object it points at can still be mutated freely, because final constrains the variable, not the contents.
Common mistakes
Seeing a setter call inside a method take effect and concluding that Java passes objects by reference, then writing swap(a, b) or a method that assigns a new object to its parameter: the code compiles, does nothing, and the caller silently keeps the old values with no error to point at.
Writing s = s.trim() on a String parameter and expecting the caller's variable to be cleaned up; the assignment rebinds only the local copy, so the caller still has the untrimmed text and the whitespace bug survives.
Handing a caller's ArrayList or array to a helper that sorts, clears or overwrites it in place; because both sides share one object the caller's data is quietly reordered or emptied, and the symptom appears far away from the method that caused it.
Try it yourself
Change, predict, then run
Write void doubleAll(int[] a) that multiplies each element by two in place, and int[] doubledCopy(int[] a) that returns a new array of doubled values. Call each on the same {1, 2, 3} array and print Arrays.toString of the original after each call to see which one changed it.
Open the Java workspaceCheck your understanding
A method's parameter is a List<String> named items. The caller created that list with new ArrayList<>() and put two names in it. The method body runs items.add("Zoe"); and then items = new ArrayList<>();. After the call returns, what does the caller's list hold?
- The two original names plus Zoe
- Only the two original names, because the later reassignment undid the add
- Nothing, because the caller's variable now refers to the new empty list
- The two original names plus Zoe, but only if the parameter is declared final
Show answer
The add call travelled through the copied reference into the object the caller created, so it is a permanent change to that list. The assignment afterwards only points the method's own parameter variable at a different list and is discarded at return, which is why option 2 is wrong even though it feels like the last statement should win: nothing a method assigns to a parameter can reach the caller's variable.