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