What to Do If Class Field References a Mutable Object
If an immutable class must contain a field referencing a mutable object (such as java.util.Date, an array int[], a collection ArrayList, or a mutable domain entity), you must ap...
🟢 Junior Level
30-Second Summary
If an immutable class must contain a field referencing a mutable object (such as java.util.Date, an array int[], a collection ArrayList, or a mutable domain entity), you must apply the Two-Point Defensive Copying Rule:
- Point 1 (Ingress / Constructor): Never store a direct reference to an externally passed mutable object. Always create an independent copy and store the reference to that copy.
- Point 2 (Egress / Getter): Never return a direct reference to an internal mutable object. Always return a fresh clone or an unmodifiable view (Unmodifiable View) so callers cannot mutate the object’s internal state.
Practical Example: Employee Birth Date (java.util.Date)
import java.util.Date;
import java.util.Objects;
public final class Employee {
private final String name;
private final Date birthDate; // Date is mutable via setTime()!
public Employee(String name, Date birthDate) {
this.name = Objects.requireNonNull(name, "Name required");
Objects.requireNonNull(birthDate, "Birth date required");
// 1. INGRESS DEFENSIVE COPY:
// Create an independent Date instance with identical timestamp to isolate state
this.birthDate = new Date(birthDate.getTime());
}
public String getName() {
return name;
}
public Date getBirthDate() {
// 2. EGRESS DEFENSIVE COPY:
// Return a fresh clone so getBirthDate().setTime(0) cannot alter the internal field
return new Date(birthDate.getTime());
}
}
🟡 Middle Level
1. Protecting Collections in Modern Java (Java 10+)
Prior to Java 10, defensive copying of collections required two steps:
// Pre-Java 10 pattern:
this.items = Collections.unmodifiableList(new ArrayList<>(items));
In modern Java (10+), the recommended approach uses List.copyOf(), Set.copyOf(), and Map.copyOf():
public final class ShoppingCart {
private final List<String> itemSkus;
public ShoppingCart(List<String> itemSkus) {
// List.copyOf validates for nulls and creates an unmodifiable, compact copy
this.itemSkus = (itemSkus == null) ? List.of() : List.copyOf(itemSkus);
}
public List<String> getItemSkus() {
// Safe to return directly because List.copyOf guarantees the list is unmodifiable
return itemSkus;
}
}
2. Protecting Arrays
Arrays in Java are always mutable; declaring an array reference final only freezes the pointer. For arrays, invoking .clone() or Arrays.copyOf() is mandatory on both ingress and egress:
public final class SecurityToken {
private final byte[] tokenBytes;
public SecurityToken(byte[] tokenBytes) {
Objects.requireNonNull(tokenBytes);
// Ingress copy
this.tokenBytes = tokenBytes.clone();
}
public byte[] getTokenBytes() {
// Egress copy
return tokenBytes.clone();
}
}
3. Deep Defensive Copying for Collections of Objects
If a collection contains mutable domain objects, List.copyOf() is insufficient because it only clones the object references, leaving the element objects themselves vulnerable to mutation:
public record Order(Long id, List<OrderItem> items) {
public Order {
// Deep copy: Clones EVERY individual element via its copy constructor
items = (items == null) ? List.of() : items.stream()
.map(item -> new OrderItem(item.getSku(), item.getPrice(), item.getQuantity()))
.toList();
}
}
🔴 Senior Level
TOCTOU Vulnerability: Defensive Copy BEFORE Validation
A critical principle of concurrent security states: always create the defensive copy of constructor parameters BEFORE executing validation checks, never after:
// ❌ VULNERABLE TO TOCTOU ATTACK (Validates before copying):
public UserPeriod(Date start, Date end) {
if (start.after(end)) { // 1. Time-of-Check (Validates original object)
throw new IllegalArgumentException("start must be before end");
}
// 💥 A concurrent thread can call start.setTime(System.currentTimeMillis() + 1000000) right here!
this.start = new Date(start.getTime()); // 2. Time-of-Use (Copies compromised state!)
this.end = new Date(end.getTime());
}
Secure Implementation (Protected Against TOCTOU):
// ✅ ABSOLUTELY SECURE (Copies BEFORE validation):
public UserPeriod(Date start, Date end) {
// 1. Take private defensive snapshots first:
Date copyStart = new Date(start.getTime());
Date copyEnd = new Date(end.getTime());
// 2. Validate the private snapshots:
if (copyStart.after(copyEnd)) {
throw new IllegalArgumentException("start must be before end");
}
// 3. Assign validated snapshots:
this.start = copyStart;
this.end = copyEnd;
}
Polymorphic Subclassing Attack on clone()
Joshua Bloch in Effective Java (Item 50) warns: never use clone() to make defensive copies of parameters whose types can be extended by untrusted code:
- If a method accepts
Date dateand invokesthis.date = (Date) date.clone();, an attacker can pass a malicious subclass:public class MaliciousDate extends Date { private Date stolenRef; @Override public Object clone() { this.stolenRef = this; // Captures internal reference return this; // Returns itself rather than a new copy! } } - The defensive copy is bypassed! Using
clone()is safe only for arrays (array.clone()), because Java arrays have final runtime types that cannot be subclassed.
Performance: Persistent Data Structures (Structural Sharing)
Creating deep defensive copies of size $N$ incurs $O(N)$ allocation and memory copy overhead. In high-frequency trading or high-throughput state machines:
- Instead of repeatedly cloning standard
java.util.Listcollections, use persistent data structure libraries (Vavr, PCollections). - Persistent collections utilize Hash Array Mapped Tries (HAMT) to achieve immutability with Structural Sharing, executing mutations in $O(\log_{32} N)$ time while allocating minimal garbage.
🎯 Interview Cheat Sheet
Defensive Copying Strategy Matrix
| Field Type | Ingress Protection (Constructor) | Egress Protection (Getter) |
| :— | :— | :— |
| Collections (List, Set, Map) | List.copyOf(input) (Java 10+) | Return this.list directly (already unmodifiable) |
| Arrays (byte[], int[]) | input.clone() | this.array.clone() |
| Legacy Date (Date) | new Date(input.getTime()) | new Date(this.date.getTime()) (or migrate to Instant) |
| Collection of Mutable Objects | input.stream().map(Item::new).toList() | Return unmodifiable list of cloned elements |
4 Tricky Interview Questions
1. Why must defensive copying in a constructor be executed BEFORE parameter validation rather than after?
Answer: To eliminate the Time-of-Check to Time-of-Use (TOCTOU) window of vulnerability. If validation occurs before copying, a concurrent thread holding a reference to the parameter can modify its state between the check and the copy. The constructor would then copy the modified, invalid data into the immutable object, permanently corrupting its class invariants. Making the copy first and validating the local copy ensures no external thread can interfere.
2. Why does Joshua Bloch prohibit clone() for defensive copying of general objects, but recommend it for arrays?
Answer: The clone() method is virtual. If an argument type is non-final (like java.util.Date), a caller can pass an instance of a malicious subclass (MaliciousDate extends Date) that overrides clone() to return a shared reference or leak the object to a registry. For Java arrays, however, the runtime type is sealed and final within the JVM; it is impossible to override array.clone(), making it 100% safe and highly optimized by HotSpot intrinsics.
3. Does List.copyOf() optimize memory allocation if passed a collection already created via List.of()?
Answer: Yes. List.copyOf(Collection<? extends E> coll) explicitly checks if (coll instanceof java.util.ImmutableCollections.AbstractImmutableCollection). If the argument is already an unmodifiable platform collection, List.copyOf performs zero memory allocation and returns the exact same reference immediately: return (List<E>) coll;.
4. If we defensively copy a list using this.items = List.copyOf(items) where elements are StringBuilder instances, is the class deeply immutable?
Answer: No, it is only shallowly immutable. List.copyOf() freezes only the list container (elements cannot be added, removed, or replaced). However, the elements inside the list are mutable StringBuilder instances. A caller executing order.getItems().get(0).append("CORRUPTED") modifies the character buffer in heap memory, mutating the state of the “immutable” object. True deep immutability requires mapping each StringBuilder to an immutable String (Object::toString) or cloning each element.
Red Flags (DO NOT Say)
- ❌ “Wrapping a list in
Collections.unmodifiableList(list)in the constructor is enough.” (It is only a view; external modifications to the original list will mutate internal state). - ❌ “Validate parameter integrity first, then create defensive copies.” (Severe security vulnerability exposing the class to multi-threaded TOCTOU race conditions).
- ❌ “Use
clone()as the default mechanism for defensive copying.” (clone()can be hijacked by subclasses for any non-final class; use copy constructors instead). - ❌ “Calling
List.copyOf()makes all objects inside the list immutable.” (It only freezes the collection structure; mutable elements inside remain fully mutable).
Related Topics
- Is It Enough to Make All Fields final for Immutability — Shallow vs deep immutability
- What is Defensive Copy — Defensive copying mechanics
- When Should You Make Defensive Copy — Architectural decision criteria
- How to Protect a Collection from Modification — Unmodifiable collections
- Difference Between Shallow Copy and Deep Copy — Object graph duplication