Trace what happens when you pass an Integer or a String into a method that 'changes' it (e.g., p++ on an Integer, or p += "x" on a String). Why does the caller never see the change?
answer
- Integer/String are immutable: no in-place change
- p++ / p += create a NEW object, reseat local p
- Pass-by-value isolates the caller's variable
- StringBuilder mutates in place = caller sees it
- str.trim() returns new String; must capture it
basics
~20 sInteger and String are immutable. Operations like p++ or p += "x" don't change the object; they create a new object and point the local parameter at it. Since the parameter is a copy of the reference, the caller's variable still points at the original. No change is visible.
solid answer
~50 sBoth Integer and String are immutable, so there is no operation that mutates them in place. When you write p++ on an Integer parameter, the compiler unboxes to int, increments, and reboxes into a brand-new Integer, then assigns it to the local parameter p; the caller's Integer is untouched. Likewise p += "x" on a String builds a new String (often via StringBuilder) and reseats the local parameter. In every case the only thing that changes is the local copy of the reference, which pass-by-value isolates from the caller. Two things combine here: pass-by-value (the parameter is a copy) and immutability (the 'modifying' operation can only produce a new object and reassign, never mutate in place). This is why you can safely pass Strings and boxed primitives around without defensive copying, and why a method can never 'increment my Integer for me' through a parameter; it must return the new value.
code
java · 10 linesstatic void bump(Integer p) { p++; } // new Integer, local only
static void append(String p) { p += "x"; } // new String, local only
static void append(StringBuilder p){ p.append("x"); } // in-place mutation
public static void main(String[] a) {
Integer n = 10; bump(n); System.out.println(n); // 10 (unchanged)
String s = "ab"; append(s); System.out.println(s); // ab (unchanged)
StringBuilder b = new StringBuilder("ab");
append(b); System.out.println(b); // abx (changed)
}go deeper
Know that Integer and String can't be changed in place, so a method that does p++ or p += creates a new object and the caller sees nothing.
Explain both factors: immutability (operation makes a new object) plus pass-by-value (only the local reference is reseated). Contrast with StringBuilder which mutates in place.
Articulate the desugaring (unbox/increment/rebox; StringBuilder for +=), the safety benefit (no defensive copies for immutables), and the correct patterns for caller-visible counters (return value or mutable holder).
Discuss design implications: immutability as a concurrency/safety strategy, allocation costs of autoboxing in hot paths, identity-vs-value pitfalls (Integer cache), and guiding teams toward immutable-by-default APIs.
## Two ideas that must both be understood 1. **Pass-by-value**: the parameter is a *copy* of the argument's value (for objects, a copy of the reference). 2. **Immutability**: `String`, `Integer`, `Long`, `Double`, etc. (the boxed primitive wrappers) expose **no mutators**. Their state is fixed at construction. Any operation that looks like it 'changes' them actually constructs a *new* object. When both hold, a method can never make a caller-visible change to such an argument — because mutation is impossible (immutability) and reassignment is local (pass-by-value). ## Walking through Integer ```java static void bump(Integer p) { p++; } Integer n = 10; bump(n); // n is still 10 ``` What `p++` compiles to, roughly: ```java int tmp = p.intValue(); // unbox tmp = tmp + 1; // increment the primitive p = Integer.valueOf(tmp); // box into a NEW Integer, reassign local p ``` - `p` started as a *copy* of `n`'s reference (pointing at the Integer for 10). - After `p++`, `p` points at a *different* Integer (11). The Integer for 10 was never modified — `Integer` has no setter. - `n` still holds its original reference (to 10). The reassignment of `p` was local. Caller sees 10. Note the **flyweight cache** wrinkle: `Integer.valueOf` caches −128..127, so identity (`==`) games differ in that range, but that is orthogonal — the point is a *new* logical value is produced and only the local parameter is reseated. ## Walking through String ```java static void append(String p) { p += "x"; } String s = "ab"; append(s); // s is still "ab" ``` `p += "x"` compiles to something like `p = new StringBuilder().append(p).append("x").toString();` — a brand-new String. The original `"ab"` String object is never mutated (String is immutable; that's also why string literals can be safely interned and shared). Only the local `p` is reseated. `s` still references `"ab"`. Caller sees `"ab"`. ## Contrast with a mutable type ```java static void append(StringBuilder p) { p.append("x"); } StringBuilder sb = new StringBuilder("ab"); append(sb); // sb is now "abx" ``` Here `append` *mutates the shared object in place* — no new object, so the caller sees the change. The difference from `String` is entirely about mutability, not about passing semantics (both are passed by value as references). ## Why this matters in practice - **No defensive copying needed for immutables.** You can freely hand `String`, boxed primitives, `BigInteger`, `LocalDate`, records-of-immutables, etc., to untrusted methods; they cannot alter them. - **'Increment my counter' must return a value.** A method cannot bump your `Integer` through a parameter. Either return the new value, or use a mutable holder (`AtomicInteger`, `int[]`, a counter object) which the method mutates in place. - **Beware accidental immutable-reassignment bugs.** Writing `str.trim();` and expecting `str` to change is the classic mistake — `trim()` returns a new String you must capture: `str = str.trim();`. - **Autoboxing hides allocation.** `p++` on an `Integer` silently unbox/rebox-allocates; in hot loops prefer `int`.
- If a method must increment a counter for the caller through a parameter, what should it take?A mutable holder it can mutate in place: an AtomicInteger (counter.incrementAndGet()), a one-element int[] (arr[0]++), or a custom counter object. Or simpler, return the incremented value and let the caller reassign.
- Does the Integer cache (-128..127) change the conclusion about pass-by-value here?No. The cache affects only == identity for boxed values in that range. The pass-by-value conclusion is unchanged: p++ still produces a (possibly cached) new logical value and reseats only the local parameter; the caller's variable is untouched.
saying these in an interview costs you the question
- Expecting p++ on an Integer parameter to change the caller's value
- Thinking str.trim()/replace() mutate the String in place
- Confusing the immutability reason with a passing-semantics reason (both apply)
- Believing boxed wrappers behave by reference because they're objects