Skip to main content

Immutability & Defensive Copies

intermediate14 min readLesson 65 of 180

Building truly immutable classes, and the two directions mutability leaks through final fields and return values.

Immutability and defensive copying

An immutable class: final fields, no setters, no leaked mutable state. Immutable objects are thread-safe by construction, safe map keys, and easy to reason about.

public final class Range {
    private final int lo, hi;
    public Range(int lo, int hi) {
        if (lo > hi) throw new IllegalArgumentException("lo > hi");
        this.lo = lo; this.hi = hi;
    }
    public int lo() { return lo; }
    public int hi() { return hi; }
}

Mutability leaks in two directions:

1. Mutable field leaked โ€” the caller can change your state without going through your methods:

public final class Team {
    private final List<String> members;
    Team(List<String> members) { this.members = members; }
    public List<String> members() { return members; }  // LEAK
}
// caller: team.members().clear();  โ€” your "final" field is now empty

Fix: return an unmodifiable view (or a copy) and copy on the way in:

Team(List<String> members) { this.members = List.copyOf(members); }
public List<String> members() { return Collections.unmodifiableList(members); }

2. Mutable field stored โ€” you keep a reference to the caller's list; they mutate it later and your object changes underneath you.

record solves the field-leak but still shallow-copies arrays and lists โ€” a record holding a mutable List is only as immutable as that list allows.

Now practice

Contract LabBuild value classes with correct equals/hashCode, seal leaky APIs, and defeat the Integer-cache trap.3 challenges ยท ยท ~35 min