A record's components are private final fields — does that make a record fully immutable? Explain the limits.
answer
- final fields => shallow immutability only
- mutable component object can still be changed via shared reference
- leak on input (caller keeps ref) and on output (accessor returns ref)
- compact constructor: List.copyOf to snapshot
- mutable key => lost HashMap entry
basics
~20 sThe record's own fields can't be reassigned, so the record is shallowly immutable. But if a component is a reference to a mutable object (like a List or array), callers can still change that object's contents through the shared reference.
solid answer
~50 sEach record component becomes a `private final` field, so the record's *own* state — the set of references it holds — cannot change after construction. That gives **shallow immutability**. It does **not** guarantee **deep immutability**: if a component is a mutable object (a mutable `List`, a `Date`, a `byte[]`, a regular mutable class), the holder of that reference — including the original caller — can mutate the referenced object's contents, and the record will reflect the change. To make a record effectively deeply immutable you defensively copy mutable inputs in a **compact constructor** and copy again in a custom accessor (or store inherently immutable types like `List.copyOf(...)`, `String`, boxed primitives, or other records). Without those copies, a record's value-based equality and hash code can silently shift if a component object is mutated, which is dangerous when the record is used as a map key.
code
java · 14 lines// Leaky: shallow immutability only
record Team(String name, List<String> members) { }
List<String> roster = new ArrayList<>(List.of("Ann"));
Team t = new Team("A", roster);
roster.add("Bob");
t.members(); // [Ann, Bob] -- mutated underneath
// Defended: effectively deeply immutable
record SafeTeam(String name, List<String> members) {
SafeTeam {
members = List.copyOf(members); // immutable snapshot on input
}
// accessor returns the unmodifiable copy -> no output leak
}go deeper
Understands that record fields are final so you cannot reassign them, and that the record is therefore mostly immutable.
Distinguishes shallow from deep immutability and recognizes that a mutable component (List, array) can still be changed via a shared reference.
Implements defensive copying in the compact constructor (and accessor when needed), prefers immutable component types, and explains the mutable-key hazard for hash collections.
Sets team conventions (immutable component types, no raw arrays, copy-on-input policy), reasons about thread-safety guarantees that depend on true immutability, and weighs allocation/copy cost against safety across API and serialization boundaries.
## Shallow vs. deep immutability **Immutable** means 'cannot change after construction.' There are two depths: - **Shallow immutability:** the object's own fields can't be reassigned. The *references* it holds are fixed. - **Deep immutability:** additionally, none of the *objects those references point to* can change either. ## What records give you Each component becomes a `private final` field. `final` forbids reassigning the field after the constructor finishes. So a record is **shallowly immutable**: you can never make `p.x` point to a different value, and you can never make a `List`-typed component point to a different list. ```java record Team(String name, List<String> members) { } ``` Here `members` (the *reference*) is fixed. But the **list it points to** may be a mutable `ArrayList`. ## The leak ```java List<String> roster = new ArrayList<>(List.of("Ann")); Team t = new Team("A", roster); roster.add("Bob"); // caller still holds the same list... t.members(); // [Ann, Bob] <- the record changed underneath you ``` The component reference never changed, yet the record's observable value did. This is an **encapsulation leak**: external code can mutate the record's state. Two ways the reference can leak: - **On input:** the caller keeps a reference to the object it passed in. - **On output:** the accessor hands the internal mutable object straight back, and the receiver mutates it. ## Why it is dangerous for records specifically Records advertise value-based `equals`/`hashCode`. If a component object mutates, the record's hash code and equality change too. If that record is a **key** in a `HashMap`/`HashSet`, the entry becomes effectively unreachable — the classic 'mutable key' hazard, now hiding behind a type that *looks* immutable. ## Making a record effectively deeply immutable Use the **compact constructor** (a record-specific constructor that omits the parameter list and lets you validate/normalize before the implicit field assignment) to defensively copy mutable inputs, and override the accessor to copy on the way out: ```java record Team(String name, List<String> members) { Team { // compact constructor members = List.copyOf(members); // immutable snapshot on input } public List<String> members() { return members; // already immutable -> safe to return } } ``` `List.copyOf` returns an **unmodifiable** list, so no extra output copy is needed. For genuinely mutable types (e.g. `Date`, `byte[]`) you copy on **both** input and output. ## Rules of thumb - Prefer **inherently immutable** component types: `String`, boxed primitives, other records, `List.of`/`copyOf`, `java.time` types. - If a mutable type is unavoidable, **defensively copy in the compact constructor and in the accessor**. - Avoid raw arrays as components — they are mutable and the generated `equals` compares them by identity anyway.
- Why is List.copyOf in the compact constructor usually enough, without also copying in the accessor?List.copyOf returns an unmodifiable list. Since the stored reference is final and the list itself rejects mutation, handing it back from the accessor is safe — the receiver cannot change it. You only need to copy on output when the stored object is itself mutable (e.g. a Date or array).
- How does a mutable component break a record used as a HashMap key?HashMap buckets an entry by the key's hashCode at insertion time. If a mutable component is later changed, the record's hashCode (derived from all components) changes, so a lookup with the 'same' key now hashes to a different bucket and the entry is effectively lost. Defensive copying / immutable component types prevent this.
saying these in an interview costs you the question
- Claiming records are always deeply immutable — they are only shallowly immutable by default.
- Storing a caller-supplied mutable List/array without copying and assuming it is safe.
- Returning the internal mutable object from a custom accessor (output leak).
- Using raw arrays as components and expecting immutability or content-based equality.
- Saying final on a reference field freezes the referenced object — it only freezes the reference.