The full picture
public final class Schedule { // 1. no subclass mischief
private final List<LocalDate> dates; // 2. private final
private final Owner owner;
public Schedule(List<LocalDate> dates, Owner owner) {
this.dates = List.copyOf(dates); // 3. defensive copy IN
this.owner = owner; // (Owner itself immutable)
}
public List<LocalDate> dates() { return dates; } // List.copyOf is already
public Schedule plus(LocalDate d) { // unmodifiable — safe OUT
var next = new ArrayList<>(dates); next.add(d);
return new Schedule(next, owner); // 4. "mutators" return new
}
}
// Records: record Schedule(List<LocalDate> dates, Owner owner) {
// Schedule { dates = List.copyOf(dates); } // compact ctor still must copy!
// }- JMM guarantee: a properly constructed object's
finalfields are visible to all threads without synchronization — the only free safe-publication mechanism in Java. - Costs & mitigations: allocation churn for 'modified copies' — usually irrelevant (young-gen deaths are cheap), and persistent-structure libraries or builders handle hot cases.
- Where it matters most: map keys, cache values, messages between threads/actors, configuration snapshots, and anything crossing an API boundary.
- Records don't make deep immutability automatic — a record holding a mutable
Listis a shallow shell; copy in the compact constructor.