skip to content

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?

level: seniorimportance: should knowfreq 52%

answer

  1. Integer/String are immutable: no in-place change
  2. p++ / p += create a NEW object, reseat local p
  3. Pass-by-value isolates the caller's variable
  4. StringBuilder mutates in place = caller sees it
  5. str.trim() returns new String; must capture it

basics

~20 s

Integer 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 s

Both 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 lines
java
static 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

for a junior

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.

for a middle

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.

for a senior

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).

for a principal

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

context