How does Collections.unmodifiableList() work internally?
Collections.unmodifiableList() returns a wrapper over the original list that prohibits all modifying operations.
🟢 Junior Level
Collections.unmodifiableList() returns a wrapper over the original list that prohibits all modifying operations.
List<String> mutable = new ArrayList<>();
mutable.add("A");
mutable.add("B");
List<String> unmodifiable = Collections.unmodifiableList(mutable);
unmodifiable.add("C"); // ❌ UnsupportedOperationException!
unmodifiable.get(0); // ✅ OK — reading works
Key point: unmodifiableList is not a copy, but a wrapper. If you modify the original list, the changes will be visible through the wrapper:
mutable.add("C");
System.out.println(unmodifiable); // [A, B, C] — change is visible!
🟡 Middle Level
Internal implementation
Collections.unmodifiableList() returns an object of the internal class Collections.UnmodifiableList:
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess)
? new UnmodifiableRandomAccessList<>(list)
: new UnmodifiableList<>(list);
}
Two wrapper variants:
UnmodifiableRandomAccessList— if the list supports random access (ArrayList)UnmodifiableList— if it does not (LinkedList)
This is a performance optimization: RandomAccess lists use O(1) index-based access, not iterators.
UnmodifiableList structure
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
private final List<? extends E> list;
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list; // reference to the original, not a copy!
}
// All modifying methods throw UnsupportedOperationException
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
// Read methods delegate to the original
public E get(int index) {
return list.get(index); // direct call to the original
}
public int size() {
return list.size();
}
}
UnmodifiableList vs List.copyOf (Java 10+)
| Characteristic | unmodifiableList() |
List.copyOf() |
|---|---|---|
| Copying | No (wrapper) | Yes (defensive copy) |
| Original changes | Visible through wrapper | No effect |
| Null elements | Allowed | Not allowed |
| Return type | UnmodifiableList |
ImmutableCollections.ListN |
| Java version | Since Java 1.2 | Since Java 10 |
RandomAccess — a marker interface meaning O(1) index-based access. Defensive copy — copying data to protect against external modifications.
List<String> original = new ArrayList<>(List.of("A", "B"));
// unmodifiableList — wrapper
List<String> wrapper = Collections.unmodifiableList(original);
original.add("C");
System.out.println(wrapper); // [A, B, C] — change is visible!
// List.copyOf — copy
List<String> copy = List.copyOf(original);
original.add("D");
System.out.println(copy); // [A, B, C] — change is NOT visible
ListIterator wrapper
ListIterator<String> it = unmodifiable.listIterator();
it.next(); // OK
it.set("X"); // ❌ UnsupportedOperationException
it.add("Y"); // ❌ UnsupportedOperationException
it.remove(); // ❌ UnsupportedOperationException
An UnmodifiableListIterator is returned, which also blocks all modifications.
Performance
| Operation | ArrayList directly |
unmodifiableList |
|---|---|---|
get(i) |
O(1) | O(1) (one delegation call) |
size() |
O(1) | O(1) (delegation) |
contains() |
O(n) | O(n) (delegation) |
iterator() |
O(1) | O(1) (wrapped iterator) |
Overhead is minimal: one level of delegation (~1-2 ns per call).
🔴 Senior Level
Deep JDK source code analysis
// Collections.java (OpenJDK)
static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
final Collection<? extends E> c;
UnmodifiableCollection(Collection<? extends E> c) {
if (c == null) throw new NullPointerException();
this.c = c;
}
// All mutating methods throw an exception:
public boolean add(E e) { throw new UnsupportedOperationException(); }
public boolean remove(Object o) { throw new UnsupportedOperationException(); }
public void clear() { throw new UnsupportedOperationException(); }
// Read methods delegate:
public int size() { return c.size(); }
public boolean isEmpty() { return c.isEmpty(); }
public boolean contains(Object o) { return c.contains(o); }
public Iterator<E> iterator() {
return new Iterator<E>() {
private final Iterator<? extends E> i = c.iterator();
public boolean hasNext() { return i.hasNext(); }
public E next() { return i.next(); }
public void remove() { throw new UnsupportedOperationException(); }
};
}
}
UnmodifiableRandomAccessList optimization
static class UnmodifiableRandomAccessList<E>
extends UnmodifiableList<E>
implements RandomAccess {
UnmodifiableRandomAccessList(List<? extends E> list) {
super(list);
}
// Overrides subList — also returns unmodifiable
public List<E> subList(int from, int to) {
return new UnmodifiableRandomAccessList<>(
list.subList(from, to)
);
}
}
This optimization is important: UnmodifiableList (non-RandomAccess) converts index-based operations to iterator-based ones, which for ArrayList would be O(n) instead of O(1).
Can be bypassed through reflection!
List<String> list = new ArrayList<>(List.of("A", "B"));
List<String> unmod = Collections.unmodifiableList(list);
// Through reflection — access to the `list` field in UnmodifiableList
Field field = unmod.getClass().getDeclaredField("list");
field.setAccessible(true);
List<String> underlying = (List<String>) field.get(unmod);
underlying.add("C"); // Modification bypasses the protection!
System.out.println(unmod); // [A, B, C]
This is not a bug, but a characteristic of the wrapper approach. List.copyOf() (Java 10+) solves this problem by creating a truly immutable collection.
If your project is on Java 8 — List.copyOf() is not available. Use Collections.unmodifiableList(new ArrayList<>(original)) as a defensive copy.
Serialization
UnmodifiableList implements Serializable. On deserialization:
// The wrapper is restored with a reference to the deserialized list
// If the original changed after the wrapper was serialized — it will NOT affect the wrapper
When to use
| Scenario | Recommendation |
|---|---|
| Read API, original controlled by you | unmodifiableList() |
| Full immutability, Java 10+ | List.copyOf() |
| Thread-safe immutable list | List.copyOf() or unmodifiableList() + synchronize the original |
| Deep immutability (nested objects) | None of these methods — use immutable DTO |
Best Practices
// ✅ Return unmodifiable from public API
public List<String> getNames() {
return Collections.unmodifiableList(names);
}
// ✅ Java 10+ — List.copyOf for full immutability
public List<String> getNames() {
return List.copyOf(names);
}
// ❌ Do not use unmodifiableList for protection against concurrent modifications
// For that — use CopyOnWriteArrayList or Collections.synchronizedList()
🎯 Interview Cheat Sheet
Must know:
unmodifiableList()returnsUnmodifiableRandomAccessListorUnmodifiableListdepending onRandomAccess- All modifying methods throw
UnsupportedOperationException - Read methods delegate directly to the original — overhead is 1-2 ns
subList()also returns an unmodifiable wrapper- Through reflection you can reach the original and bypass the protection
List.copyOf()(Java 10+) — true immutability, defensive copy, no connection to the original- Iterator/ListIterator are also wrapped in unmodifiable versions (remove/set/add are prohibited)
Frequent follow-up questions:
- Why are there two classes — UnmodifiableList and UnmodifiableRandomAccessList? — Optimization: RandomAccess lists get O(1) index-based access, others — through iterator.
- Can the unmodifiableList protection be bypassed? — Yes, through reflection (
getDeclaredField("list")).List.copyOf()solves this problem. - What is the overhead of calling wrapper methods? — One level of delegation, ~1-2 ns — practically zero.
- What does
subList()of an unmodifiable list return? — Another unmodifiable wrapper on the sublist.
Red flags (DO NOT say):
- “unmodifiableList creates a copy of the collection” — no, it’s the wrapper pattern, stores a reference to the original
- “Protection against modifications is 100% guaranteed” — reflection allows bypassing it
- “unmodifiableList is thread-safe” — no, synchronization needs to be handled separately
- “Iterator from unmodifiableList allows removing elements” — no,
UnmodifiableListIteratoralso blocks modifications
Related topics:
- [[23. What is Collections.unmodifiableList()]]
- [[25. What is the difference between Iterator and ListIterator]]
- [[22. How to get a synchronized collection]]