Is it enough to make all fields final for immutability?
If the class consists only of primitives (int, double, boolean) and immutable types (String, BigDecimal, LocalDate), then final for all fields + final for the class — is suffici...
Basic Level
No, it’s not enough! final only protects the variable itself (the reference), not the data it points to.
Example of the problem
public final class NotReallyImmutable {
private final List<String> items; // field is final
public NotReallyImmutable(List<String> items) {
this.items = items; // saved a direct reference!
}
public List<String> getItems() {
return items; // returned a direct reference!
}
}
List<String> myItems = new ArrayList<>();
myItems.add("A");
NotReallyImmutable obj = new NotReallyImmutable(myItems);
myItems.add("B"); // Changed the "immutable" object from outside!
// this.items = items saves the same reference as myItems.
// Therefore myItems.add("B") changes the list inside the object.
obj.getItems().clear(); // Can also be changed through the getter!
What else is needed
- The class must be
final - Make copies of collections in the constructor
- Return copies (or immutable wrappers) from getters
final fields vs immutable class
| final fields | Immutable class | |
|---|---|---|
| Fields cannot be reassigned | Yes | Yes |
| Mutable fields protected | No | Yes (defensive copy) |
| Inheritance prohibited | No | Yes (final class) |
| Getters return copies | No | Yes |
Intermediate Level
Shallow Immutability vs Deep Immutability
Shallow — final fields, but contents of mutable objects can be changed:
private final List<User> users; // the list cannot be replaced, but elements can be changed
Deep — everything, including nested objects, is immutable:
public final class Group {
private final List<String> names; // String is immutable — deep protection ensured
public Group(List<String> names) {
this.names = List.copyOf(names); // copy
}
public List<String> getNames() {
return names; // List.copyOf already returned an immutable list
}
}
When final is sufficient
If the class consists only of primitives (int, double, boolean) and immutable types (String, BigDecimal, LocalDate), then final for all fields + final for the class — is sufficient.
When final is NOT sufficient
- Arrays — elements can be changed
- Collections (
List,Map,Set) — contents can be changed Date— hassetTime(),setYear()methods, etc.- Custom mutable classes
Advanced Level
Reflection Attack
Even a “perfectly” immutable class can be attempted to be changed via Reflection:
Field field = MyClass.class.getDeclaredField("value");
field.setAccessible(true);
field.set(obj, newValue); // breaks immutability
Starting with Java 9 (Project Jigsaw), the module system limits reflection, and with Java 16 (JEP 396), access to java.base fields is blocked by default.
The complete formula for immutability
final class + final fields + defensive copies + no this-escape = True Immutability
Summary for Advanced
finalonly protects the reference, not the data at the reference- For collections and arrays, defensive copying is mandatory
- Immutability is a property of the entire object hierarchy, not just the top level
- Remember:
final fields + final class + defensive copies = Immutable - In Java 14+, use
record— it applies most rules automatically
Interview Cheat Sheet
Must know:
finalonly protects the reference, not the data at the reference- Shallow Immutability — final fields, but contents of mutable objects can be changed
- Deep Immutability — everything, including nested objects, is immutable
- Formula:
final class + final fields + defensive copies + no this-escape = True Immutability - When final is sufficient: primitives + immutable types (String, BigDecimal, LocalDate)
- When final is NOT sufficient: arrays, collections, Date, custom mutable classes
- Reflection Attack: even a perfectly immutable class can be changed via reflection (before Java 16)
Frequent follow-up questions:
- Why doesn’t final List protect? —
list.add()changes the contents, the reference is the same - When is final sufficient? — Only if the class consists of primitives and immutable types
- What is Shallow vs Deep Immutability? — Shallow: container is unchanged, elements — not; Deep: everything is unchanged
- Can reflection break immutability? — Yes, but in Java 16+ access to java.base fields is blocked
Red flags (DO NOT say):
- “Final fields = immutable class” — without defensive copy and final class, not enough
- “List.copyOf not needed if field is final” — final protects the reference, not the contents
- “clone() is the best way to copy” — clone() is considered broken (Effective Java Item 13)
- “Reflection — not a problem” — before Java 16 this is a real threat
Related topics:
- [[1. What is an immutable object]]
- [[7. What is the final keyword and how does it help create immutable classes]]
- [[9. What to do if a class field references a mutable object]]
- [[10. What is a defensive copy]]
- [[14. What is the difference between shallow copy and deep copy]]