Immutable and unmodifiable collections
List.of, Map.copyOf, Collections.unmodifiableList — knowing which is which prevents subtle bugs.
Runs on a sandboxed JVM via the public Piston API. Edit the code and hit Run.
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.
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.
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.
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.
Runs on a sandboxed JVM via the public Piston API. Edit the code and hit Run.
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.
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).
Related lessons & next topics
Keep going — these pair well with Immutable and unmodifiable collections.
- § 6.01 · Data Structures in JavaArrays and ArrayList — the workhorses
Fixed-size arrays vs dynamic ArrayList. Memory layout, amortized O(1) append, and when each one shines.
Beginner · 22 min - § 6.02 · Data Structures in JavaLinkedList and the Deque interface
Doubly-linked nodes, O(1) insert at either end, and why LinkedList is almost never the right choice.
Beginner · 20 min - § 6.03 · Data Structures in JavaStacks, queues, and ArrayDeque
LIFO, FIFO, and why java.util.Stack is a historical mistake you should avoid.
Intermediate · 24 min - § 6.04 · Data Structures in JavaHashMap, equals, and hashCode
How hashing works, why you must override equals and hashCode together, and the load factor that keeps lookups O(1).
Intermediate · 28 min