6.2 Argument Passing: Pass-by-Value Mechanics
Key Takeaways
- Java strictly and unconditionally evaluates all method arguments using pass-by-value; there is no pass-by-reference mechanism anywhere in the Java language.
- When passing primitive data types, a duplicate copy of the literal bit pattern is passed; modifying the parameter inside the callee method has zero effect on the caller's variable.
- When passing object reference variables, a copy of the memory address (reference) is passed by value; both caller and callee point to the exact same object instance on the heap.
- Invoking mutating methods or modifying fields on a passed object reference alters the shared heap object, whereas reassigning the parameter variable itself affects only the callee's local stack frame.
- Instances of String and primitive wrapper classes are strictly immutable; methods operating on them cannot alter their heap state, and parameter reassignment has zero effect on the caller.
6.2 Argument Passing: Pass-by-Value Mechanics
[!NOTE] Exam Focus: Pass-by-value semantics is one of the most heavily tested and commonly misunderstood concepts on the Oracle Certified Foundations Associate, Java (1Z0-811) examination. Certification questions routinely present intricate code snippets involving primitive variables, mutable objects (
StringBuilder, arrays, custom classes), and immutable objects (String, wrapper classes) passed into methods to determine whether candidates can accurately track variable state across method boundaries.
A foundational architectural principle of the Java Virtual Machine is its evaluation model for method arguments: Java is strictly and unconditionally pass-by-value. There is no pass-by-reference in Java. Regardless of whether an argument is a primitive 32-bit int or an object reference like ArrayList<String>, Java always passes a copy of the value stored inside the variable.
However, widespread confusion arises because the "value" of an object variable is the memory reference (address) pointing to the object on the Java Heap. Mastering how this evaluation model operates across Stack frames and Heap storage is the key to solving every parameter-passing question on the examination.
JVM Memory Architecture: Stack vs. Heap
To understand parameter passing, one must understand how the JVM partitions runtime data:
- The JVM Call Stack: Each executing thread maintains its own call stack. Whenever a method is invoked, a new stack frame is pushed onto the stack. The stack frame stores the method's local variables, formal parameter variables, and intermediate calculation operands. When the method completes, its stack frame is popped off and discarded, instantly destroying all local variables contained within it.
- The Java Heap: The heap is the global, shared memory region where all objects and array instances reside. Objects are dynamically allocated on the heap using the
newoperator and persist until they become unreachable and are reclaimed by the Garbage Collector.
Stack Frame (Caller) Stack Frame (Callee) Java Heap Memory
┌──────────────────┐ ┌──────────────────┐ ┌─────────────────────────┐
│ int a = 50 │ │ int x = 50 (copy)│ │ (Heap Objects Shared) │
│ Ref b = 0x10A4 │─────────┼─> Ref p = 0x10A4 ┼─────────>│ Object @ 0x10A4 │
└──────────────────┘ └──────────────────┘ └─────────────────────────┘
Passing Primitive Data Types by Value
When a primitive variable (byte, short, int, long, float, double, char, boolean) is passed as an argument, a copy of its literal bit pattern is created and assigned to the method's formal parameter.
Execution Lifecycle of Primitive Passing
- The caller variable holds a concrete primitive value in its stack frame.
- Upon invocation, a new stack frame is pushed for the callee, and the parameter variable is initialized with a copy of the primitive value.
- Any arithmetic operation, modification, or reassignment applied to the parameter inside the callee operates exclusively within the callee's stack frame.
- When the callee returns, its stack frame is destroyed. The caller's variable in the caller frame remains entirely unchanged.
public class PrimitivePassingDemo {
public static void modifyPrimitive(int value) {
value = value + 100; // Modifies only the local parameter 'value'
System.out.println("Inside modifyPrimitive: " + value); // Prints 150
}
public static void main(String[] args) {
int original = 50;
System.out.println("Before call: " + original); // Prints 50
modifyPrimitive(original); // Passes a bit-level copy of 50
System.out.println("After call: " + original); // Still prints 50!
}
}
Even though the parameter variable value was modified to 150 inside modifyPrimitive(), the caller's variable original retains its initial value 50. There is no mechanism in Java for a callee to modify the caller's primitive variable.
Passing Object Reference Variables by Value
In Java, variables declared with a class, interface, or array type do not store the object itself. Instead, they hold a reference (a memory pointer address) that indicates where the object resides on the heap.
When an object reference variable is passed to a method, Java still applies pass-by-value: the reference address itself is copied by value.
What Occurs in Memory During Object Passing
- The caller holds a reference variable containing an address (e.g.,
0x4A20) pointing to a heap object. - When invoking a method, the address
0x4A20is copied into the callee's parameter variable in the callee's new stack frame. - Both the caller's variable and the callee's parameter now contain copies of the same address. Both point to the exact same underlying object on the heap!
This leads to two fundamentally different outcomes depending on what actions the callee takes:
- Mutating the Object's State: Modifying the object's instance fields or invoking state-altering methods modifies the single shared object on the heap. The caller will see these changes.
- Reassigning the Parameter Variable: Assigning a new object or
nullto the parameter variable modifies only the callee's local reference pointer. The caller's reference variable is completely unaffected.
Mutating Object State vs. Reassigning Reference Parameters
Distinguishing between state mutation and reference reassignment is the single most critical parameter-passing skill evaluated on the 1Z0-811 examination.
Scenario 1: Mutating Object State (Affects Caller)
class Account {
int balance;
Account(int b) { this.balance = b; }
}
public class MutationDemo {
public static void creditBonus(Account acc) {
acc.balance += 250; // Mutates the state of the shared object on the Heap!
}
public static void main(String[] args) {
Account primary = new Account(1000);
creditBonus(primary);
System.out.println("Primary balance: " + primary.balance); // Prints 1250
}
}
Analysis: In this scenario, both primary (in main) and acc (in creditBonus) hold the exact same heap memory address. Executing acc.balance += 250 modifies the balance field of that heap object. When creditBonus() returns, primary.balance reflects the updated balance 1250.
Scenario 2: Reassigning the Parameter Variable (Zero Effect on Caller)
public class ReassignmentDemo {
public static void resetAccount(Account acc) {
// TRAP: Instantiating a new object and reassigning the local parameter
acc = new Account(0);
acc.balance = 500; // Mutates ONLY the new object @ 0x9B11!
}
public static void main(String[] args) {
Account primary = new Account(1000); // Resides @ 0x4A20
resetAccount(primary);
System.out.println("Primary balance: " + primary.balance); // Still prints 1000!
}
}
Analysis: Inside resetAccount(), the expression acc = new Account(0) creates a second, distinct Account object at address 0x9B11 on the heap and updates the local parameter acc to hold 0x9B11. The caller's variable primary in main() continues holding 0x4A20. Modifying acc.balance or setting acc = null has zero impact on primary.
The Classic "Swap" Method Fallacy
In programming languages supporting true pass-by-reference (such as C++ with & references), a function can swap two caller variables. In Java, writing a method to swap two variables is completely impossible:
public class SwapFallacy {
public static void swap(Integer first, Integer second) {
Integer temp = first;
first = second;
second = temp;
// Only local parameter variables in swap()'s stack frame were swapped!
}
public static void main(String[] args) {
Integer num1 = 10;
Integer num2 = 20;
swap(num1, num2);
System.out.println("num1=" + num1 + ", num2=" + num2); // Prints: num1=10, num2=20
}
}
Because swap() merely exchanges the pointer addresses stored in its local stack parameter slots first and second, the caller variables num1 and num2 in main() remain entirely unchanged.
The Immutable Objects Trap: String and Wrapper Classes
A favorite exam scenario involves passing instances of String or primitive wrapper classes (Integer, Double, Boolean, etc.) into a method. Novice programmers often mistakenly expect them to behave like mutable objects because they are reference types.
However, String and all standard primitive wrapper classes are strictly immutable. Once instantiated on the heap, their internal state can never be modified!
public class StringTrapDemo {
public static void updateString(String text) {
text.concat(" World"); // Creates a new String "Hello World", but return value is discarded!
text = text + "!"; // Creates a new String "Hello!" and reassigns local parameter 'text'
}
public static void main(String[] args) {
String greeting = "Hello";
updateString(greeting);
System.out.println(greeting); // Prints "Hello", NOT "Hello World" or "Hello!"
}
}
In contrast, StringBuilder is a mutable character sequence:
public class StringBuilderDemo {
public static void updateBuilder(StringBuilder sb) {
sb.append(" World"); // Mutates the internal buffer of the shared heap object directly!
}
public static void main(String[] args) {
StringBuilder greeting = new StringBuilder("Hello");
updateBuilder(greeting);
System.out.println(greeting); // Prints "Hello World"!
}
}
Passing Arrays to Methods
In Java, arrays are first-class objects. Consequently, passing an array to a method follows the exact same object reference mechanics:
public class ArrayPassingDemo {
public static void modifyArray(int[] numbers) {
numbers[0] = 999; // Mutates shared heap array element (Affects caller!)
numbers = new int[]{ 1, 2 }; // Reassigns local parameter pointer (Zero caller effect!)
}
public static void main(String[] args) {
int[] values = { 10, 20, 30 };
modifyArray(values);
System.out.println(values[0]); // Prints 999!
System.out.println(values.length); // Still prints 3!
}
}
Summary Comparison Table: Evaluation Models
| Technical Dimension | Primitive Types (int, double, etc.) | Mutable Reference Types (StringBuilder, custom objects, arrays) | Immutable Reference Types (String, Integer, etc.) |
|---|---|---|---|
| What value is passed? | Copy of literal primitive bits | Copy of heap memory address | Copy of heap memory address |
| Storage location | Stack frame only | Reference on Stack; Object on Heap | Reference on Stack; Object on Heap |
| Effect of field / element mutation | Not applicable (no fields) | Mutates shared heap object; caller observes change | Not applicable (all fields are immutable/private) |
| Effect of calling transforming method | Not applicable | Mutates object (e.g., append()) | Returns new object; original object unchanged |
Effect of reassigning parameter (p = ...) | Modifies local stack slot only; zero caller effect | Modifies local stack slot only; zero caller effect | Modifies local stack slot only; zero caller effect |
Effect of setting parameter to null | Compile error (cannot assign null to primitive) | Clears local stack slot only; zero caller effect | Clears local stack slot only; zero caller effect |
What is the output of executing the following Java program?
public class PrimitivePassTest {
public static void calculate(int value) {
value = value * 2;
value += 5;
}
public static void main(String[] args) {
int count = 5;
calculate(count);
System.out.println(count);
}
}
Consider the following Java program:
What is printed to the console upon execution?class Container {
int capacity;
Container(int c) { this.capacity = c; }
}
public class ContainerTest {
public static void modify(Container c1, Container c2) {
c1.capacity = 50;
c2 = new Container(100);
}
public static void main(String[] args) {
Container boxA = new Container(10);
Container boxB = new Container(20);
modify(boxA, boxB);
System.out.println(boxA.capacity + " " + boxB.capacity);
}
}
What is the output of executing the following Java program?
public class StringMutateTest {
public static void transform(String s, StringBuilder sb) {
s.concat(" World");
sb.append(" World");
}
public static void main(String[] args) {
String str = "Hello";
StringBuilder bld = new StringBuilder("Hello");
transform(str, bld);
System.out.println(str + " | " + bld);
}
}