Defensive Copies and Unmodifiable Collections

The Hole in "final"

final List<String> items means the reference cannot change, but the list itself can. If the caller keeps a reference to the list they passed in, they can modify your "immutable" object.

Example

List.copyOf makes an unmodifiable copy. Collections.unmodifiableList is only a read-only view: changes to the original still show through.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> source = new ArrayList<>(List.of("pen", "book"));

        Order leaky = new Order(source, false);
        Order safe = new Order(source, true);
        List<String> view = Collections.unmodifiableList(source);

        source.add("hacked-item");
        System.out.println("leaky: " + leaky.items());
        System.out.println("safe:  " + safe.items());
        System.out.println("view:  " + view);

        try {
            safe.items().add("x");
        } catch (UnsupportedOperationException e) {
            System.out.println("safe.items() cannot be modified");
        }
    }
}

record Order(List<String> items) {
    Order(List<String> items, boolean copy) {
        this(copy ? List.copyOf(items) : items);
    }
}

Output

leaky: [pen, book, hacked-item]
safe:  [pen, book]
view:  [pen, book, hacked-item]
safe.items() cannot be modified

Records with Defensive Copies

A record is shallowly immutable. Use the compact constructor to validate and copy mutable components.

record Invoice(String id, List<LineItem> lines) {
    Invoice {
        Objects.requireNonNull(id, "id");
        if (lines.isEmpty()) throw new IllegalArgumentException("invoice needs at least one line");
        lines = List.copyOf(lines);        // defensive, unmodifiable copy
    }
}

Practice

Exercise: Make this class immutable: class Team { String name; List<String> members; }.

Show answer
final class Team {
    private final String name;
    private final List<String> members;

    Team(String name, List<String> members) {
        this.name = name;
        this.members = List.copyOf(members);
    }
    String name() { return name; }
    List<String> members() { return members; }          // already unmodifiable
    Team withMember(String m) {
        List<String> copy = new ArrayList<>(members);
        copy.add(m);
        return new Team(name, copy);
    }
}