A teammate passes a List to a method, the method calls list.add(...), and the caller sees the new element. Does this prove Java is pass-by-reference? Explain mutation vs reassignment.
answer
- add() mutates the shared object = expected, not pass-by-reference
- list = new ArrayList<>() inside method = local only
- Reassignment litmus test
- Defensive copy / unmodifiable view to protect inputs
- Reachability decides who sees mutation
basics
~20 sNo. The method got a copy of the reference pointing at the same list, so add() changes the shared list. That's mutation, not pass-by-reference. If the method did list = new ArrayList<>(), the caller would see nothing change.
solid answer
~50 sIt does not prove pass-by-reference. When you pass the list, Java copies the reference value; the parameter and the caller's variable now point at the same underlying ArrayList. Calling add() goes through that shared reference and mutates the one object on the heap, so the caller observes the new element. That is the expected behavior of pass-by-value when the value is a reference. The decisive test is reassignment: if inside the method you write list = new ArrayList<>(), you only reseat the local parameter copy, and the caller's list is unchanged. True pass-by-reference would propagate that reassignment back. So the rule is: mutation of the shared object is visible because both references reach the same object; reassignment of the parameter is invisible because the parameter is just a copy. This is why defensive copying (copying inputs you don't want callers to mutate, or returning copies) matters for API safety.
code
java · 14 linesimport java.util.*;
static void mutate(List<String> p) { p.add("x"); } // visible to caller
static void reseat(List<String> p) { p = new ArrayList<>(); } // invisible
public static void main(String[] a) {
List<String> list = new ArrayList<>();
mutate(list); System.out.println(list); // [x] shared object mutated
reseat(list); System.out.println(list); // [x] reassignment had no effect
// Protect an input: pass an unmodifiable view
List<String> safe = List.copyOf(list);
// mutate(safe); -> would throw UnsupportedOperationException
}go deeper
Recognize that a visible add() is mutation of a shared object, not pass-by-reference, and that reassigning the parameter would NOT be visible.
Cleanly separate mutation (visible, via the shared reference) from reassignment (local), prove the point with the reassignment test, and name defensive copying as the mitigation.
Tie it to API design: defensive copies on input and output, unmodifiable views, and why immutability removes the whole hazard. Reason about reachability and ownership of mutable state.
Set policy: where the codebase draws ownership boundaries, when to mandate immutability vs defensive copying, the performance trade-offs of copying, and how to audit getters/setters that leak internal mutable state.
## Setup This question targets the single most common 'gotcha' that makes people *think* Java is pass-by-reference. It isn't. Let's define the moving parts. - A **reference** is a handle pointing at an object on the **heap** (the region where all objects live). A variable of a reference type stores a reference, not the object. - **Mutation** = changing the *internal state* of the object the reference points to (e.g., `list.add(x)`, `dog.setName(...)`, `array[0] = ...`). The object's identity is the same; its contents changed. - **Reassignment** = pointing the *variable* at a different object (`list = new ArrayList<>()`). The old object is untouched; the variable now holds a different reference. ## What actually happens on the call ```java void addOne(List<String> p) { p.add("x"); } List<String> myList = new ArrayList<>(); addOne(myList); // myList now contains "x" ``` Step by step: 1. `myList` holds reference R, which points at an ArrayList object on the heap. 2. At the call, Java **copies the value of `myList`** — i.e., it copies reference R — into the parameter `p`. Now `p` and `myList` are two *separate variables* that both hold reference R. 3. `p.add("x")` follows R to the one ArrayList and appends to it. 4. The method returns. `p` is gone. But the ArrayList R points to was permanently modified, and `myList` still holds R, so the caller sees `"x"`. Nothing here contradicts pass-by-value. A *copy* of the reference was passed; both copies just happen to address the same object. ## The reassignment litmus test ```java void swapList(List<String> p) { p = new ArrayList<>(); p.add("new"); } List<String> myList = new ArrayList<>(List.of("old")); swapList(myList); // myList still contains ["old"] ``` Here `p` is reseated to a brand-new list. Because `p` is a *copy* of the reference, reseating it changes only the copy. `myList` still holds the original reference. The caller sees `["old"]`. If Java were pass-by-reference, `p = ...` would reseat `myList` too, and the caller would see `["new"]`. It doesn't — proof of pass-by-value. ## The general principle - **Reachability decides visibility of mutation.** If both the caller and callee can reach the same object, either can mutate it and both observe it. - **The parameter is a private copy of the reference.** Reassigning it is purely local. ## Practical consequences (why interviewers ask this) 1. **Don't trust that passing an object protects it.** A method can mutate any mutable object you hand it. If you must prevent that, pass an **unmodifiable view** (`List.copyOf`, `Collections.unmodifiableList`) or a **defensive copy**. 2. **Returning internal mutable state leaks it.** A getter returning the live list lets callers mutate your internals — defensively copy on the way out too. 3. **Immutability sidesteps the whole problem.** If the object can't be mutated (records with immutable fields, `String`, boxed primitives), 'mutation visible to caller' can't happen; only reassignment, which is always local. 4. **'Out parameters' require a mutable container.** To hand a value back through a parameter you mutate a shared holder (single-element array, holder object), because you can't reseat the caller's variable.
- How would you prevent a method from mutating a List you pass to it?Pass an unmodifiable view (List.copyOf(list) or Collections.unmodifiableList) so mutating calls throw, or pass a defensive copy (new ArrayList<>(list)) so the method mutates the copy, not your list.
- If String is a reference type, why can't a method mutate the String I pass?String is immutable: it exposes no mutators, so there is no operation the method could call to change its state. The method can only reassign its local parameter (e.g., p = p + "x"), which the caller never sees.
saying these in an interview costs you the question
- Concluding 'pass-by-reference' from a visible add()/setter call
- Forgetting that reassignment inside the method is invisible to the caller
- Assuming passing an object makes it safe from mutation
- Returning the live internal collection from a getter without copying