Java 创建不可变集合,用户希望在 Java 9 中创建不可变列表、集合或映射,可以使用 Java 9 新增的静态工厂方法 List.of
、Set.of
与 Map.of
。根据 Javadoc 的描述,可以使用 Java 9 引入的各种 List.of()
静态工厂方法方便地创建不可变列表。通过这些方法创建的 List
实例具有以下特征。
Java 创建不可变集合 问题描述
用户希望在 Java 9 中创建不可变列表、集合或映射。
Java 创建不可变集合 解决方案
使用 Java 9 新增的静态工厂方法 List.of
、Set.of
与 Map.of
。
Java 创建不可变集合 具体实例
根据 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 用于创建不可变列表的静态工厂方法
static <E> List<E> of()
static <E> List<E> of(E e1)
static <E> List<E> of(E e1, E e2)
static <E> List<E> of(E e1, E e2, E e3)
static <E> List<E> of(E e1, E e2, E e3, E e4)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)
static <E> List<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)
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)
static <E> List<E> of(E... elements)
正如 Javadoc 指出的那样,通过上述方法创建的列表在结构上是不可变的,因此无法在 List
上调用任何常规的更改器方法。换言之,调用 add
、addAll
、clear
、remove
、removeAll
、replaceAll
以及 set
都会抛出 UnsupportedOperationException
。例 10-11 显示了部分测试用例。
例 10-11 不可变列表的应用
@Test(expected = UnsupportedOperationException.class)
public void showImmutabilityAdd() throws Exception {
List<Integer> intList = List.of(1, 2, 3);
intList.add(99);
}
@Test(expected = UnsupportedOperationException.class)
public void showImmutabilityClear() throws Exception {
List<Integer> intList = List.of(1, 2, 3);
intList.clear();
}
@Test(expected = UnsupportedOperationException.class)
public void showImmutabilityRemove() throws Exception {
List<Integer> intList = List.of(1, 2, 3);
intList.remove(0);
}
@Test(expected = UnsupportedOperationException.class)
public void showImmutabilityReplace() throws Exception {
List<Integer> intList = List.of(1, 2, 3);
intList.replaceAll(n -> -n);
}
@Test(expected = UnsupportedOperationException.class)
public void showImmutabilitySet() throws Exception {
List<Integer> intList = List.of(1, 2, 3);
intList.set(0, 99);
}
然而,如果列表包含的对象本身是可变的,那么列表也可能发生变化。为说明这个问题,我们创建一个名为 Holder
的类。这个类很简单,它包含一个可变值 x
,如例 10-12 所示。
例 10-12 包含可变值的简单类
public class Holder {
private int x;
public Holder(int x) { this.x = x; }
public void setX(int x) {
this.x = x;
}
public int getX() {
return x;
}
}
如果创建一个 Holder
实例的不可变列表,由于 Holder
包含的值是可变的,列表也会发生变化,如例 10-13 所示。
例 10-13 修改包装的整数
@Test
public void areWeImmutableOrArentWe() throws Exception {
List<Holder> holders = List.of(new Holder(1), new Holder(2)); ➊
assertEquals(1, holders.get(0).getX());
holders.get(0).setX(4); ➋
assertEquals(4, holders.get(0).getX());
}
❶ Holder
实例的不可变列表
❷ 修改 Holder
中包含的值
虽然上述代码可以运行且不违反文档规定,但有悖于开发所应遵循的最佳实践。换言之,如果计划使用不可变列表,应尽量在列表中包含不可变对象。
类似地,根据 Javadoc 的描述,可以使用各种 Set.of()
方法方便地创建不可变集合。通过这些方法创建的 Set
实例具有以下特征。
- 创建时(creation time)拒绝重复元素,传递给静态工厂方法的重复元素会导致
IllegalArgumentException
。 - 未指定集合元素的迭代顺序,因此它可能会发生变化。
所有 Set.of()
方法的签名与对应的 List.of()
方法相同,只不过返回的是 Set<E>
。
Map.of()
方法同样如此,但其签名传入交替的键和值作为参数,如例 10-14 所示。
例 10-14 用于创建不可变映射的静态工厂方法
static <K,V> Map<K,V> of()
static <K,V> Map<K,V> of(K k1, V v1)
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2)
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3)
static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4)
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)
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)
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)
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)
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)
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)
static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)
在创建包含最多 10 个条目的映射时,虽然可以使用相应的 Map.of()
方法(交替传入键和值),不过这并非最佳方案,因此 Map
接口还定义了另外两种静态方法,即 ofEntries
和 entry
:
static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)
static <K,V> Map.Entry<K,V> entry(K k, V v)
例 10-15 显示了如何利用上面介绍的各种方法创建不可变映射。
例 10-15 利用各种方法创建不可变映射
@Test
public void immutableMapFromEntries() throws Exception {
Map<String, String> jvmLanguages = Map.ofEntries( ➊
Map.entry("Java", "http://www.oracle.com/technetwork/java/index.html"),
Map.entry("Groovy", "http://groovy-lang.org/"),
Map.entry("Scala", "http://www.scala-lang.org/"),
Map.entry("Clojure", "https://clojure.org/"),
Map.entry("Kotlin", "http://kotlinlang.org/"));
Set<String> names = Set.of("Java", "Scala", "Groovy", "Clojure", "Kotlin");
List<String> urls = List.of("http://www.oracle.com/technetwork/java/index.html",
"http://groovy-lang.org/",
"http://www.scala-lang.org/",
"https://clojure.org/",
"http://kotlinlang.org/");
Set<String> keys = jvmLanguages.keySet();
Collection<String> values = jvmLanguages.values();
names.forEach(name -> assertTrue(keys.contains(name)));
urls.forEach(url -> assertTrue(values.contains(url)));
Map<String, String> javaMap = Map.of("Java", ➋
"http://www.oracle.com/technetwork/java/index.html",
"Groovy",
"http://groovy-lang.org/",
"Scala",
"http://www.scala-lang.org/",
"Clojure",
"https://clojure.org/",
"Kotlin",
"http://kotlinlang.org/");
javaMap.forEach((name, url) -> assertTrue(
jvmLanguages.keySet().contains(name) && \
jvmLanguages.values().contains(url)));
}
❶ 使用 Map.ofEntries
方法
❷ 使用 Map.of
方法
可以看到,将 ofEntries
与 entry
方法结合在一起使用有助于简化代码。