Immutable Collections in Java: List.of vs unmodifiableList
List.of and copyOf build immutable Java collections, Collections.unmodifiableList wraps a live one, and Arrays.asList is fixed-size. Plus the utility toolkit.
- Course: Java study plan
- Module: Collections framework
- Kind: Lesson
- Reading time: 12 min
- Runtime: OpenJDK 18
How do you create an immutable list in Java?
Create an immutable list in Java with List.of(...), added in Java 9, or copy an existing collection with List.copyOf(source) from Java 10. Both reject nulls and throw UnsupportedOperationException from every mutator. Collections.unmodifiableList(list) is different: a read-only view that still shows later changes to the underlying list. Immutability is shallow — the elements themselves may be mutable.
Lesson
Most collections a program creates are never modified after being built — a set of allowed values, a map of configuration, the result of a query. Making them immutable turns that intention into a guarantee: no accidental add, safe sharing across threads, safe use as keys, and a clear API contract ("this list will not change under you"). Java offers three mechanisms with different semantics, plus a toolkit of utility methods in Collections and Arrays that this lesson gathers in one place.
The three kinds of "unmodifiable"
| Mechanism | What it is | Nulls | Notes |
|---|---|---|---|
List.of, Set.of, Map.of, Map.ofEntries (Java 9) | A new, truly immutable collection | rejected | Set.of/Map.of reject duplicate keys; iteration order randomised |
List.copyOf, Set.copyOf, Map.copyOf (Java 10) | An immutable copy of a collection | rejected | Returns the same instance if the source is already one of these |
Collections.unmodifiableList/Set/Map(x) | A read-only view of a live collection | allowed | Changes to x show through the view |
Stream.toList() (Java 16) | An unmodifiable list | allowed | vs collect(toList()), which is mutable in practice but unspecified |
Arrays.asList(arr) | A fixed-size view over an array | allowed | set works (writes to the array); add/remove throw |
All of them throw UnsupportedOperationException from mutators; the type is still List/Set/Map, so the compiler cannot warn you — read the source of a collection before assuming you may modify it, and prefer to return immutable collections from APIs so callers cannot corrupt your state.
Choosing
- Building a constant:
List.of(...),Set.of(...),Map.of(...)(up to ten pairs;Map.ofEntries(Map.entry(k, v), …)beyond). - Returning a snapshot of internal state:
List.copyOf(items)— cheap when items is already immutable, safe otherwise. - Exposing internal state as read-only without copying (large, frequently read):
Collections.unmodifiableList(items)— remember the caller sees later changes. - Need nulls in an immutable collection:
Collections.unmodifiableList(new ArrayList<>(withNulls)).
Shallow, again
Immutable collections hold references; the elements are as mutable as they were. List.of(new StringBuilder()) is an immutable list of one mutable builder. Immutable elements (strings, records, numbers) make the whole structure immutable in practice.
The Collections toolkit
Collections.sort(list); Collections.sort(list, cmp);
Collections.reverse(list); Collections.shuffle(list); Collections.shuffle(list, new Random(42));
Collections.swap(list, i, j); Collections.rotate(list, k);
Collections.max(coll); Collections.min(coll); Collections.max(coll, cmp);
Collections.frequency(coll, x); // how many equal to x
Collections.disjoint(a, b); // no elements in common?
Collections.nCopies(n, x); // immutable list of n copies
Collections.fill(list, x); // overwrite every element
Collections.addAll(coll, a, b, c); // varargs add
Collections.emptyList(); emptySet(); emptyMap(); // typed immutable empties (List.of() etc. today)
Collections.singletonList(x); singleton(x); singletonMap(k, v); // immutable one-element
Collections.unmodifiableList(x); …Set; …Map; …Collection;
Collections.synchronizedList(x); …Map; // coarse-locked wrappers (Module 17 — prefer concurrent collections)
Collections.binarySearch(sortedList, key);
Collections.reverseOrder(); reverseOrder(cmp);
Collections.emptyList() returns one shared immutable instance — the idiomatic "nothing" return before List.of() existed; both are fine. singletonList remains useful for passing one element to an API that wants a list without allocating an ArrayList.
The Arrays bridge
Arrays.asList(T...) (fixed-size view), Arrays.stream(array), Arrays.sort, Arrays.fill, Arrays.copyOf, Arrays.equals, Arrays.hashCode, Arrays.toString — Module 6. list.toArray(new T[0]) / toArray(T[]::new) go the other way; Arrays.asList(intArray) does not box (one element).
Collection equality and hashing
List.equals: same elements in order, any implementations. Set.equals: same elements, any implementations. Map.equals: same entries. hashCode follows: List from elements in order, Set as the sum of element hashes, Map as the sum of entry hashes. So List.of(1, 2).equals(Arrays.asList(1, 2)) is true, Set.of(1, 2).equals(new TreeSet<>(List.of(2, 1))) is true, and collections can be map keys — if they are immutable.
Copies and defensive copies
List<String> mutableCopy = new ArrayList<>(source); // independent, growable
List<String> frozen = List.copyOf(source); // independent, immutable, no nulls
Map<K, V> mapCopy = new HashMap<>(source);
A constructor or setter that receives a collection and stores it should copy it (List.copyOf when nulls are impossible); a getter that returns internal state should return a copy or an unmodifiable view (Module 7). "Whose list is this?" is the question; copying at boundaries is the answer.
Wrappers and views, summarised
Views are cheap and live: subList, keySet/values/entrySet, headMap/tailSet, unmodifiableX, Arrays.asList, descendingMap. Copies are independent: constructors (new ArrayList<>(x)), copyOf, clone, toArray, stream().toList(). Knowing which you hold is what prevents both aliasing bugs and needless copying.
Interview angle
- "
List.ofvsArrays.asListvsCollections.unmodifiableList?" Immutable null-hostile copy / fixed-size array view / read-only view of a live collection. - "How do you return internal state safely?"
List.copyOfor an unmodifiable view. - "Is an immutable list deeply immutable?" No — elements may still be mutable.
- "What does
Collections.emptyList()return?" A shared immutable empty list;List.of()is the modern equivalent. - "Can two different implementations be equal?" Yes —
List/Set/Mapequality is by contents.
Key takeaways
List.of/Set.of/Map.ofandcopyOf: immutable, no nulls.Collections.unmodifiableX: a read-only view.Arrays.asList: fixed-size, writes through.- Return immutable snapshots or views from APIs; copy on the way in.
- Immutability is shallow — prefer immutable elements.
Collectionsholds the algorithms (sort, reverse, shuffle, max, frequency, nCopies…);Arraysbridges arrays.- Collections compare and hash by contents across implementations.
Common questions
What is the difference between List.of, Arrays.asList and Collections.unmodifiableList?
List.of creates a new immutable list that rejects nulls. Arrays.asList is a fixed-size view over an array — set writes through, add and remove throw. Collections.unmodifiableList is a read-only view of a live list, so changes to the original show through it.
Is an immutable list in Java deeply immutable?
No. An immutable collection holds references, so the elements are as mutable as they were: List.of(new StringBuilder()) is an immutable list of one mutable builder. Use immutable elements — strings, numbers, records — to make the whole structure immutable in practice.
How do you return a collection field safely from a Java class?
Return a copy or a read-only view so callers cannot change your internal state: List.copyOf(items) gives an immutable snapshot, and Collections.unmodifiableList(items) a view without copying. Copy incoming collections in constructors and setters too.
What does Collections.emptyList() return?
One shared, immutable, typed empty list, reused on every call. It was the idiomatic way to return "nothing" before Java 9's List.of(), its modern equivalent; either is fine, and both beat returning null.
Can two different collection classes be equal in Java?
Yes. Collection equality is by contents: a List equals any list with the same elements in the same order, a Set any set with the same elements, and a Map any map with the same entries. List.of(1, 2).equals(Arrays.asList(1, 2)) is true.
Exercises
Views versus copies
Read an integer n and n words into a mutable ArrayList. Create view = Collections.unmodifiableList(list), copy = List.copyOf(list) and fixed = Arrays.asList(list.toArray(new String[0])). Then add the word extra to the original list and try view.add("x"), copy.add("x") and fixed.set(0, "first"), catching UnsupportedOperationException where it occurs. Print: view=<view.toString()>, copy=<copy>, fixed=<fixed>, and unsupported=<how many of the three attempts threw>. Also print frequency=<Collections.frequency(list, first word)> and max=<Collections.max(list)>.
Example: 2 then b a →
view=[b, a, extra]
copy=[b, a]
fixed=[first, a]
unsupported=2
frequency=1
max=extraThe Collections toolkit tour
Read n words. Print frequency(first)=<Collections.frequency of the first word>, nCopies=<Collections.nCopies(3, first word)>, then rotate a copy of the list by 1 with Collections.rotate and print rotated=<…>, shuffle a copy with Collections.shuffle(copy, new Random(42)) and print shuffled=<…> (a seeded Random makes it reproducible), and finally try to add to Collections.unmodifiableList(words) and print unmodifiable=<true if it threw>.
Example: 4 then a b a c →
frequency(first)=2
nCopies=[a, a, a]
rotated=[c, a, b, a]
shuffled=[c, b, a, a]
unmodifiable=trueIn this module: Collections framework
- The collections framework — the map and how to choose
- Lists — ArrayList, LinkedList and the List API
- Sets — HashSet, LinkedHashSet and TreeSet
- Maps — HashMap internals, TreeMap, LinkedHashMap and the modern API
- Queues, deques and PriorityQueue
- Sorting, Comparable and Comparator in depth
- Iterators, fail-fast behaviour and safe modification
- Immutable collections and the utility toolkit (this lesson)
- Checkpoint — Collections framework
← Iterators, fail-fast behaviour and safe modification · Checkpoint — Collections framework →