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.

1// File: PrimitivePassDemo.java 2 3public class PrimitivePassDemo { 4 5 // This method attempts to double the value — but it works on a copy 6 public static void doubleIt(int number) { 7 System.out.println(" Inside before: " + number); 8 number = number * 2; 9 System.out.println(" Inside after : " + number); 10 } 11 12 // Simulates applying a discount — modifies a local copy only 13 public static void applyDiscount(double price, double discountPercent) { 14 price = price - (price * discountPercent / 100); 15 System.out.println(" Price inside method: Rs." + price); 16 // caller's price variable is untouched 17 } 18 19 public static void main(String[] args) { 20 21 int originalNumber = 50; 22 System.out.println("=== int ==="); 23 System.out.println("Before method call: " + originalNumber); 24 doubleIt(originalNumber); 25 System.out.println("After method call : " + originalNumber); // still 50 26 27 System.out.println(); 28 29 double productPrice = 1500.0; 30 System.out.println("=== double ==="); 31 System.out.println("Before method call: Rs." + productPrice); 32 applyDiscount(productPrice, 20); 33 System.out.println("After method call : Rs." + productPrice); // still 1500.0 34 } 35}
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.

1// File: ObjectMutationDemo.java 2 3public class ObjectMutationDemo { 4 5 static class StudentProfile { 6 String name; 7 int attendancePercent; 8 String status; 9 10 StudentProfile(String name, int attendancePercent) { 11 this.name = name; 12 this.attendancePercent = attendancePercent; 13 this.status = "ACTIVE"; 14 } 15 16 @Override 17 public String toString() { 18 return name + " | Attendance: " + attendancePercent 19 + "% | Status: " + status; 20 } 21 } 22 23 // This method MUTATES the object — caller will see the change 24 public static void updateAttendance(StudentProfile student, int newPercent) { 25 student.attendancePercent = newPercent; 26 27 if (newPercent < 75) { 28 student.status = "DETAINED"; 29 } 30 System.out.println(" Inside method: " + student); 31 } 32 33 public static void main(String[] args) { 34 35 StudentProfile priya = new StudentProfile("Priya Sharma", 85); 36 37 System.out.println("Before: " + priya); 38 updateAttendance(priya, 68); 39 System.out.println("After : " + priya); // object changed — caller sees it 40 } 41}
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.

1// File: ReferenceReassignDemo.java 2 3public class ReferenceReassignDemo { 4 5 static class BankAccount { 6 String accountId; 7 double balance; 8 9 BankAccount(String accountId, double balance) { 10 this.accountId = accountId; 11 this.balance = balance; 12 } 13 14 @Override 15 public String toString() { 16 return accountId + " | Balance: Rs." + balance; 17 } 18 } 19 20 // Attempts to replace the account object — will NOT affect caller 21 public static void replaceAccount(BankAccount account) { 22 23 System.out.println(" Inside (before reassign): " + account); 24 25 // This creates a NEW BankAccount and makes the LOCAL parameter 26 // 'account' point to it. The caller's variable is unaffected. 27 account = new BankAccount("NEW-999", 0.0); 28 29 System.out.println(" Inside (after reassign) : " + account); 30 } 31 32 // Mutates the object — caller WILL see this change 33 public static void addFunds(BankAccount account, double amount) { 34 account.balance += amount; // modifies the existing object 35 } 36 37 public static void main(String[] args) { 38 39 BankAccount savings = new BankAccount("SAV-001", 15000.0); 40 41 System.out.println("=== Reference Reassignment (no effect on caller) ==="); 42 System.out.println("Before: " + savings); 43 replaceAccount(savings); 44 System.out.println("After : " + savings); // still SAV-001 45 46 System.out.println(); 47 48 System.out.println("=== Object Mutation (visible to caller) ==="); 49 System.out.println("Before: " + savings); 50 addFunds(savings, 5000.0); 51 System.out.println("After : " + savings); // balance changed 52 } 53}
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.

1// File: StringPassDemo.java 2 3public class StringPassDemo { 4 5 public static void tryToModify(String name) { 6 System.out.println(" Inside (before): " + name); 7 name = name.toUpperCase(); // creates a NEW String — does not modify original 8 System.out.println(" Inside (after) : " + name); 9 } 10 11 public static void tryToModifyBuilder(StringBuilder sb) { 12 sb.append(" — VERIFIED"); // modifies the existing object 13 System.out.println(" Inside: " + sb); 14 } 15 16 public static void main(String[] args) { 17 18 String customerName = "priya mehta"; 19 System.out.println("=== String (immutable) ==="); 20 System.out.println("Before: " + customerName); 21 tryToModify(customerName); 22 System.out.println("After : " + customerName); // unchanged — String is immutable 23 24 System.out.println(); 25 26 StringBuilder transactionNote = new StringBuilder("Payment received"); 27 System.out.println("=== StringBuilder (mutable) ==="); 28 System.out.println("Before: " + transactionNote); 29 tryToModifyBuilder(transactionNote); 30 System.out.println("After : " + transactionNote); // changed — StringBuilder is mutable 31 } 32}
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.

Real-World Example 1 — Student Registration System

The Business Problem

A student registration system at a college or coaching institute processes student data through several stages: validation, fee calculation, and enrolment confirmation. Understanding what changes when data is passed between methods prevents the most common freshers' mistake — assuming that passing a student object to a method and modifying it inside is always safe, or assuming it never changes.

1// File: Student.java 2 3public class Student { 4 5 private String studentId; 6 private String name; 7 private String course; 8 private double feePaid; 9 private boolean enrolled; 10 11 public Student(String studentId, String name, String course) { 12 this.studentId = studentId; 13 this.name = name; 14 this.course = course; 15 this.feePaid = 0.0; 16 this.enrolled = false; 17 } 18 19 public String getStudentId() { return studentId; } 20 public String getName() { return name; } 21 public String getCourse() { return course; } 22 public double getFeePaid() { return feePaid; } 23 public boolean isEnrolled() { return enrolled; } 24 25 public void setFeePaid(double amount) { this.feePaid = amount; } 26 public void setEnrolled(boolean status) { this.enrolled = status; } 27 28 @Override 29 public String toString() { 30 return "[" + studentId + "] " + name 31 + " | Course: " + course 32 + " | Fee Paid: Rs." + feePaid 33 + " | Enrolled: " + enrolled; 34 } 35}
1// File: RegistrationService.java 2 3public class RegistrationService { 4 5 private static final double BASE_FEE = 25000.0; 6 7 // Calculates fee — works on primitive copies, no mutation 8 public static double calculateFee(String course, int durationMonths) { 9 double rate = switch (course.toLowerCase()) { 10 case "btech" -> 1.5; 11 case "mtech" -> 1.8; 12 case "mba" -> 2.0; 13 default -> 1.0; 14 }; 15 // These are primitive parameters — changes here never reach the caller 16 double fee = BASE_FEE * rate * (durationMonths / 12.0); 17 return fee; 18 } 19 20 // Records fee payment — MUTATES the student object 21 public static void recordPayment(Student student, double amountPaid) { 22 student.setFeePaid(amountPaid); // modifies the shared object 23 System.out.println(" [PAYMENT] Recorded Rs." + amountPaid 24 + " for " + student.getName()); 25 } 26 27 // Completes enrolment — MUTATES the student object 28 public static void completeEnrolment(Student student) { 29 if (student.getFeePaid() <= 0) { 30 System.out.println(" [ENROLMENT] Rejected — no fee paid."); 31 return; 32 } 33 student.setEnrolled(true); // modifies the shared object 34 System.out.println(" [ENROLMENT] " + student.getName() 35 + " enrolled in " + student.getCourse()); 36 } 37 38 // Attempts to swap two students — this DOES NOT work (Java is pass by value) 39 public static void swapStudents(Student a, Student b) { 40 // This only swaps the LOCAL copies of the references 41 Student temp = a; 42 a = b; 43 b = temp; 44 System.out.println(" Inside swap: a=" + a.getName() + ", b=" + b.getName()); 45 } 46}
1// File: RegistrationDemo.java 2 3public class RegistrationDemo { 4 5 public static void main(String[] args) { 6 7 Student s1 = new Student("S-2024-001", "Priya Sharma", "BTech"); 8 Student s2 = new Student("S-2024-002", "Rohan Mehta", "MBA"); 9 10 System.out.println("=== Initial State ==="); 11 System.out.println(s1); 12 System.out.println(s2); 13 14 System.out.println("\n=== Fee Calculation (primitive — no mutation) ==="); 15 // calculateFee works on primitive copies — no effect on student objects 16 double s1Fee = RegistrationService.calculateFee("btech", 48); 17 double s2Fee = RegistrationService.calculateFee("mba", 24); 18 System.out.println("Calculated fee for " + s1.getName() + ": Rs." + s1Fee); 19 System.out.println("Calculated fee for " + s2.getName() + ": Rs." + s2Fee); 20 21 System.out.println("\n=== Fee Recording (object mutation — visible) ==="); 22 RegistrationService.recordPayment(s1, s1Fee); 23 RegistrationService.recordPayment(s2, s2Fee); 24 25 System.out.println("\n=== Enrolment (object mutation — visible) ==="); 26 RegistrationService.completeEnrolment(s1); 27 RegistrationService.completeEnrolment(s2); 28 29 System.out.println("\n=== After Registration ==="); 30 System.out.println(s1); 31 System.out.println(s2); 32 33 System.out.println("\n=== Swap Attempt (reference reassignment — no effect) ==="); 34 System.out.println("Before swap: s1=" + s1.getName() + ", s2=" + s2.getName()); 35 RegistrationService.swapStudents(s1, s2); 36 System.out.println("After swap : s1=" + s1.getName() + ", s2=" + s2.getName()); // unchanged 37 } 38}
Output:
=== Initial State ===
[S-2024-001] Priya Sharma | Course: BTech | Fee Paid: Rs.0.0 | Enrolled: false
[S-2024-002] Rohan Mehta | Course: MBA | Fee Paid: Rs.0.0 | Enrolled: false

=== Fee Calculation (primitive — no mutation) ===
Calculated fee for Priya Sharma: Rs.37500.0
Calculated fee for Rohan Mehta: Rs.50000.0

=== Fee Recording (object mutation — visible) ===
  [PAYMENT] Recorded Rs.37500.0 for Priya Sharma
  [PAYMENT] Recorded Rs.50000.0 for Rohan Mehta

=== Enrolment (object mutation — visible) ===
  [ENROLMENT] Priya Sharma enrolled in BTech
  [ENROLMENT] Rohan Mehta enrolled in MBA

=== After Registration ===
[S-2024-001] Priya Sharma | Course: BTech | Fee Paid: Rs.37500.0 | Enrolled: true
[S-2024-002] Rohan Mehta | Course: MBA | Fee Paid: Rs.50000.0 | Enrolled: true

=== Swap Attempt (reference reassignment — no effect) ===
Before swap: s1=Priya Sharma, s2=Rohan Mehta
  Inside swap: a=Rohan Mehta, b=Priya Sharma
After swap : s1=Priya Sharma, s2=Rohan Mehta

Three behaviours visible in one example: primitives (course string and durationMonths) used for calculation without any mutation; object mutation (setFeePaid, setEnrolled) visible to the caller; and reference reassignment (swapStudents) having no effect on the caller's variables.

Real-World Example 2 — Shopping Cart for an Online Store

The Business Problem

A shopping cart on an app like Meesho or Flipkart gets passed between methods for validation, discount application, and checkout. A fresher joining the team might accidentally duplicate items by creating a new cart, or wonder why their cart came back modified from a validation method. These behaviours trace directly back to pass-by-value semantics.

1// File: CartItem.java 2 3public class CartItem { 4 5 private final String productName; 6 private final double price; 7 private int quantity; 8 9 public CartItem(String productName, double price, int quantity) { 10 this.productName = productName; 11 this.price = price; 12 this.quantity = quantity; 13 } 14 15 public String getProductName() { return productName; } 16 public double getPrice() { return price; } 17 public int getQuantity() { return quantity; } 18 public void setQuantity(int q){ this.quantity = q; } 19 public double getLineTotal() { return price * quantity; } 20 21 @Override 22 public String toString() { 23 return String.format("%-20s x%d @ Rs.%-8.2f = Rs.%.2f", 24 productName, quantity, price, getLineTotal()); 25 } 26}
1// File: ShoppingCart.java 2 3import java.util.ArrayList; 4import java.util.List; 5 6public class ShoppingCart { 7 8 private final String customerId; 9 private final List<CartItem> items; 10 private double discountPercent; 11 12 public ShoppingCart(String customerId) { 13 this.customerId = customerId; 14 this.items = new ArrayList<>(); 15 this.discountPercent = 0.0; 16 } 17 18 public void addItem(CartItem item) { items.add(item); } 19 public List<CartItem> getItems() { return items; } 20 public String getCustomerId() { return customerId; } 21 public double getDiscountPercent() { return discountPercent; } 22 public void setDiscountPercent(double d) { this.discountPercent = d; } 23 24 public double getSubtotal() { 25 return items.stream().mapToDouble(CartItem::getLineTotal).sum(); 26 } 27 28 public double getFinalTotal() { 29 double sub = getSubtotal(); 30 return sub - (sub * discountPercent / 100); 31 } 32 33 @Override 34 public String toString() { 35 StringBuilder sb = new StringBuilder(); 36 sb.append("Cart[").append(customerId).append("]\n"); 37 items.forEach(item -> sb.append(" ").append(item).append("\n")); 38 sb.append(String.format(" Discount: %.0f%% | Total: Rs.%.2f", 39 discountPercent, getFinalTotal())); 40 return sb.toString(); 41 } 42}
1// File: CartService.java 2 3public class CartService { 4 5 // Validates minimum order — uses primitives only, no mutation 6 public static boolean meetsMinimumOrder(double cartTotal, double minimumAmount) { 7 // cartTotal and minimumAmount are copies — no mutation possible 8 return cartTotal >= minimumAmount; 9 } 10 11 // Applies coupon — MUTATES the cart object 12 public static void applyCoupon(ShoppingCart cart, String couponCode) { 13 double discount = switch (couponCode.toUpperCase()) { 14 case "MEESHO10" -> 10.0; 15 case "SAVE20" -> 20.0; 16 case "FLAT50" -> cart.getSubtotal() >= 500 ? 15.0 : 0.0; 17 default -> 0.0; 18 }; 19 cart.setDiscountPercent(discount); // mutates the shared cart object 20 System.out.println(" Coupon " + couponCode + " applied: " 21 + discount + "% discount"); 22 } 23 24 // Adjusts quantity — MUTATES the item inside the cart 25 public static void updateQuantity(ShoppingCart cart, 26 String productName, int newQuantity) { 27 for (CartItem item : cart.getItems()) { 28 if (item.getProductName().equalsIgnoreCase(productName)) { 29 item.setQuantity(newQuantity); // mutates the CartItem object 30 System.out.println(" Updated " + productName 31 + " quantity to " + newQuantity); 32 return; 33 } 34 } 35 System.out.println(" Product not found: " + productName); 36 } 37 38 // Attempts to replace cart — DOES NOT affect caller 39 public static void clearCart(ShoppingCart cart) { 40 // This reassigns the local parameter only — caller's cart is unaffected 41 cart = new ShoppingCart(cart.getCustomerId()); 42 System.out.println(" Inside clearCart: items = " + cart.getItems().size()); 43 // caller's cart still has all items 44 } 45 46 // Correct way to clear — mutate the existing object 47 public static void clearCartCorrectly(ShoppingCart cart) { 48 cart.getItems().clear(); // mutates the list inside the existing object 49 cart.setDiscountPercent(0.0); 50 System.out.println(" Cart cleared correctly."); 51 } 52}
1// File: ShoppingCartDemo.java 2 3public class ShoppingCartDemo { 4 5 public static void main(String[] args) { 6 7 ShoppingCart cart = new ShoppingCart("CUST-501"); 8 cart.addItem(new CartItem("Kurti Set", 399.0, 2)); 9 cart.addItem(new CartItem("Palazzo Pants", 249.0, 1)); 10 cart.addItem(new CartItem("Earrings", 99.0, 3)); 11 12 System.out.println("=== Initial Cart ==="); 13 System.out.println(cart); 14 15 System.out.println("\n=== Validation (primitive — no mutation) ==="); 16 double total = cart.getSubtotal(); 17 boolean valid = CartService.meetsMinimumOrder(total, 300.0); 18 System.out.println("Subtotal: Rs." + total + " | Meets minimum: " + valid); 19 System.out.println("Cart unchanged: Rs." + cart.getSubtotal()); 20 21 System.out.println("\n=== Apply Coupon (object mutation — visible) ==="); 22 CartService.applyCoupon(cart, "FLAT50"); 23 System.out.println("After coupon: " + cart.getFinalTotal()); 24 25 System.out.println("\n=== Update Quantity (object mutation — visible) ==="); 26 CartService.updateQuantity(cart, "Kurti Set", 3); 27 28 System.out.println("\n=== Wrong clearCart (reassignment — no effect) ==="); 29 System.out.println("Items before: " + cart.getItems().size()); 30 CartService.clearCart(cart); 31 System.out.println("Items after wrong clear: " + cart.getItems().size()); // unchanged 32 33 System.out.println("\n=== Correct clearCart (mutation — visible) ==="); 34 CartService.clearCartCorrectly(cart); 35 System.out.println("Items after correct clear: " + cart.getItems().size()); // 0 36 37 System.out.println("\n=== Final Cart State ==="); 38 System.out.println(cart); 39 } 40}
Output:
=== Initial Cart ===
Cart[CUST-501]
  Kurti Set            x2 @ Rs.399.00   = Rs.798.00
  Palazzo Pants        x1 @ Rs.249.00   = Rs.249.00
  Earrings             x3 @ Rs.99.00    = Rs.297.00
  Discount: 0%  |  Total: Rs.1344.00

=== Validation (primitive — no mutation) ===
Subtotal: Rs.1344.0 | Meets minimum: true
Cart unchanged: Rs.1344.0

=== Apply Coupon (object mutation — visible) ===
  Coupon FLAT50 applied: 15.0% discount
After coupon: Rs.1142.40

=== Update Quantity (object mutation — visible) ===
  Updated Kurti Set quantity to 3

=== Wrong clearCart (reassignment — no effect) ===
Items before: 3
  Inside clearCart: items = 0
Items after wrong clear: 3

=== Correct clearCart (mutation — visible) ===
  Cart cleared correctly.
Items after correct clear: 0

=== Final Cart State ===
Cart[CUST-501]
  Discount: 0%  |  Total: Rs.0.00

The shopping cart example shows every scenario a fresher encounters: primitive validation with no side effects, object mutation that is intentional and visible, the wrong way to clear a cart (reference reassignment — a common bug), and the correct way (mutating the existing object's list). This mirrors exactly how a real cart service would be written on a team.

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

1int balance = 1000; 2addBonus(balance, 500); // hoping balance becomes 1500 3System.out.println(balance); // still 1000 — primitives are copied 4 5// Fix — return the new value 6int balance = 1000; 7balance = addBonus(balance, 500); // capture the return value 8System.out.println(balance); // 1500

Mistake 2 — Expecting a Swap Method to Work

1// Swap does NOT work for object references in Java 2public static void swap(Object a, Object b) { 3 Object temp = a; 4 a = b; 5 b = temp; 6 // Only local copies a and b are swapped — caller's variables unchanged 7} 8 9String x = "hello"; 10String y = "world"; 11swap(x, y); 12System.out.println(x); // still "hello" 13System.out.println(y); // still "world"

Mistake 3 — Mutating Shared Objects Without Realising It

1public static ShoppingCart applyMidnightSale(ShoppingCart cart) { 2 // Intention: return a discounted copy 3 // Reality: mutating the ORIGINAL cart — caller's cart is modified too 4 cart.setDiscountPercent(50.0); 5 return cart; // returns the same object, not a copy 6} 7 8ShoppingCart myCart = new ShoppingCart("CUST-001"); 9ShoppingCart saleCart = applyMidnightSale(myCart); 10// 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

1String name = "rohan"; 2makeUpperCase(name); // hoping name becomes "ROHAN" 3System.out.println(name); // still "rohan" — String is immutable 4 5public static void makeUpperCase(String s) { 6 s = s.toUpperCase(); // creates a new String — does not modify original 7} 8 9// Fix — return the new String 10name = makeUpperCase(name); 11 12public static String makeUpperCase(String s) { 13 return s.toUpperCase(); 14}

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