CodeGym /课程 /JAVA 25 SELF /CopyOnWrite 集合与不可修改包装器

CopyOnWrite 集合与不可修改包装器

JAVA 25 SELF
第 34 级 , 课程 2
可用

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]

也就是说,如果有人在代码的其他地方向原始列表添加/删除元素,包装器会看到这些变化。它不是“冻结”,只是禁止通过包装器本身来修改。

其他集合的包装器

同样可以为 SetMap,甚至更“奇特”的结构创建包装器:

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) 是对现有集合的包装器。如果原始列表发生变化,包装器也会随之变化。

表格:方法对比

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.ofSet.ofMap.of

何时使用: 当你希望从一开始就创建一个常量式的不可修改集合,完全不允许来自任何地方的修改。

线程安全性: 有保证(集合完全不会改变)。

CopyOnWrite 集合

何时使用: 多线程场景下“读多写少”。例如订阅者列表。

线程安全性: 是,完全线程安全。

不可变性: 否。集合可以被修改,但每次修改都会创建新副本,从而不影响读者。

4. 常见错误与实现要点

错误 #1:指望通过包装器“冻结”原始集合。 很多人以为 Collections.unmodifiableList(list) 会让集合完全不可修改。实际上,只要有人还持有原始列表的引用,他就能修改它,这些更改也会通过包装器可见。 解决方案: 如果需要真正的不可变性——使用 List.copyOf(list)(Java 10+)或 List.of(...)

错误 #2:在频繁修改的集合上使用 CopyOnWrite。 如果在 CopyOnWriteArrayList 中频繁添加或删除元素,会导致性能与内存问题。CopyOnWrite 只适用于“多读少写”的场景。

错误 #3:以为包装器就是线程安全的。 Collections.unmodifiableList 并不会让集合变得线程安全!如果原始列表在不同线程中被修改,仍然可能出错。

错误 #4:将来自 List.ofSet.of 的集合与 null 一起使用。 与普通集合不同,这些工厂方法不允许 null——尝试添加,甚至使用 null 创建集合,都会导致 NullPointerException

1
任务
JAVA 25 SELF, 第 34 级, 课程 2
已锁定
禁止词列表保护 🚫
禁止词列表保护 🚫
1
任务
JAVA 25 SELF, 第 34 级, 课程 2
已锁定
实时事件日志 📈
实时事件日志 📈
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION