How do List.of / Set.of / Map.of differ from Collections.unmodifiableList / unmodifiableSet / unmodifiableMap?
answer
- Wrapper = view (shares backing); factory = standalone value
- unmodifiable* leaks if backing is mutated
- copyOf = copy then immutable
- Factories reject null; wrappers don't
- Set/Map.of order unspecified
basics
~20 sThe factory methods create a brand-new collection that is truly immutable and copies nothing live. The Collections.unmodifiable* methods only wrap an existing collection in a read-only view, so if the original changes, the view changes too.
solid answer
~40 sBoth block mutation through the reference you hold, but the guarantee differs. Collections.unmodifiableList returns a thin read-only *wrapper* around an existing collection — it forwards reads to the backing list and throws on writes. Crucially, it does not copy: if anyone still holds the original and mutates it, those changes are visible through the wrapper, so it is only conditionally immutable. List.of, by contrast, returns a self-contained, genuinely immutable instance with no live backing collection to leak through. Other differences: factory collections reject null elements/keys/values (the wrappers allow nulls if the backing collection does), factories are memory-optimized for small sizes, and Set.of/Map.of give an unspecified, run-varying iteration order. For a true immutable snapshot you should either use the factories or copy-then-wrap (or List.copyOf), never wrap a collection you keep a mutable reference to.
code
java · 11 linesList<String> original = new ArrayList<>(List.of("a"));
List<String> view = Collections.unmodifiableList(original);
original.add("b");
System.out.println(view); // [a, b] -- view reflects the change
List<String> snapshot = List.copyOf(original);
original.add("c");
System.out.println(snapshot); // [a, b] -- copy is independent
List<String> literal = List.of("x", "y"); // truly immutable, no backinggo deeper
Knows both prevent modification through the returned reference.
Explains that unmodifiable* is a non-copying view that leaks backing mutations, while factories are standalone and truly immutable, plus null handling.
Adds copyOf for snapshots, memory/order differences, and guidance on which to use when returning collections from APIs.
Sets team conventions: prefer immutable returns to eliminate defensive copying, document escape-hatch cases for views, and reason about leakage in concurrent code.
## Two ways to get a "you can't change this" collection Java has long offered `Collections.unmodifiableList(...)`, `unmodifiableSet(...)`, and `unmodifiableMap(...)`. Java 9 added `List.of`, `Set.of`, `Map.of`. They look similar but the *kind* of guarantee is different. ## What a wrapper is A *wrapper* (or *view*) is an object that holds a reference to another object and forwards calls to it. `Collections.unmodifiableList(original)` returns a small object that says: for read methods (`get`, `size`, `iterator`), ask `original`; for write methods (`add`, `set`, `remove`), throw `UnsupportedOperationException`. The important consequence: the wrapper shares the *same underlying data* as `original`. It does **not** copy. So: ```java List<String> original = new ArrayList<>(List.of("a")); List<String> view = Collections.unmodifiableList(original); original.add("b"); // mutate the backing list System.out.println(view); // [a, b] <-- the view changed! ``` The view is read-only *through that reference*, but it is not immutable as a value — it is only as stable as whoever still holds `original`. This is sometimes called a "view of" rather than a "copy of." ## What the factories give `List.of("a", "b")` builds a fresh, self-contained collection. There is no separate backing collection that someone else could hold and mutate. It is *truly immutable*: its contents are fixed for its entire life. There is nothing to leak. ## Other concrete differences 1. **Null handling.** Factory methods throw `NullPointerException` on a null element, key, or value. The `unmodifiable*` wrappers happily contain nulls if the backing collection did. 2. **Memory layout.** Factories have dedicated implementations for 0–10 elements that avoid extra allocation; wrappers always carry a backing collection plus the wrapper object. 3. **Iteration order.** `Set.of` and `Map.of` have *unspecified* order that may even change between runs. `Collections.unmodifiableSet(new LinkedHashSet<>(...))` preserves whatever order the backing set had. 4. **Identity optimizations.** Some factory results may be shared/cached for empty cases; wrappers always create a new wrapper object. ## How to make a true immutable snapshot of existing data If you have a mutable collection and want an immutable, independent copy, *copy first*: ```java List<String> snapshot = List.copyOf(original); // Java 10+, copies then makes immutable ``` `List.copyOf` / `Set.copyOf` / `Map.copyOf` (Java 10) take any source, copy its current contents, and return an immutable result — combining "copy" with the factory guarantees. Wrapping without copying (`unmodifiableList(original)`) is the classic mistake when you wanted a snapshot. ## Rule of thumb - New, fixed literal data → `List.of(...)`. - Immutable copy of something you already have → `List.copyOf(...)`. - Read-only *view* that should intentionally reflect future changes to the backing collection → `Collections.unmodifiableList(...)`.
- If I wrap a mutable list with Collections.unmodifiableList and then add to the original, what does the wrapper show?The wrapper reflects the new element, because it is a live view over the same backing list, not a copy.
- What is the right way to get an immutable, independent copy of an existing collection?Use List.copyOf / Set.copyOf / Map.copyOf (Java 10+), which copy the current contents and return a truly immutable collection.
saying these in an interview costs you the question
- Claiming unmodifiableList copies the data (it does not)
- Saying both are equally immutable regardless of who holds the original
- Using unmodifiableList(original) when you needed a snapshot