skip to content

What is defensive copying, and why must it happen on both input and output for a mutable field?

level: middleimportance: must knowfreq 70%

answer

  1. aliasing = two references to the same mutable object
  2. two leaks: retained constructor arg (in) + returned field (out)
  3. copy first, validate the copy (TOCTOU)
  4. unmodifiableList is a view; List.copyOf is a true copy
  5. shallow copy shares elements — deep copy when elements are mutable

basics

~20 s

Defensive copying means storing and returning copies of mutable objects instead of the originals. You copy in the constructor so the caller can't change your data later, and copy in the getter so the caller can't change the object you hand back. Both are needed because each closes a different leak.

solid answer

~50 s

Defensive copying protects an object's internal state from outside mutation by never sharing a reference to a mutable field with the outside world. There are two leaks to close. On **input**: if the constructor stores the exact object the caller passed (`this.list = list`), the caller still holds that reference and can mutate it afterward, changing your supposedly-immutable state — so you store a copy. On **output**: if a getter returns the field directly, the caller can mutate the returned object and thereby reach your internal field — so you return a copy or an unmodifiable view. You only do this for genuinely mutable types (arrays, `Date`, mutable collections); immutable types like `String` or `LocalDate` are safe to share. Watch the pitfalls: `Collections.unmodifiableList` is a view, not a copy (the backing list can still change through your retained reference); `clone()`/`Arrays.copyOf` are shallow; and you should validate *after* copying to avoid a time-of-check/time-of-use race.

code

java · 19 lines
java
public final class Schedule {
    private final List<Date> slots;

    public Schedule(List<Date> slots) {
        // copy IN; deep-copy elements because Date is mutable
        List<Date> copy = new ArrayList<>(slots.size());
        for (Date d : slots) copy.add(new Date(d.getTime()));
        this.slots = copy;
        // validate AFTER copying (no TOCTOU on caller's list)
        if (copy.isEmpty()) throw new IllegalArgumentException("empty");
    }

    public List<Date> getSlots() {
        // copy OUT; again deep because elements are mutable
        List<Date> copy = new ArrayList<>(slots.size());
        for (Date d : slots) copy.add(new Date(d.getTime()));
        return Collections.unmodifiableList(copy);
    }
}

go deeper

for a junior

Knows you should copy mutable objects rather than store them directly, but may not articulate both the input and output paths or the aliasing cause.

for a middle

Explains aliasing, names both leaks (retained constructor arg, returned field), and writes correct copy-in/copy-out code.

for a senior

Handles the pitfalls: unmodifiable-view vs true copy, shallow vs deep, validate-after-copy for TOCTOU, and arrays having no read-only wrapper.

for a principal

Reasons about the cost/benefit (allocation vs safety), promotes immutable element types so deep copies aren't needed, and sets a team rule (List.copyOf, records + compact constructors) rather than ad-hoc copies.

## The core problem: aliasing In Java, a variable of object type holds a **reference** (a pointer) to an object, not the object itself. When you write `this.field = arg`, you copy the *reference* — now `this.field` and the caller's variable point at the **same** object. This shared-reference situation is called **aliasing**. If that object is **mutable** (its state can change), then whoever holds *any* alias can change it for *everyone*. That is exactly what breaks immutability. ## Why two copies — the two leaks There are two boundaries where a reference can leak: the constructor (data coming **in**) and the getter (data going **out**). Closing only one leaves the other open. ### Leak 1 — input (constructor) ```java class Holder { private final List<String> items; Holder(List<String> items) { this.items = items; } // BUG: aliases the caller's list } List<String> mine = new ArrayList<>(List.of("a")); Holder h = new Holder(mine); mine.add("b"); // mutates h's internal state from outside! ``` The caller kept `mine`, which is the same list `h` uses. The fix is **copy on input**: `this.items = new ArrayList<>(items);` (or `List.copyOf(items)`). Now `h` has its **own** list; the caller's later mutations don't touch it. ### Leak 2 — output (getter) ```java class Holder { private final List<String> items = new ArrayList<>(List.of("a")); List<String> getItems() { return items; } // BUG: returns the internal list } h.getItems().clear(); // reaches in and empties h's internal state! ``` The getter handed out the *same* list it stores. The fix is **copy on output**: `return List.copyOf(items);` or `return Collections.unmodifiableList(items);`. Now the caller can't mutate what you returned in a way that reaches your field. Because input and output are *independent* paths, you must close **both**. Closing only the constructor still lets `getItems().clear()` corrupt you; closing only the getter still lets the retained constructor argument corrupt you. ## When you do NOT need to copy Defensive copying costs an allocation, so only do it for **mutable** types. If the field's type is **immutable** — `String`, the boxed numerics (`Integer`, `Long`…), `LocalDate`/`Instant` and the rest of `java.time`, `BigInteger`/`BigDecimal`, `enum`s, `UUID`, or another class you've made properly immutable — sharing the reference is perfectly safe, and copying it would be wasteful and a sign you misunderstand the rule. ## Pitfalls 1. **Unmodifiable ≠ immutable.** `Collections.unmodifiableList(x)` returns a **view**: the caller can't mutate *through the wrapper*, but if you still hold the original mutable `x`, you (or anything aliasing `x`) can change it and the change shows through the wrapper. Combine it with a copy, or just use `List.copyOf(x)` (Java 10+) which copies into a genuinely immutable list — that's the cleaner one-liner for both input and output. 2. **Shallow vs deep.** `new ArrayList<>(src)`, `Arrays.copyOf`, and `clone()` copy the **container** but share the **elements**. If the elements are themselves mutable (e.g. `List<Date>`), the caller can still mutate an element. You then need a **deep copy** (copy each element too). 3. **TOCTOU on input.** If you **validate before copying**, an attacker can mutate the argument *between* your check and your copy (a time-of-check/time-of-use race), passing validation but storing bad data. Always **copy first, then validate the copy**. 4. **Arrays have no read-only view.** There's no `unmodifiableArray`; you must copy the array (`arr.clone()` / `Arrays.copyOf`) on both input and output, or expose it via an unmodifiable `List` instead. ## Records A `record` does not copy for you. Put the input copy in the **compact constructor** and the output copy in an overridden **accessor** — same two-leak rule applies.

  • Why validate after copying instead of before?
    If you validate the caller's mutable argument first and copy second, the caller can mutate the object in the gap between the two operations (a time-of-check/time-of-use race), sneaking invalid state past your check. Copying first then validating your private copy removes that window.
  • Is `Collections.unmodifiableList(internalList)` from a getter enough?
    It stops the caller from mutating through the returned wrapper, which is usually fine for output. But it's a view: it does not protect against mutation through any other alias to the same backing list. List.copyOf is safer; unmodifiable wrappers are best paired with a list you alone control.

saying these in an interview costs you the question

  • Closing only one leak (copy in but return the field directly, or vice versa).
  • Treating Collections.unmodifiableList as equivalent to a copy.
  • Validating the argument before copying it (TOCTOU window).
  • Doing a shallow copy when the collection's elements are themselves mutable.
  • Copying immutable fields like String/LocalDate unnecessarily.

context