Skip to content

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:

  1. Don't provide mutators (no setters, no methods that change state).
  2. Make the class final (or use a private constructor + factory) so subclasses can't add mutable state.
  3. Make all fields private final.
  4. 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 + final class + private final fields + no leaked references.
  • final freezes the reference, not the pointed-to object — mutable internals still leak.
  • Copy in (constructor) and copy out (accessor); List.copyOf does 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

Lesson Quiz · Immutability & Defensive Copying0 / 4
  1. A field is `private final List<String> tags`. Is the object's state safe from outside mutation?

    • AYes — final makes it immutable
    • BNo — final freezes the reference, but the List contents can still be changed via a leaked reference
    • CYes — private is enough
    • DOnly if the list is empty
  2. Which constructor line correctly defends an immutable type's List state?

    • Athis.items = items;
    • Bthis.items = new ArrayList<>(items);
    • Cthis.items = (List) items;
    • Ditems.clear();
  3. A record has a `List<Integer> points` component. What keeps it a true value type?

    • ANothing — records are always fully immutable
    • BMarking points transient
    • CA compact constructor that does points = List.copyOf(points)
    • DAdding a setter
  4. What does List.copyOf(src) give you?

    • AA mutable copy
    • BThe same list reference
    • CAn unmodifiable copy — safe to both store and hand out
    • DA view that reflects later changes to src

Next: Object Contracts & Common Patterns. Run the matching code in labs/src/main/java/com/jse21/m12_design/.