All topics
library
intermediate

Collections.unmodifiable* vs List.of() vs List.copyOf()

Choose the right unmodifiable/immutable collection factory and understand the differences in mutability guarantees.

Three ways to get a read-only list, with three different guarantees. "Unmodifiable" is not "immutable".

Collections.unmodifiableList = putting a 'DO NOT TOUCH' sign on a whiteboard (someone can still erase it from behind). List.of = a printed poster (truly cannot be changed).

Key Concepts

1
    List<String> src = new ArrayList<>(List.of("a"));
2
    var view = Collections.unmodifiableList(src);   // a VIEW of src
    var fixed = List.of("a");                        // truly immutable
    var copy = List.copyOf(src);                     // an immutable copy
3
The difference that causes bugs.
    src.add("b");
    view.size();        // 2 -- the view sees it
    copy.size();        // 1 -- the copy does not
4
Collections.unmodifiableList wraps the original. Mutating through the wrapper throws, but the backing list is still mutable by anyone holding it, and those changes show through. So returning one from a getter does not protect your state.
Collections.unmodifiableList
5
    public List<Item> items() {
      return Collections.unmodifiableList(items);   // caller cannot add...
    }                                               // ...but sees every change
6
List.copyOf is what you want for defensive copying. It snapshots, so the caller has data that cannot change under them.
List.copyOf
7
List.of has no backing collection at all. Nothing else holds a reference to its array, so there is nothing that could modify it.
List.of
8
The null rules differ, and this bites.
    List.of("a", null)              NullPointerException
    List.copyOf(listWithNull)       NullPointerException
    Collections.unmodifiableList(listWithNull)    fine
    Arrays.asList("a", null)        fine
9
Arrays.asList is the odd one out — fixed size, but not read-only:
Arrays.asList
10
    var l = Arrays.asList("a", "b");
    l.set(0, "z");          // allowed
    l.add("c");             // UnsupportedOperationException
11
It is a view over the array, so set writes through to it. Passing it to new ArrayList<>(...) is the usual way to get a real list.
setnew ArrayList<>(...)
12
A small optimisation worth knowing. List.copyOf may return the same instance when the source is already an immutable list, so copying one twice costs nothing.
List.copyOf
13
The map and set equivalents.
    Map.of(k, v, ...)        up to 10 pairs
    Map.ofEntries(Map.entry(k, v), ...)    for more
    Set.of(...)   Map.copyOf(...)   Set.copyOf(...)
14
What the interviewer is probing.1. "Is Collections.unmodifiableList immutable?" Probing: the distinction the question turns on. Stalls: "Yes." Moves up: no — it is an unmodifiable view; the backing list can still be changed by anyone holding it, and the change shows through.
15
2. "Which should a getter return to protect internal state?" Probing: defensive copying. Stalls: "The unmodifiable view." Moves up: List.copyOf — the view prevents the caller mutating through it but still exposes every change you make.
16
3. "How do the null rules differ?" Probing: a real source of NPEs on migration. Stalls: "They are the same." Moves up: List.of and copyOf reject null; Collections.unmodifiableList and Arrays.asList allow it.
17
4. "What is Arrays.asList actually?" Probing: the odd one out. Stalls: "An immutable list." Moves up: a fixed-size view over the array — set writes through to the array, while add and remove throw.