Loading content...
We've secured our inputs with defensive copies. But we've only closed one of two doors. Every getter method that returns a reference to mutable internal state is an open door through which external code can enter and modify our object.
Consider our improved Period class: we now copy input parameters in the constructor. But the getStart() and getEnd() methods still return direct references to our internal Date objects. Callers can call getStart() and then mutate the returned Date, corrupting our Period's invariants.
This page addresses the second attack vector: how to protect your objects by creating defensive copies when returning internal references through getters.
By the end of this page, you will master defensive output copying: how to return safe references from getters, when to return copies versus unmodifiable views, strategies for collections and arrays, and how to maintain performance while protecting encapsulation.
Let's recall how getters can be exploited to breach encapsulation. Even with defensive input copying, our Period class remains vulnerable:
12345678910111213141516171819202122232425262728293031323334
// Period with defensive INPUT copying (still vulnerable)public final class Period { private final Date start; private final Date end; public Period(Date start, Date end) { // Input is protected via defensive copy this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException("Invalid period"); } } // STILL VULNERABLE: Returns internal reference public Date getStart() { return start; } public Date getEnd() { return end; }} // ATTACK through getter:Period period = new Period( new Date(2024, 0, 1), // Jan 1, 2024 new Date(2024, 11, 31) // Dec 31, 2024); // Get internal reference and mutate itDate internalStart = period.getStart();internalStart.setYear(2030); // Modifies Period's internal state! // Worse: break the invariantperiod.getEnd().setYear(2000); // Now end < start! System.out.println(period); // Period[2030 to 2000] — INVALID!The problem:
When getStart() returns the internal start Date reference, it grants the caller direct access to modify Period's internal state. The caller now has a reference to the exact same object that Period holds. Any mutation through that reference immediately affects the Period.
Memory model after getter call:
1234567891011121314
HEAP MEMORY AFTER: Date start = period.getStart();┌─────────────────────────────────────────────────────────────┐│ ││ Period's internal Date @0x2001 ││ ┌─────────────────┐ ││ │ time: 1704067200 │ ││ └─────────────────┘ ││ ▲ ▲ ││ │ │ ││ period.start start (from getter) ││ (internal) (caller's variable) ││ ││ SAME object! Caller can mutate Period's internals! │└─────────────────────────────────────────────────────────────┘A getter method that returns a mutable internal object is functionally equivalent to making that field public. The 'private' keyword provides an illusion of encapsulation, but the actual protection is zero. Anyone calling the getter can modify the internal state.
The fix mirrors what we did for inputs: return a copy instead of the original. This severs the reference link, giving callers an independent object they can mutate freely without affecting our internal state.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
/** * Period with COMPLETE defensive copying. * Now truly immutable and encapsulation-safe. */public final class Period { private final Date start; private final Date end; public Period(Date start, Date end) { // Defensive copy on INPUT this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (this.start.compareTo(this.end) > 0) { throw new IllegalArgumentException( "Start date must not be after end date" ); } } // Defensive copy on OUTPUT public Date getStart() { return new Date(start.getTime()); // Return copy! } public Date getEnd() { return new Date(end.getTime()); // Return copy! } @Override public String toString() { return "Period[" + start + " to " + end + "]"; }} // NOW ALL ATTACKS FAIL: // Attack 1: Through constructor parameter — BLOCKEDDate inputStart = new Date();Date inputEnd = new Date(inputStart.getTime() + 86400000L);Period period = new Period(inputStart, inputEnd);inputStart.setYear(2050); // Only affects inputStart, not period // Attack 2: Through getter — BLOCKEDDate retrievedStart = period.getStart();retrievedStart.setYear(2050); // Only affects the copy, not period // period is completely isolated and truly immutable!Complete encapsulation achieved:
With both input and output defensive copying:
A class that defensively copies both input and output for all mutable internal state is effectively immutable, even if it contains mutable fields. No external code can observe or cause state changes. This is the gold standard for thread-safe, reliable components.
Collections require special consideration because they're commonly returned from getters. There are three main strategies for protecting internal collections, each with different tradeoffs.
Return a mutable copy. The caller can modify their copy freely; our internal collection is unaffected.
Pros:
Cons:
1234567891011121314151617181920212223
public class ShoppingCart { private final List<Item> items = new ArrayList<>(); public void addItem(Item item) { items.add(new Item(item)); // Defensive copy on input } // Return a mutable copy public List<Item> getItems() { // Shallow copy if Item is immutable return new ArrayList<>(items); // Deep copy if Item is mutable // return items.stream() // .map(Item::new) // .collect(Collectors.toList()); }} // Caller gets independent copyList<Item> myItems = cart.getItems();myItems.clear(); // Only affects myItemscart.getItems().size(); // Still has all items| Strategy | Caller Can Modify? | Sees Internal Changes? | Memory Cost | Best For |
|---|---|---|---|---|
| Mutable Copy | Yes | No | O(n) per call | When caller needs to manipulate results |
| Unmodifiable View | No (throws) | Yes | O(1) | When caller should see live state |
| Immutable Copy | No (throws) | No | O(n) per call | Safety + isolation (most cases) |
Arrays are particularly tricky because Java provides no built-in unmodifiable array wrapper. Every array is mutable, and you cannot prevent callers from modifying array elements. Your only option is to return a copy.
1234567891011121314151617181920212223242526272829303132333435
public class Statistics { private final double[] values; public Statistics(double[] values) { // Defensive copy on input this.values = values.clone(); } // WRONG: Exposes internal array public double[] getValuesWrong() { return values; // Caller can modify! } // CORRECT: Return defensive copy public double[] getValues() { return values.clone(); // Safe } // ALTERNATIVE: Return as immutable List (Java 9+) public List<Double> getValuesAsList() { return Arrays.stream(values) .boxed() .collect(Collectors.toUnmodifiableList()); }} // Attack on wrong implementation:Statistics stats = new Statistics(new double[]{1.0, 2.0, 3.0});double[] leaked = stats.getValuesWrong();leaked[0] = 999.0; // Corrupts internal state! // Safe usage with correct implementation:double[] copy = stats.getValues();copy[0] = 999.0; // Only affects the copystats.getValues()[0]; // Still 1.0Because arrays cannot be made unmodifiable, consider using List<T> internally instead of T[]. Lists can be wrapped with Collections.unmodifiableList() for efficient read-only access. Arrays force you to copy on every access.
Arrays of Objects: Double Trouble
For arrays of mutable objects, you need deep copying just as with collections:
12345678910111213141516171819202122
public class Roster { private final Player[] players; public Roster(Player[] players) { // Deep copy on input this.players = Arrays.stream(players) .map(Player::new) .toArray(Player[]::new); } // WRONG: Shallow copy — callers can modify Player objects public Player[] getPlayersShallow() { return players.clone(); // New array, same Player objects! } // CORRECT: Deep copy — fully isolated public Player[] getPlayersDeep() { return Arrays.stream(players) .map(Player::new) // Copy each Player .toArray(Player[]::new); }}Recall that we warned against using clone() for defensive input copying because a malicious subclass could override clone(). For output copying, clone() is acceptable.
Why the difference?
For input, you're calling clone() on an object provided by (potentially untrusted) external code. They control the implementation.
For output, you're calling clone() on your own internal object. You know exactly what type it is because you created it. There's no opportunity for subclass mischief.
12345678910111213141516171819202122
public class DateRange { private final Date start; // You created this; it's definitely a Date private final Date end; public DateRange(Date start, Date end) { // INPUT: Don't use clone — use copy constructor this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); } public Date getStart() { // OUTPUT: clone() is safe — you know this.start is a Date return (Date) start.clone(); // Or still use copy constructor (also fine): // return new Date(start.getTime()); } public Date getEnd() { return (Date) end.clone(); }}While clone() is safe for output, many developers prefer to use copy constructors consistently for both input and output. This reduces cognitive load and makes the code more uniform. Either approach is correct.
If a getter is called frequently and the internal state rarely changes, creating a new copy on every call wastes memory and CPU. A common optimization is to cache the defensive copy and invalidate it when the internal state changes.
12345678910111213141516171819202122232425262728293031323334
public class DataSet { private final List<Point> points = new ArrayList<>(); // Cached defensive copy private List<Point> cachedPoints = null; public void addPoint(Point p) { points.add(new Point(p)); cachedPoints = null; // Invalidate cache } public void removePoint(int index) { points.remove(index); cachedPoints = null; // Invalidate cache } public List<Point> getPoints() { // Create cached copy only if needed if (cachedPoints == null) { cachedPoints = List.copyOf(points); } return cachedPoints; // Return immutable cached copy }} // Multiple calls return same cached instance (efficient)List<Point> first = dataSet.getPoints();List<Point> second = dataSet.getPoints();// first == second (same object, until addPoint/removePoint called) // After mutation, cache is invalidateddataSet.addPoint(new Point(1, 2));List<Point> third = dataSet.getPoints(); // Fresh copy created// third != first (new cached instance)The simple caching pattern above is NOT thread-safe. In multithreaded environments, you need synchronization or lock-free patterns. For simple cases, accepting the allocation cost of copying on every call is often preferable to the complexity of thread-safe caching.
Thread-safe cached copy pattern:
123456789101112131415161718192021222324252627282930313233
public class ThreadSafeDataSet { private final List<Point> points = new CopyOnWriteArrayList<>(); private volatile List<Point> cachedPoints = null; public synchronized void addPoint(Point p) { points.add(new Point(p)); cachedPoints = null; } public List<Point> getPoints() { List<Point> result = cachedPoints; if (result == null) { synchronized (this) { result = cachedPoints; if (result == null) { cachedPoints = result = List.copyOf(points); } } } return result; }} // Or simply: avoid caching in concurrent contextspublic class SimpleConcurrentDataSet { private final List<Point> points = Collections.synchronizedList(new ArrayList<>()); public List<Point> getPoints() { synchronized (points) { return List.copyOf(points); // Fresh copy each time } }}For large objects or collections where copying is expensive and reads are much more common than writes, you can use lazy copying (copy-on-write). The idea: share the data until someone tries to write, then make a copy.
This is an advanced pattern used in performance-critical code. Standard library classes like CopyOnWriteArrayList implement this pattern.
123456789101112131415161718192021222324252627282930313233
/** * Conceptual copy-on-write wrapper. * Real implementations are more sophisticated. */public class CopyOnWriteData<T> { private T data; private boolean shared = false; public CopyOnWriteData(T data, Copier<T> copier) { this.data = data; this.copier = copier; } // Mark as shared when giving out reference public T getForRead() { shared = true; return data; // Safe for reading } // Copy before modifying if shared public T getForWrite() { if (shared) { data = copier.copy(data); // Make private copy shared = false; } return data; }} // Usage pattern:// Multiple readers share the same data (efficient)// First write triggers a copy (cost deferred until needed)// Subsequent writes operate on private copy (no more copying)Copy-on-write is appropriate when: (1) data is large and expensive to copy, (2) reads vastly outnumber writes, and (3) you can afford the complexity. For most applications, simple eager defensive copying is clearer and sufficient.
We've now closed the second door. With defensive copying on both input and output, your objects can achieve true encapsulation. Let's consolidate the key principles:
What's next:
We've covered both input and output defensive copying. But when should you actually apply these techniques? The next page provides practical guidance on when defensive copying is necessary versus when it's overkill, helping you make informed decisions in real-world code.
You now know how to protect your objects from external mutation through getter methods. Combined with input protection, you can create truly encapsulated objects. Next, we'll learn when to apply these techniques and when simpler approaches suffice.