1. Unmodifiable wrappers:集合的只读包装器
有时代码里已经有了一个集合,可能会被别人不小心(或故意)修改。比如,你有一个用户列表需要对外提供,但又不希望别人修改它:
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");
你把这个列表从方法中返回,结果有人调用 users.add("Hacker"); —— 系统里就多了一个未授权的用户!如何防护?
来自 Collections 的包装器
Java 很早就提供了类 Collections 中的专用包装方法:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
这些方法返回你的集合之上的包装器,通过该包装器无法对集合进行修改。尝试通过包装器添加、删除或替换元素会抛出 UnsupportedOperationException。
示例:
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<String> modifiable = new ArrayList<>();
modifiable.add("Alice");
modifiable.add("Bob");
// 创建不可修改的包装器
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
System.out.println(unmodifiable); // [Alice, Bob]
// 尝试通过包装器添加元素
try {
unmodifiable.add("Charlie"); // 砰!UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify collection: " + e);
}
}
}
重要!
- 包装器并不会使原始集合不可变。只要有人持有对原始集合的引用,他仍然可以修改它。
- 对原始集合的所有更改都会通过包装器可见。
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]
也就是说,如果有人在代码的其他地方向原始列表添加/删除元素,包装器会看到这些变化。它不是“冻结”,只是禁止通过包装器本身来修改。
其他集合的包装器
同样可以为 Set、Map,甚至更“奇特”的结构创建包装器:
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);
与工厂方法(List.of 等)的比较
- List.of(...) 会创建全新的不可修改集合,从一开始就不能往里添加元素。
- Collections.unmodifiableList(list) 是对现有集合的包装器。如果原始列表发生变化,包装器也会随之变化。
表格:方法对比
|
|
|
|---|---|---|
| 能添加吗? | 否 | 否(通过包装器) |
| 能向原始集合添加吗? | 不适用 | 是 |
| 更改是否可见? | 否 | 是 |
| 可以放入 null 吗? | 否(NPE) | 是(如果原始集合允许) |
| 实现 | 自有实现 | 基于你的集合的包装器 |
2. CopyOnWrite 集合
在多线程程序中常见这样的场景:一个(或多个)线程读取集合,另一个(或多个)线程偶尔修改集合。普通集合不适合这种情况:可能出现竞态、错误、ConcurrentModificationException 等多线程“乐子”。
为此发明了 CopyOnWrite 集合——它们专为“读多写少”的场景而设计。
工作原理
- 每次修改(添加、删除、替换)时,集合都会创建内部数组的新副本。
- 所有读取线程都会拿到“自己的”数组版本,在读取期间不会被修改。
- 这使得读取完全安全,且不需要同步。
主要类
- CopyOnWriteArrayList<E>
- CopyOnWriteArraySet<E>
它们位于包 java.util.concurrent 中。
使用示例
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");
// 即使有人并发添加元素,也可以安全地遍历
for (String s : cowList) {
System.out.println(s);
cowList.add("Gamma"); // 不会抛出 ConcurrentModificationException!
}
System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
}
}
特点:
- CopyOnWrite 集合的迭代器总是“看到”创建迭代器那一刻的快照。
- 如果在创建迭代器之后有人添加了元素,迭代器看不到这些元素。
- 遍历期间可以安全地添加/删除元素——不会出现 ConcurrentModificationException!
何时使用 CopyOnWrite 集合?
它适用于程序中有很多线程主要执行读取、而修改操作非常少的情况。经典示例是事件监听器(event listeners)列表:新增或删除监听器并不频繁,但通知这些监听器的操作非常频繁。
示例——事件订阅者
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(); // 安全,即使此刻有人订阅/取消订阅!
}
}
}
CopyOnWrite 集合的缺点
- 频繁修改时很慢:每次修改都要创建新副本,时间和内存开销都很大。
- 对大集合不友好:如果集合很大,复制数组是昂贵的操作。
3. 对比:什么时候用什么?
Unmodifiable wrappers(Collections.unmodifiable...)
何时使用: 当你已经有一个集合,希望对外部代码禁止修改,但允许集合的“所有者”从内部进行修改。
线程安全性: 不保证!如果原始集合在其他线程中被修改,仍可能出现竞态与错误。
工厂方法(List.of、Set.of、Map.of)
何时使用: 当你希望从一开始就创建一个常量式的不可修改集合,完全不允许来自任何地方的修改。
线程安全性: 有保证(集合完全不会改变)。
CopyOnWrite 集合
何时使用: 多线程场景下“读多写少”。例如订阅者列表。
线程安全性: 是,完全线程安全。
不可变性: 否。集合可以被修改,但每次修改都会创建新副本,从而不影响读者。
4. 常见错误与实现要点
错误 #1:指望通过包装器“冻结”原始集合。 很多人以为 Collections.unmodifiableList(list) 会让集合完全不可修改。实际上,只要有人还持有原始列表的引用,他就能修改它,这些更改也会通过包装器可见。 解决方案: 如果需要真正的不可变性——使用 List.copyOf(list)(Java 10+)或 List.of(...)。
错误 #2:在频繁修改的集合上使用 CopyOnWrite。 如果在 CopyOnWriteArrayList 中频繁添加或删除元素,会导致性能与内存问题。CopyOnWrite 只适用于“多读少写”的场景。
错误 #3:以为包装器就是线程安全的。 Collections.unmodifiableList 并不会让集合变得线程安全!如果原始列表在不同线程中被修改,仍然可能出错。
错误 #4:将来自 List.of 或 Set.of 的集合与 null 一起使用。 与普通集合不同,这些工厂方法不允许 null——尝试添加,甚至使用 null 创建集合,都会导致 NullPointerException。
GO TO FULL VERSION