Java Defensive Copy: When and How to Use It
java defensive copy: Learn how defensive copying in Java protects mutable state, when to apply it, and how to avoid common pitfalls in constructors, getters, and colle...
When a Java object holds a reference to a mutable field, that field can be changed from outside the object's control. Consider a simple Person class with a Date field. If the constructor stores the passed Date directly, the caller can modify that Date after construction, silently altering the Person's state. This is the core problem that java defensive copy solves: creating a copy of an object before storing or returning it, so that internal state cannot be mutated through external references.
The Problem: Shared Mutable State
Mutable objects like Date, ArrayList, HashMap, or any class with setters create hidden coupling when shared. For example:
public class Event { private final Date startTime; public Event(Date startTime) { this.startTime = startTime; } public Date getStartTime() { return startTime; } }
Here, the caller can do:
Date date = new Date(); Event event = new Event(date); date.setTime(0); // Mutates the event's internal state!
Similarly, getStartTime() returns the internal reference, so any caller can modify it. This breaks encapsulation and can lead to subtle bugs that are hard to trace. Defensive copying addresses this by never exposing the internal reference directly and never storing a reference that the caller can still mutate.
Defensive Copy in Constructors
When a constructor receives a mutable object, it should copy it before storing it. The copy must be a new instance that is independent of the original. For Date, you can use its copy constructor or clone():
public Event(Date startTime) { this.startTime = new Date(startTime.getTime()); }
Now the original Date can be modified without affecting the Event. The same principle applies to any mutable type. For a custom class, you need a copy mechanism—either a copy constructor, a static factory, or a clone() method, but clone() is often discouraged due to its shallow-copy pitfalls. A copy constructor is explicit and type-safe:
public class Address { private final String street; private final String city; public Address(Address other) { this.street = other.street; this.city = other.city; } }
When the field itself is immutable (like String), copying is unnecessary because the reference cannot be mutated. But for mutable fields, copying in the constructor is essential.
Defensive Copy in Getters
Getters that return mutable fields should return a copy, not the internal reference. This prevents the caller from modifying the object's state through the getter. For a Date field:
public Date getStartTime() { return new Date(startTime.getTime()); }
For a collection, returning a copy is more nuanced. You can return a new collection containing the same elements, but if the elements themselves are mutable, you may need a deep copy. The decision depends on whether the caller should be able to modify the elements. If the collection is meant to be read-only, an unmodifiable view is often a better choice than a full copy (see the section on alternatives).
Copying Collections and Arrays
Collections and arrays are common sources of unintended mutation. Storing a List passed to a constructor directly is dangerous:
public class Course { private final List<String> students; public Course(List<String> students) { this.students = students; // Bad: caller can modify the list } }
Instead, copy the list:
public Course(List<String> students) { this.students = new ArrayList<>(students); }
This is a shallow copy: the list structure is new, but the elements are the same references. If the elements are mutable, you must decide whether to deep-copy them. For a list of String (immutable), shallow copy is sufficient. For a list of mutable objects, you need to copy each element.
Arrays have a similar issue. Use Arrays.copyOf or clone() for primitive arrays, but for object arrays, the copy is shallow. A common pattern is:
public class Buffer { private final byte[] data; public Buffer(byte[] data) { this.data = Arrays.copyOf(data, data.length); } public byte[] getData() { return Arrays.copyOf(data, data.length); } }
For collections, returning an unmodifiable view is often preferable to copying because it avoids the cost of copying while still preventing modification. Collections.unmodifiableList(list) wraps the original list, but any attempt to modify the view throws UnsupportedOperationException. However, if the underlying list is changed later, the view reflects those changes. If you need a snapshot that is independent of the original, a copy is required.
Performance and When to Skip the Copy
Defensive copying adds runtime cost: allocation, copying elements, and potential deep-copy overhead. In performance-sensitive code, copying every mutable field on every call can be expensive. The key is to identify where the risk of mutation actually exists.
If the object is used only within a single thread and the caller never retains a reference after passing it, copying may be unnecessary. For example, a method that takes a List, processes it immediately, and does not store it can safely use the original list. But if the list is stored in a field, or returned to the caller, copying becomes important.
Another factor is the size of the data. Copying a large collection on every getter call could be a bottleneck. In such cases, consider returning an unmodifiable view instead of a copy, or redesign the API to expose only immutable data. The cost of copying is proportional to the size of the collection, so for large structures, the overhead can be significant.
Alternatives: Immutability and Unmodifiable Wrappers
Instead of copying, you can make the field itself immutable. If a class has no setters and all its fields are final and immutable, then no defensive copy is needed. For example, using LocalDate instead of Date, or List.copyOf() to create an immutable list:
public class Event { private final LocalDate startDate; public Event(LocalDate startDate) { this.startDate = startDate; // LocalDate is immutable } }
For collections, Java 9+ provides List.copyOf, Set.copyOf, and Map.copyOf which create unmodifiable copies. These are ideal when you want to guarantee that the collection cannot be modified by anyone, including the original owner. However, they are shallow copies; if the elements are mutable, the elements can still be changed.
Unmodifiable wrappers (Collections.unmodifiableList) are a lighter-weight alternative when you only need to prevent modification through the wrapper, but the underlying collection can still change. They are useful when you want to expose a read-only view without copying the data.
The choice between defensive copy, immutable fields, and unmodifiable wrappers depends on the use case:
| Approach | Protects against | Cost | Use when |
|---|---|---|---|
| Defensive copy | All external mutation | High (copy each access) | You need a snapshot independent of the original |
| Immutable field | All mutation (if truly immutable) | None at runtime | The field can be made immutable |
| Unmodifiable wrapper | Modification through the wrapper | Low (no copy) | You want a read-only view but the underlying data may change |
Common Pitfalls with Cloning
Using clone() for defensive copying is error-prone. The clone() method in Object performs a shallow copy and requires implementing Cloneable. If the class has mutable fields, the shallow copy still shares references to those fields, so the copy is not truly independent. For example:
public class Person implements Cloneable { private Date birthDate; @Override public Person clone() { try { return (Person) super.clone(); // Shallow copy } catch (CloneNotSupportedException e) { throw new AssertionError(); } } }
Here, clone() returns a new Person but the birthDate field still points to the same Date object. Modifying the original's birthDate affects the clone. To make a deep copy, you must manually copy mutable fields:
@Override public Person clone() { Person copy = (Person) super.clone(); copy.birthDate = new Date(birthDate.getTime()); return copy; }
Because clone() is easy to misuse and has awkward semantics, many developers prefer copy constructors or static factory methods. A copy constructor is explicit, does not rely on casting, and can be made to handle deep copying in a clear way. For collections, using new ArrayList<>(original) is a common pattern, but remember it is shallow. If you need a deep copy of a list of mutable objects, you must copy each element manually.
Another pitfall is copying an array with clone() on a reference type. array.clone() returns a shallow copy, so the elements are shared. Use Arrays.copyOf for primitives, but for objects, you still need to copy each element if they are mutable. Always verify the copy depth matches the level of protection you need.