Records auto-generate a constructor and accessors. Does that make a record fully immutable, and where can immutability still leak?
answer
- Record = final fields + accessors + no setters => shallow immutability free
- final fixes the reference, not the pointed-to object
- Mutable component leaks via input capture AND accessor return
- Compact constructor: List.copyOf / clone() to copy on input
- Deep immutability needs immutable elements too (transitive)
basics
~20 sA record's own fields are final and have no setters, so the record itself can't be reassigned. But if a component is a mutable type like a List or array, the record just stores the reference - callers can still change that list. To be truly immutable you must copy mutable components when they go in and when they come out.
solid answer
~40 sA record makes each component a private final field with a matching accessor and no setter, so the record reference can't be reseated - it's shallowly immutable. But Java gives you no deep immutability automatically: if a component is a mutable type (array, ArrayList, Date), the record stores the caller's reference, and that caller (or anyone the accessor hands the reference to) can mutate the underlying object, changing the record's observed state. To close this, use the record's compact constructor to defensively copy mutable inputs (e.g. List.copyOf(items)) and return defensive copies or unmodifiable views from accessors by overriding them. Even then, an immutable wrapper around a mutable element still exposes a mutable element. So records give you immutability of structure for free, but deep immutability is your responsibility wherever a component is itself mutable.
go deeper
Knows a record has no setters and final fields, but may not realize a mutable component can still be changed from outside.
Demonstrates the input- and output-aliasing leaks and fixes them with List.copyOf/clone() in the compact constructor; knows final fixes only the reference.
Explains shallow vs deep immutability, the transitivity requirement on elements, array-vs-List differences, and weighs the per-construction copy cost against the safety it secures.
Codifies 'record components should be immutable types' as a guideline, reasons about defensive-copy cost at API boundaries vs trusted internals, and when to expose unmodifiable views vs copies.
## What a record gives you automatically A `record` declaration like `record Range(int lo, int hi) {}` generates: a `private final` field per component, a public **accessor** per component (`lo()`, `hi()`), a **canonical constructor** that assigns the components, plus `equals`, `hashCode`, and `toString` derived from the components. Crucially, there are **no setters** and the fields are `final`. So the **record reference can never be reseated** after construction - this is **shallow immutability** and it's free. ## Why that isn't deep immutability A `final` field only fixes the **reference**, not the object it points to. If a component's type is **mutable** - an array, an `ArrayList`, a `java.util.Date`, a mutable builder - the record stores **whatever reference the caller passed**, and the accessor **returns that same reference**. Two leaks follow: ### Leak 1: capture from the caller (input aliasing) ``` List<String> src = new ArrayList<>(List.of("a")); var r = new Tags(src); // record Tags(List<String> values) src.add("b"); // r.values() now shows [a, b] - record changed! ``` The record kept the caller's live list, so the caller can mutate the record's state from outside. ### Leak 2: escape through the accessor (output aliasing) ``` r.values().add("c"); // accessor handed back the internal list - mutated again ``` Anyone calling the accessor gets the internal mutable object and can change it. ## Closing the leaks: defensive copying Use the **compact constructor** to copy mutable inputs, and **override accessors** to return copies/unmodifiable views: ``` record Tags(List<String> values) { Tags { // compact constructor values = List.copyOf(values); // copy + make unmodifiable on the way IN } // List.copyOf already returns an unmodifiable list, so the accessor is safe to leave // as generated. For arrays you'd override: public int[] data() { return data.clone(); } } ``` `List.copyOf` both snapshots (breaking input aliasing) and returns an unmodifiable list (so the generated accessor can't be used to mutate). For an **array** component, neither copy is automatic - you must `clone()` in the compact constructor **and** return `data.clone()` from an overridden accessor, because arrays are always mutable and have no unmodifiable view. ## A subtlety even copies don't fix Defensive copying stops aliasing of the **container**, but if the **elements** are themselves mutable (`List<Date>`), copying the list still shares the same `Date` objects. Truly deep immutability requires the elements to be immutable too (use `LocalDate`, immutable value types, or copy elements). Immutability is **transitive only if every layer is immutable**. ## Tie to benefits and trade-offs This is where immutability's **trade-off** meets its **benefit**: the defensive copies cost an allocation per construction (and per accessor call if you copy on output), but in return you get the thread-safety, safe-sharing, and valid-key guarantees that depend on the object genuinely never changing. A record with all-immutable components (primitives, `String`, `LocalDate`, other records) needs **no** defensive copying and is deeply immutable for free - which is why immutable components are the idiomatic choice for record fields.
- Where in a record do you put the defensive copy, and what does List.copyOf buy you?In the compact constructor (e.g. `values = List.copyOf(values);`). List.copyOf does two jobs: it snapshots the input so later caller mutation can't affect the record, and it returns an unmodifiable list, so the generated accessor can't be used to mutate the internal list either - closing both the input and output leaks at once.
- Why do array components need extra care compared to List?Arrays are always mutable and have no unmodifiable view. List.copyOf gives you an unmodifiable copy in one call; for an array you must clone() it in the compact constructor to break input aliasing AND override the accessor to return a clone, or callers can mutate the backing array through the returned reference.
saying these in an interview costs you the question
- Claiming records are 'always immutable' regardless of component types
- Assuming final makes a List component unmodifiable
- Copying the list but sharing mutable elements and calling it deeply immutable
- Forgetting arrays need clone() on BOTH input and accessor (no unmodifiable view)