Java Pass by Reference Myth: Why Java Uses Pass by Value
java pass by reference myth: Explains why Java is always pass by value, how object references behave as copied values, and why mutation works while reassignment does not.
The Myth and Why It Persists
The claim that Java passes objects by reference is one of the most persistent misconceptions in the language. The java pass by reference myth usually surfaces in interviews, code reviews, and debugging sessions when a method mutates an object and the change is visible to the caller. That observable behavior looks like pass by reference, but it is not what is happening under the hood.
Java is strictly pass by value. Every argument passed to a method is a copy of the original value. For primitive types, that copy is the primitive value itself. For reference types, that copy is the reference value — the memory address of the object — not the object itself. The distinction matters because it determines what a method can and cannot change from the caller's perspective.
What Pass by Value and Pass by Reference Actually Mean
Before examining Java's behavior, it helps to define the two terms precisely.
Pass by value means the callee receives a copy of the argument's value. Changes made to the parameter inside the method do not affect the caller's original variable.
Pass by reference means the callee receives a reference to the caller's variable itself. Assigning a new value to the parameter inside the method changes the caller's variable.
The key difference is not whether the callee can modify the underlying data. The difference is whether the callee can rebind the caller's variable. Under pass by reference, reassigning the parameter changes the caller's variable. Under pass by value, it does not.
How Java Handles Primitive Arguments
With primitives, Java's behavior is unambiguous:
public class PrimitiveExample { public static void main(String[] args) { int number = 10; increment(number); System.out.println(number); // prints 10 } static void increment(int value) { value = value + 1; } }
The increment method receives a copy of number. Changing value inside the method has no effect on number in main. This is textbook pass by value, and no developer disputes it.
How Java Handles Object Arguments
The confusion begins with objects. Consider this example:
public class ObjectExample { public static void main(String[] args) { StringBuilder builder = new StringBuilder("Hello"); appendWorld(builder); System.out.println(builder.toString()); // prints Hello World } static void appendWorld(StringBuilder sb) { sb.append(" World"); } }
The output is Hello World, which appears to prove that Java passes objects by reference. But the mechanism is different. The builder variable holds a reference value — an address pointing to the StringBuilder object on the heap. When appendWorld is called, Java copies that reference value into the parameter sb. Both builder and sb now point to the same object. Calling sb.append mutates the object that both variables reference, so the change is visible through builder as well.
The reference was copied. The object was not passed at all.
Reassignment vs Mutation: The Decisive Test
The clearest way to distinguish pass by value from pass by reference is to attempt reassignment inside the method:
public class ReassignmentExample { public static void main(String[] args) { StringBuilder builder = new StringBuilder("Hello"); replace(builder); System.out.println(builder.toString()); // prints Hello } static void replace(StringBuilder sb) { sb = new StringBuilder("Replaced"); } }
The output is Hello, not Replaced. If Java passed objects by reference, the parameter sb would be an alias for the caller's builder variable, and assigning a new object to sb would rebind builder. That does not happen. The assignment only changes the local parameter, which is discarded when the method returns.
This is the decisive test. A language that truly passes by reference would allow a method to swap two variables passed in from the caller:
// This does not work in Java public class SwapExample { public static void main(String[] args) { String a = "first"; String b = "second"; swap(a, b); System.out.println(a + " " + b); // still prints first second } static void swap(String x, String y) { String temp = x; x = y; y = temp; } }
The swap has no effect on a and b because x and y are copies of the reference values. Reassigning them only changes the local copies. In a language with true pass by reference, such as C# with the ref keyword, the swap would work because the parameters would alias the caller's variables.
Practical Implications for API Design
Understanding this distinction changes how you design method signatures. When a method mutates an object passed in, the mutation is visible to the caller. That can be intentional, as with Collections.sort(list), or accidental, as when a method modifies a collection it was only supposed to read.
If a method should not mutate its input, consider whether the caller can be affected by the mutation. Passing a reference value means the callee can modify the shared object. To prevent that, the method can either document the contract or work on a defensive copy:
public class DefensiveCopyExample { public static List<String> normalize(List<String> input) { List<String> copy = new ArrayList<>(input); // modify copy, not input return copy; } }
This matters in production code where a shared collection is passed through multiple layers. A mutation deep in the call stack can change behavior far from the point of origin, making the bug difficult to trace.
Null Assignment and Its Effect on the Caller
A related edge case is assigning null to a parameter inside a method:
public class NullExample { public static void main(String[] args) { StringBuilder builder = new StringBuilder("Hello"); setNull(builder); System.out.println(builder == null); // prints false } static void setNull(StringBuilder sb) { sb = null; } }
The caller's builder still references the original object. Setting the parameter to null only clears the local copy of the reference. This is consistent with pass by value: the callee can never change what the caller's variable points to.
Immutability as a Design Response
Because Java passes reference values, the risk of unintended mutation is real. Immutable types eliminate that risk. When a method receives an immutable object, it cannot modify the object's state; it can only create a new object. This is why String is immutable and why records and immutable collections are increasingly common in Java codebases.
public record Point(int x, int y) {} public class ImmutableExample { public static Point shift(Point p, int dx, int dy) { return new Point(p.x() + dx, p.y() + dy); } }
The shift method returns a new Point rather than modifying the input. The caller's original Point remains unchanged. This design choice is only necessary because Java passes reference values; if Java passed objects by value, mutation of the parameter would not affect the caller, and defensive copying would be unnecessary.
Memory and Performance Considerations
Passing reference values is cheap. A reference in the JVM is typically the size of a native pointer, so copying it into a parameter is a single machine-word operation. This is why Java can pass large objects to methods without copying the object's contents. The cost of copying a reference is constant regardless of the object's size.
This efficiency is a direct consequence of pass by value applied to references. The alternative — copying the entire object on every method call — would be prohibitively expensive for large collections or graphs. The tradeoff is that the callee can mutate the shared object, which places the responsibility for safe sharing on the developer.