Question 9 · Section 13

What to do if a class field of an immutable class references a mutable object?

If your field references a mutable object (e.g., ArrayList, Date, array), you need to make a copy of it.

Language versions: English Russian Ukrainian

Basic Level

If your field references a mutable object (e.g., ArrayList, Date, array), you need to make a copy of it.

Simple example

public final class Person {
    private final String name;
    private final Date birthDate; // Date is mutable!

    public Person(String name, Date birthDate) {
        this.name = name;
        // Alternative: birthDate.clone() — for Date this works, but clone()
        // in general is considered broken (Effective Java Item 13).
        this.birthDate = new Date(birthDate.getTime()); // COPY!
    }

    public Date getBirthDate() {
        return new Date(birthDate.getTime()); // COPY!
    }
}

The two-point rule

  1. On input (constructor) — copy incoming mutable objects
  2. On output (getter) — return copies of internal mutable objects

When defensive copy is excessive

If you control all the code that passes data, and this is an internal API — you can document the contract “caller must not mutate”. But be careful: future developers might not read the documentation.


Intermediate Level

Protecting collections

public final class ImmutableClass {
    private final List<String> items;

    public ImmutableClass(List<String> items) {
        // Bad: this.items = items;
        // Good:
        this.items = List.copyOf(items); // Java 10+
    }

    public List<String> getItems() {
        return items; // List.copyOf returned an immutable list — safe to return
    }
}

Older Java versions

// Java 8
this.items = Collections.unmodifiableList(new ArrayList<>(items));

// Or
this.items = new ArrayList<>(items);
// ...
return Collections.unmodifiableList(items);

Deep copying

If the list contains mutable objects:

public record Order(Long id, List<Item> items) {
    public Order {
        items = items.stream()
            .map(item -> new Item(item.getName(), item.getPrice())) // copy of each
            .toList();
    }
}

Modern alternatives

  • Records — simplify structure, but collections are copied manually
  • Guava ImmutableCollectionsImmutableList.copyOf()
  • Vavr — persistent collections

Advanced Level

TOCTOU attack (Time-of-Check to Time-of-Use)

If you validate incoming data, make a copy before validation:

public ImmutableClass(List<String> items) {
    List<String> copy = List.copyOf(items); // copy first
    if (copy.isEmpty()) throw new IllegalArgumentException(); // then check
    this.items = copy;
}

Otherwise, in a multi-threaded environment, the state of the original list may change between the check and the use.

Shallow vs Deep Copy — critical distinction

List.copyOf(users) creates a new list, but references to the same User objects. By changing user.setName() in external code, you change the data inside the “immutable” class.

Solution: clone each element.

Performance

Copying large collections is an O(n) complexity operation, which for large n and frequent calls creates significant overhead. In “hot paths”, consider:

  • Persistent data structures (Vavr)
  • Copy-on-write
  • Immutable types at the API level

Summary for Advanced

  • Treat all incoming mutable objects with distrust
  • Always make copies of arrays and standard collections
  • Distinguish Shallow and Deep Copy
  • Copy before validation
  • If possible, use List.of, Map.copyOf

Interview Cheat Sheet

Must know:

  • Two-point rule: copy on input (constructor) and on output (getter)
  • For Date: new Date(date.getTime()) — creates an independent copy
  • For collections: List.copyOf(input) — Java 10+, creates an immutable copy
  • TOCTOU attack: copy BEFORE validation, otherwise data changes between check and use
  • Shallow vs Deep Copy: List.copyOf(users) copies the container, but not the User objects
  • For deep copying: stream().map(item -> new Item(item)).toList()
  • When defensive copy is excessive: fully controlled internal API

Frequent follow-up questions:

  • What to copy — on input or output? — Both: incoming data and data returned from getter
  • List.copyOf vs Collections.unmodifiableList? — List.copyOf makes a copy; unmodifiableList — a wrapper over the original
  • What to do with nested mutable objects? — Deep Copy: clone each element
  • When can you NOT copy? — Internal API, caller guaranteed not to mutate

Red flags (DO NOT say):

  • “You can return the original if getter is private” — future developers might not read the documentation
  • “unmodifiableList in constructor is enough” — it’s a wrapper, the original can be changed
  • “clone() is a normal way to copy” — clone() is considered broken
  • “Copying is always expensive” — for small collections the overhead is negligible

Related topics:

  • [[8. Is it enough to make all fields final for immutability]]
  • [[10. What is a defensive copy]]
  • [[11. When should you make a defensive copy]]
  • [[12. How to protect a collection from modification]]
  • [[14. What is the difference between shallow copy and deep copy]]