What is Collections.unmodifiableList() and How Does It Work
Collections.unmodifiableList() does not allocate a new element array and does not clone data:
🟢 Junior Level
30-Second Summary
Collections.unmodifiableList() is a static factory method in the Java Standard Library (introduced in Java 1.2) that wraps an existing list in a read-only view (decorator):
- Any invocation of a state-modifying method (
add(),remove(),clear(),set(),replaceAll(),sort()) immediately throws anUnsupportedOperationException. - Read-only operations (
get(),size(),contains(),iterator()) execute normally by delegating directly to the underlying backing list.
Critical Trap: It is a WRAPPER, Not an Independent Copy!
Collections.unmodifiableList() does not allocate a new element array and does not clone data:
- It simply stores a reference to the original list.
- If the original list is modified elsewhere in the application, those modifications instantly reflect in the unmodifiable view!
List<String> original = new ArrayList<>();
original.add("Java");
List<String> view = Collections.unmodifiableList(original);
// Direct mutation via wrapper is blocked:
view.add("Kotlin"); // ❌ Throws UnsupportedOperationException!
// Mutation of the backing list succeeds:
original.add("Kotlin"); // ✅ Allowed!
// The change is visible through the "unmodifiable" wrapper:
System.out.println(view); // Prints: [Java, Kotlin]
Real-World Analogy
Consider a museum glass display case:
- A visitor views the artifact through the glass (
unmodifiableList) and cannot touch or take it. - However, a museum curator with a key on the back side (
original) can swap or remove the artifact at any moment.
🟡 Middle Level
The Decorator Design Pattern
Internally, Collections.unmodifiableList() instantiates the private static class Collections.UnmodifiableList:
static class UnmodifiableList<E> extends UnmodifiableCollection<E> implements List<E> {
final List<? extends E> list; // Reference to the backing list
UnmodifiableList(List<? extends E> list) {
super(list);
this.list = list;
}
// Delegated read operations:
public E get(int index) { return list.get(index); }
public int size() { return list.size(); }
public boolean contains(Object o) { return list.contains(o); }
// Intercepted and blocked mutating operations:
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();
}
public void clear() {
throw new UnsupportedOperationException();
}
}
Iterator Wrapping
Calling iterator() or listIterator() on an UnmodifiableList returns an anonymous iterator wrapper that delegates hasNext() and next() calls, but intercepts and blocks remove():
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(); // Blocked!
}
};
}
The RandomAccess Marker Interface
If the backing list implements RandomAccess (such as ArrayList), Collections.unmodifiableList() returns an instance of UnmodifiableRandomAccessList:
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
This preserves $O(1)$ fast-path indexed access for JDK algorithms like Collections.binarySearch(), preventing performance regressions from $O(\log N)$ to $O(N)$.
🔴 Senior Level
Concurrency Hazards: The Illusion of Thread Safety
A dangerous misconception among developers is that an unmodifiable list is automatically thread-safe. It is not!
ConcurrentModificationExceptionDuring Parallel Iteration:- If Thread A iterates through
unmodifiableListwhile Thread B callsoriginalList.add(), the backingArrayList.modCountincrements. - On the very next
iterator.next()call, Thread A crashes with aConcurrentModificationException.
- If Thread A iterates through
- Lack of Memory Barriers (Safe Publication):
UnmodifiableListintroduces novolatilevariables or JMM memory barriers around the elements of the backing list.- Reader threads can observe torn reads or stale cached values in CPU L1/L2 caches.
Serialization Semantics
UnmodifiableList implements java.io.Serializable:
- During serialization, both the wrapper and the underlying backing list reference are written to the stream.
- Upon deserialization, both objects are restored.
- Important: If the underlying list does not implement
Serializable, attempting to serialize theunmodifiableListwrapper throwsNotSerializableException.
The equals() and hashCode() Architectural Nuance
In the JDK implementation:
UnmodifiableCollectiondeliberately does not overrideequals()andhashCode(), inheriting identity comparison fromObject. This is becauseList(position-dependent) andSet(position-independent) have mutually incompatible equality contracts.- In contrast,
UnmodifiableListexplicitly overridesequals()andhashCode(), delegating directly tolist.equals(o)andlist.hashCode()to satisfy the strictjava.util.Listcontract.
🎯 Interview Cheat Sheet
Summary of UnmodifiableList Properties
- Design Pattern: Decorator (View).
- Complexity: $O(1)$ construction time and minimal memory overhead.
- Data Isolation: None; mutations to the source collection reflect in the wrapper.
- Concurrency: NOT thread-safe; concurrent modification of the backing list throws
ConcurrentModificationException. - Indexed Access: Preserves the
RandomAccessmarker interface.
Comparison: Collections.unmodifiableList vs. List.copyOf
| Dimension | Collections.unmodifiableList(orig) | List.copyOf(orig) |
| :— | :— | :— |
| Mechanism | Read-only Decorator (View) over original | Independent defensive copy snapshot |
| Creation Cost | $O(1)$ wrapper allocation | $O(N)$ allocation (or $O(1)$ fast-path) |
| Isolation | ❌ Linked to backing list mutations | ✅ 100% isolated snapshot |
| Null Elements | ✅ Permitted (if present in original) | ❌ Null-hostile (throws NullPointerException) |
4 Tricky Interview Questions
1. Why does Collections.unmodifiableCollection() deliberately NOT override equals() and hashCode(), while unmodifiableList() DOES?
Answer: The java.util.Collection interface deliberately leaves equals() and hashCode() unspecified because its subinterfaces enforce conflicting contracts:
List.equals()requires identical elements in the exact same order.Set.equals()requires identical elements regardless of order. IfUnmodifiableCollectionimplemented a generic equality algorithm, it would violate polymorphism for either sets or lists. Therefore,UnmodifiableCollectioninherits identity equality fromObject, while subclassesUnmodifiableListandUnmodifiableSetoverride them to delegate to their respective backing instances.
2. Does Collections.sort(unmodifiableList) or unmodifiableList.sort(null) compile, and what happens at runtime?
Answer: Both compile cleanly, because unmodifiableList implements List<T>, satisfying method signatures. At runtime:
unmodifiableList.sort(null): TheUnmodifiableListclass explicitly overridesList.sort(Comparator)to throwUnsupportedOperationException.Collections.sort(unmodifiableList): Internally delegates tolist.sort(), hitting the same overridden method and throwingUnsupportedOperationException.
3. How does UnmodifiableRandomAccessList differ from UnmodifiableList, and why is this critical for performance?
Answer: UnmodifiableRandomAccessList is identical except that it declares implements RandomAccess. This empty marker interface is checked by standard JDK algorithms (such as Collections.binarySearch()):
if (list instanceof RandomAccess || list.size() < BINARYSEARCH_THRESHOLD)
return indexedBinarySearch(list, key); // O(log N) indexed access
else
return iteratorBinarySearch(list, key); // O(N) sequential traversal
If the wrapper omitted this marker, binary search over an unmodifiable ArrayList would degrade from $O(\log N)$ to $O(N)$ time.
4. In what scenarios is Collections.unmodifiableList() preferable to List.copyOf() in modern Java?
Answer: Two specific scenarios:
- Zero-Allocation Memory Efficiency ($O(1)$): When wrapping a massive, already-isolated internal list containing millions of elements, where allocating tens of megabytes for an $O(N)$ copy is prohibitive.
- Collections Containing
nullElements:List.copyOf()is strictly null-hostile and throwsNullPointerExceptionif any element isnull. If business logic requiresnullelements,Collections.unmodifiableList()safely wraps the list while preserving nulls.
Red Flags (DO NOT Say)
- ❌ “Calling
Collections.unmodifiableList()copies the list elements into a new unmodifiable array.” (It is merely a decorator holding a reference to the original list). - ❌ “An
unmodifiableListis thread-safe because it cannot be modified.” (Modifications to the backing list by another thread triggerConcurrentModificationException). - ❌ “Using
unmodifiableListprotects the original list from external changes.” (Anyone with a reference to the original mutable list can continue modifying it). - ❌ “You cannot iterate over an
unmodifiableList.” (Iterating is fully supported; onlyiterator.remove()is blocked).
Related Topics
- What is Defensive Copy — Mechanics and invariants
- When Should You Make Defensive Copy — Decision matrix
- How to Protect a Collection from Modification — Protection strategies
- Difference Between Shallow Copy and Deep Copy — Copy mechanics
- How to Properly Work with Collections in Immutable Classes — Enterprise patterns