skip to content

Why must an immutable class prevent subclassing, and what are the ways to do it?

level: juniorimportance: should knowfreq 48%

answer

  1. subclass could add mutable state or override to return changing values
  2. base-type reference can't trust immutability if subclassing is allowed
  3. final class = compiler-enforced, simplest
  4. private ctor + static factory = also caching, flexible return type, naming
  5. records are implicitly final

basics

~20 s

If a class can be subclassed, a subclass could add changeable fields or override methods to return different values, breaking the promise that the object never changes. You stop this by marking the class final, or by hiding the constructor (making it private) and creating instances through a static factory method.

solid answer

~40 s

An immutable class makes a contract: 'my state never changes.' Subclassing can break that contract — a subclass can introduce its own **mutable** field, or **override** a method to return values that vary over time, so a caller holding a reference of the base type can't trust immutability anymore. The simplest defense is to make the class **`final`**, which the compiler enforces — no subclass is possible. An alternative is a **private (or package-private) constructor plus a static factory method** (`Point.of(x, y)`); since outside code can't invoke a constructor, it can't subclass either, and the factory gives you extra flexibility — returning cached instances, returning a different (controlled) subtype, or skipping construction entirely. This factory approach is what `Integer.valueOf` and `List.of` use. Records take the first route automatically: a record is **implicitly final**.

code

java · 15 lines
java
// Route 1: final class (simplest)
public final class Money {
    private final long cents;
    public Money(long cents) { this.cents = cents; }
}

// Route 2: private constructor + static factory (also enables caching)
public class Currency {
    private static final Map<String, Currency> CACHE = new ConcurrentHashMap<>();
    private final String code;
    private Currency(String code) { this.code = code; }   // no public ctor -> no subclassing
    public static Currency of(String code) {              // can return a cached instance
        return CACHE.computeIfAbsent(code, Currency::new);
    }
}

go deeper

for a junior

Can state that you mark the class final to stop subclassing and that subclassing would let the object change.

for a middle

Explains concretely how a subclass breaks immutability (extra mutable field, overriding accessors) and names both prevention techniques.

for a senior

Articulates the factory-method advantages (caching, controlled return types, naming) and when they outweigh plain final; ties in records being implicitly final.

for a principal

Considers API evolution and encapsulation trade-offs (final vs sealed vs factory), library examples (Integer.valueOf, List.of), and sets conventions for value types across a codebase.

## The contract immutability makes When a class is immutable, callers rely on a guarantee: *any reference of this type points to an object whose state will never change.* Code is written trusting that guarantee — caching the object, using it as a map key, sharing it across threads without locks. ## How subclassing breaks it Inheritance lets a subclass do two dangerous things: 1. **Add mutable state.** A subclass can declare its own non-final field and a setter. Now an instance of that subclass *can* change, even though it's-a (substitutable for) the immutable base type. A method that accepts the base type and assumes immutability is now wrong. ```java class Point { final int x, y; /* immutable */ } class Movable extends Point { int dx; void shift() { dx++; } } // breaks the promise ``` 2. **Override methods to lie.** A subclass can override an accessor or `equals`/`hashCode` to return values that vary over time or that violate the value contract, making the 'immutable' base reference behave mutably or inconsistently (e.g. a changing hash code, which corrupts `HashMap` keys). Because a base-type reference might actually point to such a subclass, the immutability guarantee can't be trusted unless subclassing is **prevented**. ## Two ways to prevent it ### 1. `final` class (simplest) ```java public final class Point { ... } ``` The compiler refuses any `extends Point`. Done. This is the default, idiomatic choice for a value type. Records are **implicitly final**, so they get this for free. ### 2. Private/package-private constructor + static factory Make the constructor inaccessible from outside, and expose creation through a static method: ```java public class Point { private final int x, y; private Point(int x, int y) { this.x = x; this.y = y; } public static Point of(int x, int y) { return new Point(x, y); } } ``` A subclass constructor must call a superclass constructor; if none is accessible, you **can't** subclass — so the class is effectively final (any subclass would have to live in the same package for a package-private constructor). This route has bonus advantages over `final`: - **Caching / interning:** the factory can return a shared instance (`Integer.valueOf` caches -128..127; `Boolean.TRUE`). - **Returning subtypes you control:** the factory's declared return type can be the public type while it actually returns an internal optimized implementation (`List.of` returns different hidden classes by size). This is impossible with a constructor, which always returns its exact type. - **Naming & no required allocation:** factory methods can have descriptive names and needn't create a new object on every call. ## Which to choose For a plain value type, **`final` class** (or a **record**) is the clear, simplest answer. Reach for the **private-constructor + factory** when you also want caching, flexible return types, or named creators — i.e. when the extra control pays for the extra ceremony.

  • What advantage does a static factory + private constructor give over simply marking the class final?
    Both prevent subclassing, but the factory also lets you cache/intern instances, return a controlled subtype hidden behind the public type, give the creator a descriptive name, and avoid allocating a new object on every call (e.g. Integer.valueOf, List.of).
  • Are records final?
    Yes — records are implicitly final and cannot be extended, so they satisfy the no-subclassing requirement automatically. You don't (and can't) add the final keyword to a record.

saying these in an interview costs you the question

  • Thinking final fields alone prevent subclassing (final on a field ≠ final class).
  • Believing a subclass can't break immutability because it inherits final fields — it can add its own mutable ones.
  • Forgetting that a package-private constructor still allows same-package subclasses.
  • Claiming records need an explicit final keyword (they're implicitly final).
  • Choosing the factory pattern's complexity for a plain value type that just needs `final`.

context