Loading learning content...
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 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:
this.start = start, we use this.start = new Date(start.getTime())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.
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.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:
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}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.
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 |
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.
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); }}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:
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.
We've thoroughly covered how to protect your objects from mutable parameters. Let's consolidate the key principles:
What's next:
We've secured our inputs. But securing inputs is only half the battle—we also return references through getters. The next page covers defensive copying on output, showing how to prevent returned references from being used to mutate internal state.
You now know how to create defensive copies of input parameters to prevent external code from corrupting your object's internal state. Next, we'll learn how to protect against the second attack vector: references returned through getters.