Topic 3.3
Java Is Always Pass-by-Value
In one line
When you call a method, Java copies the value of each argument into the parameter. For primitives the copy is the number itself; for objects and arrays the copy is the reference (the address), so the method can change the object but can never make the caller's variable point somewhere else.
Think of it like this
Sharing a house address on a sticky note. If I photocopy my note and give you the copy, you can drive to the house and paint the door red, and when I visit, I see the red door. But if you scribble a different address on your copy, my note still says my house. You got a copy of the address, not my note itself.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Pass-by-value
- Calling a method by giving it a copy of each argument's value. Java always does this.
- Pass-by-reference
- Calling a method by giving it the caller's variable itself, so assigning to the parameter changes the caller's variable. C++
int&and C#refdo this. Java can't. - Reference
- The value stored in a variable of an object or array type. It identifies where the object is on the heap, a bit like an address.
- Heap
- The area of memory where all objects and arrays live. Topic 3.11 covers it in depth.
- Stack frame
- The private storage a method call gets for its parameters and local variables. It disappears when the call ends.
- Mutate
- Change the contents of an object, such as setting an array element or appending to a
StringBuilder. - Reassign
- Make a variable hold a different value, with
=. For a reference variable, that means pointing it at a different object. - Immutable
- Can't be changed after creation.
Stringobjects are immutable: methods liketoUpperCase()return a new string. - Alias
- Two variables holding references to the same object. A change made through one is seen through the other.
Step by step
01Primitives: the method gets its own copy
int score = 5; addTen(score); copies the value 5 into the parameter x. x and score are two separate boxes in two separate stack frames that happen to hold the same number.
When addTen does x = x + 10, only its own box changes to 15. When the method returns, its frame (and x) is thrown away. score was never touched, so it's still 5.
static void addTen(int x) {
x = x + 10; // changes only the copy
}
int score = 5;
addTen(score);
System.out.println(score); // 502Arrays and objects: the reference is copied
int[] nums = {1, 2, 3}; creates an array object on the heap. The variable nums (on the stack) holds a reference to it, drawn below as an arrow.
Calling doubleFirst(nums) copies that reference into arr. Now two arrows point at one array. arr[0] = arr[0] * 2 follows the arrow and changes the array itself, so when main looks through its own arrow, it sees [2, 2, 3].
Nothing was passed by reference here. A value was copied, as always. It's just that the value is an arrow, and both copies of an arrow lead to the same place.
03Reassigning the parameter cuts the link
Now replace(nums) does arr = new int[] {99, 99, 99};. That creates a new array on the heap and points the method's copy, arr, at it. nums in main still points to the old array.
Any later change through arr hits the new array, which main can't see. When replace returns, nothing refers to the new array any more, and the garbage collector will reclaim it later (Topic 14.3).
This is the whole difference: mutating the object through the reference is visible to the caller; reassigning the parameter is not.
04The swap test: proof that Java is not pass-by-reference
In a pass-by-reference language, swap(a, b) can exchange the caller's two variables. In Java it can't, for primitives or for objects.
swap(Integer a, Integer b) swaps its own two copies of the references. The caller's variables still point where they did. The same goes for swap(int[] a, int[] b) or swap(String a, String b).
What you *can* do is swap inside an object both sides share: swap(int[] arr, int i, int j) exchanges two elements of one array. That's mutation through a reference, and it's how every sorting algorithm swaps elements (see the DSA course, /dsa/sorting).
05Why String looks like a primitive
static void exclaim(String s) { s = s + "!"; } doesn't change the caller's string. s + "!" builds a new String, and s = reassigns the local copy, which, as you've seen, the caller never notices.
You couldn't mutate the original even if you wanted to: String has no method that changes its characters. Use a StringBuilder (Topic 6.3), which is mutable, and sb.append("!") is seen by the caller.
The wrapper types (Integer, Long, Double...) behave the same way: count++ on an Integer parameter creates a new Integer and reassigns the copy.
06Defensive copies: protecting the caller
Because a method can mutate any array or object you pass it, sometimes you need to protect your data. Pass a copy instead: process(nums.clone()) or Arrays.copyOf(nums, nums.length).
The opposite also matters: a method that stores a caller's array in a field keeps an alias to it. If the caller later changes the array, the stored one changes too. Immutable classes copy arrays on the way in and out for exactly this reason (Topic 4.11).
Note that clone() on an array is a shallow copy: for an array of objects (or a 2D array, Topic 3.9), it copies the references, not the objects they point to.
07The one-sentence rule
Java copies the variable's value. For primitives that value is the data; for objects it's a reference. Mutating the object through the reference is visible to the caller; reassigning the parameter is not.
If someone says "objects are passed by reference in Java", the precise answer is: object *references* are passed by value. Interviewers ask this exact question to see whether you know the difference.
Try it yourself
- 1
Predict before you run
In "Mutating an array vs reassigning the parameter", add
arr[1] = 50;as the first line ofreplace, before the reassignment. Predict the last line of output. (It becomes[2, 50, 3]: the change happened whilearrstill pointed at the shared array.) - 2
Protect the caller with a copy
Change
doubleFirst(nums);todoubleFirst(nums.clone());. Run it and confirmnumsstays[1, 2, 3]: the method now mutates a separate copy. - 3
Return instead of mutate
Rewrite
exclaim(String s)tostatic String exclaim(String s) { return s + "!"; }and call it astext = exclaim(text);. This is how you "change" an immutable value: return the new one and let the caller reassign its own variable.
Code & diagrams
Expected output
inside addTen: x = 15
after the call: score = 5Mutation through the copied reference is visible. Reassignment of the copy is not.
Expected output
after doubleFirst: [2, 2, 3]
inside replace: [100, 99, 99]
after replace: [2, 2, 3]Expected output
inside swap: a = 2, b = 1
after swap: x = 1, y = 2
after swapElements: [2, 1]Expected output
String: hello
StringBuilder: hello!Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Expect a method to reset the caller's array
Write static void reset(int[] arr) { arr = new int[arr.length]; } and call it, expecting zeros.
int[] data = {4, 5, 6};
reset(data);
System.out.println(Arrays.toString(data));Break #2
Increment an Integer parameter
Write static void increment(Integer n) { n++; } and expect the caller's counter to grow.
Myth vs fact
Myth
Java passes primitives by value and objects by reference.
Fact
Java passes everything by value. For objects the value is a reference, so the reference is copied. That's why reassigning a parameter never affects the caller.
Myth
Since a method can change my array, the array was passed by reference.
Fact
Changing the array's contents only needs a copy of the reference. Pass-by-reference would let the method replace the caller's variable itself, which Java never allows.
Myth
Strings are passed by value and other objects by reference, because String changes don't show.
Fact
Strings are passed exactly like other objects. Changes don't show because String is immutable and every "change" makes a new object and reassigns the local copy.
Myth
final on a parameter stops the method changing the object.
Fact
final int[] arr only stops arr = ... reassignment. arr[0] = 9 is still allowed. final protects the variable, not the object.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
JLS 8.4.1 says it directly: when a method is invoked, the values of the actual argument expressions initialise newly created parameter variables. In bytecode, the caller pushes values onto its operand stack and the JVM copies them into the callee's local-variable slots (
aload_0,iload_1...). There is no instruction for taking the address of a local variable, so pass-by-reference is impossible by design. - ▸
A reference isn't necessarily a raw memory address. With compressed oops (the default on 64-bit HotSpot for heaps under about 32 GB) a reference is a 32-bit value that the JVM shifts and adds to a base. And the GC moves objects and updates every reference, so treat references as opaque handles, never as numbers.
- ▸
Mutability of arguments is an API contract. JDK methods document it (
Arrays.sortsorts your array in place;List.copyOfmakes an unmodifiable copy). In your own APIs, either document that you mutate an argument or don't do it; surprising mutation is a common cause of bugs between teams.
Remember this
- 1
Pass-by-value means the method receives a copy of each argument's value. Java uses it for every call, with no exceptions. There is no way in Java to pass a variable itself so that the method can reassign the caller's variable.
- 2
For a primitive (
int,double,boolean...), the value is the number itself. The method gets its own copy, so changing the parameter never changes the caller's variable. - 3
For an object or array, the variable doesn't hold the object. It holds a reference: a value that says where the object lives on the heap. Passing it copies the reference, so caller and method now hold two references to the same object.
- 4
Through that copied reference, the method can change the object's contents (
arr[0] = 9,sb.append("!")), and the caller sees the change because it's the same object. - 5
But if the method reassigns the parameter (
arr = new int[3]), only its own copy now points to a new object. The caller's variable still points to the original. This is the test that proves Java isn't pass-by-reference: a classicswap(a, b)method can't work. - 6
Some objects can't be changed at all:
String,Integerand the other wrappers are immutable (Topic 4.11). Any "change" creates a new object and reassigns the parameter, so the caller never sees it. That's whys = s + "!"inside a method has no effect outside.
Explain it without notes
Is Java pass-by-value or pass-by-reference? Give a precise answer an interviewer would accept.
Why can a method change the elements of an array passed to it, but not make the caller's variable point to a new array?
Why doesn't void exclaim(String s) { s = s + "!"; } change the caller's string?
How would you write a method that "swaps" two values for its caller in Java?
What is a defensive copy, and when do you need one?
Practice
Write static void fillWith(int[] arr, int value) that sets every element to value, and show that the caller's array changes.
Write static int[] doubledCopy(int[] arr) that returns a new array with every element doubled and leaves the original unchanged. Print both arrays.
Predict, then verify: what does this print? static void change(int[] a, int b) { a[0] = b; b = 100; a = null; } called with int[] arr = {1, 2}; int n = 5; change(arr, n); then printing arr[0] and n.
Trade-offs
- ↔
Mutating an argument in place avoids allocating a new array (good for large data and hot loops) but surprises callers. Returning a new value is safer and easier to reason about, at the cost of a copy.
- ↔
Defensive copies protect your data but cost memory and time on every call. Immutable types (records with immutable fields,
List.of) remove the need for copies altogether.
Done when you can
Done when you can state precisely why Java is always pass-by-value.
Done when you can draw the stack and heap for a method call that receives an array.
Done when you can predict whether a change inside a method is visible to the caller: mutation yes, reassignment no.
Done when you can explain why
swap(a, b)ands = s + "!"have no effect outside the method.Done when you can protect an array with a defensive copy.