Careeroza
Defensive Copies and Unmodifiable Collections
The Hole in "final"
final List<String> items means the reference cannot change, but the list itself can. If the caller keeps a reference to the list they passed in, they can modify your "immutable" object.
Example
List.copyOf makes an unmodifiable copy. Collections.unmodifiableList is only a read-only view: changes to the original still show through.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> source = new ArrayList<>(List.of("pen", "book"));
Order leaky = new Order(source, false);
Order safe = new Order(source, true);
List<String> view = Collections.unmodifiableList(source);
source.add("hacked-item");
System.out.println("leaky: " + leaky.items());
System.out.println("safe: " + safe.items());
System.out.println("view: " + view);
try {
safe.items().add("x");
} catch (UnsupportedOperationException e) {
System.out.println("safe.items() cannot be modified");
}
}
}
record Order(List<String> items) {
Order(List<String> items, boolean copy) {
this(copy ? List.copyOf(items) : items);
}
}Output
leaky: [pen, book, hacked-item] safe: [pen, book] view: [pen, book, hacked-item] safe.items() cannot be modified
Records with Defensive Copies
A record is shallowly immutable. Use the compact constructor to validate and copy mutable components.
record Invoice(String id, List<LineItem> lines) {
Invoice {
Objects.requireNonNull(id, "id");
if (lines.isEmpty()) throw new IllegalArgumentException("invoice needs at least one line");
lines = List.copyOf(lines); // defensive, unmodifiable copy
}
}Practice
Exercise: Make this class immutable: class Team { String name; List<String> members; }.
Show answer
final class Team {
private final String name;
private final List<String> members;
Team(String name, List<String> members) {
this.name = name;
this.members = List.copyOf(members);
}
String name() { return name; }
List<String> members() { return members; } // already unmodifiable
Team withMember(String m) {
List<String> copy = new ArrayList<>(members);
copy.add(m);
return new Team(name, copy);
}
}