← The curriculum
§ 6.12 · Data Structures in Java

Immutable and unmodifiable collections

List.of, Map.copyOf, Collections.unmodifiableList — knowing which is which prevents subtle bugs.

Intermediate · 18 minute read
Try it yourself
Live Java · editable

Runs on a sandboxed JVM via the public Piston API. Edit the code and hit Run.

§ 6.12.01

Three shades of read-only

Java gives you three distinct read-only options and they behave differently: *immutable* factories (List.of, Set.of, Map.of), *defensive-copy* factories (List.copyOf, Map.copyOf), and *view* wrappers (Collections.unmodifiableList). Mixing them up is a subtle bug source.

§ 6.12.02

List.of and friends — truly immutable

List.of(1, 2, 3), Set.of(...), Map.of(...) (Java 9+) return compact, structurally immutable instances. Every mutating method throws UnsupportedOperationException. They also reject nulls — a stricter contract than the classic collections. Use them for constants and small literals.

§ 6.12.03

unmodifiableList — a view, not a copy

Collections.unmodifiableList(src) returns a wrapper that blocks mutations *through the wrapper*, but the underlying source can still change and the wrapper reflects those changes live. That's dangerous when you hand the wrapper out expecting a stable snapshot. If callers mutate the source, your 'unmodifiable' view mutates with it.

§ 6.12.04

copyOf — the defensive snapshot

List.copyOf(source) returns an immutable *snapshot* — a fresh, structurally immutable list. Later mutations to the source have no effect. This is the correct return value at API boundaries when the caller shouldn't be able to alter your internal state and shouldn't see later changes either.

Example
Live Java · editable

Runs on a sandboxed JVM via the public Piston API. Edit the code and hit Run.

§ 6.12.05

Deep vs shallow immutability

All of these are *shallow* — they prevent structural changes to the collection, not changes to the objects inside. If you store mutable StringBuilder values, callers can still mutate them. For true deep immutability, use immutable element types (records with immutable fields, String, primitives) all the way down.

§ 6.12.06

Stream.toList vs Collectors.toList

Since Java 16, stream.toList() returns an *unmodifiable* list — a common gotcha for teams migrating from Collectors.toList() (which returned a mutable ArrayList). If you need a mutable result from a stream, use Collectors.toCollection(ArrayList::new).