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.
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
- On input (constructor) — copy incoming mutable objects
- 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 ImmutableCollections —
ImmutableList.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]]