Beyond thread-safety, why prefer immutable classes by default, and what are the trade-offs?
answer
- thread-safe w/o locks; safe publication via final fields
- shareable, cacheable, internable; valid map keys (stable hash)
- cost: new object per change → allocation/GC pressure
- mitigate: withers, builders, mutate-then-freeze, copy-on-write
- Effective Java: immutable by default, else limit mutability
basics
~20 sImmutable objects can't change, so they're thread-safe without locks, safe to share and cache, and safe as map keys, and they're easier to reason about. The trade-off is that 'changing' a value means creating a new object, which costs extra allocations and can be wasteful for large or frequently-updated state.
solid answer
~60 sImmutability is a good **default** because it removes whole categories of bugs. An object that never changes is **thread-safe with no synchronization**, **freely shareable and cacheable** (you can intern instances), **safe as a `HashMap` key or `HashSet` element** because its hash code is stable, and far **easier to reason about** since no method call can change it behind your back. It also makes **safe publication** trivial: with `final` fields, a correctly-constructed instance is visible to other threads without extra synchronization. The cost is that every "modification" produces a **new object** — `withX(...)` returns a fresh instance — which means more **allocation and GC pressure**, and it can be awkward or expensive for large objects or hot update loops (e.g. building a big collection element-by-element). Mitigations: use copy-on-write only where reads dominate writes, expose **withers** for small tweaks, provide a **builder** for many-field construction, or batch mutations on a temporary mutable structure and freeze it once at the end. The guidance (Effective Java): make classes immutable unless there's a good reason not to; if not, limit mutability as much as possible.
go deeper
Knows immutable objects are thread-safe and can't be changed; may not list caching, map-key, or publication benefits.
Articulates the main benefits (thread-safety, sharing, map keys) and the basic cost (a new object per change, e.g. String concatenation).
Adds safe-publication via final fields, failure atomicity, and the mitigations (withers, builders, mutate-then-freeze, copy-on-write), and quotes the 'immutable by default' guidance.
Frames it as a system-wide default with explicit, measured exceptions; reasons about GC/allocation budgets, escape analysis, and where mutable hot-path types or persistent data structures are justified.
## Why immutability is a strong default An **immutable** object's state can't change after construction. That single property cascades into several concrete benefits: 1. **Thread-safety for free.** Data races happen when two threads access shared *mutable* state and at least one writes. If nothing writes after construction, there's nothing to race on — so immutable objects need **no `synchronized`, no locks, no `volatile`**. This is the headline benefit. 2. **Safe publication via final fields.** The Java Memory Model gives a special guarantee: if an object's fields are `final` and the object doesn't leak `this` during construction, then any thread that sees a reference to the fully-constructed object is **guaranteed to see the correct, fully-initialized field values** — without extra synchronization. Mutable objects need careful publication (e.g. `volatile`) to avoid seeing partially-constructed state. 3. **Free sharing, caching, interning.** Because no one can corrupt them, you can hand the same instance to many callers and even **cache/intern** them (like `Integer.valueOf` caching small ints, or `BigDecimal.ZERO`). No defensive copies are needed when *passing them around* — only mutable fields *inside* a class need copying. 4. **Valid map keys / set members.** A `HashMap` key's `hashCode` (and `equals`) must not change while it's in the map, or the entry becomes unreachable. Immutable objects guarantee a stable hash, so they're always safe keys. 5. **Failure atomicity & simpler reasoning.** Since an immutable object is either fully constructed or not at all (validation throws in the constructor), you never have a half-updated object. And callers can trust that a reference they hold means a fixed value, eliminating "who changed this?" bugs. ## The trade-offs Nothing is free: 1. **A new object per change.** To "change" one field you must create a whole new instance (`p.withX(5)` returns a new `Point`). For objects changed often or in tight loops, this means many short-lived objects and **GC pressure**. Example: concatenating in a loop with immutable `String` is O(n²) garbage — which is exactly why mutable `StringBuilder` exists. 2. **Cost for large state.** If the object is big (a large array or collection) and you change one element, a naive immutable approach copies the whole thing. Persistent/structural-sharing data structures mitigate this but aren't in the JDK by default. 3. **More objects = more memory churn**, which can matter in latency-sensitive or memory-constrained contexts. ## Mitigations / when to relax - **Withers** for small, occasional changes (`withX`) keep the immutable API ergonomic. - **Builders** make constructing many-field immutable objects readable without setters. - **Companion mutable type for the hot path.** The classic pattern: do heavy mutation on a mutable helper (`StringBuilder`, a local `ArrayList`), then **freeze** it into an immutable value once (`toString()`, `List.copyOf(...)`). You pay copy cost once, not per step. - **Copy-on-write** (`CopyOnWriteArrayList`) suits read-heavy, write-rare shared state. ## The guidance Joshua Bloch's *Effective Java* states it plainly: **"Classes should be immutable unless there's a very good reason to make them mutable"**, and if you can't make a class immutable, **limit its mutability as much as possible** (minimize the number of mutable fields, make everything you can `final`). Immutability is the default; mutability is the justified exception (performance hot paths, JavaBeans/ORM requirements, genuinely stateful objects like buffers).
- Give a concrete example where immutability hurts performance, and the standard fix.Building a large String by `+=` in a loop creates a new String every iteration — O(n²) characters copied and lots of garbage. The fix is the mutate-then-freeze pattern: accumulate in a mutable `StringBuilder` and call `toString()` once at the end.
- Why are immutable objects safe to publish across threads without synchronization?The Java Memory Model guarantees that final fields of a properly-constructed object (one that doesn't leak `this` during construction) are visible to any thread that obtains a reference, with correct initial values. So another thread can't see a partially-constructed immutable object.
saying these in an interview costs you the question
- Claiming immutable objects are slow with no nuance — the cost is allocation, often negligible and JIT-optimized.
- Saying immutables need synchronization to be thread-safe (they don't).
- Forgetting the String-concatenation-in-a-loop pitfall as the canonical cost example.
- Treating immutability as all-or-nothing rather than 'limit mutability where you can't eliminate it'.
- Ignoring that passing immutables around needs no copy — only internal mutable fields do.