4.8 创建不可变集合

问题

用户希望利用 Stream API 创建不可变的列表、集合或映射。

方案

使用 Collectors 类新增的静态方法 collectingAndThen

讨论

函数式编程强调并行(parallelization)以及语义的清晰,倾向于尽可能使用不可变对象。Java 1.2 引入了集合框架,提供以现有集合为基础创建不可变集合的各种方法,但使用起来有些不便。

Collections 工具类定义了 unmodifiableListunmodifiableSetunmodifiableMap 方法(以及其他以 unmodifiable 为前缀的方法),如例 4-30 所示。

例 4-30 Collections 类定义的以 unmodifiable 为前缀的方法

  1. static <T> List<T> unmodifiableList(List<? extends T> list)
  2. static <T> Set<T> unmodifiableSet(Set<? extends T> s)
  3. static <K,V> Map<K,V> unmodifiableMap(Map<? extends K,? extends V> m)

上述三种方法的参数分别为现有的列表、集合与映射。结果列表、集合与映射中包含的元素和参数相同,但存在一个重要的区别:所有可以修改集合的方法(如 addremove)现在都能抛出 UnsupportedOperationException

在 Java 8 之前,如果通过可变参数列表获取到单个值作为参数,则会生成一个不可修改的列表或集合,如例 4-31 所示。

例 4-31 创建不可修改的列表或集合(Java 8 之前)

  1. @SafeVarargs
  2. public final <T> List<T> createImmutableListJava7(T elements) {
  3. return Collections.unmodifiableList(Arrays.asList(elements));
  4. }

  5. @SafeVarargs

  6. public final <T> Set<T> createImmutableSetJava7(T elements) {

  7. return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(elements)));

  8. }

➊ 用户承诺不会破坏输入数组类型。详见附录 A。

两段代码首先传入输入值,并将它们转换为 List。第一段代码使用 unmodifiableList 包装生成的列表。而对于 Set,第二段代码先将列表用作集合构造函数的参数,再使用 unmodifiableSet

借由 Java 8 引入的 Stream API,我们可以使用 Collectors 类定义的静态方法 collectingAndThen,如例 4-32 所示。

例 4-32 创建不可修改的列表或集合(Java 8)

  1. import static java.util.stream.Collectors.collectingAndThen;
  2. import static java.util.stream.Collectors.toList;
  3. import static java.util.stream.Collectors.toSet;
  4.  
  5. // 采用以下方法来定义类
  6.  
  7. @SafeVarargs
  8. public final <T> List<T> createImmutableList(T... elements) {
  9. return Arrays.stream(elements)
  10. .collect(collectingAndThen(toList(),
  11. Collections::unmodifiableList));
  12. }
  13.  
  14. @SafeVarargs
  15. public final <T> Set<T> createImmutableSet(T... elements) {
  16. return Arrays.stream(elements)
  17. .collect(collectingAndThen(toSet(),
  18. Collections::unmodifiableSet));
  19. }

➊ “终止器”对生成的集合进行包装

collectingAndThen 方法传入两个参数,一个是下游 Collector,另一个是称为终止器(finisher)的 Function。该方法的作用是读取输入元素,并将它们收集到 ListSet,然后利用不可修改的函数包装结果集合。

将一系列输入元素转换为一个不可修改的 Map 看起来并不直观,部分原因在于很难看出哪些输入元素将作为键,哪些将作为值。例 4-334 采用实例初始化器(instance initializer),以一种很别扭的方式创建了一个不可变的 Map

4灵感源自 Carl Martensen 的博文“Java 9's Immutable Collections Are Easier To Create But Use With Caution”。

例 4-33 创建不可变的 Map

  1. Map<String, Integer> map = Collections.unmodifiableMap(
  2. new HashMap<String, Integer>() {{
  3. put("have", 1);
  4. put("the", 2);
  5. put("high", 3);
  6. put("ground", 4);
  7. }});

熟悉 Java 9 的读者想必已经了解,本范例中的所有问题都可以通过工厂方法 List.ofSet.ofMap.of 来解决,这些方法能极大提高效率。

另见

Java 9 新增的工厂方法能自动创建不可变集合,相关讨论请参见范例 10.3。