What is Defensive Copy
A Defensive Copy (protective copy) is a design idiom where an independent duplicate of a mutable object is created to isolate the internal state of a class from external modific...
🟢 Junior Level
30-Second Summary
A Defensive Copy (protective copy) is a design idiom where an independent duplicate of a mutable object is created to isolate the internal state of a class from external modifications.
The primary purpose of a defensive copy is to guarantee class invariants and immutability regardless of how external caller code behaves. It is implemented via the Two-Point Rule:
- Point 1 (Ingress / Constructor): Create an independent copy of mutable input parameters so external callers cannot mutate internal state via their retained references.
- Point 2 (Egress / Getter): Return an independent copy (or an unmodifiable view) of internal mutable objects so callers cannot corrupt state via references obtained from accessors.
Real-World Analogy
Consider a notarized photocopy of an identification document:
- When a service provider requests your passport, you provide a photocopy.
- If the provider stamps or writes notes on the photocopy, your original passport in your pocket remains completely pristine and unmodified.
Practical Example: Appointment Time (java.util.Date)
import java.util.Date;
public final class Appointment {
private final String title;
private final Date time; // java.util.Date is mutable via setTime()!
public Appointment(String title, Date time) {
this.title = title;
// POINT 1: Ingress defensive copy
this.time = new Date(time.getTime());
}
public String getTitle() {
return title;
}
public Date getTime() {
// POINT 2: Egress defensive copy
return new Date(time.getTime());
}
}
🟡 Middle Level
1. Shallow vs. Deep Defensive Copying
When creating defensive copies of collections, element mutability determines the required approach:
- Shallow Defensive Copy: Allocates a new container, but elements inside refer to the original object instances. Sufficient if elements are inherently immutable (
String,Integer,UUID):// Sufficient because String is immutable: this.tags = List.copyOf(tags); - Deep Defensive Copy: Recursively clones both the container and every nested mutable element. Mandatory if elements have mutable state:
// Deep copy of mutable OrderItem elements via copy constructor: this.items = items.stream() .map(item -> new OrderItem(item.getSku(), item.getPrice())) .toList();
2. Defensive Copy vs. Unmodifiable Wrapper (View)
A frequent interview confusion is Collections.unmodifiableList() vs. List.copyOf():
Collections.unmodifiableList(original)is merely an unmodifiable view (wrapper) with $O(1)$ time and memory overhead. It blocks mutation methods on the wrapper itself, but does not protect against modifications to the original list. If external code modifiesoriginal, the view instantly reflects those changes!List.copyOf(original)(Java 10+) is a genuine defensive copy with $O(N)$ allocation. It severs all ties to the original collection and returns an unmodifiable platform collection.
List<String> original = new ArrayList<>(List.of("A", "B"));
List<String> view = Collections.unmodifiableList(original);
List<String> copy = List.copyOf(original);
original.add("C");
System.out.println(view); // [A, B, C] — THE VIEW MUTATED!
System.out.println(copy); // [A, B] — THE DEFENSIVE COPY PRESERVED STATE!
3. When Defensive Copying is NOT Needed
- Inherently Immutable Types: Primitives,
String,BigDecimal, and modernjava.timeclasses (LocalDate,Instant). - Trusted Internal Components: Package-private or private subsystem components where callers and callees cooperate and are guaranteed not to mutate shared state for extreme performance.
🔴 Senior Level
TOCTOU Vulnerability: Defensive Copy BEFORE Validation
In concurrent applications, parameter validation performed prior to defensive copying exposes an exploitation window known as Time-of-Check to Time-of-Use (TOCTOU):
// ❌ VULNERABLE TO TOCTOU ATTACK:
public TimeInterval(Date start, Date end) {
if (start.compareTo(end) > 0) { // Time-of-Check
throw new IllegalArgumentException();
}
// 💥 VULNERABILITY WINDOW: Another thread can call start.setTime(Long.MAX_VALUE) right here!
this.start = new Date(start.getTime()); // Time-of-Use (Copies compromised value!)
this.end = new Date(end.getTime());
}
// ✅ SECURE IMPLEMENTATION: Defensive copy BEFORE validation!
public TimeInterval(Date start, Date end) {
// 1. First take defensive snapshot in local variables:
Date localStart = new Date(start.getTime());
Date localEnd = new Date(end.getTime());
// 2. Validate the private, isolated snapshot:
if (localStart.compareTo(localEnd) > 0) {
throw new IllegalArgumentException();
}
this.start = localStart;
this.end = localEnd;
}
JMM §17.5 Guarantees on Defensive Copies
When a defensive copy is instantiated inside a constructor and assigned to a final field:
- All objects allocated during the copy process (backing arrays, wrapper nodes) are treated as part of the enclosing object’s initialization.
- The JMM Freeze Action executes a
StoreStorebarrier at constructor exit, guaranteeing that all memory writes to the defensive copy are flushed before the object reference becomes visible to other threads. - Concurrent readers are guaranteed to observe the fully initialized defensive copy without requiring synchronization locks.
JIT Allocation Elimination: Escape Analysis & Scalar Replacement
Defensive copying in accessors introduces $O(N)$ allocation overhead. However, the HotSpot C2 compiler can optimize this away:
- Consider a caller that reads a property for immediate computation:
long epoch = appointment.getTime().getTime(); - If the getter is inlined, C2 performs Escape Analysis.
- Detecting that the defensive copy of
Datenever escapes the local stack frame, C2 executes Scalar Replacement: the heap allocation ofDateis canceled entirely, and its fields are mapped directly to CPU registers, reducing defensive copying overhead to zero!
🎯 Interview Cheat Sheet
Summary of Defensive Copying Strategies
| Object Type | Ingress (Constructor) | Egress (Getter) | Complexity |
| :— | :— | :— | :— |
| Collection (Immutable Elements) | List.copyOf(input) | Return this.list directly | $O(N)$ Ingress / $O(1)$ Egress |
| Collection (Mutable Elements) | stream().map(Item::new).toList() | Return unmodifiable list of clones | $O(N)$ Deep Copy |
| Arrays (byte[], int[]) | input.clone() | this.array.clone() | $O(N)$ Ingress / $O(N)$ Egress |
| Legacy java.util.Date | new Date(input.getTime()) | new Date(this.date.getTime()) | $O(1)$ Allocation |
| Modern java.time.Instant | Direct assignment (immutable) | Direct return | $O(1)$ Zero Allocation |
4 Tricky Interview Questions
1. In what scenario is returning Collections.unmodifiableList(this.list) from a getter 100% safe without per-call allocation overhead?
Answer: This approach is 100% safe if this.list was securely copied during construction (via new ArrayList<>(input)), assigned to a private final field, never mutated by any method, and wrapped once in the constructor into a dedicated field:
this.unmodifiableView = Collections.unmodifiableList(this.list);
The getter simply returns this.unmodifiableView in $O(1)$ time with zero allocation overhead and zero risk of external modification.
2. Does List.copyOf(input) in a constructor protect against a malicious List implementation?
Answer: Yes. List.copyOf() is explicitly engineered against untrusted collection implementations. If the incoming collection is not an internal trusted JDK class (ImmutableCollections), List.copyOf() extracts elements into a private flat array via toArray() and wraps them in sealed, unmodifiable JDK classes (List12, ListN). Any malicious subclass overriding methods like get() or iterator() is neutralized.
3. Why is array.clone() mandatory for array fields even when declared private final?
Answer: In Java, final on an array only freezes the reference pointer; individual array elements can still be directly mutated by index (array[0] = 999;). Because arrays have direct indexing without accessor methods, cloning the array on both entry and exit is the only way to isolate the internal array from external mutation.
4. Can the C2 JIT compiler eliminate the allocation cost of a defensive copy returned from a getter? Under what condition?
Answer: Yes, via Escape Analysis and Scalar Replacement. If the getter is inlined into the caller and C2 proves that the returned defensive copy does not escape the caller’s stack frame (e.g., calling obj.getArray().length or obj.getDate().getTime()), C2 eliminates heap allocation completely and reads values directly from CPU registers or stack memory.
Red Flags (DO NOT Say)
- ❌ “Using
Collections.unmodifiableList()in a constructor creates an independent defensive copy.” (It is an unmodifiable view over the original list; modifying the original mutates the view). - ❌ “Perform parameter validation first, then create the defensive copy.” (Exposes the class to multi-threaded TOCTOU race conditions).
- ❌ “Defensive copies are needed for
StringandLocalDatefields.” (Immutable objects never need defensive copies; copying them wastes CPU and RAM). - ❌ “Declaring an array field
finalmakes its elements immutable.” (finalonly locks the array reference; elements can be modified freely).
Related Topics
- Is It Enough to Make All Fields final for Immutability — Shallow vs deep immutability
- What to Do If Class Field References a Mutable Object — Mutable field handling
- When Should You Make Defensive Copy — Architectural decision criteria
- How to Protect a Collection from Modification — Collection protection
- Difference Between Shallow Copy and Deep Copy — Copy depth