Topic 9.11
Immutable and Unmodifiable Collections
In one line
List.of, Set.of and Map.of (Java 9) create collections that can never change, and List.copyOf (Java 10) makes a frozen copy. Collections.unmodifiableList is different: a read-only view that still shows changes made to the original list underneath.
Think of it like this
A printed train timetable on the station wall is immutable: nobody can add a train to it, and if the railway changes the schedule they print a new sheet. A screen at the station that mirrors the control room's live schedule is unmodifiable: passengers can't change it, but it updates the moment the control room edits the real schedule. Both stop you from writing; only one is truly frozen.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Immutable
- Can never change after it's created. To change it you make a new one.
- Unmodifiable view
- A read-only window onto another collection: you can't change it through the view, but it shows changes made to the original.
- Factory method
- A static method that creates objects for you, like List.of(...), instead of calling a constructor with new.
- UnsupportedOperationException
- The exception thrown when you call a method the object doesn't allow, like add on an immutable list.
- Defensive copy
- A private copy you make so that changes to the caller's collection can't affect yours (and the other way round).
- Shallow
- Only the outer collection is frozen; the objects inside can still change if they're mutable.
- Thread-safe
- Safe to use from several threads at the same time without extra locking.
Step by step
01Three ways to say "read only"
List.of("a", "b"): a brand-new immutable list. List.copyOf(source): an immutable snapshot of source as it is right now. Collections.unmodifiableList(source): a read-only window onto source, which still changes when source changes.
Pick by asking: do I want a frozen value (use of/copyOf) or do I want to stop callers changing my live data while I keep changing it (use the unmodifiable view)?
02What happens when you try to change one
Every mutating method of an immutable list (add, remove, set, clear, sort, replaceAll) throws UnsupportedOperationException. The compiler can't catch this: the type is still List<String>, which declares add. That's a known design trade-off of the collections API (there is no separate read-only List interface).
This is why returning List.of(...) from a method surprises callers who then call add. Document it, or name methods clearly (getSnapshot).
03The null and duplicate rules
List.of("a", null) throws NullPointerException immediately, at creation. Set.of("x", "x") throws IllegalArgumentException: duplicate element: x. Map.of("k", 1, "k", 2) throws IllegalArgumentException: duplicate key: k.
Even queries are strict: List.of("a").contains(null) and Map.of("a", 1).containsKey(null) throw NullPointerException instead of returning false. If nulls are possible, check first or use a different collection.
04Map.of has a limit of 10 pairs
Map.of has overloads for 0 to 10 key-value pairs. For more, use Map.ofEntries(Map.entry("a", 1), Map.entry("b", 2), ...), which takes any number of entries.
List.of and Set.of have fixed overloads up to 10 plus a varargs version, so they take any number of elements.
Map<String, Integer> codes = Map.ofEntries(
Map.entry("IN", 91),
Map.entry("US", 1),
Map.entry("UK", 44));05copyOf is smart
List.copyOf(x) copies only when it must. If x is already an immutable list made by the factories, it returns the same object, because copying something that can't change is pointless.
If x contains a null, copyOf throws NullPointerException, the same rule as of.
06Immutable outside, mutable inside
List.of(sb) where sb is a StringBuilder: you can't add or remove elements, but list.get(0).append("!") still changes the builder. The list is shallowly immutable.
For a fully immutable value, use immutable elements: strings, boxed numbers, records with immutable fields, java.time types. That's what makes immutable collections safe to share across threads.
07Streams: toList() vs Collectors.toList()
stream.collect(Collectors.toList()) (Java 8) returns a list with no guarantees; in practice an ArrayList you can modify. stream.toList() (Java 16) returns an unmodifiable list that allows nulls. Collectors.toUnmodifiableList() (Java 10) returns an unmodifiable list and rejects nulls.
When moving old code to stream.toList(), check that nothing later calls add or sort on the result.
Try it yourself
- 1
Predict, then run
In "Unmodifiable view vs immutable copy", add
source.remove("a");right aftersource.add("c");. Predict the three lines before you press Run. (source and view: [b, c]; copy still [a, b].) - 2
Find the null trap
In "List.of, Set.of and Map.of rules", replace
days.contains("Fri")withdays.contains(null). Run it and read the exception: immutable factory collections reject null even in queries.
Code & diagrams
The map's entries are read with get, not printed: Map.of's iteration order is deliberately unspecified.
Expected output
[Mon, Tue, Wed] size 3
add -> UnsupportedOperationException
List.of with null -> NullPointerException
Set.of duplicate -> duplicate element: x
pen: 10, ink: 4
contains Fri? falseExpected output
source: [a, b, c]
view: [a, b, c]
copy: [a, b]
view.add -> UnsupportedOperationException
copyOf an immutable list is the same object? trueExpected output
Collectors.toList is modifiable: [3, 1, 2, 4]
Stream.toList is unmodifiable: [3, 1, 2]
Stream.toList allows null: [x, null]public final class Team {
private final List<String> members = new ArrayList<>();
public void join(String name) { members.add(name); }
// callers can read, never change, and always see the latest members
public List<String> members() { return Collections.unmodifiableList(members); }
// or: a frozen snapshot that won't change later
public List<String> snapshot() { return List.copyOf(members); }
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Sort a list from List.of
Write List<Integer> nums = List.of(3, 1, 2); Collections.sort(nums);.
Myth vs fact
Myth
Collections.unmodifiableList makes a list immutable.
Fact
It makes a read-only view. Changes to the underlying list still show through it.
Myth
Map.of keeps entries in the order you wrote them.
Fact
Its iteration order is unspecified and may change between runs. Use LinkedHashMap or TreeMap when order matters.
Myth
An immutable list of objects is fully immutable.
Fact
Only the list is. Mutable objects inside it can still be changed.
Myth
Stream.toList() and Collectors.toList() are the same.
Fact
toList() (Java 16) is unmodifiable and allows nulls; Collectors.toList() gives no guarantee and is modifiable in practice.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
The factory collections are value-based and randomise iteration order per JVM run using a salt (
ImmutableCollections.SALT), precisely so code can't accidentally depend on the order ofSet.ofandMap.of. - ▸
List.ofreturns compact classes:List12stores one or two elements in fields,ListNstores an exact-size array. There's no spare capacity and nomodCount, so they're smaller and faster to iterate thanArrayList. - ▸
Immutable collections are safe to publish between threads through a final field without synchronization (the JMM freeze rule for final fields, Topic 13.4), which makes them ideal for configuration and lookup tables.
- ▸
Collections.unmodifiable*wrappers have a cost: an extra indirection on every call. For hot read paths returning internal state, prefer handing out an immutable snapshot that you rebuild when the data changes (copy-on-write style).
Remember this
- 1
Immutable means the collection itself can never change after creation: no add, no remove, no set.
List.of(...),Set.of(...),Map.of(...),Map.ofEntries(...)(Java 9) andList.copyOf,Set.copyOf,Map.copyOf(Java 10) all return immutable collections. Every mutating method throwsUnsupportedOperationException. - 2
Unmodifiable view means *you* can't change it through this reference, but it's a window onto another collection.
Collections.unmodifiableList(list)(since Java 1.2) wraps the original: if someone changes the original, the view shows the change. It protects against callers, not against the owner. - 3
The
offactories are strict: they rejectnullelements, keys and values withNullPointerException, andSet.of/Map.ofreject duplicates withIllegalArgumentException("duplicate element: x"). EvenList.of(...).contains(null)throwsNullPointerException. Iteration order ofSet.ofandMap.ofis deliberately unspecified and can differ between runs, so never depend on it. - 4
Immutability is about the collection, not the elements.
List.of(new StringBuilder("a"))can't gain or lose elements, but theStringBuilderinside can still be changed. Truly immutable data needs immutable elements too: strings, records of immutable fields,LocalDate(Topic 4.11). - 5
Why bother? Immutable collections are thread-safe without locks (nothing can change), safe to share and return from methods (Topic 4.11, defensive copies), safe as map keys and set elements, and small: the JDK uses compact internal classes (
List12for 1–2 elements,ListNfor more) with no spare capacity. - 6
Two related methods matter in streams:
Collectors.toList()makes no promise (today it returns a modifiableArrayList), whileStream.toList()(Java 16) returns an unmodifiable list that, unlikeList.of, allows nulls.Collectors.toUnmodifiableList()(Java 10) is the null-rejecting version.
Explain it without notes
What is the difference between an immutable list and an unmodifiable view?
Why do List.of and Map.of reject nulls, and what other strict rules do they have?
When would you return Collections.unmodifiableList and when List.copyOf from a getter?
Practice
Create an immutable map of three country codes to dial codes with Map.ofEntries and print one lookup.
Write a method that returns a sorted, immutable copy of any List<String> without changing the input.
Show that List.of(builder) is only shallowly immutable by changing the StringBuilder inside it.
Trade-offs
- ↔
Immutable collections are safe to share and need no locks, but every change means building a new collection.
- ↔
Unmodifiable views are free to create and always current, but don't protect against the owner's changes or concurrent modification.
- ↔
The strict null rules catch bugs early but force you to handle nulls before building the collection.
Done when you can
Done when you can explain immutable vs unmodifiable view with an example.
Done when you know the null, duplicate and ordering rules of List.of, Set.of and Map.of.
Done when you can choose between returning a view and returning a copy.
Done when you know the difference between Stream.toList(), Collectors.toList() and toUnmodifiableList().