Defensive Copying in Java
A class can make its list field private final and still lose control of it. If the constructor stores the caller's list as is, the caller still holds a reference and can change it later. If a getter returns the internal list, anyone can call clear() on it. Defensive copying closes both holes: copy mutable objects when they come in and when they go out, so the class owns the only reference to its internals.
The Leak and the Fix
import java.util.ArrayList;
import java.util.List;
public class Teams {
static final class LeakyTeam {
private final List<String> members;
LeakyTeam(List<String> members) { this.members = members; }
List<String> members() { return members; }
}
static final class SafeTeam {
private final List<String> members;
SafeTeam(List<String> members) { this.members = List.copyOf(members); }
List<String> members() { return members; }
}
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Navika", "Srihitha"));
LeakyTeam leaky = new LeakyTeam(names);
SafeTeam safe = new SafeTeam(names);
names.add("Someone else");
System.out.println("Leaky after caller's change: " + leaky.members());
System.out.println("Safe after caller's change: " + safe.members());
leaky.members().clear();
System.out.println("Leaky after clear(): " + leaky.members());
try {
safe.members().clear();
} catch (UnsupportedOperationException e) {
System.out.println("Safe team refused clear()");
}
}
}Leaky after caller's change: [Navika, Srihitha, Someone else]
Safe after caller's change: [Navika, Srihitha]
Leaky after clear(): []
Safe team refused clear()How it works
LeakyTeamstores the same list the caller passed in. When the caller later adds tonames, the team changes too.- Its getter hands out that same list, so
leaky.members().clear()wipes the team. SafeTeamstoresList.copyOf(members): a new, unmodifiable list. The caller's later changes don't reach it.- Because the stored copy is unmodifiable, the getter can return it directly;
clear()throws instead of damaging the team.
How to Copy Common Types
| Type | Copy on the way in | Safe on the way out |
|---|---|---|
List, Set, Map | List.copyOf(list), Set.copyOf, Map.copyOf | Return the unmodifiable copy itself |
| Mutable list you'll keep changing inside | new ArrayList<>(list) | Collections.unmodifiableList(list) or List.copyOf(list) |
| Arrays | array.clone() or Arrays.copyOf(array, n) | Return array.clone() |
java.util.Date (legacy, mutable) | new Date(date.getTime()) | Return a new Date |
LocalDate, String, Integer | No copy needed: they're immutable | Return directly |
Copy Before You Check
When a constructor validates a mutable argument, copy it first and validate the copy. Otherwise another thread could change the original between the check and the copy:
Exam(int[] marks) {
this.marks = marks.clone(); // 1. copy
for (int m : this.marks) { // 2. validate the copy
if (m < 0 || m > 100) {
throw new IllegalArgumentException("Bad mark " + m);
}
}
}List.copyOf also rejects null elements with a NullPointerException, which is often exactly the check you want.
Common Mistakes
Assuming final makes a list unchangeable. final stops reassignment, not add or clear.
Copying in the constructor but not in the getter (or the other way round). Both doors must be closed.
Defensive copies of immutable objects. Copying a String or LocalDate is wasted work.
Practice
- Easy: Write a
Playlistclass whose constructor takes aList<String>of songs. Show that changing the caller's list afterwards doesn't affect the playlist. - Harder: Write a class
Schedulewith aint[] slotsfield. Add a constructor and a getter that both copy, then try to break it frommainand show that you can't.
Key Takeaways
Was this tutorial helpful?