1. 引言
传统集合:灵活性与陷阱
当你通过 new ArrayList<>() 创建集合时,你得到的是可以自由修改的结构:添加、删除、替换元素。这在你“临时”构建数据时很方便。但如果你把这个集合传给了不应该修改它的其他类或方法怎么办?或者你不小心把集合暴露给了外部,有人对它做了修改?问题就从这里开始了。
典型错误示例
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 将集合“暴露”给外部
processNames(names);
// 预期列表没有被修改...
System.out.println(names);
}
public static void processNames(List<String> list) {
// 结果有人把元素删掉了!
list.remove("Bob");
}
}
输出:
[Alice, Charlie]
你的集合被改了,而你完全没有计划让它发生变化。在大型项目中,这样的“惊喜”很容易演变成非常恼人且难以定位的 bug。
危险在于,处理集合的代码可能会突然遇到数据被不可预测地修改。另外总存在信息丢失的风险——有人不小心删除了元素或覆写了它。若集合同时被多个线程修改,不仅会遇到 ConcurrentModificationException,还可能遭遇更阴险的问题——数据不一致。
保护集合:旧方法
在 Java 9 之前,只能使用类似 Collections.unmodifiableList(...) 这样的包装器(我们在上一层级讲过)。它们能返回“冻结”的集合。但这种方式并不总是方便,也不能解决所有问题(有关它的细节——在下一讲中)。
2. 现代方案:工厂方法 List.of、Set.of、Map.of
在 Java 9 中,集合接口新增了静态方法:List.of、Set.of、Map.of。它们能快速便捷地创建不可修改的集合。就好像你创建了集合并立刻把它浇筑在混凝土里——没人能够添加、删除或修改元素。
创建不可变集合的示例
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
输出:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
它如何工作?
- List.of(...) —— 创建不可变列表。
- Set.of(...) —— 创建不可变集合。
- Map.of(...) —— 创建不可变 Map(最多 10 对键值;更多时请使用 Map.ofEntries(...))。
注意! 由这些方法创建的集合不允许修改。任何尝试添加、删除或替换元素的操作都会抛出异常。
3. 用法示例与“坑点”
示例:尝试修改集合
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // 运行时错误!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("无法添加元素: " + ex.getClass().getSimpleName());
}
}
}
输出:
无法添加元素: UnsupportedOperationException
示例:尝试添加 null
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("禁止 null: " + ex.getClass().getSimpleName());
}
}
}
输出:
禁止 null: NullPointerException
示例:Set.of 中的重复元素
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("禁止重复: " + ex.getClass().getSimpleName());
}
}
}
输出:
禁止重复: IllegalArgumentException
示例:Map.of 的大量键值对
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of 最多支持 10 对键值对
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// 数量更多时请使用 Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ...等等
);
System.out.println(bigMap);
}
}
4. 不可变集合的特性与限制
不可修改。
任何尝试添加、删除或修改元素的操作都会导致抛出 UnsupportedOperationException。即便是通常可用的方法(add、remove、set)也无法工作。
不可使用 null。
如果你尝试将 null 作为列表或集合的元素,或作为 Map 的键/值加入,会得到 NullPointerException。这是出于安全考虑:包含 null 的元素常常导致集合中的错误。
不保证具体实现。
通过 List.of 等创建的集合,其底层具体类不可知。不要对它做 instanceof ArrayList 或尝试强制转换为某个具体类型。
元素顺序。
— 对于 List.of,元素顺序会被保留(就像普通列表)。
— 对于 Set.of,不保证顺序(实践中可能与传入参数顺序一致,但不要依赖)。
— 对于 Map.of,键值对的顺序不保证。
性能。
通过工厂方法创建的集合通常比对可变集合的包装器更高效,因为它们不会为多余的可变能力浪费内存。
5. 何时以及为何使用不可变集合
常量数据集
如果你有在程序运行期间不应改变的列表、集合或 Map,请使用 List.of、Set.of、Map.of。例如:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
这样就没人能往这个列表里额外添加角色。
从方法返回集合
如果你从方法返回一个集合,又不希望它在外部被修改:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
接收者将无法破坏你的数据。
在应用层之间传递
当你在程序的不同部分之间传递集合(例如 Web 应用中的 Controller 与 Service 层之间),最好使用不可变集合,防止有人“悄悄地”对其修改。
安全与线程安全
不可变集合在读取层面上天然线程安全:既然没人能修改它们,那么就可以在多个线程中放心地无同步读取。
6. 通用应用的实战示例
假设在我们的教学应用中有一个受支持命令列表:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
如果你尝试这样做:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
你会得到一个异常,无法对应用造成破坏。
再比如,有一个错误码的 Map:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
任何尝试添加新代码都会抛出异常。
7. 集合创建方式对比
| 创建方式 | 可修改? | 可使用 null? | 允许重复? | 线程安全 | 示例 |
|---|---|---|---|---|---|
|
是 | 是 | 是 | 否 | |
|
否 | 否 | 是 | 是* | |
|
否 | 否 | 否 | 是* | |
|
否 | 否 | 否 | 是* | |
|
否 | 取决于原集合 | 是 | 否 | |
* — 线程安全仅指不可变性:只要没有人修改该集合,就可以在多个线程中安全地读取。
8. 使用 List.of、Set.of、Map.of 时的常见错误
错误 №1:尝试修改集合。
最常见的错误是尝试在通过 List.of、Set.of 或 Map.of 创建的集合上添加或删除元素。例如,names.add("Dmitry") 或 ages.remove("Bob")。这总是会在运行时导致 UnsupportedOperationException。
错误 №2:尝试添加 null。
如果你不小心向任一方法传入了 null(例如,List.of("Alice", null)),会得到 NullPointerException。Java 9+ 的不可变集合不接受 null——这其实是件好事。
错误 №3:在 Set.of 或 Map.of 中出现重复元素。
Set.of("a", "b", "a") 或 Map.of("x", 1, "x", 2) 会导致 IllegalArgumentException。按定义,集合与 Map 的键都不能重复。
错误 №4:期望某个具体实现。
不要这样做:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // 这始终为 false!
内部实现是隐藏的——不要依赖实现细节。
错误 №5:尝试调用修改集合的方法。
即便是 clear()、(列表的)set(index, value) 等方法也会抛出异常。记住:通过工厂方法创建的集合是不可变的。
GO TO FULL VERSION