1. 引言
简单来说,mutable 集合是指在创建之后仍可修改的集合:可以添加、删除和修改元素。Immutable(不可变)集合是在创建之后就不能再修改的集合。就像混凝土凝固之后:可以看、可以摸,但不能再塑形了。
可变集合就像带铅笔的笔记本:写、擦、再添加新笔记。不可变集合就像被覆膜的页面:此后谁也无法增删其内容。
可变(mutable)集合的示例
在 Java 中,几乎所有标准集合默认都是可变的。例如:
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- LinkedHashMap
- 以及许多其他类
示例:ArrayList
import java.util.*;
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // 将 Bob 替换为 Charlie
names.remove("Alice"); // 删除 Alice
System.out.println(names); // [Charlie]
在这里我们可以对集合做任何操作:添加、删除、交换元素等。当集合在运行中动态构建时(例如从文件读取数据或处理用户输入),这非常方便。
2. 不可变(immutable)集合的示例
这些“家伙”在 Java 9 中出现,你也许已经见过它们,但仍需要习惯它们的用法:
- List.of(...)
- Set.of(...)
- Map.of(...)
- List.copyOf(collection)
- Set.copyOf(collection)
- Map.copyOf(map)
示例:List.of
List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // 将抛出 UnsupportedOperationException!
尝试修改该集合会导致运行时异常。
示例:Collections.unmodifiableList
List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!
但这里有个坑:如果你修改了原始集合,包装视图也会随之改变!
modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — 新元素出现了!
3. Mutable 与 Immutable 集合的主要区别
| 属性 | Mutable(可变) | Immutable(不可变) |
|---|---|---|
| 可以添加元素吗? | 可以 | 不可以 |
| 可以删除元素吗? | 可以 | 不可以 |
| 可以修改元素吗? | 可以(例如,set) | 不可以 |
| 线程安全性 | 否(默认) | 是(无可变状态——无可修改内容) |
| 可以添加 null 吗? | 可以(通常) | 不可以(Java 9+ 的工厂方法) |
| 实现 | ArrayList、HashSet 等 | List.of、Set.of、Map.of、copyOf |
4. 为什么还需要不可变集合?
一个常见问题是:既然可变集合这么灵活,为什么还要使用不可变集合?原因很多,主要与代码的安全性、可读性和可预测性有关。
安全性与防错
当你将集合暴露给外部(例如从方法或类返回)时,你希望确保没有人会不小心修改其内容。尤其当集合包含初始化后不应再更改的“关键”数据时,这一点尤为重要。
示例:
public class Team {
private final List<String> players;
public Team(List<String> players) {
// 复制为不可变集合,防止任何人篡改名单
this.players = List.copyOf(players);
}
public List<String> getPlayers() {
return players;
}
}
现在,拿到球员列表的任何代码都无法往里添加自己的“朋友”。
线程安全性
可变集合在多个线程并发访问时并不安全。相反,不可变集合可以在各个线程之间自由传递——没有人能把它“搞坏”。
简化调试
如果集合不可变,你总是知道里面是什么。不必担心有人在代码的其他地方“悄悄”改了它。
作为其他集合中的键或值使用
不可变对象非常适合作为 Map 的键或 Set 的元素。如果对象在放入集合后还可能改变,你可能会因为 hashCode/equals 发生变化而失去对它的访问。
5. 什么时候更适合使用可变集合?
以下场景适合使用可变集合:
- 集合需要分阶段、在循环中或从多个来源构建。
- 需要频繁修改:添加、删除、排序。
- 集合仅用于内部,不会被“外部”代码破坏。
示例:构建列表
List<String> shoppingList = new ArrayList<>();
shoppingList.add("牛奶");
shoppingList.add("面包");
shoppingList.add("苹果");
// 构建完成后——可以转换为不可变版本
List<String> finalList = List.copyOf(shoppingList);
6. 什么时候更适合使用不可变集合?
- 用于保存常量数据(例如一周的工作日列表)。
- 在应用各层之间传递集合(例如从 DAO 到服务层)。
- 从方法返回集合时,保护其不被修改。
- 在强调安全性的多线程场景中。
示例:常量数据
public static final List<String> WEEKDAYS = List.of(
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);
示例:对外返回
public List<String> getReadOnlyNames() {
return List.copyOf(names); // 任何人都无法修改该列表
}
7. 特性与坑点
不可变 ≠ 线程安全
不可变集合只能防止自身被修改,但并不意味着在多线程环境中就没有其他问题(例如,集合中的元素本身是可变对象)。
List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // 仍然可以修改内部列表!
包装视图 vs 真实拷贝
如前所述,Collections.unmodifiableList 只是一个包装视图;如果原始集合被修改,包装视图也会随之改变。而 List.copyOf 会创建真正独立的副本。
List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);
base.add("c");
System.out.println(wrap); // [a, b, c] — 发生了变化!
System.out.println(copy); // [a, b] — 保持不变!
NullPointerException
工厂方法(List.of、Set.of、Map.of)不允许包含 null:
List<String> bad = List.of("a", null); // 在创建时就会抛出 NullPointerException!
8. 方案对比:优缺点
可变集合
可变集合的最大优点是灵活。你可以随时添加、删除元素,并在程序逻辑演进时重组集合。这种方式在需要快速搭建临时结构或动态调整内容时尤其方便。
但这种便利也有代价。可变集合始终存在被意外修改的风险:代码的某处可能不小心改动了数据,从而引发难以定位的错误。在多线程程序中更糟——并发修改很容易导致竞态和意外故障。管理此类数据的生命周期也更复杂:你必须时刻记住谁在何时可能会修改集合。
不可变集合
使用不可变集合更省心。你可以确定:没人会“背着你”改动它们。这让代码更安全,更易于调试和测试,而且这些集合可以在多线程之间方便地传递而无需额外加锁。
缺点是有时需要牺牲一定的性能。若需添加新元素,往往要创建集合的新副本,这会带来额外的内存与时间开销。此外,直接将大型、复杂的结构一次性构建为不可变形式并不方便。通常的做法是先在临时的可变集合中构建,待一切就绪后再“冻结”。
9. 常见错误
错误 № 1:向外部返回可变集合。 如果你从方法返回一个普通的 ArrayList,任何外部代码都可以添加或删除元素。这可能导致难以追踪的缺陷。
错误 № 2:用包装视图代替拷贝。 如果你使用 Collections.unmodifiableList,但原始集合在别处仍会被修改,那么“不可变性”——只是错觉。
错误 № 3:在不可变集合中放入可变对象。 即便集合自身不可变,其元素也可能是可变的。这会导致状态被意外修改。
错误 № 4:向通过工厂方法创建的集合中添加 null。 与旧式集合不同,新的工厂方法不允许包含 null——会得到 NullPointerException。
错误 № 5:依赖具体实现。 通过 List.of 或 Set.of 创建的集合不保证具体实现类型(不一定是 ArrayList 或 HashSet)。不要依赖这一点。
GO TO FULL VERSION