🔒 Section 13 · Question #1

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, and record types (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

  1. Zero mutator methods (no setters like setX(), no in-place modification methods).
  2. All fields are declared private final.
  3. The class is declared final (or provides only private constructors) to prevent subclasses from overriding behavior.
  4. 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):

  1. Freeze Action: Writing to a final field inside a constructor is paired with a compiler/hardware memory barrier (StoreStore barrier) executed prior to constructor exit.
  2. 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 final fields, 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):

  1. 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.
  2. 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.
  3. 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 final fields 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 hash is intentionally not final.
  • When a string is constructed, hash defaults to 0. The hashcode is computed lazily upon the first call to hashCode() 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 or volatile semantics.

🎯 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 final class (or private constructors), private final fields, zero mutator methods, defensive copies of mutable components on both input and output, and avoiding this escape in constructors. Key benefits include thread safety without synchronization, safe usage as HashMap keys, and reduced GC overhead by eliminating Old-Gen Card Table write barriers.”

Key Takeaways for Senior Interviews

  • 5 Rules of Bloch: final class, private final fields, no mutators, defensive copy in/out, safe publication.
  • JMM §17.5 Final Freeze: Memory barriers guarantee all threads observe initialized final fields without locks.
  • This Escape: Publishing this from 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 record types 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).