Java Tutorial
🔍

Java Pass by Value vs Pass by Reference

Java Pass by Value vs Pass by Reference

If someone asks you "does Java pass objects by reference?", the correct answer is no — and explaining why is one of the most revealing questions in a Java interview. The confusion exists because Java's behaviour with objects looks like pass by reference. Mutating an object inside a method changes the original. But reassigning the variable inside a method has no effect on the caller. That distinction is the entire point.

Java passes everything by value — always. For primitives, the value itself is copied. For objects, a copy of the reference (the memory address) is passed. The object in memory is never duplicated — only the address pointing to it is. Understanding what gets copied and what gets shared is the foundation of writing methods that behave predictably.

The Core Concept — What Gets Copied

PRIMITIVE — the value itself is copied:

  Caller                     Method
  ─────────────────          ─────────────────
  int price = 500;           void increase(int p)
      │                           │
      │  copy of 500 ──────────►  p = 500
      │                           │
  price = 500 (unchanged)    p = 600 (local copy changed)


OBJECT — a copy of the reference (address) is copied:

  Caller                     Method
  ─────────────────          ─────────────────
  Order order ──┐            void process(Order o)
                │                  │
                └──── address ──►  o ──┐
                                       │
                            BOTH point to the SAME Order object
                            Mutating the object affects the original

  But if the method does: o = new Order();
  The caller's 'order' still points to the original — only the
  local copy 'o' now points somewhere new.

Primitives — Pure Copy, No Surprise

When a primitive is passed to a method, the method receives a completely independent copy. Changes inside the method have absolutely no effect on the original variable.

// File: PrimitivePassDemo.java public class PrimitivePassDemo { // This method attempts to double the value — but it works on a copy public static void doubleIt(int number) { System.out.println(" Inside before: " + number); number = number * 2; System.out.println(" Inside after : " + number); } // Simulates applying a discount — modifies a local copy only public static void applyDiscount(double price, double discountPercent) { price = price - (price * discountPercent / 100); System.out.println(" Price inside method: Rs." + price); // caller's price variable is untouched } public static void main(String[] args) { int originalNumber = 50; System.out.println("=== int ==="); System.out.println("Before method call: " + originalNumber); doubleIt(originalNumber); System.out.println("After method call : " + originalNumber); // still 50 System.out.println(); double productPrice = 1500.0; System.out.println("=== double ==="); System.out.println("Before method call: Rs." + productPrice); applyDiscount(productPrice, 20); System.out.println("After method call : Rs." + productPrice); // still 1500.0 } }
Output:
=== int ===
Before method call: 50
  Inside before: 50
  Inside after : 100
After method call : 50

=== double ===
Before method call: Rs.1500.0
  Price inside method: Rs.1200.0
After method call : Rs.1500.0

The method sees 100 for its local copy of number — but the caller still sees 50. The applyDiscount method applies the discount to its own copy of price — the caller's variable is untouched. For every primitive type — int, double, boolean, char, long, float, byte, short — the same rule applies.

Objects — Reference Copy, Shared State

When an object is passed to a method, a copy of the reference — the memory address — is made. Both the caller and the method now hold two references that point to the same object. Mutating the object through either reference changes the same underlying data.

// File: ObjectMutationDemo.java public class ObjectMutationDemo { static class StudentProfile { String name; int attendancePercent; String status; StudentProfile(String name, int attendancePercent) { this.name = name; this.attendancePercent = attendancePercent; this.status = "ACTIVE"; } @Override public String toString() { return name + " | Attendance: " + attendancePercent + "% | Status: " + status; } } // This method MUTATES the object — caller will see the change public static void updateAttendance(StudentProfile student, int newPercent) { student.attendancePercent = newPercent; if (newPercent < 75) { student.status = "DETAINED"; } System.out.println(" Inside method: " + student); } public static void main(String[] args) { StudentProfile priya = new StudentProfile("Priya Sharma", 85); System.out.println("Before: " + priya); updateAttendance(priya, 68); System.out.println("After : " + priya); // object changed — caller sees it } }
Output:
Before: Priya Sharma | Attendance: 85% | Status: ACTIVE
  Inside method: Priya Sharma | Attendance: 68% | Status: DETAINED
After : Priya Sharma | Attendance: 68% | Status: DETAINED

The method received a copy of the reference to priya's object. Changing student.attendancePercent and student.status through that copied reference modifies the actual object in memory. The caller's priya variable still holds the same reference — now pointing to the modified object.

Reference Reassignment — Where "Looks Like Reference" Breaks Down

This is the crucial test that distinguishes pass-by-value-of-reference from true pass-by-reference. If Java were true pass-by-reference, reassigning the parameter inside a method would change the caller's variable. It does not.

// File: ReferenceReassignDemo.java public class ReferenceReassignDemo { static class BankAccount { String accountId; double balance; BankAccount(String accountId, double balance) { this.accountId = accountId; this.balance = balance; } @Override public String toString() { return accountId + " | Balance: Rs." + balance; } } // Attempts to replace the account object — will NOT affect caller public static void replaceAccount(BankAccount account) { System.out.println(" Inside (before reassign): " + account); // This creates a NEW BankAccount and makes the LOCAL parameter // 'account' point to it. The caller's variable is unaffected. account = new BankAccount("NEW-999", 0.0); System.out.println(" Inside (after reassign) : " + account); } // Mutates the object — caller WILL see this change public static void addFunds(BankAccount account, double amount) { account.balance += amount; // modifies the existing object } public static void main(String[] args) { BankAccount savings = new BankAccount("SAV-001", 15000.0); System.out.println("=== Reference Reassignment (no effect on caller) ==="); System.out.println("Before: " + savings); replaceAccount(savings); System.out.println("After : " + savings); // still SAV-001 System.out.println(); System.out.println("=== Object Mutation (visible to caller) ==="); System.out.println("Before: " + savings); addFunds(savings, 5000.0); System.out.println("After : " + savings); // balance changed } }
Output:
=== Reference Reassignment (no effect on caller) ===
Before: SAV-001 | Balance: Rs.15000.0
  Inside (before reassign): SAV-001 | Balance: Rs.15000.0
  Inside (after reassign) : NEW-999 | Balance: Rs.0.0
After : SAV-001 | Balance: Rs.15000.0

=== Object Mutation (visible to caller) ===
Before: SAV-001 | Balance: Rs.15000.0
After : SAV-001 | Balance: Rs.20000.0

replaceAccount creates a brand new object and points its local account variable to it. The caller's savings still points to the original SAV-001 account — completely unaffected. addFunds modifies the balance on the existing object — and the caller sees that change because both variables point to the same object.

Memory Diagram — Three Scenarios Side by Side

SCENARIO 1 — Primitive (int):

  Stack                           Heap
  ─────────────────────
  main frame:
    price = 500    ─────────────► (no heap object — int lives on stack)

  After passing to method:
    main:   price = 500  (original — unchanged forever)
    method: p     = 500  (copy — method can do anything to it)


SCENARIO 2 — Object Mutation:

  Stack                           Heap
  ─────────────────────           ──────────────────────
  main frame:                     ┌──────────────────┐
    order ───────────────────────►│ Order            │
                                  │ id = "ORD-001"   │
  method frame:                   │ amount = 1000    │
    o     ───────────────────────►└──────────────────┘
                                     ▲
  Both arrows point to the SAME object — mutations visible to both


SCENARIO 3 — Reference Reassignment:

  Stack                           Heap
  ─────────────────────           ──────────────────────
  main frame:                     ┌──────────────────┐
    order ───────────────────────►│ Order (original) │
                                  │ id = "ORD-001"   │
  method frame (after reassign):  └──────────────────┘
    o     ──────────────────────► ┌──────────────────┐
                                  │ Order (new)       │
                                  │ id = "ORD-NEW"    │
                                  └──────────────────┘
  main's 'order' still points to the original — unaffected

Primitive vs Object Parameter Behaviour — Comparison Table

AspectPrimitive ParameterObject Parameter
What is copiedThe value itselfThe reference (memory address)
Does the method see original dataYes — read-only copyYes — through the shared reference
Can method change caller's variable valueNo — neverNo — reassignment only changes local copy
Can method change caller's object stateNot applicableYes — mutations through the reference affect the shared object
Example typeint, double, boolean, charString, ArrayList, any class
After method returnsCaller's variable unchangedCaller's object may have changed state
Memory locationCopy on stackReference on stack, object on heap
True pass by reference?NoNo — Java is always pass by value
RiskAccidentally assuming mutation worksAccidentally mutating shared state
How to prevent mutationNot needed — primitives are immutableUse defensive copy or immutable types

Strings — The Special Case

String is an object in Java, but it behaves like a primitive for reassignment purposes — because String is immutable. You cannot change the content of a String object. Every operation that appears to modify a String actually creates a new String object.

// File: StringPassDemo.java public class StringPassDemo { public static void tryToModify(String name) { System.out.println(" Inside (before): " + name); name = name.toUpperCase(); // creates a NEW String — does not modify original System.out.println(" Inside (after) : " + name); } public static void tryToModifyBuilder(StringBuilder sb) { sb.append(" — VERIFIED"); // modifies the existing object System.out.println(" Inside: " + sb); } public static void main(String[] args) { String customerName = "priya mehta"; System.out.println("=== String (immutable) ==="); System.out.println("Before: " + customerName); tryToModify(customerName); System.out.println("After : " + customerName); // unchanged — String is immutable System.out.println(); StringBuilder transactionNote = new StringBuilder("Payment received"); System.out.println("=== StringBuilder (mutable) ==="); System.out.println("Before: " + transactionNote); tryToModifyBuilder(transactionNote); System.out.println("After : " + transactionNote); // changed — StringBuilder is mutable } }
Output:
=== String (immutable) ===
Before: priya mehta
  Inside (before): priya mehta
  Inside (after) : PRIYA MEHTA
After : priya mehta

=== StringBuilder (mutable) ===
Before: Payment received
  Inside: Payment received — VERIFIED
After : Payment received — VERIFIED

String.toUpperCase() returns a new String object. The method's local name variable points to this new object. The caller's customerName still points to "priya mehta" — untouched. StringBuilder.append() modifies the existing object in place, so the caller sees the change.

Basic Example

A short program showing both behaviours side by side: a primitive parameter change never reaches the caller, but a field mutated through an object parameter does.

public class Main { static class Counter { int value; Counter(int value) { this.value = value; } } static void addTen(int number) { number = number + 10; } static void addTenToCounter(Counter counter) { counter.value = counter.value + 10; } public static void main(String[] args) { int number = 5; addTen(number); System.out.println("number after addTen: " + number); Counter counter = new Counter(5); addTenToCounter(counter); System.out.println("counter.value after addTenToCounter: " + counter.value); } }

Output

number after addTen: 5
counter.value after addTenToCounter: 15

addTen only ever modifies its own local copy of number — the value copied into it when the method was called — so number back in main is unaffected. addTenToCounter receives a copy of the reference to counter, but that copy still points at the exact same Counter object main holds, so changing counter.value through it is visible the moment the method returns.

Best Practices

Return the modified value when you need to update a primitive. Since a method cannot modify the caller's primitive variable, the correct pattern is to return the new value: int discountedPrice = applyDiscount(price, 10). This is explicit — the caller decides whether to update their variable.

Document when a method mutates its object parameters. A method that modifies an object passed to it should have a clear name and Javadoc indicating this. updateStatus(Order order) is clear — it mutates. getTotal(Order order) is clear — it reads. Naming like processOrder(Order order) without documentation is ambiguous.

Use defensive copies to prevent unintended mutation. If a method stores an object reference (in a field or collection) without copying it, and the caller later mutates the object, the stored reference reflects those changes. Create a new object from the passed one: this.address = new Address(address) instead of this.address = address.

Never assume swapping two object parameters works. A method that tries to swap two object references passed to it will always fail — both parameters are local copies. To swap, return both values wrapped in a result object, or swap fields inside the objects instead.

Common Mistakes

Mistake 1 — Expecting to Modify a Primitive Through a Method

int balance = 1000; addBonus(balance, 500); // hoping balance becomes 1500 System.out.println(balance); // still 1000 — primitives are copied // Fix — return the new value int balance = 1000; balance = addBonus(balance, 500); // capture the return value System.out.println(balance); // 1500

Mistake 2 — Expecting a Swap Method to Work

// Swap does NOT work for object references in Java public static void swap(Object a, Object b) { Object temp = a; a = b; b = temp; // Only local copies a and b are swapped — caller's variables unchanged } String x = "hello"; String y = "world"; swap(x, y); System.out.println(x); // still "hello" System.out.println(y); // still "world"

Mistake 3 — Mutating Shared Objects Without Realising It

public static ShoppingCart applyMidnightSale(ShoppingCart cart) { // Intention: return a discounted copy // Reality: mutating the ORIGINAL cart — caller's cart is modified too cart.setDiscountPercent(50.0); return cart; // returns the same object, not a copy } ShoppingCart myCart = new ShoppingCart("CUST-001"); ShoppingCart saleCart = applyMidnightSale(myCart); // myCart is ALSO discounted now — unintended side effect

Fix: create a new ShoppingCart inside the method and copy the data — a defensive copy.

Mistake 4 — Thinking String Behaves Like a Mutable Object

String name = "rohan"; makeUpperCase(name); // hoping name becomes "ROHAN" System.out.println(name); // still "rohan" — String is immutable public static void makeUpperCase(String s) { s = s.toUpperCase(); // creates a new String — does not modify original } // Fix — return the new String name = makeUpperCase(name); public static String makeUpperCase(String s) { return s.toUpperCase(); }

Interview Questions

Q1. Is Java pass by value or pass by reference?

Java is always pass by value. For primitive types, the value itself is copied into the parameter. For object types, a copy of the reference — the memory address — is passed. Both the caller and the method hold separate references that point to the same object on the heap. Mutating the object through the method's copy changes the shared object, which the caller also sees. But reassigning the parameter to a new object only changes the local copy — the caller's reference is unaffected. Java never passes the memory location of the caller's variable itself, which is what true pass by reference would mean.

Q2. What is the difference between mutating an object and reassigning a reference?

Mutating an object means changing the state of the existing object — modifying a field, adding to a list, changing a status. Because both caller and method hold references to the same object, mutations are visible to both. Reassigning a reference means making the local parameter variable point to a different object — param = new Something(). This only changes where the local variable points; the caller's reference still points to the original object. Reassignment never affects the caller.

Q3. Why does swapping two objects inside a method not work in Java?

A swap method receives copies of the two references — it does not receive the caller's variable locations. Swapping the two local copies (temp = a; a = b; b = temp) only changes where the method's local variables point. When the method returns, those local variables are destroyed. The caller's variables still hold their original references. Java has no mechanism to modify which object a caller's variable points to from inside another method.

Q4. Why does String behave like a primitive even though it is an object?

String is immutable — its character content cannot be changed after creation. Every method that appears to modify a String (toUpperCase(), trim(), replace()) returns a new String object. Inside a method, assigning the result of s.toUpperCase() to s only changes the local parameter to point to a new String. The caller's String variable still points to the original. This makes String behave like a primitive in terms of visibility of changes — but for a different reason (immutability vs value copy).

Q5. When would you use a return value instead of mutating a parameter?

Use a return value when the result of the computation is a new or transformed version of the input — especially for primitives, immutable types like String, or when you do not want to modify the caller's object. Mutating a parameter is appropriate when the method's explicit purpose is to update the object's state — like recordPayment(account, amount) where the intent is clearly to change the account. In general, prefer returning new values for functional operations and document mutations clearly for state-update methods.

Q6. What is a defensive copy and when do you need it?

A defensive copy is creating a new object from the passed reference rather than storing the reference directly. Without it, storing this.items = passedList means any mutation the caller makes to their passedList later will also change your stored list — because both references point to the same object. With this.items = new ArrayList<>(passedList), your stored list is independent. Use defensive copies in constructors and setters whenever you store a reference to a mutable object passed from outside the class.

FAQs

Can a method modify a caller's int variable?

No. Primitives are passed by value — a complete copy is made. Whatever the method does to its local copy has no effect on the caller's variable. If you need a method to produce a new value for a primitive, return it from the method and assign the result at the call site.

What happens when an array is passed to a method?

An array is an object in Java. The method receives a copy of the reference to the array. Modifying elements — arr[0] = 99 — changes the shared array and the caller sees the change. Reassigning the parameter — arr = new int[5] — only changes the local copy and does not affect the caller.

Does Java ever truly pass by reference?

No. Java never passes by reference in the traditional sense — where the method could change which object the caller's variable points to. Every parameter in Java is a copy: a copy of the value for primitives, a copy of the reference for objects. The behaviour that looks like pass by reference — mutations visible to the caller — is because the copy of the reference still points to the same object on the heap.

Why do some people say Java passes objects by reference?

Because object mutations made inside a method are visible to the caller. This looks identical to pass-by-reference behaviour. The difference only becomes apparent when you try to reassign the reference — replacing an object with a new one inside a method — and find that the caller's variable is completely unchanged. Java is more precisely described as "pass by value of the reference" for objects.

How do you return two modified values from a method in Java?

Wrap both values in a result object or a record. record SwapResult(String first, String second) {} lets a method return new SwapResult(b, a) and the caller can use result.first() and result.second(). This is the correct Java way to work around the limitation that methods cannot modify caller variables.

Summary

Java is always pass by value — the parameter is always a copy of something. For primitives, it is a copy of the value: completely independent, modification-proof, never changes the caller. For objects, it is a copy of the reference: both caller and method look at the same heap object, mutations are shared, but pointing the local copy at a new object never affects the caller.

The one-sentence test: if you set param = new Something() inside a method and the caller's variable still points to the old object after the call, you are in a pass-by-value language. Java always passes this test.

For interviews, be ready to explain the difference between mutation and reassignment with a concrete example, explain why String behaves like a primitive even though it is an object, and demonstrate why a swap method cannot work in Java. These three points together show complete understanding of the topic.

What to Read Next

Practice Programs

Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.

Browse all Java programs →