What is an Immutable Object
An Immutable Object is an object whose internal state (the values of all its fields) cannot be modified in any way after its construction has completed.
🟢 Junior Level
An Immutable Object is an object whose internal state (the values of all its fields) cannot be modified in any way after its construction has completed.
Whenever a modification is required (such as changing a property, adding an element, or shifting a date), the method never alters the original instance. Instead, it constructs and returns a brand-new instance containing the updated values.
In a Nutshell (30 Seconds)
- The original object remains perpetually unchanged and inherently thread-safe.
- Standard JDK examples:
String, primitive wrapper classes (Integer,Double,Boolean), modern Date-Time APIs (LocalDate,LocalTime,Instant),BigDecimal,UUID, andrecordtypes (Java 14+).
// Idiomatic Immutable Class:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// "Mutation" operation returns a NEW instance:
public Point move(int dx, int dy) {
return new Point(this.x + dx, this.y + dy);
}
}
Core Hallmarks of an Immutable Class
- Zero mutator methods (no setters like
setX(), no in-place modification methods). - All fields are declared
private final. - The class is declared
final(or provides only private constructors) to prevent subclasses from overriding behavior. - Any internal mutable objects are shielded using Defensive Copying.
🟡 Middle Level
The 5 Rules for Creating an Immutable Class (Effective Java, Item 17)
Joshua Bloch outlined five mandatory principles for designing immutable classes:
| # | Rule | Architectural Justification |
|---|---|---|
| 1 | Don’t provide mutator methods | Eliminates any public API that alters state (set*, add*, remove*). |
| 2 | Ensure the class cannot be extended | Declaring the class final (or using private constructors) prevents malicious subclasses from adding mutable state or overriding getters. |
| 3 | Make all fields final |
Enforces single initialization and guarantees Safe Publication under the Java Memory Model (JMM §17.5). |
| 4 | Make all fields private |
Encapsulates fields against direct external modification. |
| 5 | Ensure exclusive access to mutable components | Never store external mutable references directly; never return internal mutable references directly. Always perform defensive copies. |
Shallow Immutability vs. Deep Immutability
- Shallow Immutability: All field references declared directly in the class are
final, but the objects they point to can still be modified from the outside. - Deep (Transitive) Immutability: The entire object graph reachable from the root instance is completely immutable.
// BUG: Shallow Immutability (The class is NOT truly immutable!)
public final class Order {
private final List<String> items; // 'final' only protects the reference itself!
public Order(List<String> items) {
this.items = items; // BUG: Retaining external mutable reference!
}
public List<String> getItems() {
return items; // BUG: Leaking internal reference to callers!
}
}
// External code mutates Order's internal state:
List<String> list = new ArrayList<>();
list.add("Book");
Order order = new Order(list);
list.add("Laptop"); // Order's internal items changed externally!
order.getItems().clear(); // Order's items cleared completely!
Correct Implementation with Defensive Copying:
public final class Order {
private final List<String> items;
public Order(List<String> items) {
// Defensive copy on entry (List.copyOf returns an unmodifiable list)
this.items = List.copyOf(items);
}
public List<String> getItems() {
return items; // Safe: unmodifiable list cannot be mutated by callers
}
}
Modern Records (record, Java 16+)
Starting in Java 16, Java record declarations automate boilerplate for shallowly immutable data carriers:
public record User(String id, String email) {}
The compiler automatically marks the class final, declares fields as private final, and synthesizes accessor methods, equals(), hashCode(), and toString().
🔴 Senior Level
Java Memory Model Final Field Semantics (JLS §17.5)
Immutable objects in Java offer Safe Publication guarantees without requiring explicit synchronization (synchronized or volatile):
- Freeze Action: Writing to a
finalfield inside a constructor is paired with a compiler/hardware memory barrier (StoreStore barrier) executed prior to constructor exit. - Happens-Before Relationship: Any thread obtaining a reference to an immutable object after its constructor completes is guaranteed to observe the fully initialized values of all its
finalfields, even if the object reference was published through an unsynchronized data race.
The Constructor Escape Vulnerability (this Leak)
The JMM §17.5 guarantee is strictly contingent on one rule: the this reference must never escape during constructor execution:
public final class BrokenImmutable {
private final int value;
public static BrokenImmutable publishedInstance;
public BrokenImmutable(int value) {
this.value = value;
// FATAL: 'this' escapes before the Freeze Action executes!
publishedInstance = this;
// Or passing 'this' to an event listener: eventBus.register(this);
}
}
If a concurrent thread reads publishedInstance before the constructor’s freeze action completes, it may observe default uninitialized values (value = 0), destroying thread-safety guarantees.
Garbage Collector Synergies: Eliminating Card Table Write Barriers
Immutability provides massive efficiency benefits for HotSpot Garbage Collectors (G1, ZGC, Parallel GC):
- Generational Hypothesis: Most objects die young. When an old object in the Old Generation is mutated to reference a newly allocated Young Generation object, HotSpot must record this cross-generational pointer.
- Card Marking Write Barrier: On every reference mutation, the CPU executes a JIT write barrier that marks a byte in the GC Card Table as “dirty”, consuming CPU cycles and memory bus bandwidth.
- Zero Card Table Overhead: Once an immutable object is promoted to the Old Generation, it never mutates its references. Consequently, it produces zero dirty cards, reducing Young GC scanning times and pause overhead.
Persistent Data Structures & Structural Sharing
In deep immutability, creating full copies ($O(N)$) of large collections upon every update introduces severe memory and GC overhead. To eliminate this cost, functional architectures use Persistent Data Structures with Structural Sharing (such as the Hash Array Mapped Trie, HAMT, in libraries like Vavr):
- When an element is added or updated, only the path from the tree root to the modified leaf is re-allocated ($O(\log N)$ nodes).
- The remaining 99% of the tree nodes are shared directly between the old and new versions without copying.
4 Tricky Questions
1. Is a class declared with the record keyword guaranteed to be deeply immutable?
Answer:
No, it is not.
The record keyword guarantees strictly Shallow Immutability:
- The compiler generates
private finalfields and omits setter methods. - However, if any record component is a mutable object (e.g.,
record Order(String id, List<Item> items)), external callers can freely mutate that component:order.items().add(newItem). - To achieve deep immutability in a record, the developer must explicitly define a compact constructor that performs defensive copying:
public record Order(String id, List<Item> items) { public Order { items = List.copyOf(items); // Ensures true immutability } }
2. Is marking an immutable class final strictly mandatory? How can immutability be achieved without final?
Answer:
Marking the class final is not strictly mandatory if subclassing is prevented by alternative architectural design:
- Declare all constructors
private. - Provide instance creation exclusively through static factory methods:
public class ComplexNumber { private final double re; private final double im; private ComplexNumber(double re, double im) { this.re = re; this.im = im; } public static ComplexNumber of(double re, double im) { return new ComplexNumber(re, im); } }Because external classes cannot invoke
super(), subclassing is physically impossible. This guarantees immutability while allowing flexible package-private implementations.
3. Why must an immutable class constructor perform defensive copying of a mutable argument BEFORE checking its validity (TOCTOU Attack)?
Answer: To prevent Time-of-Check to Time-of-Use (TOCTOU) race condition security vulnerabilities:
// VULNERABLE: Checked before copied
public Period(Date start, Date end) {
if (start.after(end)) throw new IllegalArgumentException();
this.start = new Date(start.getTime()); // Copied AFTER validation!
}
If the passed Date instance is shared across threads, a malicious or concurrent thread could mutate start between the validation check and the copy constructor execution.
Rule: Always construct the defensive copy first, and validate invariants strictly on the isolated local copy.
4. Can an immutable object have mutable private fields for internal performance optimizations?
Answer:
Yes, as long as it does not violate Observable Immutability (contractual immutability).
The quintessential example in the JDK is java.lang.String:
- The field
private int hashis intentionally notfinal. - When a string is constructed,
hashdefaults to0. The hashcode is computed lazily upon the first call tohashCode()and cached in this field. - Because the computation is purely deterministic and external observers can never detect a state change, the object is contractually immutable.
Note: In multi-threaded code, non-final fields used for caching must be either idempotent primitives (like
String.hash) or protected via atomic orvolatilesemantics.
🎯 Interview Cheat Sheet
30-Second Summary
“An immutable object is an object whose observable state cannot change after construction; modifications always return a new instance. Implementing immutability requires a
finalclass (or private constructors),private finalfields, zero mutator methods, defensive copies of mutable components on both input and output, and avoidingthisescape in constructors. Key benefits include thread safety without synchronization, safe usage asHashMapkeys, and reduced GC overhead by eliminating Old-Gen Card Table write barriers.”
Key Takeaways for Senior Interviews
- 5 Rules of Bloch:
finalclass,private finalfields, no mutators, defensive copy in/out, safe publication. - JMM §17.5 Final Freeze: Memory barriers guarantee all threads observe initialized
finalfields without locks. - This Escape: Publishing
thisfrom inside a constructor breaks JMM safe publication guarantees. - Records (Java 16+): Provide shallow immutability only; mutable components like lists require explicit defensive copies in compact constructors.
- GC Card Table: Old Generation immutable objects never update references, producing zero Card Table write barrier overhead.
- Observable Immutability: Internal non-final fields are permissible for deterministic caching (e.g.,
String.hash).
Red Flags to Avoid
- ❌ Assuming a class is immutable just because it has no setters (ignoring mutable internal references).
- ❌ Claiming that Java
recordtypes are automatically deeply immutable. - ❌ Returning internal mutable arrays or lists without defensive copying.
- ❌ Asserting that immutability is always slower (in concurrent systems, lock-free reads deliver dramatic throughput gains).
Related Questions
- What Advantages Do Immutable Objects Provide — Architectural benefits and design
- How to Create an Immutable Class in Java — Concrete implementation blueprint
- Is It Enough to Make All Fields final for Immutability — Shallow vs deep immutability
- Why Are Immutable Objects Thread-Safe — Concurrency and JMM guarantees
- What is Defensive Copy — Copying strategies and TOCTOU prevention