1. Unmodifiable wrappers: wrappers around collections
Sometimes your code already has a collection that someone might accidentally (or not so accidentally) modify. For example, you have a list of users that you want to expose externally, but you do not want anyone to modify it:
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");
You return this list from a method, and someone does users.add("Hacker"); — and now you already have a new unauthorized user in the system! How to protect yourself?
Wrappers from Collections
Java has long had special wrapper methods in the Collections class:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
These methods return a wrapper over your collection that does not allow it to be modified through the wrapper itself. An attempt to add, remove, or replace an element through the wrapper will throw UnsupportedOperationException.
Example:
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<String> modifiable = new ArrayList<>();
modifiable.add("Alice");
modifiable.add("Bob");
// Create an unmodifiable wrapper
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
System.out.println(unmodifiable); // [Alice, Bob]
// Try to add an element via the wrapper
try {
unmodifiable.add("Charlie"); // Boom! UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify the collection: " + e);
}
}
}
Important!
- The wrapper does NOT make the underlying collection immutable. If someone holds a reference to the original collection, they can still modify it.
- All changes to the underlying collection are visible through the wrapper.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]
That is, if someone somewhere in the code adds/removes an element in the original list, the wrapper will see it. This is not a “freeze”, it is just a ban on modifications through the wrapper itself.
Wrappers for other collections
Likewise, you can create wrappers for Set, Map, and even for more exotic structures:
Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);
Comparison with factory methods (List.of and others)
- List.of(...) creates a new immutable collection that cannot be modified at all.
- Collections.unmodifiableList(list) — this is a wrapper over an existing collection. If the original list changes, so does the wrapper.
Table: approach comparison
|
|
|
|---|---|---|
| Can add elements? | No | No (via the wrapper) |
| Can add to the original? | Not applicable | Yes |
| Are changes visible? | No | Yes |
| Can insert null? | No (NPE) | Yes (if the backing collection allows it) |
| Implementation | Dedicated implementation | A wrapper over your collection |
2. CopyOnWrite collections
In multithreaded programs, a common task arises: one thread (or several) reads a collection, while another (or others) occasionally modifies it. Ordinary collections are not suitable here: races, errors, ConcurrentModificationException, and other multithreading pitfalls are possible.
For such cases, CopyOnWrite collections were devised — they are specifically designed for scenarios where reads happen frequently and modifications are rare.
How does it work?
- On each modification (add, remove, replace), the collection creates a new copy of the internal array.
- All reading threads receive “their own” snapshot of the array that does not change while they read it.
- This makes reading completely safe and requires no synchronization.
Main classes
- CopyOnWriteArrayList<E>
- CopyOnWriteArraySet<E>
They are in the java.util.concurrent package.
Usage example
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("Alpha");
cowList.add("Beta");
// It is safe to iterate, even if someone adds elements in parallel
for (String s : cowList) {
System.out.println(s);
cowList.add("Gamma"); // Will not throw ConcurrentModificationException!
}
System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
}
}
Characteristics:
- The iterator of CopyOnWrite collections always sees a snapshot of the collection at the moment the iterator is created.
- If someone adds elements after the iterator is created, the iterator will not see them.
- You can safely add/remove elements during traversal — no ConcurrentModificationException!
When to use CopyOnWrite collections?
They are suitable for situations where many threads mostly read data from the collection, and modification operations are very rare. A classic example is a list of event listeners: new listeners are added or removed infrequently, while notifying these listeners happens constantly.
Example — event subscribers
import java.util.concurrent.CopyOnWriteArrayList;
public class EventBus {
private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();
public void subscribe(Runnable listener) {
listeners.add(listener);
}
public void publishEvent() {
for (Runnable listener : listeners) {
listener.run(); // safe even if someone subscribes/unsubscribes right now!
}
}
}
Drawbacks of CopyOnWrite collections
- Slow for frequent modifications: each modification creates a new copy of the array, which is costly in both memory and time.
- Inefficient for large collections: if the collection is large, copying the array is an expensive operation.
3. Comparison: when to use what?
Unmodifiable wrappers (Collections.unmodifiable...)
When to use: When you already have a collection that you want to protect from modifications by external code, but modifications from inside (the owner of the collection) are acceptable.
Thread safety: Not guaranteed! If the backing collection is modified from another thread, races and errors are possible.
Factory methods (List.of, Set.of, Map.of)
When to use: When you want to create an immutable constant collection right away, with no possibility of modification from anywhere.
Thread safety: Guaranteed (the collection never changes).
CopyOnWrite collections
When to use: In multithreaded scenarios with many reads and few writes. For example, for subscriber lists.
Thread safety: Yes, fully thread-safe.
Immutability: No, the collection can be modified, but each time a new copy is created so that readers are not affected.
4. Common mistakes and implementation details
Mistake #1: Expecting to “freeze” the underlying collection via a wrapper. Many think that Collections.unmodifiableList(list) makes the collection completely immutable. In fact, if someone keeps a reference to the original list, they can modify it, and these changes will be visible through the wrapper. Solution: If you need true immutability, use List.copyOf(list) (Java 10+) or List.of(...).
Mistake #2: Using CopyOnWrite for a frequently changing collection. If elements are constantly added to or removed from a CopyOnWriteArrayList, this will lead to performance and memory problems. CopyOnWrite is suitable only for “many readers, few writers” scenarios.
Mistake #3: Expecting wrappers to be thread-safe. Collections.unmodifiableList does not make a collection thread-safe! If the original list is modified from different threads, errors are possible.
Mistake #4: Using collections from List.of or Set.of with null. Unlike ordinary collections, the factory methods do not allow null — attempting to add or even create a collection with null will result in a NullPointerException.
GO TO FULL VERSION