Immutability & defensive copies
final class + final fields + no setters + copying mutable inputs and outputs — and why immutability pays.
What makes a class truly immutable?
An immutable object's state never changes after construction. Java's own String, Integer, LocalDate, and records are all immutable. To write one from scratch, apply four rules:
final class— prevents subclasses from adding mutable state or overriding methods.private finalfields — no reassignment after construction.- No setters — obvious, but easy to overlook a "convenience" mutator added later.
- Defensive copies of any mutable field on the way in *and* on the way out.
import java.util.ArrayList;
import java.util.List;
public final class Schedule {
private final List<String> tasks;
public Schedule(List<String> tasks) {
this.tasks = new ArrayList<>(tasks); // defensive copy IN
}
public List<String> getTasks() {
return List.copyOf(tasks); // defensive copy OUT
}
}
Without the copy-in, a caller could hold the original list and mutate it after construction. Without the copy-out, a caller could mutate the internal list through the returned reference. Both paths silently break immutability.
Why bother?
Thread safety for free. Immutable objects can be shared across threads without synchronisation — there is nothing to race on.
Safe as Map keys and Set elements. HashMap relies on hashCode staying stable. If a key's state can change after insertion, the map can no longer find it.
Easier reasoning. When you pass an immutable object into a method, you know the method cannot surprise you by changing it.
List.of and friends
For simple cases, List.of(...), Set.of(...), and Map.of(...) produce unmodifiable views immediately — no explicit copy needed. These are the go-to for read-only collection fields.