skip to content

When you compose a mutable object inside a class, what goes wrong without defensive copying, and how do you fix it?

level: seniorimportance: should knowfreq 55%

answer

  1. References passed by value → shared heap object → leak
  2. final stops reassigning the ref, not mutating the object
  3. Copy in (constructor/setter) AND out (getter)
  4. Copy before validate (TOCTOU)
  5. Unmodifiable view ≠ copy; immutable part ⇒ no copy

basics

~20 s

If you store a caller's mutable object directly, the caller still holds a reference and can change your object's internals behind your back. Fix it by copying the object when it comes in (constructor/setter) and when it goes out (getter), so outside code can't reach the part you own.

solid answer

~50 s

Composition implies exclusive ownership of the part, but Java passes references by value, so storing a caller-supplied mutable object directly keeps the caller's reference alive — they can mutate your internal state afterward, breaking encapsulation and any invariants you guard. The same leak happens on the way out: returning the internal reference from a getter lets callers mutate it. The fix is **defensive copying**: copy mutable inputs in the constructor or setter before storing them, and copy mutable fields again when returning them from accessors. Make the copy before validating so a malicious caller can't mutate the object between your check and your store (a TOCTOU race). Prefer truly independent copies — `new ArrayList<>(input)` or a deep copy for nested mutable graphs; an unmodifiable *view* still wraps the live object and isn't enough if the caller kept the original. If the part type is immutable (String, records of immutables, `List.copyOf`), no copying is needed, which is one reason immutability simplifies composition.

code

java · 14 lines
java
final class Period {
    private final Date start;
    private final Date end;

    Period(Date start, Date end) {
        this.start = new Date(start.getTime());  // copy in, before validating
        this.end   = new Date(end.getTime());
        if (this.start.after(this.end))
            throw new IllegalArgumentException("start after end");
    }

    Date start() { return new Date(start.getTime()); }  // copy out
    Date end()   { return new Date(end.getTime());   }
}

go deeper

for a junior

Recognizes that storing a caller's object means both share it, and that a getter returning the internal object lets outsiders change it; can apply a simple copy-in/copy-out fix when shown the pattern.

for a middle

Explains the leak via pass-by-value of references, knows final guards the reference not the object, and copies mutable inputs and outputs; distinguishes a copy from an unmodifiable view.

for a senior

Copies before validating to avoid TOCTOU, chooses deep vs shallow copy for nested graphs, avoids clone() on non-final parameter types, and decides per-relationship whether copying applies (composition yes, aggregation no).

for a principal

Weighs defensive copying against allocation/GC cost and API ergonomics, drives immutability as the systemic fix, and sets team conventions (immutable value types, record-based parts, copy boundaries at module edges) so the codebase avoids reference-leak classes of bugs by default.

## Why composition needs defensive copying **Composition** means a class *exclusively owns* a part — no outside code should be able to reach in and change it. Java makes that ownership easy to violate because of two facts: 1. **Java is pass-by-value, but for objects the *value* is the reference.** When you pass an object to a constructor, the method gets a **copy of the pointer**, not a copy of the object. Both the caller and your field now point at the **same** object on the heap. 2. **Many objects are mutable** — `Date`, `ArrayList`, `int[]`, most domain objects — meaning their internal state can be changed after construction. Put those together and you get a **reference leak**. ### The inbound leak ```java final class Period { private final Date start; Period(Date start) { this.start = start; } // stores the caller's object Date start() { return start; } } Date d = new Date(); Period p = new Period(d); d.setTime(0); // caller still holds d → just mutated Period's internal state! ``` Even though `start` is `final`, `final` only stops the **reference** from being reassigned — it does nothing to stop the **referenced object** from being mutated. The caller kept `d`, so they can rewrite the period after the fact, defeating encapsulation and any invariant (e.g. start ≤ end). **Fix — copy on the way in:** ```java Period(Date start) { this.start = new Date(start.getTime()); } ``` Now the field points at a private copy the caller cannot reach. ### The outbound leak ```java Date start() { return start; } // returns the live internal reference ... p.start().setTime(0); // caller mutates the internal Date through the getter ``` **Fix — copy on the way out:** ```java Date start() { return new Date(start.getTime()); } ``` ### Copy *before* you validate (the TOCTOU trap) If you validate the argument and *then* copy, a malicious caller running on another thread can mutate the object in the gap between your check and your copy — a **time-of-check-to-time-of-use (TOCTOU)** attack. Always **copy first, then validate the copy**: ```java Period(Date start, Date end) { this.start = new Date(start.getTime()); // copy first this.end = new Date(end.getTime()); if (this.start.after(this.end)) // validate the copies throw new IllegalArgumentException("start after end"); } ``` Also: don't use `clone()` to copy a parameter whose type is non-final, because the caller could pass a malicious subclass whose `clone()` does something hostile. ### Collections: copy, don't just wrap For a `List` field, `Collections.unmodifiableList(input)` returns a **view** over the caller's live list — the caller still holds the original and can add to it, and the change shows through your 'unmodifiable' view. To truly own it, **copy**: `new ArrayList<>(input)` (mutable internal) or `List.copyOf(input)` (immutable snapshot). For nested mutable elements you need a **deep copy** of each element too, not just a new outer list. ### The shortcut: immutable parts If the part type is **immutable** — `String`, `Integer`, `LocalDate`, an immutable record, a `List.copyOf(...)` of immutables — there is **nothing to copy** because no one can mutate it. This is a major reason 'favor immutability' pairs with 'favor composition': immutable parts make exclusive ownership free. ### Aggregation is the deliberate exception Defensive copying is for **composition** (exclusive ownership). In **aggregation** the part is *meant* to be shared and live independently, so you intentionally store the passed-in reference *without* copying — sharing is the feature. Knowing which relationship you're modeling tells you whether to copy. ### Summary checklist - Mutable composed part? Copy in (constructor/setter) **and** out (getter). - Copy **before** validating. - Use a real copy (`new ArrayList<>`, `new Date(...)`), not an unmodifiable *view*. - Deep-copy nested mutable graphs. - Immutable part ⇒ no copy needed. - Aggregation ⇒ share on purpose, don't copy.

  • Why isn't a final field enough to protect a composed mutable object?
    final only prevents reassigning the reference variable; it does not freeze the object it points to. A final Date field can still have setTime() called on it. To truly protect ownership you must copy the object (or use an immutable type), not just mark the field final.
  • Why copy a parameter before validating it rather than after?
    If you validate the original then copy, another thread holding the same reference can mutate the object in the window between the check and the copy (a TOCTOU race), so you'd validate one value but store a different one. Copying first and validating the copy closes that window.
  • When should you deliberately NOT defensively copy?
    When you're modeling aggregation, where the part is meant to be shared and have an independent lifecycle, or when the part is immutable. In aggregation, sharing the reference is the intended behavior; with immutable types there is nothing to protect against.

Giving someone the original house key (the reference) lets them keep their own copy and walk in later. Defensive copying is handing them a photo of the key instead — they can look but can't open your door.

saying these in an interview costs you the question

  • Believing a final field makes the referenced object immutable — it only fixes the reference.
  • Returning the internal mutable reference from a getter and assuming the field's privacy protects it.
  • Using Collections.unmodifiableList(input) and thinking you now own the list — it's a view over the caller's still-mutable original.
  • Validating the argument before copying it (TOCTOU window).
  • Using clone() to copy a parameter of a non-final type a caller might subclass maliciously.

context