final only freezes the reference, not the object it points to. A final Date means you can't reassign the variable, but the Date itself stays fully mutable. Immutability is a property of the entire reachable object graph, not of a single field declaration.
The illusion
public final class Box {
private final List<String> items = new ArrayList<>();
// looks immutable — final field, final class, no setter
public List<String> items() { return items; }
}
Box b = new Box();
b.items().add("oops"); // perfectly legal — mutated through the live reference
The field items is final, so b.items = somethingElse would not compile. But items.add(...) doesn't touch the reference — it mutates the ArrayList instance. Final says nothing about that.
Three layers of defense
To make Box truly immutable you need all three:
public final class Box {
private final List<String> items;
public Box(List<String> items) {
this.items = List.copyOf(items); // 1. unmodifiable snapshot
}
public List<String> items() {
return items; // 2. already unmodifiable, safe to return
}
}
finalkeeps the reference fixed.List.copyOfbuilds an unmodifiableListfrom a snapshot — mutating the source post-construction doesn't affect us, and callers can't mutate what we return.- The class is
finalso a subclass can't add a mutable field.
The arrays gotcha
Arrays are always mutable and there is no immutable array type. A private final int[] xs is exactly as leaky as a mutable field:
public final class Stats {
private final int[] xs;
public Stats(int[] xs) { this.xs = xs; } // shared
public int[] data() { return xs; } // leaked
}
Fix with .clone() on the way in and on the way out, or expose List<Integer> / a read-only view (IntStream, length() + get(int)).