Is Java pass-by-value or pass-by-reference?

Asked constantlyjunior0–6 yrs8 min readJava 5 (1.5)Java 16

Java is always pass-by-value, with no exceptions. For an object the value copied is the reference, which is why a method can mutate your object but can never swap or replace it.

The Answer

Say this in the room. 45 seconds.

  • Java is pass-by-value. Always. There is no pass-by-reference in Java.
  • For a primitive, the value copied is the number.
  • For an object, the value copied is the reference — the address, not the object.
  • So the method gets its own copy of the arrow pointing at your object. Both arrows point at the same thing.
  • That means the method can mutate your object — list.add(...), sb.append(...) — and you will see it.
  • But it cannot make your variable point somewhere else. Reassigning the parameter only moves the method's copy of the arrow.
  • The proof: write a swap method. It fails for objects too, not just primitives. If Java were pass-by-reference, swapping would work.

Understand It

Two words that get conflated

The confusion is that "reference" appears in both phrases. Pull them apart:

What gets copied into the parameter
pass-by-valuethe value of the variable
pass-by-referencean alias to the variable itself

Java only ever does the first. When the variable holds an object, its value is a reference — so a reference gets copied. A copied reference is not pass-by-reference. That single sentence is the whole answer, and it's why "Java is pass-by-reference for objects" is wrong rather than merely imprecise.

The primitive case nobody argues about

Compiled and run on this build
int x = 1;
int y = 2;
swapPrimitives(x, y);
System.out.println("x=" + x + " y=" + y);
Output
x=1 y=2

The method got copies. It swapped its copies. Nothing happened out here. Everyone accepts this.

The object case, which settles the argument

Now the identical method, on objects:

Compiled and run on this build
StringBuilder p = new StringBuilder("first");
StringBuilder q = new StringBuilder("second");
swapObjects(p, q);
System.out.println("p=" + p + " q=" + q);
Output
p=first q=second

Nothing swapped. This is the experiment that decides the question, and it's the one most articles skip — they demonstrate the primitive swap, which tells you nothing about objects, and then assert that objects behave differently.

In a genuinely pass-by-reference language, swapObjects would work: the parameters would be aliases for p and q themselves, and assigning to them would reassign the caller's variables. That's exactly what C++ void swap(T& a, T& b) does. Java cannot express it. Not with a keyword, not with a trick — the language has no syntax for it.

But methods clearly do change my objects

They do, and this is where the intuition comes from. Both arrows point at one object, so anything the method does to that object is visible to you:

Compiled and run on this build
StringBuilder mutated = new StringBuilder("original");
mutate(mutated);
System.out.println("mutate()   -> " + mutated);

StringBuilder replaced = new StringBuilder("original");
reassign(replaced);
System.out.println("reassign() -> " + replaced);
Output
mutate()   -> original mutated
reassign() -> original

Same parameter type, same call shape, opposite outcomes. The difference is one word in the method body:

  • sb.append(...) — follow the arrow, change the object. You see it.
  • sb = new StringBuilder(...) — point the method's own arrow somewhere else. You see nothing.

Draw it once and you never get this wrong again:

before the call          caller's variable ──────► [ "original" ]

inside the method        caller's variable ──────► [ "original" ]
                         parameter (a copy) ─────►      ▲

  sb.append("x")     ── changes the box ─────────────────┘   caller sees it

  sb = new ...       ── moves only the parameter's arrow      caller sees nothing
                         parameter ────► [ "brand new" ]

Arrays are objects, so the same split applies

Compiled and run on this build
int[] numbers = { 1, 2, 3 };

mutateArray(numbers);
System.out.println("after mutateArray   : " + Arrays.toString(numbers));

reassignArray(numbers);
System.out.println("after reassignArray : " + Arrays.toString(numbers));
Output
after mutateArray   : [99, 2, 3]
after reassignArray : [99, 2, 3]

Writing into a slot is visible. Replacing the whole array is not — the second call did nothing at all, which is why "the method takes an array and returns nothing" is nearly always a bug when the method meant to replace its contents.

Why String and Integer feel like exceptions

They aren't. They're immutable, so the mutating half of the story simply isn't available:

Compiled and run on this build
String text = "hello";
tryToChangeString(text);
System.out.println("String  -> " + text);

Integer count = 5;
tryToChangeInteger(count);
System.out.println("Integer -> " + count);
Output
String  -> hello
Integer -> 5

s = s + " changed" builds a new String and points the parameter at it. i = i + 100 unboxes, adds, and boxes into a new Integer. Neither can modify the original because neither type has a mutator at all.

This is why immutable types are easier to reason about: with StringBuilder you have to know whether the method mutates or reassigns. With String there is only one possibility. Autoboxing makes it worse rather than better — Integer count reads like a plain number, so people expect count to change, and the boxing hides that an immutable object was involved.

What to do about it

Three practical consequences.

You cannot write a swap method in Java. Return a value, return a small record or array holding both, or mutate a container the caller owns. If you find yourself wanting output parameters, that's a design signal.

A method that takes your mutable object can change it. That's the argument for immutability at boundaries: records, List.copyOf, defensive copies. If a method must not modify your list, don't hand it a mutable one.

"Pass by reference" as a phrase is fine in casual speech and wrong in an interview. The interviewer is checking whether you know the difference between copying an arrow and sharing a variable.


Interviewer's Next Move

The question after the question. This is what the round is actually testing.

1. "So objects are passed by reference?" No. The reference is passed by value — the method gets its own copy of it. Both copies point at the same object, which is why mutation is visible, but the method cannot repoint the caller's variable. A copied reference is not pass-by-reference.

2. "Prove it." Write swap(a, b) and call it with two objects. It fails, exactly as it does for primitives. In a pass-by-reference language the parameters would alias the caller's variables and the swap would work — that's what C++ T& does. Java has no syntax for it.

3. "Then why did my list change after I passed it to a method?" Because the method followed the reference and mutated the object. list.add(x) changes the object both arrows point at. list = new ArrayList<>() inside the method changes nothing for you.

4. "Are arrays special?" No, arrays are objects. arr[0] = 99 is visible to the caller; arr = new int[]{...} is not. A method that intends to replace an array's contents has to write into the existing one or return a new array.

5. "Why do String and Integer seem to behave differently?" They're immutable, so there is no mutation to observe — only reassignment, which is never visible. Nothing about parameter passing changes. With Integer autoboxing hides that an object was involved at all, which is where the confusion usually starts.

6. "How would you write a method that swaps two values, then?" You don't, in the C sense. Return both — a record, an array, or a small holder object the caller passes in and the method mutates. Wanting output parameters usually means the method should be returning something instead.

Code traps

Trap A — predict before you run:

static void grow(List<String> list) {
    list.add("added");
    list = new ArrayList<>();
    list.add("invisible");
}

List<String> items = new ArrayList<>();
grow(items);
System.out.println(items);
Answer

[added]. The first line mutates the object the caller can see. The reassignment then points the parameter at a fresh list, so "invisible" goes into an object nobody else holds a reference to — it becomes garbage the moment the method returns. Both statements are in the same method, one is visible and one isn't, and the only difference is whether the line follows the arrow or moves it.

Trap B:

static void clear(StringBuilder sb) {
    sb = null;
}

StringBuilder sb = new StringBuilder("still here");
clear(sb);
System.out.println(sb.length());
Answer

10 — no NullPointerException. Setting the parameter to null only nulls the method's copy of the reference. The caller's variable still points at the object. People write this expecting to free something or defend against reuse; it does nothing at all.

Common wrong answers

Said in interviewsReality
"Primitives by value, objects by reference."Everything by value. The value of an object variable is a reference.
"Java is pass-by-reference because my list changed."The method mutated the shared object. It could not repoint your variable.
"You can swap two objects with a helper method."You cannot. Try it — it fails just like the primitive version.
"Arrays are passed by reference."Arrays are objects; identical rules. Mutation visible, reassignment not.
"Setting a parameter to null frees the object."It nulls the copy. The caller still holds a reference.

Check Yourself

Q1. In one sentence, why can a method change your object but not replace it?

AnswerIt receives a copy of the reference, so it can follow that copy to the shared object and change it, but assigning to the parameter only moves the method's own copy and never touches the caller's variable.

Q2. Which single experiment settles "is Java pass-by-reference?" and what does it show?

AnswerA swap method taking two objects. It fails to swap, so the parameters cannot be aliases for the caller's variables — which is what pass-by-reference means. The primitive swap proves nothing about objects, which is why showing only that one is a weak answer.

Q3. You must guarantee a method cannot modify the list you pass it. What do you do?

AnswerDon't pass it something mutable — hand over List.copyOf(items) (an immutable copy), or make the element type immutable too if the elements themselves matter. Documentation and good intentions are not a mechanism; parameter passing gives you no protection at all.


Practice

TierExerciseTime
Warm-upTry to write swap5 min
ChallengeMutate or reassign20 min
ProductionThe method that cleared the caller's cart45 min
InterviewFull round replay10 min

What changed, and when

Version boundaries are where follow-ups live. Know the change and the behaviour it replaced.

  1. Java 5 (1.5)

    Autoboxing let you pass an int where an Integer is expected, so parameters that look like plain numbers are quietly immutable objects.

    Before Java 5 (1.5): You wrote new Integer(x) and Integer.intValue() by hand, which at least made it obvious you were passing an object.

  2. Java 16

    Records give you a standard way to pass data that cannot be mutated by the callee at all, so the whole question stops having consequences.

    Before Java 16: Passing a mutable POJO meant any method you handed it to could change your object, and the only defence was a defensive copy.

Practice ladder

Reading this page is not knowing it. Four tiers, ending in a real incident.

Where this question goes next

Questions that lead here

  • What is the difference between String, StringBuilder and StringBuffer?

    String is immutable, so every change allocates a new one. StringBuilder is a growable buffer you mutate in place. StringBuffer is the same thing with every method synchronized, which you almost never need. Concatenating in a loop is the only case where the difference is dramatic — and it is dramatic.

    Asked constantlyjunior0–6 yrs9 min readStrings

Every runnable example above was compiled and executed against openjdk 21.0.11 on this build, and its output diffed against what this page claims. Last updated 2026-08-27.