Immutability and Defensive Copying — Specification Reading Guide¶
Immutability is a programming idiom — but the guarantees that make it work in Java are language and memory-model rules. This file maps the idiom to the binding spec text: the
final-field publication semantics of JLS §17.5, the definite-assignment rules of §16 that let the compiler trustfinal, the record specification of JLS §8.10 (JEP 395), the value-class preview of JEP 401 that points at where the language is heading, and the JDK methods (List.copyOf,Map.copyOf,Set.copyOf) that depend on those guarantees.
1. Where to find the canonical text¶
| Concept | Authoritative source |
|---|---|
| Final fields, semantics during/after construction | JLS §17.5 — Final Field Semantics |
| Final field declarations and modifiers | JLS §8.3.1.2, §8.3.1.3 |
| Definite assignment | JLS §16 — Definite Assignment |
| Constructors | JLS §8.8 |
Class declarations, final and sealed modifiers | JLS §8.1.1.2 |
| Records | JLS §8.10 — Record Classes (JEP 395) |
| Record canonical and compact constructors | JLS §8.10.4 |
| Record accessors | JLS §8.10.3 |
| Frozen arrays / immutable types (value classes) | JEP 401 (preview) — Value Classes and Objects |
| Java Memory Model overview | JLS §17 |
| Happens-before | JLS §17.4.5 |
List.copyOf, Set.copyOf, Map.copyOf | Javadoc of java.util.List, Set, Map (Java 10+) |
Collections.unmodifiableList | Javadoc of java.util.Collections |
The JLS is what javac enforces; the JVMS is what the JVM enforces; the Javadoc is what the JDK library promises. Immutability draws from all three.
2. JLS §17.5 — the final-field publication guarantee¶
The single most important paragraph in the language specification for immutability is JLS §17.5. Verbatim (slightly trimmed):
"An object is considered to be completely initialized when its constructor finishes. A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields."
The guarantee has three parts.
- Final fields, set during construction, are visible to all threads that see the reference afterwards. Without
final, the JMM only promises this if you publish through a happens-before edge (avolatilewrite, asynchronizedblock, anAtomic*.set, etc.). - No synchronization is required for this guarantee. A plain field read of a
finalfield of a properly published object yields the initialised value, full stop. - The guarantee applies only if
thisdoes not escape the constructor. If the constructor publishesthis(via a listener registration, a thread start, a static-field store) before returning, the guarantee is forfeited.
The implementation mechanism is a store-store barrier at the end of every constructor that writes a final field. The JIT respects it; the JVMS requires it. The result is that all final-field writes are visible before the reference becomes reachable.
public final class Snapshot {
private final long version;
private final Map<String, Integer> data;
public Snapshot(long version, Map<String, Integer> data) {
this.version = version;
this.data = Map.copyOf(data);
// The store-store barrier here is guaranteed by JLS §17.5.
}
}
This is why you do not need volatile on a field of Snapshot even when that field is read from multiple threads — provided you publish the Snapshot reference safely.
3. JLS §16 — definite assignment makes final trustworthy¶
JLS §16 — Definite Assignment — is what lets the compiler refuse a class where a final field could be read before it is assigned. The relevant rule: "For every access of a final field, the compiler must prove that the field has been definitely assigned."
public final class Money {
private final long cents;
private final Currency currency;
public Money(long cents, Currency c) {
if (c == null) throw new NullPointerException(); // legal — no field read yet
this.cents = cents;
this.currency = c;
// At this point, both fields are definitely assigned.
}
}
Two consequences:
- The compiler refuses to leave a
finalfield unassigned. A constructor that doesn't assign everyfinalfield on every code path is a compile error. - The compiler refuses to assign a
finalfield twice. A constructor that conditionally re-assigns is a compile error.
This is what makes the §17.5 guarantee actually buy you something — final is more than a politeness; the compiler enforces single-assignment so the runtime can rely on a stable value at the publication barrier.
4. JLS §8.10 — records and JEP 395¶
Records, introduced as a preview in JEP 359 (Java 14) and finalised as JEP 395 (Java 16), are defined in JLS §8.10 — Record Classes. The relevant rules:
- A record class is implicitly
final(§8.10.1). No subclass can change the behaviour of the record. - Each record component declares a corresponding
private finalfield (§8.10.2). You cannot declare these fields explicitly; the compiler generates them. - A record has a canonical constructor whose parameter list matches the component list (§8.10.4). You can write it explicitly, or the compiler generates it.
- A compact constructor (§8.10.4.2) elides the parameter list and the implicit field assignments. The body runs before the implicit assignments.
public record Order(long id, List<LineItem> items) {
public Order { // compact constructor
items = List.copyOf(items);
// implicit: this.id = id; this.items = items;
}
}
The compact-constructor form is the specification's blessing for defensive copying. You modify the parameter; the implicit assignment that follows stores the modified value into the final field.
- A record has an accessor method for each component (§8.10.3). The default returns
this.<component>. You can override the accessor:
public record Period(LocalDate start, LocalDate end) {
// override to enforce some invariant
public LocalDate end() {
return end == null ? LocalDate.MAX : end;
}
}
In practice, overriding accessors is rarely needed for immutability — the default accessor returns the final field, which is already safe.
- A record cannot extend another class (§8.10.5). It implicitly extends
java.lang.Record. It may implement interfaces. - A record's
equals,hashCode,toStringare derived from the components (§8.10.3). The compiler generates them; you can override but rarely should.
The whole point of §8.10 is that the language enforces four of Bloch's five rules. Rule 5 — defensive copying of mutable components — lives in the compact constructor.
5. JEP 401 — value classes (preview)¶
JEP 401 (preview, available in Java 23+) introduces value classes and value objects — a long-running thread of Project Valhalla. The key spec changes:
value classmodifier. A class declaredvaluehas no identity. There is no==test that can distinguish two value objects with the same fields; there is noSystem.identityHashCodethat returns different values for them; there is nosynchronized(valueObject)that has meaning.- All fields are implicitly
final. A value class cannot have mutable state. - No
wait/notify/notifyAll/synchronizedon a value object. The intent is to allow the JVM to flatten value instances into arrays and into fields of other classes, without an object header. - A value class can extend a regular abstract class with no fields and a specific
valuemodifier, allowing polymorphism over values.
// JEP 401 syntax — preview
public value class Point implements Comparable<Point> {
public Point(double x, double y) { ... }
public double x() { ... }
public double y() { ... }
public int compareTo(Point o) { ... }
}
For immutability, JEP 401 is the next chapter:
- No copy-on-write needed. A value object passed to a method is the value; there is no shared reference to defend.
- No defensive copy needed for arrays of value objects. A
Point[]holds the points inline; mutating one element doesn't affect another. The "two threads see two values" guarantee is even stronger thanfinalpublication, because there is no reference to publish. List<Point>becomes much cheaper. No boxing, no per-element object header.- Records under JEP 401 become value records (a subsequent JEP extends record semantics to value classes), eliminating the last bit of overhead for
record-shaped immutables.
Today (Java 23 preview, Java 24 onward), JEP 401 is in preview. Treat it as where the language is going. The advice from senior.md — "design with records as if Valhalla were imminent" — relies on §8.10 and JEP 401 converging.
6. final field semantics in detail (JLS §17.5.1, §17.5.2)¶
The full §17.5 is broken into subsections worth knowing about.
- §17.5.1 — Semantics of final fields. Defines the freeze action that happens at the end of every constructor invocation in which a
finalfield is set. - §17.5.2 — Reading final fields during construction. Inside the constructor, before the freeze, a
finalfield's value can be read normally — but other threads have no publication guarantee yet. This matters if you call a method onthisfrom the constructor and that method readsfinalfields.
public final class Bad {
private final int x;
public Bad(int x) {
new Thread(this::reportX).start(); // `this` escapes!
this.x = x; // freeze happens AFTER thread start
}
public void reportX() {
System.out.println(x); // could print 0
}
}
The reporting thread can run before the freeze, see the default value 0, and print it. The §17.5 guarantee does not protect this code because this escaped the constructor (line 4) before construction finished (line 5).
- §17.5.3 — Subsequent modification of final fields. A
finalfield, after construction, may be modified by reflection (Field.setAccessible(true); Field.set(...)). The spec is unusually candid: "If a final field is modified after construction... the new value of the final field may not be visible to other threads...". Reflection breaks the §17.5 guarantee. Don't.
7. List.copyOf / Set.copyOf / Map.copyOf semantics¶
The Javadoc of java.util.List.copyOf(Collection) (Java 10+) specifies:
"Returns an unmodifiable List containing the elements of the given Collection, in its iteration order. The given Collection must not be null, and it must not contain any null elements. If the given Collection is subsequently modified, the returned List will not reflect such modifications."
Three properties matter:
- Returned list is unmodifiable. Calls to
add,remove,setthrowUnsupportedOperationException. - Snapshot semantics. Subsequent mutations to the source are not visible.
- Null intolerance. A
nullelement throwsNullPointerException— defensible defensive copy by default.
The implementation has a performance fast-path: if the argument is already an unmodifiable List (one of the JDK's internal ImmutableCollections.ListN variants returned by List.of, List.copyOf, Stream.toList, etc.), List.copyOf returns the same instance. So:
List<String> a = List.of("x", "y");
List<String> b = List.copyOf(a);
System.out.println(a == b); // true — no allocation
Set.copyOf and Map.copyOf have analogous semantics.
The semantic distinction with Collections.unmodifiableList(List):
Collections.unmodifiableListwraps — the wrapper is unmodifiable, but the underlying list can still be mutated by anyone holding the original reference.List.copyOfsnapshots — the new list is independent.
For immutability, use List.copyOf. For "expose a read-only view of a list I own and continue to mutate", use Collections.unmodifiableList.
8. JLS §8.3.1.2 / §8.3.1.3 — what final actually means on fields¶
JLS §8.3.1.2 covers volatile; §8.3.1.3 covers final on fields. The key rules:
- A
finalfield must be definitely assigned at the end of every constructor of the class that declares it (§16.9). - A blank
finalfield — declaredfinalwithout an initializer — must be assigned in every constructor. - A
finalfield that has an initializer is assigned at the point of declaration; it may not be re-assigned in a constructor.
public final class Cache {
private final Map<String, String> store = new ConcurrentHashMap<>(); // initialised at declaration
private final long capacity; // blank final
public Cache(long capacity) {
this.capacity = capacity; // must be assigned here
// this.store = ...; // would not compile — already initialised
}
}
static finalfields follow the same rules but the initialiser runs in<clinit>(§8.7). Astatic finalfield that holds an immutable value (aMap.copyOf(...)of a constant) is fully initialised before any thread can see the class.
The combination — definite assignment, single assignment, store-store barrier at constructor exit — is what makes final fields a language-level guarantee rather than a politeness.
9. JLS §17.4.5 — happens-before for safe publication¶
For non-final fields, you need an explicit happens-before edge to safely publish a constructed object. JLS §17.4.5 enumerates the relations that establish happens-before:
- Volatile write happens-before subsequent volatile read of the same variable.
Thread.start()happens-before the started thread's first action.- A monitor unlock happens-before every subsequent lock of the same monitor.
- Constructor of an object completes happens-before any thread observing the object through a
finalfield. (This is §17.5 in another guise.) - All actions in a thread happen-before any other thread successfully returning from
Thread.join()on that thread.
For immutable types with final fields, you do not need to invoke any of these; §17.5 gives you the guarantee for free. For effectively immutable types (mutable type, no actual mutation post-publication), you need one of the edges above.
// Effectively immutable with safe publication via AtomicReference (volatile semantics)
public class EffectivelyImmutableHolder {
private final AtomicReference<Map<String, String>> ref = new AtomicReference<>();
public void publish(Map<String, String> data) {
ref.set(Map.copyOf(data)); // volatile write — happens-before
}
public Map<String, String> read() {
return ref.get(); // volatile read — sees the published state
}
}
The AtomicReference.set/get establishes happens-before; the Map.copyOf makes the published value structurally immutable. The combination matches what final-field publication gives you for free in a record.
10. JEP references and immutability¶
| JEP | Feature | Relevance to immutability |
|---|---|---|
| JEP 269 | List.of, Set.of, Map.of (Java 9) | Immutable factory methods for small collections |
| JEP 269 + JDK | List.copyOf, Set.copyOf, Map.copyOf (Java 10) | Snapshot defensive copy |
| JEP 359, 384, 395 | Records (preview → final, Java 14 → 16) | Rules 1-4 of Bloch's recipe automated |
| JEP 405, 441 | Pattern matching for records and switch | Read components without breaking immutability |
| JEP 401 (preview) | Value classes | Identity-free values; immutability without overhead |
| JEP 466 (preview) | Class-File API | Tooling for verifying immutability invariants |
| JEP 442 (preview) | Foreign Function & Memory API | Off-heap immutable data; MemorySegment is treatable as immutable |
Modern Java is the most immutability-friendly the language has ever been. Records collapse the boilerplate; List.copyOf makes defensive snapshots one call; JEP 401 will eventually remove the last allocation cost for value-shaped types.
11. Reading list¶
- JLS §17.5 — Final Field Semantics. The single most important section for immutability and concurrency.
- JLS §16 — Definite Assignment. The compiler-side enforcement that makes
finaltrustworthy. - JLS §8.10 — Record Classes. Defines records, canonical and compact constructors, accessors.
- JLS §8.3.1.3 — Final Fields. The field modifier rules.
- JLS §17.4 — Memory Model. Read §17.4.5 (happens-before) for the broader concurrency context.
- JEP 395 — Records (Standard). The motivating document for records.
- JEP 401 — Value Classes and Objects (preview). Where the language is heading.
- Joshua Bloch — Effective Java (3rd ed.). Item 17: "Minimize mutability". Item 50: "Make defensive copies when needed". Item 1: "Static factory methods" (safe publication idiom).
- Brian Goetz et al. — Java Concurrency in Practice. Chapters 3 ("Sharing Objects") and 4 ("Composing Objects"); these are the canonical treatment of
final-field publication and safe-publication idioms. - Doug Lea — Concurrent Programming in Java (2nd ed.). The grandfather text. Predates many of the modern JEPs but the design ideas are the same.
- Aleksey Shipilëv — Java Memory Model Pragmatics (online lecture notes). The clearest non-spec explanation of JLS §17 currently available.
- JDK Javadoc —
java.util.List.copyOf,java.util.Set.copyOf,java.util.Map.copyOf,java.util.Collections.unmodifiableList. The methods you use most often; read their full contracts.
The spec sections do not teach immutability — they give you the vocabulary to point at when defending a design. When a reviewer asks "why does this not need volatile?", you cite JLS §17.5 and the absence of this escape. When a junior asks "why is record enough?", you cite JLS §8.10. When a security review asks "what about reflection?", you cite §17.5.3 and the JPMS opens rules. Immutability is judgement; the spec gives you the levers.