The swap that did nothing
Riya is writing a seat-exchange feature for the college bus. Two students want to swap seats, so she writes swap(seatA, seatB). Inside the method she prints the values: swapped. Back in main: unchanged. Yet last week, a method that changed an array's elements worked perfectly.
Sir, my swap method swaps the numbers inside, but main doesn't see it. Is Java passing them by reference or not?
Never by reference. Java always passes a copy. For an int, the copy is the number itself, so the method only changes its own copy.
Then how did my array method change the marks?
For an array, the variable holds a reference, so the copy is a copy of the reference. Both point to the same array. Change an element and everyone sees it. But point the parameter at a new array, and only the method's copy moves. That one rule explains everything today.
What does pass-by-value mean?
Pass-by-value means that when a method is called, the value of each argument is copied into a new parameter variable in the method's stack frame. Java uses pass-by-value for all arguments. For a primitive, the value copied is the data itself. For an object or array, the value copied is the reference (the object's address); the object itself is not copied. Hence a method can modify the state of an object it receives, but assigning a new object to the parameter never affects the caller's variable.
Giving a method an int is like giving someone a photocopy of a number: they can scribble on it, your original stays clean. Giving a method an array is like giving someone a copy of your house address: if they paint the house, you'll see it; if they cross out the address and write a different one, your house is unaffected.
Words you'll meet in this lesson
a[0] = 5, sb.append("x").a = new int[3].The rule, case by case
| Primitive (int, double…) | The number is copied. Changing the parameter never affects the caller. |
|---|---|
| Array or object: mutate | a[0] = 5; changes the shared object. The caller sees it. |
| Array or object: reassign | a = new int[3]; moves only the parameter. The caller doesn't see it. |
| String | Immutable, so it can't be mutated; s = s + "!" is a reassignment. The caller never sees a change. |
| StringBuilder | Mutable: sb.append("!") inside a method is visible to the caller (Lesson 4.6). |
| To give back a new value | return it, and let the caller assign it: x = f(x); |
Exam tip: the one-line answer is "Java is always pass-by-value; for objects, the reference is passed by value." If asked to prove it, use the swap example: a swap(Integer a, Integer b) or swap(int[] a, int[] b) that reassigns its parameters can't swap the caller's variables.
Three things a method can do with an argument
Change a primitive copy
n = n + 5;
Only the method's copy changes. The caller is unaffected.
Mutate a shared object
a[0] = 99;
Both references reach the same object, so the caller sees the change.
Reassign the parameter
a = new int[]{-1};
The method's reference now points elsewhere. The caller still has the old object.
public class Main {
static void change(int n, int[] a) {
n = 50; // changes the copy only
a[0] = 99; // changes the shared array
a = new int[]{-1}; // moves the copy of the reference only
}
public static void main(String[] args) {
int x = 10;
int[] arr = {10};
change(x, arr);
System.out.println("x = " + x + ", arr[0] = " + arr[0]);
}
}Pass-by-value vs pass-by-reference
| Java (pass-by-value) | True pass-by-reference (e.g. C++ int&) | |
|---|---|---|
| What the parameter is | A new variable holding a copy | Another name for the caller's variable |
swap(a, b) on two ints | Can't swap the caller's variables | Swaps them |
| Mutating an object | Visible (shared object) | Visible |
| Assigning a new object to the parameter | Not visible to the caller | Replaces the caller's object |
| How to send a new value back | return it | Assign to the parameter |
Common misconception: "objects are passed by reference in Java". Not quite: the reference is passed by value. If objects were truly passed by reference, a = new int[]{-1}; inside the method would replace the caller's array too. It doesn't.
What happens to each argument during a call
The caller's variables
In main's frame,int x = 10; int[] arr = {10};xholds the number 10 directly.arrholds a reference; the array itself is an object on the heap (Lesson 4.1).The call copies both values
A new frame is pushed.change(x, arr);ngets a copy of 10.agets a copy of the reference, so it points to the same array asarr. No array is copied.Changing the primitive copy
Only the slot in change's frame is overwritten. main'sn = 50;xlives in a different frame and stays 10.Mutating through the reference
The JVM follows the reference ina[0] = 99;ato the heap and writes into the array. Sincearrpoints to that same array, main will see 99.Reassigning the parameter, then returning
A new array is created and onlya = new int[]{-1};ais pointed at it. When the method returns, its frame is popped, nothing refers to the new array any more, and the garbage collector can reclaim it.arrwas never touched.
Copies in, shared object changed: both views
Left: each line and its effect. Right: the two frames on the stack and the arrays on the heap.
change(x, arr) 6 steps- Setup:
xis 10 andarrrefers to an array holding 10. - Call: the method gets
n, a copy of 10, anda, a copy of the reference. - Primitive copy: setting
nto 50 has no effect outside the method. - Mutation: writing
a[0] = 99changes the array thatarralso refers to. - Reassignment:
anow points to a brand-new array.arrdoesn't move. - Result: the real output is
x = 10, arr[0] = 99.
- main's frame holds
x(the value 10) andarr(a reference to array #1 on the heap). - The call pushes change's frame.
nreceives 10;areceives the same reference asarr. Two arrows, one array: nothing on the heap was copied. n = 50changes a slot in change's frame. main'sxis in a different frame and stays 10.a[0] = 99follows the arrow and writes into #1. main'sarrleads to the same place, so main will see 99.a = new int[]{-1}creates #2 and moves onlya's arrow.arr's arrow is in main's frame, out of the method's reach.- On return the frame is popped. #2 has no references left and will be garbage-collected; #1 keeps the 99.
Pass-by-value in real systems
Knowing exactly what a method can and can't change is how developers avoid bugs where data changes "by itself", or doesn't change when it should.
Watch it with an example: three ways a discount method can go wrong (or right)
Java always passes a copy. For an int that's a copy of the number; for an object it's a copy of the reference. Watch what each method can and can't change.
- Setup:
priceholds 500 directly.cartholds a reference to Cart object #1 on the heap. - cut(price): the method gets its own copy
p= 500 and changes it to 450. Only the copy changes. - Back in main:
priceis still 500. A method can never change the caller's primitive variable. - cut(cart): the copy
cholds the same reference, soc.total -= 50changes object #1. main sees total 450. - reset(cart):
c = new Cart(0)points the copy at a new object #2. main'scartis untouched. - Rule: Java is always pass-by-value. Through a reference copy you can change the object, but you can't make the caller's variable point somewhere else. That's why library methods like
Collections.sort(list)work in place.
1. In-place library methods
Arrays.sort(a) and Arrays.fill(a, 0) return nothing. They work because they receive a reference to your array and rearrange or overwrite its elements directly.
2. Defensive copies
A banking class that receives an array of interest rates copies it before storing, and returns copies from getters. Otherwise any caller holding the same reference could change the bank's internal data (see the scenario in Lesson 4.1).
3. Returning new values
Methods that calculate (tax, discount, formatted text) take inputs and return a result, leaving their inputs untouched. Callers write total = addTax(total);. This style is easier to test and safe to reuse.
4. Why Strings are safe to pass around
Because a String can't be mutated, passing a customer's name or an account number to any method is safe: no method can change the caller's String. This is one reason Strings are used for IDs and keys.
Examples are simplified. The same rule applies to every object you'll create in Module 6: a method receives a copy of the reference, never a copy of the object.
Riya's seat swap: the failing and the working version
import java.util.Arrays;
public class Main {
static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("Inside swap: a = " + a + ", b = " + b);
}
static void swapItems(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int x = 1, y = 2;
swap(x, y);
System.out.println("After swap: x = " + x + ", y = " + y);
int[] seats = {1, 2};
swapItems(seats, 0, 1);
System.out.println("After swapItems: " + Arrays.toString(seats));
}
}swap(int a, int b)a and b are copies of x and y. Swapping copies leaves the originals as they were."Inside swap: a = 2, b = 1"Proof that the swap itself works; it just happens in the wrong frame.swapItems(int[] arr, int i, int j)Receives a copy of the reference, so it changes the one array that main also uses.[2, 1]The working way to "swap" for a caller: keep the values inside an object both sides share.
Mutate, then reassign
Watch the arrows: bal and a start at the same array (#1). One line changes #1, the next moves only a.
Worked examples: real problems, solved and traced in memory
Three bugs and fixes that come straight from pass-by-value. Run each program, then step through it and compare the variables in each frame with the objects (#n) on the heap: copies of values on the left, shared objects on the right.
The interest that never reached the balance
Problem statement
A developer wrote addInterest(balance) to add 5% to a balance, but the balance printed afterwards is unchanged. Show the bug, then fix it by returning the new value.
balance = 20000.0, rate = 5%
After addInterest: 20000.0 After withInterest: 21000.0
Approach
- Call the buggy void method and watch its parameter change while main's variable stays the same.
- Write a version that returns the new balance.
- Assign the returned value back in main:
balance = withInterest(balance);.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
A method that discounts the caller's array
Problem statement
During a sale, every price in the shop's price array must drop by 10%. Write applySale(int[] prices) that changes the array itself, and print the prices afterwards.
prices = {200, 550, 90}[180, 495, 81]
Approach
- The method receives a copy of the reference, so it points to the same array object as main.
- Loop with an index and overwrite each element:
prices[i] = prices[i] * 90 / 100. - No return is needed: main's variable still refers to the same, now-changed, array.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Reassigning a parameter vs changing the object
Problem statement
An HR tool keeps employee codes in a String[]. One method tries to "replace" the whole array; another updates one element. Predict which change main will see.
codes = {"E01", "E02"}[M01, E02]
Approach
replaceAllassigns a brand-new array to its parameter: only its own copy of the reference moves.renameFirstwrites into the existing array through the reference: the shared object changes.- Compare the heap: which object does main's
codespoint to at the end?
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 5.3 in one screen
- Java is always pass-by-value: every argument is copied into a new parameter.
- Primitive argument → the number is copied; the caller never sees changes.
- Object/array argument → the reference is copied; the object is shared, not copied.
- Mutating the object (
a[0] = 5) is visible to the caller. - Reassigning the parameter (
a = new …) is not. - Strings are immutable, so a method can never change the caller's String.
- To hand back a new value,
returnit and assign:x = f(x);
Interview and viva questions
Is Java pass-by-value or pass-by-reference?
Always pass-by-value. For primitives, the value is the data. For objects and arrays, the value is a reference, which is copied. Because both copies point to the same object, mutations are visible, but reassigning the parameter never changes the caller's variable, which proves it isn't pass-by-reference.
How would you write a method that swaps two ints for the caller?
You can't with two int parameters. Options: pass an array and two indexes and swap the elements, return the swapped pair in an array, or keep the values in an object and swap its fields. In practice, code is written so a swap isn't needed across a method boundary.
If a method receives a String and calls s.toUpperCase(), why doesn't the caller's String change?
toUpperCase() returns a new String; it never modifies the original, because Strings are immutable. Even s = s.toUpperCase() only reassigns the method's own copy of the reference. Return the new String to the caller instead.
Does passing a large array to a method copy the whole array?
No. Only the reference is copied, which is a few bytes whatever the array's size. That's why passing big arrays or objects to methods is cheap, and also why a method can accidentally modify them.
What is a side effect, and why should methods document them?
A side effect is any change a method makes beyond returning a value, such as modifying an array it received. Callers can't see side effects from the call alone, so a method like sortInPlace or applySaleTo should make it obvious in its name and documentation. Methods without side effects are easier to test and reuse.
Practice problems
For each problem, ask one question before coding: does the method need to change the caller's data (then it must change an object, or return a new value), or only read it? The last three are real business tasks.
Fix the bonus that never arrives
The method static void addGrace(int marks) { marks = marks + 5; } is supposed to add 5 grace marks, but main still prints 70. Rewrite it so that main gets 75, without using arrays.
marks = 70
Marks with grace: 75
Show solution and explanation
public class Main {
static int addGrace(int marks) {
return marks + 5;
}
public static void main(String[] args) {
int marks = 70;
marks = addGrace(marks);
System.out.println("Marks with grace: " + marks);
}
}How it works: a primitive argument is copied into the parameter, so changing the parameter can never reach main. The fix is to return the new value and let main assign it: marks = addGrace(marks);.
Add 10 to every element
Write void addToAll(int[] a, int amount) that increases every element of the caller's array.
scores = {40, 55, 72}, amount = 10[50, 65, 82]
Show solution and explanation
import java.util.Arrays;
public class Main {
static void addToAll(int[] a, int amount) {
for (int i = 0; i < a.length; i++) {
a[i] += amount;
}
}
public static void main(String[] args) {
int[] scores = {40, 55, 72};
addToAll(scores, 10);
System.out.println(Arrays.toString(scores));
}
}How it works: a receives a copy of the reference, so it points to the same array as scores. Writing a[i] changes that shared array, and main sees it. No return value is needed.
Swap two elements
A method can't swap two int variables of its caller, but it can swap two elements of an array. Write swapItems(int[] a, int i, int j).
queue = {101, 102, 103, 104}, swap positions 0 and 3[104, 102, 103, 101]
Show solution and explanation
import java.util.Arrays;
public class Main {
static void swapItems(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void main(String[] args) {
int[] queue = {101, 102, 103, 104};
swapItems(queue, 0, 3);
System.out.println(Arrays.toString(queue));
}
}How it works: the elements live inside one array object on the heap. The method changes that object through its copy of the reference, so the swap is visible in main. This is exactly how sorting methods like Arrays.sort rearrange your array.
Add a country code to a phone number
Write a method that adds +91 in front of a phone number. Remember that a String can't be changed, so return the new String.
phone = "9876543210"
9876543210 +91 9876543210
Show solution and explanation
public class Main {
static String withCountryCode(String phone) {
return "+91 " + phone;
}
public static void main(String[] args) {
String phone = "9876543210";
String full = withCountryCode(phone);
System.out.println(phone);
System.out.println(full);
}
}How it works: Strings are immutable (Lesson 4.3), so a method can't change the caller's String even though it receives a reference to it. It builds a new String and returns it. The original phone is untouched.
Reverse an array in place
Write void reverse(int[] a) that reverses the caller's array itself, without creating a second array.
a = {1, 2, 3, 4, 5}[5, 4, 3, 2, 1]
Show solution and explanation
import java.util.Arrays;
public class Main {
static void reverse(int[] a) {
for (int i = 0, j = a.length - 1; i < j; i++, j--) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
public static void main(String[] args) {
int[] a = {1, 2, 3, 4, 5};
reverse(a);
System.out.println(Arrays.toString(a));
}
}How it works: two indexes walk inward, swapping as they go. Because the method only mutates the shared array (it never assigns a = …), the caller sees the reversed order.
Adding an item needs a returned array
Arrays can't grow. Write int[] append(int[] a, int value) that returns a new array one element longer. Show why assigning inside a void method wouldn't work.
ids = {7, 8}, append 9After appendWrong: [7, 8] After append: [7, 8, 9]
Show solution and explanation
import java.util.Arrays;
public class Main {
static void appendWrong(int[] a, int value) {
a = Arrays.copyOf(a, a.length + 1);
a[a.length - 1] = value;
}
static int[] append(int[] a, int value) {
int[] bigger = Arrays.copyOf(a, a.length + 1);
bigger[bigger.length - 1] = value;
return bigger;
}
public static void main(String[] args) {
int[] ids = {7, 8};
appendWrong(ids, 9);
System.out.println("After appendWrong: " + Arrays.toString(ids));
ids = append(ids, 9);
System.out.println("After append: " + Arrays.toString(ids));
}
}How it works: appendWrong builds the bigger array but only points its own copy of the reference at it; main's ids still refers to the old array. append returns the new array, and main reassigns ids. This is the pattern behind every "grow" operation.
Scale marks out of 100
Test marks were out of 40. Write void scaleTo100(double[] marks, double outOf) that converts every mark in place.
marks = {30, 36, 22}, out of 40[75.0, 90.0, 55.0]
Show solution and explanation
import java.util.Arrays;
public class Main {
static void scaleTo100(double[] marks, double outOf) {
for (int i = 0; i < marks.length; i++) {
marks[i] = marks[i] * 100 / outOf;
}
}
public static void main(String[] args) {
double[] marks = {30, 36, 22};
scaleTo100(marks, 40);
System.out.println(Arrays.toString(marks));
}
}How it works: the array is changed in place. The parameter outOf is a primitive copy, so the method could even change it without affecting main. Mutating the caller's data is powerful but surprising, so the method name should make it clear.
Build text through a StringBuilder parameter
Write void addLine(StringBuilder sb, String text) that appends a numbered line. Compare it with a String parameter, which can't be changed.
lines: "Rice", "Dal"
SHOPPING - Rice - Dal SHOPPING
Show solution and explanation
public class Main {
static void addLine(StringBuilder sb, String text) {
sb.append("- ").append(text).append('\n');
}
static void addLineToString(String s, String text) {
s = s + "- " + text + "\n";
}
public static void main(String[] args) {
StringBuilder list = new StringBuilder("SHOPPING\n");
addLine(list, "Rice");
addLine(list, "Dal");
System.out.print(list);
String plain = "SHOPPING\n";
addLineToString(plain, "Rice");
System.out.print(plain);
}
}How it works: both methods receive a copy of a reference. append changes the StringBuilder object itself, which main also refers to. With a String, s + … creates a new object and only the method's local s points to it, so main still prints just the heading.
Apply GST to a cart in place
A cart holds item prices before tax. Write void addGst(double[] prices, double percent) that converts every price to its tax-inclusive amount, then print the cart and its total.
prices = {1000, 250, 80}, GST = 18%With GST: [1180.0, 295.0, 94.4] Total: Rs 1569.40
Show solution and explanation
import java.util.Arrays;
public class Main {
static void addGst(double[] prices, double percent) {
for (int i = 0; i < prices.length; i++) {
prices[i] = prices[i] + prices[i] * percent / 100;
}
}
static double total(double[] prices) {
double sum = 0;
for (double p : prices) {
sum += p;
}
return sum;
}
public static void main(String[] args) {
double[] cart = {1000, 250, 80};
addGst(cart, 18);
System.out.println("With GST: " + Arrays.toString(cart));
System.out.printf("Total: Rs %.2f%n", total(cart));
}
}How it works:
addGstmutates the shared array, so it needs no return value.totalonly reads the array, and returns a number.- Calling
addGsttwice would add tax twice, because the first call already changed the data. In-place updates must be done exactly once.
Deposit into an account by index
Account balances are kept in an array. Write boolean deposit(long[] balances, int account, long amount) that adds the amount and returns true, or returns false (changing nothing) for an invalid account number or amount.
balances = {15000, 2300, 98000}
deposit 5000 into account 1, then -10 into account 2, then 100 into account 7true false false [15000, 7300, 98000]
Show solution and explanation
import java.util.Arrays;
public class Main {
static boolean deposit(long[] balances, int account, long amount) {
if (account < 0 || account >= balances.length) {
return false;
}
if (amount <= 0) {
return false;
}
balances[account] += amount;
return true;
}
public static void main(String[] args) {
long[] balances = {15000, 2300, 98000};
System.out.println(deposit(balances, 1, 5000));
System.out.println(deposit(balances, 2, -10));
System.out.println(deposit(balances, 7, 100));
System.out.println(Arrays.toString(balances));
}
}How it works:
- The method both changes the shared array and returns a success flag, two separate channels back to the caller.
- Guard clauses run before any change, so a failed deposit leaves the data exactly as it was.
- Only account 1 changed: 2300 + 5000 = 7300.
Price revision without touching the original list
A shop wants to preview a 7% price increase without changing the current price list. Write int[] revised(int[] prices, int percent) that returns a new array and leaves the original untouched.
prices = {120, 450, 999}, increase = 7%Current: [120, 450, 999] Preview: [128, 482, 1069]
Show solution and explanation
import java.util.Arrays;
public class Main {
static int[] revised(int[] prices, int percent) {
int[] result = new int[prices.length];
for (int i = 0; i < prices.length; i++) {
result[i] = (int) Math.round(prices[i] * (100 + percent) / 100.0);
}
return result;
}
public static void main(String[] args) {
int[] current = {120, 450, 999};
int[] preview = revised(current, 7);
System.out.println("Current: " + Arrays.toString(current));
System.out.println("Preview: " + Arrays.toString(preview));
}
}How it works:
- Because the method could change the caller's array, choosing not to is a design decision: it writes into a fresh array instead.
- The caller keeps both versions, so the manager can compare before approving.
Math.roundreturns a long, so it is cast to int: 999 × 1.07 = 1068.93 rounds to 1069.
Real-world scenario questions (asked in company interviews)
Pass-by-value bugs show up as data that changes when it shouldn't, or doesn't change when it should. Decide your answer first, then open the model answer.
The monthly charge that never got applied
A method void applyCharge(double balance, double charge) { balance = balance - charge; } is called for every account at month end. Testers report that balances never go down. Explain the bug and fix it.
The interviewer is checking: do you know that primitive parameters are copies?
Show a strong answer
balanceis a copy of the caller's double. Subtracting changes only the copy, which disappears when the method returns.- Fix: return the new value,
static double afterCharge(double balance, double charge), and assign it:balance = afterCharge(balance, charge);. - In a real system the balance belongs to an account object or a database row, and the method would update that (Module 6).
- Add a test that checks the balance after the call, not just that the method runs.
Avoid: "Java passes by reference, so it should have worked." That's the misconception causing the bug.
The report that changed the live prices
A "festival preview" report calls applyDiscount(prices, 20) to show what prices would look like after a 20% discount. After the report runs, the live website shows discounted prices too. Why, and how should it be designed?
The interviewer is checking: do you understand shared references and when a method should not mutate its input?
Show a strong answer
- The method received a copy of the reference to the live price array and mutated it. The report and the website share one array, so both changed.
- For a preview, use a method without side effects:
int[] discounted(int[] prices, int pct)that writes into a new array and returns it. - Or copy first:
applyDiscount(Arrays.copyOf(prices, prices.length), 20). - Name in-place methods clearly (e.g.
applyDiscountInPlace) so callers know they mutate.
Avoid: fixing it by "resetting" prices after the report. Another request could read them in between.
NullPointerException after "loading" the data
Code declares int[] routes = null;, calls loadRoutes(routes); (which does routes = new int[]{…}; inside), and then crashes with a NullPointerException on routes[0]. The developer insists the method creates the array. What's happening?
The interviewer is checking: can you spot a reassigned parameter that the caller never sees?
Show a strong answer
- The method does create an array, but assigns it to its own parameter, a copy of the caller's null reference. The caller's
routesis still null. - Fix: make the method return the array,
static int[] loadRoutes(), and writeroutes = loadRoutes();. - The stack trace points to the line in the caller, which is a hint that the caller's variable is the null one.
Avoid: wrapping the access in a null check. The data still never arrives.
Should calculation methods modify their inputs?
Your team is designing a library of loan calculations (EMI schedules, prepayment effects) used by many services. One developer proposes methods that take a schedule array and modify it in place "for performance". What guidelines would you set?
The interviewer is checking: can you reason about side effects, shared references and API safety?
Show a strong answer
- Default to methods without side effects: take inputs, return a new result. Callers can't be surprised, results are easy to test, and the same input can be reused safely, even from several threads.
- The cost of creating a new array of a few hundred instalments is tiny compared with the cost of a wrong loan schedule. Measure before optimising.
- If an in-place version is truly needed, make it a separate, clearly named method and document the mutation.
- Prefer immutable result types (records, unmodifiable lists) so later code can't change them either.
Avoid: mutating inputs by default. Shared references turn one service's change into another service's bug.