How to Protect a Collection from Modification
Java provides three primary mechanisms to protect collections from unwanted modification, each tailored to a specific use case:
🟢 Junior Level
30-Second Summary
Java provides three primary mechanisms to protect collections from unwanted modification, each tailored to a specific use case:
Collections.unmodifiableList(original)(Java 1.2+): Creates a lightweight wrapper (view) over an existing collection. Calling any mutator method (add(),remove(),clear()) throwsUnsupportedOperationException. However, if the underlying original collection is modified, those changes immediately reflect in the wrapper!List.of(e1, e2, ...)(Java 9+): Factory method creating compact, truly immutable collections from a known set of elements/literals. Strictly prohibitsnullelements.List.copyOf(collection)(Java 10+): Creates an independent defensive copy snapshot from an existing collection. Subsequent modifications to the original collection have zero effect on the copied list. Strictly prohibitsnullelements.
Equivalent methods exist for sets (Set.of(), Set.copyOf(), Collections.unmodifiableSet()) and maps (Map.of(), Map.copyOf(), Collections.unmodifiableMap()).
Practical Example
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CollectionProtectionDemo {
public static void main(String[] args) {
List<String> mutable = new ArrayList<>();
mutable.add("Java");
// Mechanism 1: Wrapper view (live connection)
List<String> view = Collections.unmodifiableList(mutable);
// Mechanism 2: Defensive copy (complete isolation)
List<String> copy = List.copyOf(mutable);
// Mutating protected lists is blocked:
try {
view.add("Kotlin"); // Throws UnsupportedOperationException!
} catch (UnsupportedOperationException e) {
System.out.println("Mutation via view is blocked!");
}
// Mutating the original backing list:
mutable.add("Scala");
System.out.println("view: " + view); // [Java, Scala] — THE WRAPPER CHANGED!
System.out.println("copy: " + copy); // [Java] — THE SNAPSHOT REMAINED SAFE!
}
}
🟡 Middle Level
Architectural Comparison of Protection Strategies
| Feature | Collections.unmodifiable* | List.of(...) | List.copyOf(...) | Stream.toList() (Java 16+) |
| :— | :— | :— | :— | :— |
| Creation Time | $O(1)$ | $O(N)$ (for $N \le 2$: $O(1)$) | $O(N)$ (or $O(1)$ fast-path) | $O(N)$ |
| Memory Allocation | Wrapper object only | Minimal (no spare capacity) | Minimal | Minimal |
| Backing Connection | Yes (Live View) | None | None | None |
| Null Elements | Allowed (if in source) | ❌ Throws NPE | ❌ Throws NPE | ✅ Allowed! |
| Optimization on Reuse | None | None | Returns this if immutable | None |
The Danger of Live Views (Collections.unmodifiableList)
Collections.unmodifiableList does not clone any elements. It instantiates a decorator object that maintains a direct pointer to the original ArrayList:
[Client Code] ──► [UnmodifiableList View] ──► [Original ArrayList]
▲
[External / Concurrent Code] ───────────────────────┘
If the original caller retains a reference to the Original ArrayList, calling original.add("compromised") immediately alters the data observed through the unmodifiable view, breaking class invariants.
Shallow Immutability Caveat
Standard JDK protection methods protect only the container structure:
public class User {
private String name;
public User(String name) { this.name = name; }
public void setName(String name) { this.name = name; }
public String getName() { return name; }
}
List<User> list = List.of(new User("Alice"));
// list.add(...) throws UnsupportedOperationException, but:
list.get(0).setName("Bob"); // Internal state of element 0 is mutated!
Protecting mutable element objects requires deep defensive copying across the entire element graph.
🔴 Senior Level
Immutable Collections Architecture (JEP 269)
Collections created via List.of() and List.copyOf() reside in the internal package java.util.ImmutableCollections:
- Specialized Small Sizes: For 0, 1, and 2 elements, HotSpot instantiates
List0andList12:// In List12 there is NO backing array! Elements are stored in direct object fields: static final class List12<E> extends AbstractImmutableList<E> { private final E e0; private final E e1; // null if size is 1 }This eliminates array header allocation (16 bytes) and pointer indirection, achieving dramatic memory efficiency.
- Large Sizes: For 3+ elements,
ListNwraps a compact, exact-fitObject[]array with zero spare capacity, saving up to 60% memory compared to a defaultArrayList.
Iteration Order Randomization in Set.of() and Map.of()
When the JVM initializes, it generates a per-run random salt for Set.of() and Map.of():
- The iteration order of elements in
Set.of("a", "b", "c")changes across JVM restarts. - Architectural Rationale:
- Prevents Hidden Dependencies: Discourages developers from writing code that implicitly assumes deterministic ordering in unordered collections.
- Hash-DoS Defense: Prevents attackers from exploiting hash collisions via predictable iteration patterns (Hash Flooding Attacks).
Stream.toList() vs. List.copyOf() (Java 16+)
A critical distinction often examined in senior interviews:
Stream.toList()(Java 16+) returns an unmodifiable list backed byImmutableCollections.ListN.- Key Difference:
List.of()andList.copyOf()are strictly null-hostile (throwingNullPointerException). In contrast,Stream.toList()permitsnullelements!List<String> streamList = Stream.of("A", null, "B").toList(); // ✅ Compiles & executes cleanly! List.copyOf(streamList); // ❌ Throws NullPointerException!
🎯 Interview Cheat Sheet
Summary of Collection Protection Mechanisms
| Method | When to Use | Advantages | Drawbacks |
| :— | :— | :— | :— |
| List.of(...) | Fixed constants/literals ($\le 10$) | Minimal memory, direct field storage ($N \le 2$) | Null-hostile, fixed arguments |
| List.copyOf(...) | Constructor defensive copy | Completely isolates state, fast-path for immutable input | Null-hostile, $O(N)$ allocation |
| Collections.unmodifiable* | Getter returns (when source is private) | $O(1)$ creation, permits null | Live view to source, risk of CME |
| Stream.toList() | Terminal Stream operation | Unmodifiable, permits null | $O(N)$ allocation |
4 Tricky Interview Questions
1. Can iterating over a Collections.unmodifiableList(list) throw ConcurrentModificationException?
Answer: Yes, absolutely. Collections.unmodifiableList is merely a view wrapper over the underlying list. If a background thread invokes add() or remove() on the original list while another thread is iterating through the unmodifiable wrapper, the underlying modCount changes. The iterator detects this change and immediately throws a ConcurrentModificationException. The wrapper prevents mutation through its own interface, but is not thread-safe.
2. Why do Set.of() and Map.of() deliberately randomize iteration order across JVM restarts while Collections.unmodifiableSet(new HashSet<>()) does not?
Answer: A standard HashSet iterates in a deterministic order based on standard element hashCode() and bucket capacity. In contrast, Set.of() and Map.of() mix a randomized salt (SALT = new Random().nextLong()) into bucket placement upon JVM startup. This defends against Hash-DoS attacks and prevents brittle code dependencies on hash iteration orders.
3. How does List.of("a", "b") outperform Collections.unmodifiableList(new ArrayList<>(List.of("a", "b"))) in memory footprint on 64-bit HotSpot?
Answer: The memory difference is massive:
unmodifiableList(new ArrayList<>(...)):UnmodifiableListwrapper: 24 bytes.ArrayListobject: 32 bytes.- Backing
Object[10]array: 56 bytes. - Total: Over 110 bytes of memory overhead.
List.of("a", "b"):- Instantiates
ImmutableCollections.List12. - Stores elements in fields
e0ande1with no backing array at all. - Total: Exactly 24 bytes in heap memory (>75% memory savings).
- Instantiates
4. What happens when invoking List.copyOf(input) on a collection containing null? Why was it designed this way?
Answer: It immediately throws a NullPointerException. Java architects (Brian Goetz, Stuart Marks) intentionally designed modern collection factories to be null-hostile. Allowing null in collections creates ubiquitous ambiguity (does map.get(key) == null mean the key is absent or mapped to null?). Prohibiting null causes code to fail fast at the point of origin rather than propagating corrupt state downstream.
Red Flags (DO NOT Say)
- ❌ “Using
Collections.unmodifiableList()completely isolates the collection from external modifications.” (It is a live view; mutating the original list mutates the view). - ❌ “Wrapping a list in
List.of()orList.copyOf()makes all objects inside it immutable.” (It only protects collection structure; contained objects remain mutable). - ❌ “
Collections.unmodifiableList()makes a collection thread-safe.” (Modifications to the backing collection causeConcurrentModificationException). - ❌ “
List.copyOf()andStream.toList()behave identically with respect tonull.” (List.copyOfthrows NPE on nulls, whereasStream.toList()permits nulls).
Related Topics
- What is Defensive Copy — Mechanics and invariants
- When Should You Make Defensive Copy — Decision matrix
- What is Collections.unmodifiableList() and How Does It Work — Wrapper internals
- Difference Between Shallow Copy and Deep Copy — Object graph duplication
- How to Properly Work with Collections in Immutable Classes — Enterprise patterns