What steps make a Java class immutable? Walk through the checklist.
answer
- final class, private final fields, no setters
- defensive copy IN (constructor) and OUT (getter)
- final stops reassignment, not mutation of the referent
- skip copies for already-immutable types (String, int, LocalDate)
- records: final + private final free, but copy yourself in compact ctor + accessor
basics
~20 sMake the class final so nobody can subclass it, make every field private and final, provide no setters, set all fields once in the constructor, and copy any mutable field when it goes in or comes out so outside code can't change your data.
solid answer
~50 sAn immutable class is one whose state can't change after construction. The checklist: (1) make the class final (or use a private constructor + factory) so a subclass can't add mutable state or override behavior; (2) make all fields private final, assigned exactly once in the constructor; (3) expose no setters or other mutators; (4) for any field of a mutable type (arrays, Date, collections, other mutable objects), make a defensive copy on the way in (in the constructor) and on the way out (in getters), so callers never hold a reference to your internal object. Following this, instances are thread-safe without synchronization, freely shareable, and safe as map keys. Records (Java 16+) give you final class, private final fields, and no setters automatically, but they do NOT defensively copy mutable components, so you still copy in a compact constructor and in accessors.
code
java · 17 linespublic final class Period { // 1. final: no subclassing
private final Date start; // 2. private final fields
private final Date end;
public Period(Date start, Date end) {
// 4a. defensive copy IN: store our own Date, not the caller's
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
// validate AFTER copying (avoid TOCTOU on the caller's mutable args)
if (this.start.after(this.end))
throw new IllegalArgumentException("start after end");
}
// 3. no setters. 4b. defensive copy OUT: caller can't reach our Date
public Date getStart() { return new Date(start.getTime()); }
public Date getEnd() { return new Date(end.getTime()); }
}go deeper
Can recite the core list: final class, private final fields, no setters, set everything in the constructor. May not yet grasp defensive copying.
Explains the full checklist including defensive copy on input and output, and why final alone doesn't make a referenced mutable object immutable.
Knows the nuances: unmodifiableList vs List.copyOf, shallow vs deep copy, skipping copies for immutable types, validate-after-copy to avoid TOCTOU, and that records don't auto-copy.
Frames immutability as a design default (safe publication via final fields, thread-safety, value semantics), weighs it against allocation cost, and sets team conventions (records + compact constructors, withers, when a builder is warranted).
## What "immutable" means An **immutable object** is one whose observable **state** (the values of its fields) cannot change after the object is constructed. Once you build it, every method call returns the same answer forever. The opposite is a **mutable** object, whose fields can be reassigned or whose referenced objects can be altered (e.g. via a setter like `setName(...)`). Why bother? Immutable objects are: - **Thread-safe for free.** If state never changes, multiple threads can read it concurrently with no locks, no `synchronized`, no `volatile` — there is nothing to race on. - **Safe to share and cache.** You can hand the same instance to many callers without fear they'll corrupt it for each other; you can intern/cache them (like `Integer.valueOf`). - **Safe as `HashMap` keys / `HashSet` members.** A key's hash code must not change while it's in the map; immutability guarantees that. - **Easier to reason about.** No "who changed this behind my back?" bugs. ## The checklist (each step and *why*) ### 1. Prevent subclassing — make the class `final` If someone can `extends YourClass`, they can add a mutable field or override a method to return changing values, breaking the immutability guarantee that callers rely on. The simplest fix is `public final class Point`. An alternative is a **private constructor plus a static factory method** (`Point.of(x, y)`) — outside code can't call `new`, so it can't subclass either, and you keep room to return cached instances or subtypes you control. ### 2. Make all fields `private final` - `private` so no other class can reach in and reassign the field directly. - `final` so the *compiler* guarantees the field is assigned **exactly once** and never reassigned. `final` also gives a JMM (Java Memory Model) guarantee: a correctly constructed object with final fields is safely published to other threads without extra synchronization (the **final-field safe-publication** guarantee). ### 3. Provide no setters (and no other mutators) A setter (`setX`) by definition changes state, so an immutable class has none. More broadly, expose **no method that mutates a field** — no `clear()`, `add()`, etc. To "change" a value you instead return a **new** instance: `Point withX(int x)` returns `new Point(x, this.y)`. This is the **"wither"** pattern. ### 4. Defensively copy mutable fields — on input AND on output This is the step people forget. `final` only stops the *reference* from being reassigned; it does **not** stop the *object the reference points to* from being mutated. If a field's type is mutable — arrays (`int[]`), `java.util.Date`, `Calendar`, `StringBuilder`, mutable collections (`ArrayList`, `HashMap`), or any other mutable object — then: - **Copy on input (in the constructor):** store a copy of what the caller passed, not the caller's own object. Otherwise the caller still holds a reference to your internal object and can mutate it later: ```java this.dates = new ArrayList<>(dates); // not: this.dates = dates; ``` Without this, the caller does `list.clear()` after construction and your "immutable" object silently changed. This is called a **reference leak on input** (or *aliasing*). - **Copy on output (in getters):** return a copy (or an unmodifiable view) so the caller can't mutate the object you handed back and thereby reach your internal field: ```java public List<LocalDate> getDates() { return List.copyOf(dates); } ``` Without this, `obj.getDates().clear()` reaches your internal list — a **reference leak on output**. Note: if a field is **already immutable** (`String`, `int`, `Integer`, `LocalDate`, `BigDecimal`, an `enum`, another properly-immutable class), you do **not** need to copy it — sharing the reference is safe. Copy only genuinely mutable components. Also note a subtlety: `Collections.unmodifiableList(x)` wraps but does **not** copy, so if you still hold a mutable `x` it can change *through* the wrapper — prefer `List.copyOf(...)` (Java 10+) which copies into a truly immutable list. And `clone()` / `Arrays.copyOf` are **shallow** — for nested mutable elements you may need a deeper copy. ## Records do most of this for you (Java 16+) A `record` automatically: is `final`, gives you `private final` components, a canonical constructor, accessors, and value-based `equals`/`hashCode`/`toString`. So steps 1–3 are handled. But a record does **NOT** defensively copy. If a component is mutable, you must still copy in a **compact constructor** (input) and in an **accessor** (output): ```java record Period(List<LocalDate> dates) { Period { dates = List.copyOf(dates); } // compact constructor: validate + copy IN public List<LocalDate> dates() { return List.copyOf(dates); } // copy OUT (List.copyOf is idempotent here) } ``` The **compact constructor** (no parameter list, no explicit field assignment) is the canonical spot to **validate** arguments (e.g. throw on null/illegal values) and to **defensively copy** before the implicit field assignment runs. This keeps records as airtight as a hand-written immutable class. ## Putting it together (hand-written) The canonical "immutable with a mutable field" example (Joshua Bloch's `Period`) copies the `Date` in the constructor and again in the getter; doing only one of the two leaves a hole. Validate **after** copying, not before, to avoid a time-of-check/time-of-use race where the caller mutates the object between your check and your copy.
- Why must you copy a mutable field both on input and on output, not just once?Each is a separate leak. Copy-on-input stops the caller (who still holds the object they passed) from mutating your internal state afterward. Copy-on-output stops the caller from mutating the object you return and reaching into the same internal state. Skipping either leaves one path open.
- Do you need to defensively copy a String field?No. String is immutable, so sharing the reference is safe. You only copy fields whose type is mutable (arrays, Date, mutable collections, etc.).
saying these in an interview costs you the question
- Thinking `final` on a field makes a mutable object it points to (a List or Date) immutable.
- Copying on input but not on output (or vice versa) — both leaks must be closed.
- Using Collections.unmodifiableList as a 'copy' — it's a view; the backing list can still change.
- Defensively copying immutable types like String or LocalDate (wasteful, signals misunderstanding).
- Believing a record is automatically safe with a mutable component — it does not copy for you.