1. 概述
Java 9 终于带来了期待已久的语法糖:用一行简洁代码创建小型不可变集合实例。根据 JEP 269,JDK 9 新增了便捷的集合工厂方法。
本文将深入探讨这些方法的使用方式和实现细节。
2. 历史背景与设计动机
在 Java 中创建小型不可变集合的传统方式极其啰嗦。以 Set
为例:
Set<String> set = new HashSet<>();
set.add("foo");
set.add("bar");
set.add("baz");
set = Collections.unmodifiableSet(set);
这么简单的操作需要写这么多代码,显然应该能用单行表达式完成。Map
的情况同样糟糕。
不过 List
稍好些,有个现成的工厂方法:
List<String> list = Arrays.asList("foo", "bar", "baz");
虽然比构造器初始化简洁,但违反了最小惊讶原则——谁会想到要在 Arrays
类里找创建 List
的方法?
还有其他减少冗余的方案,比如双大括号初始化:
Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
add("foo"); add("bar"); add("baz");
}});
或者用 Java 8 的 Stream
:
Stream.of("foo", "bar", "baz")
.collect(collectingAndThen(toSet(), Collections::unmodifiableSet));
双大括号技术只是稍微简洁点,但严重牺牲可读性(属于反模式)。Java 8 版本虽是单行表达式,但也有缺陷:
- 不够直观
- 仍然冗长
- 会创建不必要的临时对象
- 无法用于创建
Map
总结下来,这些方案都没把"创建小型不可变集合"当作一等公民问题来处理。
3. 使用方法详解
List
、Set
和 Map
接口新增了静态方法,接收元素参数并返回对应的集合实例。三个接口的方法都命名为 of(...)
。
3.1. List 和 Set
List
和 Set
的工厂方法签名和特性完全相同:
static <E> List<E> of(E e1, E e2, E e3)
static <E> Set<E> of(E e1, E e2, E e3)
使用示例:
List<String> list = List.of("foo", "bar", "baz");
Set<String> set = Set.of("foo", "bar", "baz");
简洁明了!示例中使用的是接收三个元素的重载版本,但实际提供了 12 个重载:
- 11 个固定参数版本(0-10 个元素)
- 1 个可变参数版本
static <E> List<E> of()
static <E> List<E> of(E e1)
static <E> List<E> of(E e1, E e2)
// ...以此类推到10个参数
static <E> List<E> of(E... elems)
⚠️ 为什么需要这么多重载?
性能!可变参数方法会隐式创建数组,固定参数版本能避免不必要的对象创建和 GC 开销。相比之下,Arrays.asList
总是创建数组,在元素少时效率较低。
创建 Set
时传入重复元素会抛出 IllegalArgumentException
:
@Test(expected = IllegalArgumentException.class)
public void onDuplicateElem_IfIllegalArgExp_thenSuccess() {
Set.of("foo", "bar", "baz", "foo");
}
⚠️ 注意自动装箱:
由于使用泛型,原始类型会被自动装箱。如果传入原始类型数组,返回的是包含该数组的集合:
int[] arr = { 1, 2, 3, 4, 5 };
List<int[]> list = List.of(arr); // 返回 List<int[]>,size=1
3.2. Map
Map
的工厂方法签名:
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3)
使用示例:
Map<String, String> map = Map.of("foo", "a", "bar", "b", "baz", "c");
同样提供 0-10 个键值对的重载版本。超过 10 个键值对时,使用 ofEntries
方法:
static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)
使用示例:
Map<String, String> map = Map.ofEntries(
new AbstractMap.SimpleEntry<>("foo", "a"),
new AbstractMap.SimpleEntry<>("bar", "b"),
new AbstractMap.SimpleEntry<>("baz", "c"));
传入重复键会抛出 IllegalArgumentException
:
@Test(expected = IllegalArgumentException.class)
public void givenDuplicateKeys_ifIllegalArgExp_thenSuccess() {
Map.of("foo", "a", "foo", "b");
}
⚠️ 原始类型同样会被自动装箱。
4. 实现细节
工厂方法创建的集合并非常用实现类。例如 List
不是 ArrayList
,Map
也不是 HashMap
。它们是 Java 9 引入的特殊内部实现类,构造器访问受限。
以下是所有集合共有的关键实现特性:
4.1. 不可变性
工厂方法创建的集合完全不可变,任何修改操作都会抛出 UnsupportedOperationException
:
@Test(expected = UnsupportedOperationException.class)
public void onElemAdd_ifUnSupportedOpExpnThrown_thenSuccess() {
Set<String> set = Set.of("foo", "bar");
set.add("baz"); // ❌ 抛出异常
}
@Test(expected = UnsupportedOperationException.class)
public void onElemModify_ifUnSupportedOpExpnThrown_thenSuccess() {
List<String> list = List.of("foo", "bar");
list.set(0, "baz"); // ❌ 抛出异常
}
@Test(expected = UnsupportedOperationException.class)
public void onElemRemove_ifUnSupportedOpExpnThrown_thenSuccess() {
Map<String, String> map = Map.of("foo", "a", "bar", "b");
map.remove("foo"); // ❌ 抛出异常
}
4.2. 禁止 null 元素
List
/Set
不允许包含 null
元素,Map
的键和值也不能为 null
。传入 null
会抛出 NullPointerException
:
@Test(expected = NullPointerException.class)
public void onNullElem_ifNullPtrExpnThrown_thenSuccess() {
List.of("foo", "bar", null); // ❌ 抛出异常
}
✅ 对比:Arrays.asList
允许 null
值。
4.3. 基于值的实例
工厂方法返回的是基于值的实例。这意味着:
- 可能返回新实例
- 也可能返回已有实例(缓存)
因此相同值的集合可能指向不同对象:
List<String> list1 = List.of("foo", "bar");
List<String> list2 = List.of("foo", "bar");
// list1 == list2 可能返回 true 或 false,取决于 JVM 实现
4.4. 序列化支持
当集合元素可序列化时,工厂方法创建的集合也支持序列化。
5. 总结
本文介绍了 Java 9 新增的集合工厂方法。通过对比传统创建方式,我们理解了这项改进的价值。重点内容包括:
- ✅ 使用
List.of()
/Set.of()
/Map.of()
创建不可变集合 - ✅ 提供多种重载版本优化性能
- ✅ 严格禁止
null
和重复元素 - ✅ 完全不可变,修改操作抛出异常
- ⚠️ 注意自动装箱和基于值实例的特性
这些集合与常用实现类(如 ArrayList
/HashMap
)有本质区别,使用时需特别注意其不可变性和限制条件。
完整源码及单元测试见 GitHub 项目。