Java 创建不可变集合

Java 创建不可变集合,用户希望在 Java 9 中创建不可变列表、集合或映射,可以使用 Java 9 新增的静态工厂方法 List.ofSet.ofMap.of。根据 Javadoc 的描述,可以使用 Java 9 引入的各种 List.of() 静态工厂方法方便地创建不可变列表。通过这些方法创建的 List 实例具有以下特征。

Java 创建不可变集合 问题描述

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

Java 创建不可变集合 解决方案

使用 Java 9 新增的静态工厂方法 List.ofSet.ofMap.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 上调用任何常规的更改器方法。换言之,调用 addaddAllclearremoveremoveAllreplaceAll 以及 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 接口还定义了另外两种静态方法,即 ofEntriesentry

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 方法
可以看到,将 ofEntriesentry 方法结合在一起使用有助于简化代码。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程