skip to content

Immutability

Building classes whose state cannot change after construction, using final, records and defensive copying, and knowing what that buys and costs on the JVM. Interviewers reach for immutability whenever thread safety, caching or map keys come up.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

10

What are the main benefits of making a Java class immutable?

level: juniorimportance: must knowfreq 78%

answer

  1. Thread-safe with no locks (no writes = no races)
  2. Safe to share/cache/intern (String pool, Integer cache)
  3. Stable hashCode -> valid HashMap/HashSet keys
  4. No defensive copies needed; failure-atomic construction
  5. Cost: new object per change -> churn, GC

basics

~20 s

An immutable object never changes after it's built. That makes it safe to share between threads with no locking, safe to cache and reuse, and safe to use as a map or set key. It's also easier to reason about because its state can't surprise you.

solid answer

~50 s

Immutability means an object's observable state is fixed once construction finishes. The big wins: (1) Thread safety for free - because no thread can mutate the object, there are no data races, so you need no synchronization to share it across threads. (2) Safe sharing and caching - since the value can never change, many references can share one instance and you can freely cache or intern it (e.g. String literals, Integer cache). (3) Valid hash keys - an immutable object's hashCode and equals never change, so it stays findable in a HashMap or HashSet. (4) Simpler reasoning and fewer bugs - you can pass it anywhere without defensive copies and trust it won't be mutated behind your back; failed operations can't leave it half-updated. The cost is that 'changing' it means allocating a new object.

go deeper

for a junior

Can state the four core benefits (thread-safe, shareable/cacheable, valid keys, easier to reason about) and knows String is immutable while StringBuilder is mutable.

for a middle

Explains WHY each benefit holds (no writes => no races; stable hash => findable keys) and names the cost: a new object per change, with the String-concatenation-in-a-loop example.

for a senior

Frames it as an engineering trade-off: immutable by default, mutable companion for hot paths; mentions safe publication, failure atomicity, and allocation/GC pressure quantitatively.

for a principal

Reasons about it at system scale: when allocation pressure actually bites (hot loops, large structures), escape analysis / generational GC mitigations, persistent data structures, and codifies immutability as a default with measured exceptions.

## What 'immutable' means An object is **immutable** when its **observable state cannot change after construction completes**. In Java the recipe is: make the class `final` (so no subclass can add mutable behavior), make every field `private final`, expose **no setters** or other mutators, and **defensively copy** any mutable field on the way in (constructor) and on the way out (getter) so no caller can reach in and alter the internals. `String`, `Integer`, `LocalDate`, and `record` types are immutable. ## Why each benefit follows ### 1. Thread safety without synchronization A **data race** happens when two threads access the same memory, at least one writes, and there is no ordering between them. If an object is never written after construction, there is no write to race against - every thread only **reads**. So an immutable object that is **safely published** (made visible to other threads correctly - e.g. via a `final` field, a `volatile`, a `static` initializer, or a concurrent collection) can be shared by any number of threads with **zero locks**. Locks (`synchronized`, `ReentrantLock`) exist to serialize writes; with no writes, they're unnecessary. This removes a whole category of bugs (lost updates, torn reads) and the overhead and deadlock risk of locking. > Subtlety: the JVM guarantees that `final` fields set in a constructor are visible to other threads **without** extra synchronization, *provided* the `this` reference didn't escape during construction. That's the memory-model basis for 'thread-safe for free'. ### 2. Safe sharing, caching, and interning Because the value can't change, one instance can be **shared** by many parts of a program with no risk that one user's change surprises another. This enables **caching/interning**: keep a pool of canonical instances and hand out the same object for the same value. The JDK does exactly this - `String` literals live in a shared pool; `Integer.valueOf` caches boxed values for -128..127; `Boolean.TRUE` is a singleton. Sharing saves memory and allocation. With a mutable object you'd have to copy before sharing, or risk aliasing bugs. ### 3. Valid keys for hash-based and sorted collections A `HashMap`/`HashSet` places an entry in a bucket chosen from the key's `hashCode()`, and finds it later by recomputing that hash. If a key's `hashCode()` (or the fields `equals()` uses) **changes after insertion**, the map looks in the wrong bucket and the entry becomes **unretrievable** - a silent, nasty bug. Immutable keys can't change, so their hash is stable and lookups always work. The same holds for `TreeMap`/`TreeSet` ordering via `compareTo`. ### 4. Simpler reasoning, fewer defensive copies, failure atomicity If you receive an immutable object you **know** it won't change underneath you, so you can store the reference directly instead of copying defensively, and you don't have to worry about who else holds it. State is established **atomically** at construction: either the object is fully built and valid, or construction throws and you have nothing - it can never be left in a half-updated, invalid state (**failure atomicity**). This makes code easier to test and verify. ## The trade-offs (the other half of the topic) Immutability isn't free. Because you can't change an instance, **every 'modification' creates a new object**: - **Object churn / allocation pressure**: a loop that 'updates' an immutable object N times allocates N objects. The classic example is `String` concatenation in a loop (`s = s + x`), which is O(n^2) in work and garbage; you use a mutable `StringBuilder` instead. - **Copy-on-modify overhead**: producing the new object may require copying internal arrays/collections, which costs time and memory proportional to size. - **GC impact**: all those short-lived objects must be collected. Modern generational GCs make young-generation garbage cheap, so this is often acceptable, but in hot paths it can matter. The engineering answer is **not** 'immutable everywhere' but: prefer immutability by default for value-like types and shared data, and reach for a mutable companion (builder, `StringBuilder`, a local mutable accumulator) for tight, allocation-heavy inner loops. This is why `String` is immutable but `StringBuilder` exists alongside it.

  • If immutable objects are so safe, why does Java provide StringBuilder?
    Because building a string by repeated concatenation of immutable Strings allocates a new String each step (O(n^2) work and garbage). StringBuilder is a mutable buffer you append into and convert to a String once, avoiding that churn - the canonical 'use a mutable companion in the hot loop' answer.
  • Does making a field final guarantee the object is immutable?
    No. final stops the reference from being reassigned, but if it points to a mutable object (e.g. an ArrayList), callers can still mutate that object. True immutability needs no mutators plus defensive copies of mutable components on input and output.

saying these in an interview costs you the question

  • Claiming immutable objects are 'always faster' - they trade allocation cost for safety
  • Saying immutability alone makes sharing thread-safe while ignoring safe publication
  • Thinking 'final field' equals 'deeply immutable' (a final field can still point to a mutable list)
  • Concatenating Strings in a loop and calling it efficient because String is immutable

context

open as a page

What steps make a Java class immutable? Walk through the checklist.

level: middleimportance: must knowfreq 78%

basics

~20 s

Make 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.

open as a page

What is defensive copying, and why must it happen on both input and output for a mutable field?

level: middleimportance: must knowfreq 70%

basics

~20 s

Defensive copying means storing and returning copies of mutable objects instead of the originals. You copy in the constructor so the caller can't change your data later, and copy in the getter so the caller can't change the object you hand back. Both are needed because each closes a different leak.

open as a page

Why are immutable objects ideal as keys in a HashMap or HashSet, and what breaks if you use a mutable key?

level: middleimportance: must knowfreq 71%

basics

~20 s

A HashMap finds entries using the key's hashCode. If a key never changes, its hashCode stays the same, so the entry is always findable. If you mutate a key after putting it in, its hashCode can change and the map looks in the wrong bucket - the entry is effectively lost.

open as a page

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

level: juniorimportance: should knowfreq 48%

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.

open as a page

Records auto-generate a constructor and accessors. Does that make a record fully immutable, and where can immutability still leak?

level: middleimportance: should knowfreq 49%

basics

~20 s

A record's own fields are final and have no setters, so the record itself can't be reassigned. But if a component is a mutable type like a List or array, the record just stores the reference - callers can still change that list. To be truly immutable you must copy mutable components when they go in and when they come out.

open as a page

Beyond thread-safety, why prefer immutable classes by default, and what are the trade-offs?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Immutable 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.

open as a page

How do Java records help build immutable classes, and what do they still NOT do for you?

level: seniorimportance: should knowfreq 62%

basics

~20 s

A record automatically makes the class final, makes its fields private and final, and gives you a constructor, getters, equals, hashCode and toString — so you get most of the immutable checklist for free. But it does NOT copy mutable fields, so if a field is a List or Date you still have to copy it yourself.

open as a page

Immutability has real costs - what are they, and how do you mitigate them when state must change frequently?

level: seniorimportance: should knowfreq 58%

basics

~20 s

Because you can't modify an immutable object, every change means building a new one. Do that in a tight loop and you create lots of short-lived objects, which costs CPU and gives the garbage collector more work. The fix is to use a mutable helper (like StringBuilder) for the hot part and produce the immutable result at the end.

open as a page

As an architect, when would you deliberately choose mutability over immutability, and how do you reason about the trade-off at system scale?

level: principalimportance: should knowfreq 34%

basics

~20 s

Default to immutable because it's safe and simple. Choose mutable when you have large or frequently-updated state where making a new object every change would be too slow or use too much memory - like a big buffer in a hot loop, or in-place updates of a large data structure. Keep that mutability local and well-controlled.

open as a page