Defensive Copying: Preserving Object Integrity
Defensive Copy on Input
Read aloud is unavailable in this browser.
Securing the Entry Points
In the previous page, we saw how mutable references can breach encapsulation through two attack vectors: captured inputs and escaped outputs. Now we address the first vector: how to protect your objects from mutable parameters passed into constructors and setters.
When external code passes an object into your constructor or setter, they retain a reference to that object. Without defensive measures, they can use that reference to modify your internal state at any time, from anywhere, completely bypassing your validation logic.
The solution is elegant in principle: don't store the object you receive; store a copy of it. This severs the reference link between the caller's object and your internal state. The caller can mutate their object all they want—your copy remains untouched.
By the end of this page, you will master the technique of defensive input copying: when to apply it, how to implement it correctly for various object types, the critical importance of copy-before-validate ordering, and how to handle nested mutable objects through deep copying.
The Defensive Copy Pattern
The defensive copy pattern for input is straightforward: when receiving a mutable object parameter, create an independent copy and store the copy instead of the original.
Let's fix our vulnerable Period class from the previous page:
123456789101112131415161718192021222324252627282930313233343536373839404142
/** * Period with defensive input copying. * Now truly protects its invariants. */public final class Period { private final Date start; private final Date end; public Period(Date start, Date end) { // STEP 1: Make defensive copies FIRST this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); // STEP 2: Validate AFTER copying if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException( "Start date must not be after end date" ); } } // Getters still need fixing (covered in next page) public Date getStart() { return start; // Still vulnerable - we'll fix this next } public Date getEnd() { return end; // Still vulnerable - we'll fix this next }} // NOW THE ATTACK FAILS:Date start = new Date();Date end = new Date(start.getTime() + 86400000L); Period period = new Period(start, end); // Attacker tries to corrupt the Period:start.setTime(end.getTime() + 86400000L); // But Period is safe! It has independent copies.// period.start still points to its own Date objectWhat changed:
- Instead of
this.start = start, we usethis.start = new Date(start.getTime()) - This creates a new Date object with the same value but independent identity
- The caller's reference and our reference now point to different objects
- Mutations through the caller's reference don't affect our Period
The memory model now looks like:
12345678910111213
HEAP MEMORY AFTER DEFENSIVE COPY:┌─────────────────────────────────────────────────────────────┐│ Caller's Date @0x1001 Period's Date @0x2001 ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ time: 1704067200 │ │ time: 1704067200 │ ││ └─────────────────┘ └─────────────────┘ ││ ▲ ▲ ││ startDate period.start ││ (caller) (Period) ││ ││ DIFFERENT objects! Caller mutations don't affect Period. ││ Caller can modify @0x1001; Period's @0x2001 stays safe. │└─────────────────────────────────────────────────────────────┘Defensive copying severs the reference link between external code and internal state. After copying, you and the caller have references to completely independent objects. Neither can affect the other. This is what true encapsulation looks like.
Critical: Copy BEFORE Validate
Notice in our fixed Period class that we copy the parameters before validating them. This ordering is critical and often overlooked. Let's see why.
1234567891011121314151617181920
// WRONG: Validate-then-copy (TOCTOU vulnerability)public Period(Date start, Date end) { // STEP 1: Validate first if (start.compareTo(end) > 0) { // ← Check happens here throw new IllegalArgumentException("Invalid period"); } // STEP 2: Copy after validation // ⚠️ WINDOW OF VULNERABILITY: Another thread could // modify 'start' or 'end' between validation and copy! this.start = new Date(start.getTime()); // ← Use happens here this.end = new Date(end.getTime());} // ATTACK SCENARIO (in multithreaded environment):// Thread 1: Calls Period constructor with valid dates// Thread 1: Passes validation check (start < end)// Thread 2: Modifies 'start' to be after 'end' ← Between check and use!// Thread 1: Creates copies of now-invalid dates// Result: Period with start > end exists!Validating before copying creates a TOCTOU (Time-of-Check to Time-of-Use) vulnerability. There's a window between when you check the values and when you use them. In that window, an attacker (or concurrent thread) can modify the values, causing your validation to be useless.
The correct order: Copy-then-Validate
By copying first, we capture the values at a single point in time. Any subsequent modifications to the original objects are irrelevant—we validate and store our independent copies.
12345678910111213141516171819
// CORRECT: Copy-then-validate (TOCTOU-safe)public Period(Date start, Date end) { // STEP 1: Copy immediately - capture values atomically this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); // STEP 2: Validate our copies // These copies cannot change; we're validating what we stored if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException( "Start date must not be after end date" ); }} // NOW ATTACK FAILS:// Even in multithreaded scenario, once we copy, the values// are locked in. Any external modification happens after// we've already captured our independent copies.- •Atomic capture: The copy operation captures the value at a specific instant. After copying, external changes are irrelevant.
- •Validate what you store: By validating the copies (not the originals), you're validating exactly what your object will contain.
- •No window of vulnerability: There's no gap between validation and storage where values could change.
- •Defense in depth: Even without threading, copy-first is safer because it prevents subtle bugs where validation and storage see different values.
Techniques for Creating Defensive Copies
Different types of objects require different copying techniques. Let's examine the most common approaches and when to use each.
Copy constructors are the preferred approach when available. A copy constructor takes an instance of the same (or compatible) type and creates an independent copy.
Advantages:
- No reliance on clone()
- Works with final classes
- Explicit and readable
- Allows copying into different types (e.g., ArrayList from any Collection)
Common examples:
12345678910111213141516171819202122232425262728
// Date copy constructor (using getTime)Date copy = new Date(original.getTime()); // Collection copy constructorsList<String> copy = new ArrayList<>(original);Set<Integer> copy = new HashSet<>(original);Map<K,V> copy = new HashMap<>(original); // Custom class with copy constructorpublic class Point { private final int x, y; public Point(int x, int y) { this.x = x; this.y = y; } // Copy constructor public Point(Point other) { this.x = other.x; this.y = other.y; }} // Usagepublic void setLocation(Point p) { this.location = new Point(p); // Defensive copy}Shallow vs Deep Copying: Protecting Nested Objects
A critical distinction in defensive copying is between shallow copies and deep copies. Understanding this difference is essential when your objects contain collections or other mutable objects.
- •Creates new container (list, array, etc.)
- •Copies references to existing elements
- •Elements themselves are shared
- •Adequate if elements are immutable
- •Insufficient if elements are mutable
- •Creates new container
- •Also creates copies of each element
- •Elements are independent copies
- •Required for mutable elements
- •More expensive but fully protective
12345678910111213141516171819202122232425262728293031323334353637
// SHALLOW COPY: New list, same elementspublic class Team { private final List<Player> players; public Team(List<Player> players) { // Shallow copy - INSUFFICIENT if Player is mutable! this.players = new ArrayList<>(players); }} // Why shallow copy fails for mutable elements:List<Player> originalList = new ArrayList<>();Player alice = new Player("Alice", 100);originalList.add(alice); Team team = new Team(originalList); // Attack through original reference:alice.setScore(0); // Modifies player inside Team! // Because Team has reference to SAME Player object // DEEP COPY: New list with copies of each elementpublic class Team { private final List<Player> players; public Team(List<Player> players) { // Deep copy - create independent copy of each player this.players = new ArrayList<>(); for (Player p : players) { this.players.add(new Player(p)); // Copy each element } }} // Now alice.setScore(0) doesn't affect Team's internal players// because Team has its own copiesDeep copy is required whenever your collection contains mutable elements AND you need to protect against modification of those elements. If elements are immutable (String, Integer, LocalDate, etc.), shallow copy is sufficient—the elements themselves cannot be modified, so sharing them is safe.
Practical guidelines:
| Element Type | Copy Strategy |
|---|---|
| Primitives (int[], double[]) | Shallow copy (primitives have value semantics) |
| Immutable objects (String, LocalDate, Integer) | Shallow copy (cannot be mutated anyway) |
| Mutable objects with copy constructors | Deep copy using copy constructor |
| Nested collections | Recursive deep copy |
| Objects without copy mechanisms | Consider making them immutable or implement Cloneable carefully |
Implementing Deep Copies for Collections
Deep copying collections requires iterating through elements and copying each one. Let's see patterns for various collection types.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Deep copy of List<MutableObject>public List<Player> deepCopyPlayers(List<Player> original) { return original.stream() .map(Player::new) // Using copy constructor .collect(Collectors.toList());} // Deep copy of Map with mutable valuespublic Map<String, Player> deepCopyPlayerMap(Map<String, Player> original) { return original.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, e -> new Player(e.getValue()) // Copy each value ));} // Deep copy of nested collection: List<List<Player>>public List<List<Player>> deepCopyNestedList(List<List<Player>> original) { return original.stream() .map(innerList -> innerList.stream() .map(Player::new) .collect(Collectors.toList())) .collect(Collectors.toList());} // Full example: Class with deep defensive copypublic class League { private final String name; private final List<Team> teams; private final Map<String, Date> eventDates; public League(String name, List<Team> teams, Map<String, Date> eventDates) { // String is immutable - direct assignment OK this.name = name; // Deep copy teams (Team is mutable) this.teams = teams.stream() .map(Team::new) // Team must have copy constructor .collect(Collectors.toCollection(ArrayList::new)); // Deep copy Map with mutable Date values this.eventDates = eventDates.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, // String keys are immutable e -> new Date(e.getValue().getTime()) // Copy Date values )); }}For deep copying to work, every level of mutable objects must support copying. If Team contains mutable Player objects, Team's copy constructor must also deep-copy its players. This creates a chain of copy constructors throughout your mutable object hierarchy.
Handling Null Inputs
Defensive copying must handle null inputs gracefully. The strategy depends on whether null is a valid input for your use case.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
// APPROACH 1: Null not allowed - fail fastpublic Period(Date start, Date end) { // Check for null before copying // (Objects.requireNonNull provides better error messages) this.start = new Date( Objects.requireNonNull(start, "start date must not be null").getTime() ); this.end = new Date( Objects.requireNonNull(end, "end date must not be null").getTime() ); // Validate after copying if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException("Invalid period"); }} // APPROACH 2: Null allowed (optional field)public class Event { private final String name; private final Date deadline; // Optional, may be null public Event(String name, Date deadline) { this.name = Objects.requireNonNull(name); // Defensive copy only if not null this.deadline = (deadline == null) ? null : new Date(deadline.getTime()); }} // APPROACH 3: Convert null to default valuepublic class Order { private final List<Item> items; public Order(List<Item> items) { // Convert null to empty list, then copy this.items = (items == null) ? new ArrayList<>() : new ArrayList<>(items); }} // Use Optional for clarity when null is meaningfulpublic class Contract { private final Date signedDate; // Required private final Date expirationDate; // Optional public Contract(Date signedDate, Optional<Date> expirationDate) { this.signedDate = new Date( Objects.requireNonNull(signedDate).getTime() ); this.expirationDate = expirationDate .map(d -> new Date(d.getTime())) .orElse(null); }}- •Fail fast: If null is invalid, check and throw immediately with a descriptive message.
- •Use Objects.requireNonNull(): It provides clear semantics and good error messages.
- •Defensive copy null-safe: Check for null before attempting to copy to avoid NullPointerException.
- •Document null behavior: Make it clear in Javadoc whether null is accepted and what it means.
- •Consider Optional: For optional parameters, Optional<T> makes the nullability explicit in the type system.
Performance Considerations
Defensive copying has costs: memory allocation for new objects and CPU time for copying data. In most cases, these costs are negligible compared to the safety benefits. However, understanding the tradeoffs helps you make informed decisions.
| Operation | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Copy simple object (Date) | O(1) | O(1) | Negligible - just allocate and copy a few fields |
| Shallow copy collection | O(n) | O(n) | n = number of elements; copies reference array only |
| Deep copy collection | O(n × c) | O(n × s) | c = cost to copy each element; s = size of element |
| Deep copy nested collection | O(n × m × c) | O(n × m × s) | n = outer size; m = inner size |
When to consider skipping defensive copies:
- •Immutable parameters: If the input is genuinely immutable (String, LocalDate, immutable records), copying is unnecessary.
- •Private/internal callers: If you control all callers and can guarantee they won't mutate passed objects, you might skip copies. Document this assumption clearly!
- •Performance-critical hot paths: With profiling evidence showing copying is a bottleneck, consider alternatives like making the input immutable or using lazy copying.
- •Transfer of ownership: If the API contract specifies that the caller 'transfers ownership' and must not use the object after passing it, copying is redundant. This pattern is risky and requires careful documentation.
Almost always, defensive copying is the right choice. The performance cost is typically insignificant, while the bugs prevented are severe and hard to debug. Only skip defensive copying when profiling proves it's necessary AND you can guarantee safety through other means.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.