Definition
Three approaches:
- List.of(a, b, c) – truly immutable. No null elements allowed. Throws on modification.
- List.copyOf(list) – copies the list into an immutable version. Modifications to the original do not affect the copy.
- Collections.unmodifiableList(list) – wraps the original. Prevents modification through the wrapper, but the original list can still be modified, and the wrapper reflects those changes.
List.of is a printed, laminated bank statement – truly unmodifiable. Collections.unmodifiableList is a glass cover over the original statement – you cannot write on it, but someone with access to the original underneath can still change it.
Understanding the Concepts
List.of() – truly immutable (Java 9+)
List.of() creates a completely immutable list. The returned list does not allow add, remove, set, or any other structural modification. It also rejects null elements at creation time.
List<String> accounts = List.of("SAV2001", "CUR3001", "LOAN5001");
// accounts.add("NEW"); // throws UnsupportedOperationException
// accounts.set(0, "X"); // throws UnsupportedOperationException
// List.of("SAV2001", null) → throws NullPointerException at creation
The list returned by List.of() is not an ArrayList. It is an internal implementation that stores elements in a compact, space-efficient form.
List.copyOf() – immutable copy (Java 10+)
List.copyOf() creates a new immutable list that is a snapshot of the original. Changes to the original list do not affect the copy.
List<String> mutable = new ArrayList<>(List.of("SAV2001", "CUR3001"));
List<String> immutable = List.copyOf(mutable);
mutable.add("LOAN5001"); // modifies the original
System.out.println(immutable); // [SAV2001, CUR3001] – unaffected!
System.out.println(mutable); // [SAV2001, CUR3001, LOAN5001]
If the source list is already an immutable List.of(), List.copyOf() may return the same instance without copying, which is an optimization.
Collections.unmodifiableList() – view only
Collections.unmodifiableList() wraps the original list in a read-only view. The wrapper rejects modifications, but the underlying list can still be changed directly, and those changes are visible through the wrapper.
List<String> original = new ArrayList<>(List.of("SAV2001"));
List<String> view = Collections.unmodifiableList(original);
// view.add("NEW"); // throws UnsupportedOperationException
original.add("CUR3001"); // modifies the original directly
System.out.println(view); // [SAV2001, CUR3001] – the view changed!
This is why Collections.unmodifiableList is not truly immutable – it is a read-only wrapper over a mutable list. If any code still holds a reference to the original list, it can mutate the data.
Comparison
| Method | Truly immutable? | Null elements? | Original can modify? |
|---|---|---|---|
| List.of(...) | Yes | No (NPE) | N/A (no original) |
| List.copyOf(list) | Yes | No (NPE) | No (independent copy) |
| Collections.unmodifiableList(list) | No (view) | Yes | Yes (view changes) |
How does this apply to banking code?
In a banking application, you often need to return a list of transactions or account summaries from a service method. Returning a mutable list lets the caller accidentally modify internal state.
package com.bank.service;
import java.util.List;
import java.util.ArrayList;
public class AccountService {
private final List<String> activeAccountIds = new ArrayList<>();
// WRONG: exposes internal mutable list
public List<String> getActiveAccounts() {
return activeAccountIds; // caller can add/remove!
}
// CORRECT: return an immutable copy
public List<String> getActiveAccountsSafe() {
return List.copyOf(activeAccountIds); // caller cannot modify
}
}
This is a defensive copy pattern. The safe version ensures that no external code can corrupt the service's internal data.
What exceptions are thrown?
All three immutable approaches throw UnsupportedOperationException on structural modifications. List.of() and List.copyOf() additionally throw NullPointerException if any element is null.
// UnsupportedOperationException on modification
List<String> immutable = List.of("SAV2001");
try {
immutable.add("CUR3001");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify immutable list");
}
// NullPointerException on null element
try {
List<String> bad = List.of("SAV2001", null);
} catch (NullPointerException e) {
System.out.println("Null elements not allowed in List.of()");
}
What should a senior engineer know?
- Map.of(), Set.of() follow the same pattern as List.of(). Map.of() takes alternating key-value pairs:
Map.of("SAV2001", account1, "CUR3001", account2). - Guava ImmutableList.Builder provides a builder for constructing immutable lists incrementally:
ImmutableList..builder().add("SAV2001").add("CUR3001").build() - Stream.toList() (Java 16+) returns an unmodifiable list. It is similar to
List.copyOf(stream.collect(toList()))but allows null elements. - Immutable lists are value-based. Do not rely on identity (
==); useequals()to compare them. - Thread safety: Truly immutable lists are inherently thread-safe. You can share them across threads without synchronization.
Visual Overview
List.of("A", "B") → immutable, independent
List.copyOf(mutableList) → immutable copy, independent
Collections.unmodifiableList → read-only VIEW of the original (not independent!)
Any modification → UnsupportedOperationException
Key Takeaway
Use List.of() or List.copyOf() for truly immutable lists. Collections.unmodifiableList() is only a view – modifications to the underlying list are still visible through it.
One sentence to remember: List.of() is truly immutable, Collections.unmodifiableList is just a read-only view over a still-mutable list.
---