Method Parameters & Arguments
Java is always pass-by-value — parameters and arguments follow one consistent rule whether they're primitives or object references, and understanding exactly what gets copied prevents a whole category of confusion.
Parameters are the variables a method declares to receive input; arguments are the actual values supplied at a specific call site. Java passes every argument — primitive or reference — strictly by value: the method receives a copy of whatever value was passed, never the original variable itself. What that copied value actually is (a primitive, or a reference) is the key to understanding Java's parameter-passing behavior correctly.
Formal Parameters vs Actual Arguments
Formal parameters are the names and types declared in a method's header. Actual arguments are the specific values supplied when the method is called. The formal parameter name has no relationship to the argument expression's name — Java matches arguments to parameters purely by position and type.
static int square(int n) { // 'n' is the formal parameter
return n * n;
}
int x = 7;
int result = square(x); // 'x' is the actual argument here
int result2 = square(20); // the argument doesn't even need to be a variableJava Is Always Pass-by-Value
This is a strict, unconditional rule in Java: every argument, no matter its type, is passed by value — the method parameter receives a copy of the value being passed, and that copy is entirely independent of the original variable used at the call site. There is no pass-by-reference in Java, ever.
This holds equally for primitives and for object references. What differs between the two is what the 'value' actually is: for a primitive, it's the primitive data itself; for a reference type, it's the reference value (essentially an object's address/handle), not the object it points to.
Primitive Argument Behavior
When a primitive is passed, the method gets its own independent copy of that value. Reassigning or modifying the parameter inside the method has no effect whatsoever on the caller's original variable.
static void tryToDouble(int n) {
n = n * 2; // only changes this method's local copy of n
}
int value = 5;
tryToDouble(value);
System.out.println(value); // still 5 - the caller's variable is untouchedReference-Value Argument Behavior
When a reference type (an object) is passed, the value copied into the parameter is the reference itself — essentially a copy of the pointer to the object, not the object. Both the caller's variable and the method's parameter now point to the exact same object, so calling mutating methods on that object through the parameter is visible to the caller too, since there's only ever one object.
This is still pass-by-value: what's copied is the reference value (the pointer), not the object itself. If it were pass-by-reference, the caller's own variable slot would be modifiable from inside the method — which brings us to reassignment, below.
static void addItem(StringBuilder sb) {
sb.append(" world"); // mutates the SAME object the caller's variable points to
}
StringBuilder message = new StringBuilder("hello");
addItem(message);
System.out.println(message); // "hello world" - the shared object was mutatedMutation vs Reassignment: The Crucial Distinction
Mutating an object through a parameter (calling a method that changes its internal state) IS visible to the caller, because caller and method share the same object. Reassigning the parameter variable itself to point to a different object is NOT visible to the caller, because that only changes the method's local copy of the reference — the caller's original variable still points to the original object.
static void mutate(StringBuilder sb) {
sb.append("!"); // MUTATION: visible to caller - same object
}
static void reassign(StringBuilder sb) {
sb = new StringBuilder("replaced"); // REASSIGNMENT: only this local copy changes
}
StringBuilder a = new StringBuilder("hi");
mutate(a);
System.out.println(a); // "hi!" - mutation was visible
StringBuilder b = new StringBuilder("hi");
reassign(b);
System.out.println(b); // still "hi" - reassignment inside the method had no outside effectArrays Are References Too
Arrays are objects in Java, so they follow the same reference-value passing rule: a method receives a copy of the array reference, meaning it shares the exact same underlying array as the caller. Modifying array elements through that parameter is visible to the caller; reassigning the parameter to point to a different array is not.
static void zeroOutFirst(int[] arr) {
arr[0] = 0; // mutation: visible to caller - same array
}
static void replaceArray(int[] arr) {
arr = new int[]{99, 99, 99}; // reassignment: only the local copy points elsewhere
}
int[] data = {1, 2, 3};
zeroOutFirst(data);
System.out.println(data[0]); // 0 - the caller's array was mutated
replaceArray(data);
System.out.println(data[0]); // still 0 - reassignment inside the method didn't affect the callerWhy This Is Still "Pass-by-Value", Not "Pass-by-Reference"
A common misconception is calling Java's object-passing behavior "pass-by-reference" because objects can be mutated through a parameter. That's not what pass-by-reference means. In a true pass-by-reference language, reassigning the parameter itself would also change the caller's variable — Java never does that, for any type. What's passed is always a value; for objects, that value just happens to be a reference.
Pass-by-value (what Java does, always):
caller's variable --copy of value--> parameter
reassigning the parameter never affects the caller's variable
Pass-by-reference (what Java does NOT do):
caller's variable and parameter would be aliases for the SAME storage slot
reassigning the parameter WOULD change what the caller's variable holdsString Is a Special, Illustrative Case
Because String is immutable, there are no mutating methods to call on it — every String method that looks like it modifies a string actually returns a brand-new String object. This makes String arguments look like they follow pass-by-value entirely (no visible "mutation" ever happens), even though under the hood it's exactly the same reference-passing mechanism as any other object.
static void tryToModify(String s) {
s = s.toUpperCase(); // creates a NEW String and reassigns the local parameter
s = s + "!"; // creates ANOTHER new String
}
String name = "ada";
tryToModify(name);
System.out.println(name); // still "ada" - reassignment never affects the caller,
// and String has no mutating methods to begin withPractical Usage
Understanding this rule tells you when to rely on a method's return value versus when to rely on mutation for output: a method can't hand back a modified primitive by reassigning its parameter, so primitives that need to change must be returned; a mutable object's state changes via its parameter are naturally visible to the caller without needing a return at all.
// Primitive: must return the new value - reassigning the parameter is useless
static int increment(int n) {
return n + 1;
}
int x = 5;
x = increment(x); // must capture the returned value
// Mutable object: mutating via the parameter is sufficient - no return needed
static void addPoints(List<Integer> scores, int points) {
scores.add(points); // visible to the caller: same List object
}Edge Cases
Passing null as a reference-type argument is legal (assuming the parameter type allows it); the parameter simply holds a copy of the null reference, and any attempt to call a method on it inside the method body throws NullPointerException, exactly as it would at the call site.
Autoboxing means a primitive int argument passed to a method expecting Integer gets boxed into a new wrapper object at the call — from that point on, the parameter is a reference like any other object reference, following the same mutation-vs-reassignment rules (though Integer itself is immutable, so this mostly matters for identity/equality reasoning, not mutation).
Complete Example
The List parameter demonstrates visible mutation through a shared reference; the int parameter demonstrates that primitive changes require an explicit return to reach the caller.
import java.util.ArrayList;
import java.util.List;
public class ParameterDemo {
static void addToList(List<String> list, String item) {
list.add(item); // mutation - visible to caller
}
static int incrementCounter(int counter) {
return counter + 1; // primitives must be returned to communicate change
}
public static void main(String[] args) {
List<String> names = new ArrayList<>();
addToList(names, "Ada");
addToList(names, "Grace");
System.out.println(names); // [Ada, Grace] - mutation visible
int count = 0;
count = incrementCounter(count);
count = incrementCounter(count);
System.out.println(count); // 2 - only visible because the return value was captured
}
}Mental Model
Picture handing someone a copy of an index card. If the card has a number written on it (a primitive), they can scribble on their copy all they like — your original card is untouched. If the card has an address written on it (a reference), they can go to that address and rearrange the furniture (mutate the object) — you'll see the changes next time you visit, because it's the same house. But if they write a different address on their copy of the card (reassign the parameter), your original card still has the old address on it.
Quick Reference
Java argument passing: ALWAYS pass-by-value, no exceptions.
Primitive argument: parameter gets a copy of the value itself
-> reassigning/modifying the parameter never affects the caller
Reference argument: parameter gets a copy of the REFERENCE (not the object)
-> mutating the object through the parameter IS visible to the caller
-> reassigning the parameter to a new object is NOT visible to the caller
Arrays and Strings are reference types and follow the reference-argument rules above
(String just has no mutating methods, so no mutation is ever observable through it).Key Takeaways
Java is always pass-by-value: a method parameter always receives a copy, never the original variable.
For primitives, the copy is the value itself, so modifying the parameter never affects the caller's variable.
For reference types, the copy is the reference (pointer) value, so caller and parameter share the same object.
Mutating a shared object through a parameter is visible to the caller; reassigning the parameter to a different object is not.
Because String is immutable, it never shows visible mutation, which is why it's often mistakenly cited as evidence Java is pass-by-value only for objects and not others — it's the same mechanism everywhere.
static void swap(int a, int b) { int temp = a; a = b; b = temp; } // caller expects x and y to be swapped after calling swap(x, y) - they are not static int[] swap(int a, int b) { return new int[]{b, a}; // return the swapped values instead } // int[] swapped = swap(x, y); x = swapped[0]; y = swapped[1];
Since Java passes primitives by value, reassigning the local parameters a and b inside swap has zero effect on the caller's original variables; to actually communicate a swap, the method must return the new values.
static void resetList(List<String> list) { list = new ArrayList<>(); // reassignment - caller's list is untouched } static void resetList(List<String> list) { list.clear(); // mutation - actually empties the caller's list }
Reassigning the list parameter to a brand-new ArrayList only changes what the local parameter points to; the caller's original list reference is unaffected. Calling a mutating method like clear() on the shared object is what actually changes what the caller sees.
static void addExclamation(String s) { s = s + "!"; // creates a new String, reassigns the local parameter only } String msg = "hi"; addExclamation(msg); System.out.println(msg); // caller mistakenly expects "hi!" static String addExclamation(String s) { return s + "!"; } String msg = "hi"; msg = addExclamation(msg); System.out.println(msg); // "hi!"
String is immutable, so there's no way to modify it through a parameter at all — every apparent modification produces a new String that must be returned and reassigned by the caller to be observed.
// Assuming Java is 'pass-by-reference for objects' leads to this incorrect claim: // "passing an object always lets the method change what the caller's variable refers to"
// The correct model: the method can mutate the shared object's STATE, // but can never make the caller's own variable point to a different object.
This is a common conceptual error. Java is pass-by-value uniformly; for objects, only the reference value is copied, which permits mutation of shared state but never permits the callee to redirect the caller's variable to a new object.
What is the difference between a formal parameter and an actual argument?
A formal parameter is the name and type declared in the method's own header; an actual argument is the specific value supplied at a particular call site. Java matches arguments to parameters by position and type, not by name.
Is Java ever pass-by-reference, for any type?
No — Java is strictly pass-by-value for every type, primitive or reference. What can differ is what value gets copied: for a primitive it's the data itself, for a reference type it's the reference (pointer) value, never the object.
If a method reassigns its object parameter to a brand-new object, does that affect the caller's original variable?
No — reassigning the parameter only changes what that method's local copy of the reference points to; the caller's original variable still refers to the original object, because only the reference value was copied, not the variable's storage slot itself.
If a method calls a mutating method on an object received as a parameter, is that change visible to the caller?
Yes — because the parameter is a copy of the reference, it points to the exact same object as the caller's variable, so mutating that object's internal state through the parameter is visible everywhere that object is referenced, including back in the caller.
Why can't a method "swap" two int variables by taking them as parameters and reassigning the parameters inside the method?
Because int is a primitive: the parameters receive independent copies of the values, so reassigning them only changes the method's local copies and has no effect whatsoever on the caller's original variables.
Why does passing a String argument never show any visible mutation, even though String is technically a reference type?
Because String is immutable — it has no methods that modify its internal character data, only methods that return a new String. So while String follows the exact same reference-passing mechanics as any object, there's simply nothing to mutate, making the pass-by-value behavior look identical to a primitive's from the caller's perspective.
How does passing an array to a method demonstrate the difference between mutation and reassignment?
Modifying an element of the array through the parameter (arr[0] = 0) changes the shared underlying array and is visible to the caller, since both point to the same array object; assigning the parameter itself to a brand-new array (arr = new int[]{...}) only changes the local copy of the reference and leaves the caller's array untouched.
Write a method incrementAll(int[] arr) that adds 1 to every element of the array in place (using mutation, not reassignment), and show that the caller sees the change.
static void incrementAll(int[] arr) { for (int i = 0; i < arr.length; i++) { arr[i] = arr[i] + 1; } } int[] values = {1, 2, 3}; incrementAll(values); System.out.println(java.util.Arrays.toString(values)); // [2, 3, 4]
Because arrays are reference types, incrementAll receives a copy of the reference to the same underlying array the caller has; writing into arr[i] mutates that shared array, so the change is visible in the caller's values array after the call returns.
What does this print, and why?
"old" — tryReplace reassigns its local parameter sb to point to a brand-new StringBuilder, but that reassignment only affects the method's local copy of the reference; the caller's original variable still points to the original "old" StringBuilder object, which was never mutated.
Write a method that takes an int and returns whether doubling it would exceed 100, without modifying the caller's variable (demonstrating that primitives require a return value, not a mutated parameter).
static boolean wouldExceed100(int n) { return n * 2 > 100; } int value = 60; boolean result = wouldExceed100(value); System.out.println(result); // true System.out.println(value); // 60 - unchanged, as expected for a primitive
Since int is a primitive, the method can only ever affect its own local copy; the only way to communicate any result back to the caller is through the method's return value, which is exactly what this method does.
What does this print, and why does the second add(1) not appear?
[1, 2, 99] — the first list.add(99) mutates the shared list the caller passed in, which is visible. Then list is reassigned to point to a brand-new, empty ArrayList — from that point on, list refers only to this new local object, so the subsequent list.add(1) only affects that new object, which the caller never sees or references.
static void tryReplace(StringBuilder sb) {
sb = new StringBuilder("new");
}
StringBuilder original = new StringBuilder("old");
tryReplace(original);
System.out.println(original);static void confuse(List<Integer> list) {
list.add(99); // mutation - visible
list = new ArrayList<>(); // reassignment - not visible
list.add(1); // only affects the new, local list
}
List<Integer> nums = new ArrayList<>(List.of(1, 2));
confuse(nums);
System.out.println(nums);