Memra

Immutability & defensive copies

◈ 4 cards

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.

what the caller doeswithout the copywith the copymutates the list it passedinSchedule changesSchedule unchangedcalls add() on getTasks()internal list changesUnsupportedOperationException
The copy in and the copy out plug two different leaks. Doing only one of them still leaves the Schedule mutable from outside.
NORMAL ~/memra/learn/java-from-zero/immutability-defensive-copies utf-8 LF