Are Record Fields final
The Java compiler implicitly applies the final modifier to every component field declared in a record header. It is impossible to reassign a field reference after the object has...
🟢 Junior Level
Yes, absolutely all fields in a Record are automatically private final.
The Java compiler implicitly applies the final modifier to every component field declared in a record header. It is impossible to reassign a field reference after the object has been constructed.
public record User(String name, int age) {}
User user = new User("Alice", 25);
// ❌ Compilation error: Cannot assign a value to final variable 'name'
user.name = "Bob";
// ❌ Attempting to declare a setter will not compile:
// public void setAge(int age) { this.age = age; } // Cannot assign a value to final variable
// ✅ To change data, instantiate a new record (immutable update):
User updatedUser = new User(user.name(), 26);
Explicitly writing the final keyword in the record header is syntactically permitted, but redundant:
public record Point(final int x, final int y) {} // 'final' is redundant here
🟡 Middle Level
1. Shallow Immutability vs. Deep Immutability
[!CAUTION] The
finalkeyword on a component field guarantees immutability of the reference pointer only, not the internal state of the referenced object!
If a record component points to a mutable object (such as an ArrayList, Date, or primitive array), that object’s internal contents can be mutated externally:
public record ShoppingCart(String user, List<String> items) {}
List<String> mutableList = new ArrayList<>();
mutableList.add("Apple");
ShoppingCart cart = new ShoppingCart("Alice", mutableList);
// Mutated via original external reference:
mutableList.add("Banana");
System.out.println(cart.items()); // ["Apple", "Banana"] — state changed!
// Mutated via getter reference:
cart.items().clear();
System.out.println(cart.items()); // [] — cart was emptied!
2. How to Guarantee Deep Immutability
To achieve true deep immutability, enforce two-way defensive copying:
- For Collections: Use
List.copyOf(),Set.copyOf(), orMap.copyOf()inside the compact constructor:public record ShoppingCart(String user, List<String> items) { public ShoppingCart { // List.copyOf creates an unmodifiable copy and defends against external mutation: items = List.copyOf(items); } } - For Arrays: Defensive copies are required in both the constructor and the accessor:
public record Buffer(byte[] data) { public Buffer { data = data.clone(); // Inbound protection } @Override public byte[] data() { return data.clone(); // Outbound protection } }
🔴 Senior Level
Java Memory Model (JMM) Semantics and Safe Publication
Because all record component fields are final, they receive the full guarantees of Final Field Semantics under the Java Memory Model (JLS §17.5).
Hardware and Processor-Level Mechanics:
- At the end of the constructor, the JVM issues a Freeze Action (a
StoreStore/LoadStorememory barrier). - This ensures that memory writes for all record fields are committed to cache/memory before the reference to the newly instantiated
Recordbecomes visible to other CPU cores. - Any thread that reads a reference to a
Record(even if published via a data race withoutvolatileorsynchronized) is guaranteed to observe fully initialized field values, never stale default values (null,0,false).
Hard Reflection Blocking (JEP 395)
In legacy Java classes, developers could bypass final fields via reflection:
Field field = NormalClass.class.getDeclaredField("name");
field.setAccessible(true);
field.set(instance, "Hacked"); // Succeeded in older Java versions
In Record, this security vulnerability is eliminated at the JVM runtime level:
- The implementation of
Field.set(...)performs a check:if (this.clazz.isRecord()) throw new IllegalAccessException(...). - Attempting to modify a record field via reflection unconditionally throws
IllegalAccessException: Can not set final field of a record class, even iffield.setAccessible(true)was previously called.
4 Tricky Questions
1. If Record fields are final, why can record Book(String title, List<String> authors) change state after instantiation?
Answer:
Because the final keyword establishes only Shallow Immutability.
It prevents reassigning the authors reference pointer to point to another object in memory, but it places no restrictions on modifying the internal elements of the referenced ArrayList (e.g., via .add(), .remove(), .clear()). If a mutable list was passed into the constructor, the record’s state can be altered from the outside. To make the record deeply immutable, apply List.copyOf() in the compact constructor.
2. What Java Memory Model (JMM) guarantees arise from the fact that Record fields are final?
Answer:
Under JLS §17.5 (Final Field Semantics), the compiler and CPU insert memory barriers at the end of the constructor.
This guarantees Safe Publication: any other thread that obtains a reference to the record instance is guaranteed to observe the fully initialized component values without requiring volatile, locks, or synchronization. The risk of encountering a “partially constructed object” is eliminated for records.
3. What happens if you attempt to modify a Record field using the Reflection API (field.setAccessible(true); field.set(...))?
Answer:
The JVM throws an IllegalAccessException: Can not set final field of a record class.
In Java 16+ (JEP 395), Field.set() includes an explicit runtime check: if clazz.isRecord() evaluates to true, modification is rejected unconditionally. Unlike regular classes where reflective field mutation was sometimes permitted with warnings, record fields are strictly immutable against reflection modification.
4. If a Record component is an array (int[]), is calling clone() in the compact constructor sufficient?
Answer:
No, it is not sufficient.
Cloning the array in the compact constructor protects the record from mutations through the reference passed in during instantiation. However, the auto-generated canonical accessor record.data() returns a direct reference to the internal array. A caller can execute record.data()[0] = 999; and mutate the record’s state. To achieve true immutability with arrays, defensive copies must be made both on ingress and egress: clone the array in the constructor (data = data.clone()) and clone it again in an overridden accessor (public int[] data() { return data.clone(); }).
🎯 Interview Cheat Sheet
Summary in 60 Seconds
- All component fields in a Record are implicitly
private final; this modifier cannot be removed. - Records provide shallow immutability by default: fields cannot be reassigned, but referenced objects (collections, arrays) can still be mutated internally.
- Collections and arrays require defensive copies (
List.copyOf(),.clone()) in compact constructors and accessors to achieve deep immutability. - JMM Safe Publication: Final field semantics guarantee that all threads observe properly initialized fields without synchronization.
- Reflection Immunity: The JVM explicitly blocks reflective mutation via
Field.set(), throwingIllegalAccessException.
Common Interview Traps
- Assuming records are deeply immutable out of the box (passing an
ArrayListallows external mutations). - Returning internal array components directly from accessors without cloning.
- Believing
finalcan be bypassed on record fields usingField.setAccessible(true).