Memra

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:

  1. final class — prevents subclasses from adding mutable state or overriding methods.
  2. private final fields — no reassignment after construction.
  3. No setters — obvious, but easy to overlook a "convenience" mutator added later.
  4. 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.

NORMAL ~/memra/learn/java-from-zero/immutability-defensive-copies utf-8 LF