Defensive copying is one of the most important design principles in Java programming, especially when working with mutable collections. It helps prevent bugs, maintain encapsulation, enforce immutability, and ensure your data is never accidentally modified by external code.
In this tutorial, you'll learn what defensive copying is, when and why to use it, and how to do it effectively with Lists, Sets, Maps, and custom objects. We'll also cover the evolution of this practice across Java versions — from Collections.unmodifiableXXX()
to List.copyOf()
.
What Is Defensive Copying?
Defensive copying is the practice of creating a copy of a mutable object before exposing it to outside code or storing it internally.
Why It's Needed
- Prevent unintended modifications to internal state
- Maintain encapsulation and immutability
- Ensure thread safety in multi-threaded systems
- Avoid side effects in reusable libraries or APIs
Common Scenarios for Defensive Copying
Scenario | Problem Without Copying | Solution with Defensive Copying |
---|---|---|
Constructor receives modifiable list | External code can modify internal state | Copy and store internally |
Getter exposes internal collection | Callers can mutate internal fields | Return a copy or unmodifiable view |
Setter assigns collection directly | External reference still holds | Assign a copy |
Java Code Example – Without Defensive Copying (❌ Bad)
public class Student {
private List<String> subjects;
public Student(List<String> subjects) {
this.subjects = subjects;
}
public List<String> getSubjects() {
return subjects;
}
}
List<String> list = new ArrayList<>(List.of("Math", "Science"));
Student s = new Student(list);
list.add("History"); // Mutates student's internal list!
Java Code Example – With Defensive Copying (✅ Good)
public class Student {
private final List<String> subjects;
public Student(List<String> subjects) {
this.subjects = List.copyOf(subjects); // Java 10+
}
public List<String> getSubjects() {
return subjects;
}
}
- Now,
Student
is immutable — subjects cannot be changed externally.
Defensive Copying Methods
1. List.copyOf()
/ Set.copyOf()
/ Map.copyOf()
(Java 10+)
Creates immutable copies.
List<String> copy = List.copyOf(original);
2. new ArrayList<>(original)
Shallow copy, still modifiable.
this.subjects = new ArrayList<>(original);
3. Collections.unmodifiableList()
Wrapper — doesn’t copy, still reflects original.
List<String> unmodifiable = Collections.unmodifiableList(original);
⚠️ Be careful: changes to the original list still reflect in the unmodifiable view.
4. Defensive Copy in Getters
public List<String> getSubjects() {
return new ArrayList<>(subjects); // Modifiable copy
}
Or for immutability:
return List.copyOf(subjects);
Functional Defensive Copying with Streams
List<String> copy = original.stream()
.filter(s -> s != null)
.collect(Collectors.toUnmodifiableList());
Java Version Tracker
📌 What's New in Java?
- Java 8
- Introduced
Stream
APIs,Collectors.toList()
for easy copying
- Introduced
- Java 9
- Added
List.of()
,Set.of()
,Map.of()
— immutable by default
- Added
- Java 10
- Added
copyOf()
methods for defensive copying
- Added
- Java 21
- Indirect improvements through better GC and structured concurrency that favor immutability
Comparisons
Method | Immutable | Reflects Original | Java Version |
---|---|---|---|
List.copyOf() |
✅ Yes | ❌ No | 10+ |
Collections.unmodifiableList() |
❌ No | ✅ Yes | 1.2+ |
new ArrayList<>(list) |
❌ No | ❌ No | Any |
List.of() |
✅ Yes | ❌ No | 9+ |
Performance Considerations
- Shallow copies are fast and lightweight.
copyOf()
creates a new object — negligible cost for small/medium collections.- Prefer immutable collections when reading is more common than writing.
Best Practices
- Always copy mutable collections before assigning them to fields.
- Use
List.copyOf()
orMap.copyOf()
for immutability. - Never expose raw internal collections through public APIs.
- Prefer immutability in DTOs, config objects, and shared state.
Anti-Patterns
- Returning internal mutable collections directly from a getter.
- Wrapping with
unmodifiableList()
but continuing to mutate the original list. - Forgetting to copy in a constructor, leading to side effects.
Refactoring Legacy Code
- Identify mutable field exposures and constructor assignments.
- Replace with immutable or defensive copies.
- Gradually migrate public APIs to return unmodifiable views or copies.
Real-World Use Cases
- Frameworks and libraries (e.g., Spring) often return unmodifiable configs
- DTOs in REST APIs must avoid shared mutable state
- Multithreaded apps use defensive copies for read consistency
- Plugins or extension points expose internal structures via read-only wrappers
Conclusion and Key Takeaways
- Defensive copying prevents external mutation of internal state.
- Use
copyOf()
for immutability,new ArrayList<>(...)
for isolation, and avoid exposing internal fields. - It's one of the simplest ways to enforce encapsulation, ensure data safety, and reduce bugs.
FAQ – Defensive Copying in Java
-
Is Collections.unmodifiableList() the same as List.copyOf()?
No — the former is a wrapper, the latter is a real immutable copy. -
Can I use copyOf() on null?
No — it throwsNullPointerException
. -
Are elements inside the copied list also copied?
No — shallow copy. You need deep copy manually. -
What’s the difference between immutable and unmodifiable?
Immutable can’t change at all. Unmodifiable might still reflect changes in the original. -
Can I use defensive copying with arrays?
Yes — useArrays.copyOf()
. -
Does defensive copying hurt performance?
Slightly, but the safety it brings is usually worth it. -
How do I deep-copy a list of custom objects?
Use a loop or stream to clone each element. -
Should I copy in both constructor and getter?
Usually yes — constructor for protection, getter for return safety. -
Can I use copyOf() for concurrent use?
Yes — immutable collections are safe across threads. -
Is defensive copying needed for primitives?
No — they are immutable and passed by value.