JDK9 中 List/Set/Map的工厂方法介绍(QFY译版)

介绍

JDK 9 在List、Set和Map接口增加了便捷的静态工厂方法,使您可以轻松创建不可变的 List、Set和Map。 如果对象在构造之后状态不能改变,则认为该对象是不可变的。在创建集合的不可变实例之后,只要存在对其的引用,它就保存相同的数据。 如果使用这些方法创建的集合包含不可变的对象,那么它们在构造之后自动是线程安全的。因为结构不需要支持修改,所以可以使得它们具有更高的空间效率。不可变集合实例通常比其可变集合实例消耗的内存少得多。 正如在“不可变性”中所讨论的,不可变集合可以包含可变对象,如果包含可变对象,则该集合可变且不是线程安全。

应用场景

不可变方法的常见用例是从已知值初始化集合,并且不做修改。如果数据不经常变化,也可以考虑使用这些方法。 为了获得最佳性能,不可变集合存储一个不变的数据集。然而,即使数据有变化,也可以利用性能和节省空间的优点。这些集合可能比可变集合提供更好的性能,即使您的数据偶尔发生更改。 如果有大量的值,可以考虑将它们存储在HashMap中。如果您不断地添加和删除条目,那么这是一个不错的选择。但是,如果您有一组从不改变、或很少改变的值,并且您从该组值中读的多,那么不可变 Map 是更有效的选择。如果数据集被频繁读取,并且值变化很少,那么即使算上销毁和重建,可能采用不可变 Map 总体速度更快。

语法

这些新集合的API很简单,特别是对于少量的元素。

不可变 List 静态工厂方法

静态工厂方法 List.of 提供了一种创建不可变列表的便捷方法。 列表是有序的集合,通常允许重复的元素。Null values are not allowed. 这些方法的语法是:

List.of()
List.of(e1)
List.of(e1, e2)         // fixed-argument form overloads up to 10 elements
List.of(elements...)   // varargs form supports an arbitrary number of elements or an array

例 3-1

JDK8 中:

List<String> stringList = Arrays.asList("a", "b", "c");
stringList = Collections.unmodifiableList(stringList);

JDK9 中:

List stringList = List.of("a", "b", "c");

不可变 Set 静态工厂方法

静态工厂方法 Set.of 提供了一种创建不可变 Set 的简便方法。 Set 是不包含重复元素的集合。如果检测到重复条目,则抛出IllegalArgumentException。不允许空值。 这些方法的语法是:

Set.of()
Set.of(e1)
Set.of(e1, e2)         // fixed-argument form overloads up to 10 elements
Set.of(elements...)   // varargs form supports an arbitrary number of elements or an array

示例3-2

在JDK 8中:

Set<String> stringSet = new HashSet<>(Arrays.asList("a", "b", "c"));
stringSet = Collections.unmodifiableSet(stringSet);

在JDK 9中:

Set<String> stringSet = Set.of("a", "b", "c");

不可变 Map 静态工厂方法

Map.of和Map.ofEntries静态工厂方法提供了创建Map的简便方法。 Map不能包含重复的键;每个键最多可以映射到一个值。如果检测到重复的键,则抛出IllegalArgumentException。Map 键和值都不能为 Null。 这些方法的语法是:

Map.of()
Map.of(k1, v1)
Map.of(k1, v1, k2, v2)    // fixed-argument form overloads up to 10 key-value pairs
Map.ofEntries(entry(k1, v1), entry(k2, v2),...)
 // varargs form supports an arbitrary number of Entry objects or an array

示例3-3

在JDK 8中:

Map<String, Integer> stringMap = new HashMap<String, Integer>();
stringMap.put("a", 1);
stringMap.put("b", 2);
stringMap.put("c", 3);
stringMap = Collections.unmodifiableMap(stringMap);

在JDK 9中:

Map stringMap = Map.of("a", 1, "b", 2, "c", 3);

Example 3-4 Map with Arbitrary Number of Pairs 如果有超过10个键-值对,那么使用Map.entry方法创建映射条目,并将这些对象传递给 Map.ofEntries 方法。例如:

import static java.util.Map.entry;
Map <Integer, String> friendMap = Map.ofEntries(
   entry(1, "Tom"),
   entry(2, "Dick"),
   entry(3, "Harry"),
   ...
   entry(99, "Mathilde"));

乱序遍历

Set元素和Map键的迭代顺序是随机的:它在不同JVM中可能不一样。这是故意的的——让你更容易发现依赖迭代顺序的代码。有时,对迭代顺序的依赖会不经意地蔓延到代码中,并导致难以调试的问题。 在 jshell 中 你可以看到重新启动jshell之前,迭代顺序是相同的。

jshell> Map stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {b=2, c=3, a=1}
jshell> Map stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {b=2, c=3, a=1}
jshell> /exit
|  Goodbye
C:\Program Files\Java\jdk-9\bin>jshell
|  Welcome to JShell -- Version 9-ea
|  For an introduction type: /help intro
jshell> Map stringMap = Map.of("a", 1, "b", 2, "c", 3);
stringMap ==> {a=1, b=2, c=3}

Set.of、Map.of和Map.ofEntries 方法创建的集合实例是乱序遍历的唯一实例。HashMap和HashSet 这些集合的遍历顺序没有改变。

关于不可变性

JDK 9中添加的便捷工厂方法返回的集合是不可变的。任何从这些集合中添加、设置或删除元素的尝试都会引发UnsupportedOperationException。 这些集合不是“不可变的持久”或“功能性”集合。如果您正在使用这些集合中的一个,那么您可以修改它,但是当您这样做,您将返回一个更新后新的集合,该集合可能共享第一个集合的结构。 不可变集合的一个优点是它是自动线程安全的。创建集合之后,可以将其交给多个线程,它们都将看到一致的视图。 然而,不可变集合对象与不可变对象集合不同。如果包含的元素是可变的,则这可能导致集合的行为不一致或使其内容看起来发生变化。 让我们看一个示例,其中不可变集合包含可变元素。在 jshell 中,用ArrayList类创建两个String对象列表,其中第二个列表是第一个列表的副本。删除了简单的jshell输出。

jshell> List<String> list1 = new ArrayList<>();
jshell> list1.add("a")
jshell> list1.add("b")
jshell> list1
list1 ==> [a, b]
jshell> List<String> list2 = new ArrayList<>(list1);
list2 ==> [a, b]
接下来,使用List.of方法,创建指向第一个列表的ilist1和ilist2。如果尝试修改ilist1,则会看到异常错误,因为ilist1是不可变的。任何修改尝试都会引发异常。
jshell> List<List<String>> ilist1 = List.of(list1, list1);
ilist1 ==> [[a, b], [a, b]]
jshell> List<List<String>> ilist2 = List.of(list2, list2);
ilist2 ==> [[a, b], [a, b]]
jshell> ilist1.add(new ArrayList<String>())
|  java.lang.UnsupportedOperationException thrown:
|        at ImmutableCollections.uoe (ImmutableCollections.java:70)
|        at ImmutableCollections$AbstractImmutableList.add (ImmutableCollections
.java:76)
|        at (#10:1)

But if you modify the original list1, ilist1 and ilist2 are no longer equal.

jshell> list1.add("c")
jshell> list1
list1 ==> [a, b, c]
jshell> ilist1
ilist1 ==> [[a, b, c], [a, b, c]]
jshell> ilist2
ilist2 ==> [[a, b], [a, b]]
jshell> ilist1.equals(ilist2)
$14 ==> false

不可变和不可修改不一样 不可变集合的行为与Collections.unmodifiable…​封装 相同。然而,这些集合不是封装——它们是由类实现的数据结构,其中任何修改数据的尝试都会引发异常。 如果创建List并将其传递给Collections.unmodifiableList方法,则会得到一个不可修改的视图。基础列表仍然是可修改的,对它的修改可以通过返回的List可见,因此它实际上不是不可变的。 为了演示此行为,创建一个List并将其传递给Collections.unmodifiableList。如果尝试直接添加到该List,则引发异常。

jshell> List<String> unmodlist1 = Collections.unmodifiableList(list1);
unmodlist1 ==> [a, b, c]
jshell> unmodlist1.add("d")
|  java.lang.UnsupportedOperationException thrown:
|        at Collections$UnmodifiableCollection.add (Collections.java:1056)
|        at (#17:1)

但是,如果您更改了原始的list1,则不会生成错误,并且unmodlist1列表已经修改。

jshell> list1.add("d")
$19 ==> true
jshell> list1
list1 ==> [a, b, c, d]
jshell> unmodlist1
unmodlist1 ==> [a, b, c, d]

空间效率

便利工厂方法返回的集合比它们的可变等价物具有更高的空间效率。 这些集合的所有实现都是隐藏在静态工厂方法背后的私有类。当调用它时,静态工厂方法根据大小选择实现类。数据可以存储在紧凑的基于字段或基于数组的布局中。 让我们看看两个替代实现所消耗的堆空间。首先,这里有一个不可修改的HashSet,它包含两个字符串:

Set<String> set = new HashSet<>(3);   // 3 buckets
set.add("silly");
set.add("string");
set = Collections.unmodifiableSet(set);

该集合包括六个对象:不可修改的包装器;包含HashMap的HashSet;桶表(数组)和两个Node实例(每个元素一个)。在典型的VM中,对于每个对象有12字节的报头,总开销为96字节+28*2=152字节。与存储的数据量相比,这是大量的开销。此外,对数据的访问不可避免地需要多个方法调用和指针取消引用。 相反,我们可以使用Set.of.

Set<String> set = Set.of("silly", "string");

因为这是基于字段的实现,所以该集合包含一个对象和两个字段。开销是20字节。新集合消耗较少的堆空间,无论是在固定开销方面,还是在每个元素的基础上。 不需要支持修改也有助于节省空间。此外,由于保存数据所需的对象更少,因此提高了引用的局部性。

译注

英文原文:Creating Immutable Lists, Sets, and Maps https://docs.oracle.com/javase/9/core/creating-immutable-lists-sets-and-maps.htm 译者: QunFanYi.com

数码
沪ICP备19006215号-4