10.3 创建不可变集合

问题

用户希望在 Java 9 中创建不可变列表、集合或映射。

方案

使用 Java 9 新增的静态工厂方法 List.ofSet.ofMap.of

讨论

根据 Javadoc 的描述,可以使用 Java 9 引入的各种 List.of() 静态工厂方法方便地创建不可变列表。通过这些方法创建的 List 实例具有以下特征。

  • 结构上是不可变的(structurally immutable),即无法执行添加、删除或替换元素的操作,且调用任何更改器方法(mutator method)都会抛出 UnsupportedOperationException。然而,如果包含的元素本身是可变的,则可能导致 List 的内容发生变化。
  • 禁止使用 null 元素,尝试创建包含 null 元素的 List 实例将抛出 NullPointerException
  • 如果所有元素是可序列化的(serializable),则 List 实例也是可序列化的。
  • 列表中元素的顺序与所提供参数的顺序相同,与所提供数组中元素的顺序也相同。
  • 根据 Serialized Form 页面的规定进行序列化。

例 10-10 列出了 List.of 方法所有可用的重载形式。

例 10-10 用于创建不可变列表的静态工厂方法

  1. static <E> List<E> of()
  2. static <E> List<E> of(E e1)
  3. static <E> List<E> of(E e1, E e2)
  4. static <E> List<E> of(E e1, E e2, E e3)
  5. static <E> List<E> of(E e1, E e2, E e3, E e4)
  6. static <E> List<E> of(E e1, E e2, E e3, E e4, E e5)
  7. static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6)
  8. static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)
  9. static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)
  10. static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)
  11. static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)
  12. static <E> List<E> of(E... elements)

正如 Javadoc 指出的那样,通过上述方法创建的列表在结构上是不可变的,因此无法在 List 上调用任何常规的更改器方法。换言之,调用 addaddAllclearremoveremoveAllreplaceAll 以及 set 都会抛出 UnsupportedOperationException。例 10-11 显示了部分测试用例。8

8完整的测试用例请参见本书源代码。

例 10-11 不可变列表的应用

  1. @Test(expected = UnsupportedOperationException.class)
  2. public void showImmutabilityAdd() throws Exception {
  3. List<Integer> intList = List.of(1, 2, 3);
  4. intList.add(99);
  5. }

  6. @Test(expected = UnsupportedOperationException.class)

  7. public void showImmutabilityClear() throws Exception {

  8. List<Integer> intList = List.of(1, 2, 3);

  9. intList.clear();

  10. }

  11. @Test(expected = UnsupportedOperationException.class)

  12. public void showImmutabilityRemove() throws Exception {

  13. List<Integer> intList = List.of(1, 2, 3);

  14. intList.remove(0);

  15. }

  16. @Test(expected = UnsupportedOperationException.class)

  17. public void showImmutabilityReplace() throws Exception {

  18. List<Integer> intList = List.of(1, 2, 3);

  19. intList.replaceAll(n -> -n);

  20. }

  21. @Test(expected = UnsupportedOperationException.class)

  22. public void showImmutabilitySet() throws Exception {

  23. List<Integer> intList = List.of(1, 2, 3);

  24. intList.set(0, 99);

  25. }

然而,如果列表包含的对象本身是可变的,那么列表也可能发生变化。为说明这个问题,我们创建一个名为 Holder 的类。这个类很简单,它包含一个可变值 x,如例 10-12 所示。

例 10-12 包含可变值的简单类

  1. public class Holder {
  2. private int x;
  3.  
  4. public Holder(int x) { this.x = x; }
  5.  
  6. public void setX(int x) {
  7. this.x = x;
  8. }
  9.  
  10. public int getX() {
  11. return x;
  12. }
  13. }

如果创建一个 Holder 实例的不可变列表,由于 Holder 包含的值是可变的,列表也会发生变化,如例 10-13 所示。

例 10-13 修改包装的整数

  1. @Test
  2. public void areWeImmutableOrArentWe() throws Exception {
  3. List<Holder> holders = List.of(new Holder(1), new Holder(2));
  4. assertEquals(1, holders.get(0).getX());

  5. holders.get(0).setX(4);                                       
  6. assertEquals(4, holders.get(0).getX());
  7. }

Holder 实例的不可变列表

❷ 修改 Holder 中包含的值

虽然上述代码可以运行且不违反文档规定,但有悖于开发所应遵循的最佳实践。换言之,如果计划使用不可变列表,应尽量在列表中包含不可变对象。

类似地,根据 Javadoc 的描述,可以使用各种 Set.of() 方法方便地创建不可变集合。通过这些方法创建的 Set 实例具有以下特征。

  • 创建时(creation time)拒绝重复元素,传递给静态工厂方法的重复元素会导致 IllegalArgumentException
  • 未指定集合元素的迭代顺序,因此它可能会发生变化。

所有 Set.of() 方法的签名与对应的 List.of() 方法相同,只不过返回的是 Set

Map.of() 方法同样如此,但其签名传入交替的键和值作为参数,如例 10-14 所示。

例 10-14 用于创建不可变映射的静态工厂方法

  1. static <K,V> Map<K,V> of()
  2. static <K,V> Map<K,V> of(K k1, V v1)
  3. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2)
  4. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3)
  5. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4)
  6. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5)
  7. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6)
  8. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7)
  9. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8)
  10. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9)
  11. static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10)
  12. static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)

在创建包含最多 10 个条目的映射时,虽然可以使用相应的 Map.of() 方法(交替传入键和值),不过这并非最佳方案,因此 Map 接口还定义了另外两种静态方法,即 ofEntriesentry

  1. static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)
  2. static <K,V> Map.Entry<K,V> entry(K k, V v)

例 10-15 显示了如何利用上面介绍的各种方法创建不可变映射。

例 10-15 利用各种方法创建不可变映射

  1. @Test
  2. public void immutableMapFromEntries() throws Exception {
  3. Map<String, String> jvmLanguages = Map.ofEntries(
  4. Map.entry("Java", "http://www.oracle.com/technetwork/java/index.html&#34;),
  5. Map.entry("Groovy", "http://groovy-lang.org/&#34;),
  6. Map.entry("Scala", "http://www.scala-lang.org/&#34;),
  7. Map.entry("Clojure", "https://clojure.org/&#34;),
  8. Map.entry("Kotlin", "http://kotlinlang.org/&#34;));

  9. Set&lt;String&gt; names = Set.of(&#34;Java&#34;, &#34;Scala&#34;, &#34;Groovy&#34;, &#34;Clojure&#34;, &#34;Kotlin&#34;);
  10. List&lt;String&gt; urls = List.of(&#34;http://www.oracle.com/technetwork/java/index.html&#34;,
  11.         &#34;http://groovy-lang.org/&#34;,
  12.         &#34;http://www.scala-lang.org/&#34;,
  13.         &#34;https://clojure.org/&#34;,
  14.         &#34;http://kotlinlang.org/&#34;);
  15. Set&lt;String&gt; keys = jvmLanguages.keySet();
  16. Collection&lt;String&gt; values = jvmLanguages.values();
  17. names.forEach(name -&gt; assertTrue(keys.contains(name)));
  18. urls.forEach(url -&gt; assertTrue(values.contains(url)));
  19. Map&lt;String, String&gt; javaMap = Map.of(&#34;Java&#34;,        ➋
  20.         &#34;http://www.oracle.com/technetwork/java/index.html&#34;,
  21.         &#34;Groovy&#34;,
  22.         &#34;http://groovy-lang.org/&#34;,
  23.         &#34;Scala&#34;,
  24.         &#34;http://www.scala-lang.org/&#34;,
  25.         &#34;Clojure&#34;,
  26.         &#34;https://clojure.org/&#34;,
  27.         &#34;Kotlin&#34;,
  28.         &#34;http://kotlinlang.org/&#34;);
  29. javaMap.forEach((name, url) -&gt; assertTrue(
  30.         jvmLanguages.keySet().contains(name) &amp;&amp; \
  31.           jvmLanguages.values().contains(url)));
  32. }

❶ 使用 Map.ofEntries 方法

❷ 使用 Map.of 方法

可以看到,将 ofEntriesentry 方法结合在一起使用有助于简化代码。

另见

有关在 Java 8 以及更早版本中创建不可变集合的讨论请参见范例 4.8。