skip to content

How do immutable, effectively immutable, unmodifiable, and a final field differ, and when do you reach for each?

level: middleimportance: should knowfreq 52%

answer

  1. final freezes the reference, not the object
  2. immutable = can't change ever; effectively immutable = won't change after publish
  3. unmodifiableList = live view; List.copyOf = real snapshot
  4. effectively immutable needs safe publication + discipline
  5. prefer record/immutable for shared state

basics

~20 s

Immutable means the object can never change. Effectively immutable means it could change but you never change it after sharing. Unmodifiable usually means a read-only wrapper over a still-mutable backing object. A final field just stops the reference from being reassigned.

solid answer

~50 s

These are four distinct ideas. A `final` field only forbids reassigning that one reference; the object it points to may still be mutable. An object is immutable if it cannot change after construction at all (final fields, no setters, defensive copies, no subclassing). An object is effectively immutable if it's technically mutable but, by discipline, never modified after it is safely published, which makes it as safe to share as a truly immutable one. An unmodifiable collection (e.g. `Collections.unmodifiableList` or `List.copyOf`) is a read-only view: callers can't mutate through it, but `List.copyOf` makes a true immutable snapshot whereas `unmodifiableList` still wraps a live backing list that someone else could change. In practice: prefer truly immutable value objects for shared state; use effectively immutable for objects you build once then freeze; use `List.copyOf`/`Map.copyOf` for immutable snapshots and reserve `unmodifiable*` wrappers for cheap read-only views you control.

code

java · 13 lines
java
List<String> source = new ArrayList<>(List.of("a", "b"));

List<String> view = Collections.unmodifiableList(source);
List<String> snap = List.copyOf(source);

source.add("c");
// view now reports [a, b, c]  -> live view reflects the change
// snap still reports [a, b]   -> immutable snapshot is frozen

// final field: reference frozen, contents not
final List<String> items = new ArrayList<>();
items.add("x"); // allowed - mutates the referent
// items = new ArrayList<>(); // compile error - reassignment forbidden

go deeper

for a junior

Knows final stops reassignment and that immutable means 'doesn't change'; may not yet separate unmodifiable views from snapshots.

for a middle

Cleanly distinguishes final field vs immutable vs effectively immutable vs unmodifiable-view, and uses List.copyOf vs unmodifiableList appropriately.

for a senior

Ties each concept to thread-safety and safe publication, knows the view-vs-snapshot trap, and chooses the right one for getters and shared state.

for a principal

Sets API/return-type conventions (snapshots vs views), defines when effectively-immutable is acceptable, and audits code for the subtle wrapper-over-mutable-backing bug.

## Four concepts that get conflated Interviewers probe these because mixing them up causes real bugs. ### 1. A `final` field `final` on a field means it can be assigned **exactly once** (in the constructor or its declaration) and never reassigned. It freezes the **reference**, not the **referent**. `private final List<String> items = new ArrayList<>();` — you can't point `items` at a different list, but you can still `items.add(...)` all day. `final` also carries the memory-model publication guarantee discussed elsewhere. So `final` is necessary for immutability but nowhere near sufficient. ### 2. Immutable object An object whose observable state cannot change after construction: class effectively non-extendable, all fields `private final`, no mutators, and mutable components defensively copied in and out. `String`, `Integer`, `LocalDate`, `BigDecimal` qualify. Immutable objects are inherently thread-safe and safe to publish even via a data race. ### 3. Effectively immutable object An object that **could** be changed (it has setters or mutable internals) but, **by convention, is never modified after it is safely published**. Once frozen and handed off through a safe-publication mechanism, it behaves like an immutable object for sharing purposes. Example: build a `Date`-bearing DTO, publish it through a `ConcurrentHashMap`, and never touch it again. The safety depends entirely on your discipline; nothing enforces it. This is a pragmatic middle ground when a class isn't designed for immutability but you still want lock-free sharing. ### 4. Unmodifiable vs. immutable collections - **`Collections.unmodifiableList(list)`** returns a **read-only view** over an existing list. You can't mutate *through the wrapper*, but the original `list` reference is still mutable — whoever holds it can change the contents, and those changes show through the wrapper. It's a *view*, not a snapshot. - **`List.copyOf(list)` / `List.of(...)`** (Java 9+) create a genuinely **immutable** collection: an independent snapshot with no backing reference exposed, that throws on any mutation. These are the safe choice for sharing. So 'unmodifiable' is about *who can mutate through this handle*, while 'immutable' is about *whether the underlying data can change at all*. ## A decision guide - **Shared, long-lived value state** (config, money, coordinates, event payloads): make it **truly immutable** (record or hand-built). Maximum safety, free publication. - **Object you assemble in stages then never touch**: **effectively immutable** is acceptable if you can guarantee no post-publication mutation and you publish safely. - **Returning internal collection state from a getter**: return `List.copyOf(internal)` for an immutable snapshot, or an `unmodifiable*` view only if a cheap read-only window (that may reflect later internal changes) is what you actually want. - **One reference you must not reseat but whose target legitimately mutates** (e.g. a lock object, a logger): plain `final` is the right tool. ## Why the distinctions matter for threading Truly immutable and (safely-published) effectively-immutable objects are shareable across threads with no locks. A `final` field alone gives you neither — a mutable referent still needs synchronization. An `unmodifiable*` wrapper over a concurrently-mutated backing list is a classic source of `ConcurrentModificationException` or torn reads, because the data underneath is still changing.

  • Does `Collections.unmodifiableList(myList)` protect against all mutation?
    No. It blocks mutation through the returned wrapper, but the original `myList` reference is still mutable; changes to it appear through the wrapper. For a true immutable snapshot use `List.copyOf(myList)`.
  • When is 'effectively immutable' good enough instead of truly immutable?
    When the class isn't designed to be immutable but you build it once, safely publish it (volatile/lock/concurrent collection), and never modify it afterward. It then shares as safely as an immutable object, but relies on that discipline being kept.

saying these in an interview costs you the question

  • Treating `Collections.unmodifiableList` as a deep-immutable copy
  • Believing a final collection field makes the collection contents immutable
  • Assuming effectively immutable is safe without safe publication
  • Returning an unmodifiable view of a list you keep mutating and expecting stable reads

context