Appearance
Lesson 01 · Immutability & Defensive Copying
Beyond the 1Z0-830 exam
Immutability isn't a 1Z0-830 objective, but it's the single highest-leverage design habit in Java: immutable objects are thread-safe for free, can't be corrupted by callers, and are safe to cache, share, and use as map keys. Records (Module 03) give you most of it; this lesson covers the part records don't hand you — defending mutable internals.
Objectives
After this lesson you will be able to:
- Build a class that is genuinely immutable (not just "has no setters").
- Apply defensive copying on the way in and out to stop reference leaks.
- Explain why a record holding a mutable component still needs a compact constructor.
What "immutable" really requires
A class is immutable only if its observable state can never change after construction. The full recipe:
- Don't provide mutators (no setters, no methods that change state).
- Make the class
final(or use a private constructor + factory) so subclasses can't add mutable state. - Make all fields
private final. - Don't leak references to mutable internals — copy on the way in and out.
The first three are easy and visible in review. The fourth is the one that bites.
The reference-leak trap
java
public final class Period {
private final List<String> labels;
public Period(List<String> labels) {
this.labels = labels; // ❌ stores the caller's list
}
public List<String> labels() {
return labels; // ❌ hands the field straight back
}
}Both lines share a reference with the outside world. The caller can mutate labels after construction, and any caller of labels() can mutate it too — the object isn't immutable at all.
Gotcha
final on a field freezes the reference, not the object it points to. private final List means the list variable never gets reassigned — but the list's contents can still change. Final ≠ immutable for reference types.
Defensive copying — in and out
java
public final class Period {
private final List<String> labels;
public Period(List<String> labels) {
this.labels = new ArrayList<>(labels); // copy IN
}
public List<String> labels() {
return Collections.unmodifiableList(labels); // expose a read-only view OUT
}
}- Copy in the constructor so later mutations of the caller's list don't touch your state.
- Copy out (or return an unmodifiable view) so callers can't reach back into your field.
List.copyOf(labels) does both jobs in one call: it returns an unmodifiable copy, so you can store it directly and hand it out safely.
What prints? — does the source mutation leak in?
java
List<String> src = new ArrayList<>(List.of("a", "b"));
Period p = new Period(src); // constructor copies
src.add("c");
System.out.println(p.labels().size());2. The constructor copied src, so the later add mutates only the caller's list. Without the copy-in, this would print 3.
Records as value types — still copy mutable components
A record makes the reference fields final and generates equals/hashCode/toString, but it does not deep-copy mutable components. Use a compact constructor:
java
public record Range(int lo, int hi, List<Integer> points) {
public Range {
if (lo > hi) throw new IllegalArgumentException("lo > hi");
points = List.copyOf(points); // defensive, unmodifiable copy
}
}Now range.points() returns an unmodifiable list, and the constructor's own copy is the only reference — the object is a true value type. Prefer immutable inputs (List.of, List.copyOf) at your API boundaries so copying is cheap and intent is clear.
SDET note
Immutable value objects are the easiest things in the world to test: no setup order, no shared mutable state between tests, safe to reuse as fixtures across parallel tests (Module 15). When you can make a type immutable, your tests get simpler for free.
When immutability is the wrong call
Immutability has a cost: producing a changed copy allocates. For large, frequently-updated structures on a hot path, a mutable type (or a builder that you mutate then "freeze") can be the right trade-off. Default to immutable; reach for mutable deliberately, with a measured reason.
Key Takeaways
- Immutable = no mutators +
finalclass +private finalfields + no leaked references. finalfreezes the reference, not the pointed-to object — mutable internals still leak.- Copy in (constructor) and copy out (accessor);
List.copyOfdoes both. - A record with a mutable component needs a compact constructor to copy defensively.
- Default to immutable; choose mutable only with a measured performance reason.
Lesson Quiz
A field is `private final List<String> tags`. Is the object's state safe from outside mutation?
Which constructor line correctly defends an immutable type's List state?
A record has a `List<Integer> points` component. What keeps it a true value type?
What does List.copyOf(src) give you?
Next: Object Contracts & Common Patterns. Run the matching code in labs/src/main/java/com/jse21/m12_design/.