Given Java's pass-by-value semantics, what API-design and safety practices follow? When do you need defensive copying, and how does immutability change the calculus?
answer
- Pass-by-value + mutability = aliasing hazard
- Defensive copy on input (store) and output (expose)
- Copy before validating (avoid TOCTOU)
- Immutability removes the need for copies
- Unmodifiable views; immutable-by-default APIs
basics
~20 sBecause methods get a copy of the reference to the same object, any mutable object you pass can be changed by the callee, and anything mutable you return can be changed by the caller. So copy mutable inputs/outputs defensively, or make objects immutable so copying isn't needed.
solid answer
~50 sPass-by-value means a method can't reseat your variables, but it shares the same heap object, so two real hazards exist: a callee mutating an input you didn't want changed, and a caller mutating internal state you returned. The discipline is defensive copying at trust boundaries: copy mutable arguments before storing them (so later caller mutations don't corrupt your invariants), and copy mutable fields before returning them (so callers can't reach in). Immutability changes the calculus entirely: if the object can't be mutated, sharing the reference is harmless, so you skip defensive copies, gain free thread-safety, and can cache and share freely. That's why Effective Java recommends minimizing mutability, using immutable value types (records of immutables, String, BigDecimal), and reserving defensive copies for the unavoidable mutable types (arrays, collections, Date). At scale, prefer unmodifiable views and immutable-by-default APIs over scattering manual copies, while being mindful of the allocation cost of copying in hot paths.
code
java · 15 linesimport java.util.*;
final class Period {
private final Date start, end;
Period(Date start, Date end) {
// copy first, then validate the copies (closes TOCTOU + aliasing)
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.after(this.end)) throw new IllegalArgumentException("start>end");
}
Date start() { return new Date(start.getTime()); } // defensive copy on output
}
// For collections, prefer immutable copies/views:
static List<String> safe(List<String> in) { return List.copyOf(in); }go deeper
Grasp that a method can change a mutable object you pass it, so passing isn't automatically safe; copying or immutability is how you protect data.
Apply defensive copying on inputs and outputs for mutable types (Date, collections, arrays) and recognize immutable types don't need it.
Implement the patterns correctly (copy-before-validate, unmodifiable views, covariant copies) and justify when each applies; weigh allocation cost.
Set codebase policy: immutable-by-default value types, controlled aliasing at trust boundaries, where to allow shared mutable state, and the performance/consistency trade-offs at scale.
## The root cause Java is pass-by-value, so a method never reseats your variables — but for reference types, the *value passed is the reference*, and caller and callee end up holding references to the **same heap object**. Combined with mutability, this creates **aliasing**: two parts of the program can reach and modify one object without either knowing about the other. Aliasing of mutable state is the source of the hazards below. ## Hazard 1 — capturing a mutable argument ```java class Period { private final Date start, end; Period(Date start, Date end) { this.start = start; this.end = end; } // BUG } Date s = new Date(), e = new Date(); Period p = new Period(s, e); e.setTime(0); // the caller still holds e and can mutate Period's internals! ``` The constructor stored the *same* `Date` the caller holds. Because the caller still references it, the caller can mutate `Period`'s state after construction, breaking any invariant (e.g., start ≤ end). **Fix — defensive copy on input:** ```java this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); ``` Copy *before* validating, and validate the copy, to avoid TOCTOU races with another thread mutating the original between check and copy. ## Hazard 2 — leaking a mutable field ```java Date getStart() { return start; } // BUG: caller can mutate internal Date ``` Returning the live field hands callers a reference to internal state. **Fix — defensive copy on output:** ```java Date getStart() { return new Date(start.getTime()); } ``` For collections, return an unmodifiable view or a copy: `List.copyOf(items)` or `Collections.unmodifiableList(...)`. ## How immutability dissolves the problem If the object **cannot be mutated**, aliasing is harmless: sharing the same reference is perfectly safe because no one can change it. So for immutable types you **skip all defensive copies**. This is why *minimize mutability* is a core Effective Java item. Immutable objects: - are **inherently thread-safe** (no synchronization needed), - can be **freely shared and cached** (and interned, like String literals), - make **safe map keys** (their hashCode can't drift), - simplify reasoning (no spooky action at a distance). Replacing `Date` with the immutable `java.time` types (`Instant`, `LocalDate`) removes the `Period` problem entirely — no copies needed. ## A decision rubric - **Argument is immutable** (String, boxed primitive, record-of-immutables, `Instant`): store/return directly, no copy. - **Argument is mutable and you store it** (`Date`, array, `ArrayList`): defensive-copy on the way in. - **Field is mutable and you expose it**: defensive-copy on the way out, or expose an unmodifiable view, or expose only immutable projections. - **You truly want shared mutable state** (rare, deliberate): document the ownership/contract explicitly and consider synchronization. ## Trade-offs and scale - **Allocation cost.** Defensive copies create garbage; in hot paths this matters. Immutability can also churn (copy-on-write), so for big aggregates consider persistent/structural-sharing structures or a clearly documented mutable core behind an immutable facade. - **Consistency over heroics.** Scattering manual copies is error-prone; prefer immutable-by-default value types and unmodifiable collections as the team norm, so the safe path is the default path. - **Arrays are always mutable** and have no immutable variant — wrap them (`List.of`/`copyOf`) or copy (`clone()`/`Arrays.copyOf`) at boundaries. ## Why this is the principal-level framing The junior fact ('Java is pass-by-value') becomes, at the architecture level, a policy: *control aliasing of mutable state at trust boundaries, and prefer immutability so the boundaries mostly vanish.* That policy is what keeps large codebases free of action-at-a-distance bugs and unnecessary synchronization.
- Why copy a mutable argument BEFORE validating it rather than after?To close a time-of-check/time-of-use gap: if you validate the caller's original and copy afterward, another thread could mutate it between the check and the copy, so you'd store an invalid value. Copying first, then validating the copy, guarantees what you validated is what you keep.
- When is it acceptable to skip defensive copying entirely?When the type is immutable (String, boxed primitives, records of immutables, java.time types), or when you deliberately and explicitly share mutable state with documented ownership. For everything else mutable that crosses a trust boundary, copy or expose an unmodifiable view.
saying these in an interview costs you the question
- Storing a passed-in mutable Date/array/collection without copying
- Returning the live internal mutable field from a getter
- Validating the original then copying (TOCTOU window)
- Adding defensive copies for already-immutable types (wasteful)
- Assuming pass-by-value alone makes inputs safe from mutation