🔒 Section 13 · Question #13

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 an UnsupportedOperationException.
  • 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!

  1. ConcurrentModificationException During Parallel Iteration:
    • If Thread A iterates through unmodifiableList while Thread B calls originalList.add(), the backing ArrayList.modCount increments.
    • On the very next iterator.next() call, Thread A crashes with a ConcurrentModificationException.
  2. Lack of Memory Barriers (Safe Publication):
    • UnmodifiableList introduces no volatile variables 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 the unmodifiableList wrapper throws NotSerializableException.

The equals() and hashCode() Architectural Nuance

In the JDK implementation:

  • UnmodifiableCollection deliberately does not override equals() and hashCode(), inheriting identity comparison from Object. This is because List (position-dependent) and Set (position-independent) have mutually incompatible equality contracts.
  • In contrast, UnmodifiableList explicitly overrides equals() and hashCode(), delegating directly to list.equals(o) and list.hashCode() to satisfy the strict java.util.List contract.

🎯 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 RandomAccess marker 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. If UnmodifiableCollection implemented a generic equality algorithm, it would violate polymorphism for either sets or lists. Therefore, UnmodifiableCollection inherits identity equality from Object, while subclasses UnmodifiableList and UnmodifiableSet override 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): The UnmodifiableList class explicitly overrides List.sort(Comparator) to throw UnsupportedOperationException.
  • Collections.sort(unmodifiableList): Internally delegates to list.sort(), hitting the same overridden method and throwing UnsupportedOperationException.

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:

  1. 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.
  2. Collections Containing null Elements: List.copyOf() is strictly null-hostile and throws NullPointerException if any element is null. If business logic requires null elements, 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 unmodifiableList is thread-safe because it cannot be modified.” (Modifications to the backing list by another thread trigger ConcurrentModificationException).
  • ❌ “Using unmodifiableList protects 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; only iterator.remove() is blocked).