Contents

Java 9 中 Collections 的新工厂方法

1. 概述

Java 9 带来了期待已久的语法糖,用于使用简洁的单行代码创建小型不可修改的*Collection实例。*根据JEP 269 ,新的便利工厂方法将包含在 JDK 9 中。 在本文中,我们将介绍它的用法以及实现细节。

2. 历史和动机

使用传统方式在 Java 中创建一个小的不可变Set非常冗长。 让我们以Set为例:

Set<String> set = new HashSet<>();
set.add("foo");
set.add("bar");
set.add("baz");
set = Collections.unmodifiableSet(set);

对于一个简单的任务来说,代码太多了,应该可以在一个表达式中完成。

上述情况也适用于Map。 但是,对于List,有一个工厂方法:

List<String> list = Arrays.asList("foo", "bar", "baz");

尽管这种List创建比构造函数初始化要好,但这不太明显 ,因为通常的直觉不会是查看Arrays类以获取创建List的方法: 还有其他减少冗长的方法,例如双括号初始化技术:

Set<String> set = Collections.unmodifiableSet(new HashSet<String>() {{
    add("foo"); add("bar"); add("baz");
}});

或使用 Java 8 Streams

Stream.of("foo", "bar", "baz")
  .collect(collectingAndThen(toSet(), Collections::unmodifiableSet));

双括号技术只是稍微不那么冗长,但大大降低了可读性(并且被认为是一种反模式)。 然而,Java 8 版本是一个单行表达式,它也有一些问题。首先,它并不明显和直观。其次,它仍然很冗长。第三,它涉及创建不必要的对象。第四,此方法不能用于创建Map。 总结缺点,上述方法都没有处理特定用例创建一个小的不可修改的Collection一流问题。

3. 说明与使用

ListSetMap接口提供了静态方法,它们将元素作为参数并分别返回ListSetMap的实例。 对于所有三个接口,此方法都命名为of(…)

3.1. ListSet

ListSet工厂方法的签名和特性是相同的:

static <E> List<E> of(E e1, E e2, E e3)
static <E> Set<E>  of(E e1, E e2, E e3)

方法的使用:

List<String> list = List.of("foo", "bar", "baz");
Set<String> set = Set.of("foo", "bar", "baz");

正如我们所看到的,它非常简单、简短、简洁。 在示例中,我们使用的方法恰好采用三个元素作为参数并返回大小为 3 的List / Set。 但是,这个方法有 12 个重载版本——11 个带有 0 到 10 个参数,一个带有 var-args:

static <E> List<E> of()
static <E> List<E> of(E e1)
static <E> List<E> of(E e1, E e2)
// ....and so on
static <E> List<E> of(E... elems)

对于大多数实际目的,10 个元素就足够了,但如果需要更多,可以使用 var-args 版本。 现在,我们可能会问,如果有一个 var-args 版本可以用于任意数量的元素,那么拥有 11 个额外的方法有什么意义。 答案是性能。每个 var-args 方法调用都会隐式创建一个数组。拥有重载的方法可以避免不必要的对象创建及其垃圾收集开销。 相反,  Arrays.asList总是创建隐式数组,因此当元素数量较少时效率较低。

在使用工厂方法创建Set期间,如果将重复元素作为参数传递,则在运行时抛出IllegalArgumentException

@Test(expected = IllegalArgumentException.class)
public void onDuplicateElem_IfIllegalArgExp_thenSuccess() {
    Set.of("foo", "bar", "baz", "foo");
}

这里要注意的重要一点是,由于工厂方法使用泛型,原始类型会自动装箱。 如果传递了原始类型的数组,则返回该原始类型的数组列表。 例如:

int[] arr = { 1, 2, 3, 4, 5 };
List<int[]> list = List.of(arr);

在这种情况下,返回大小为 1 的*List<int[]>*并且索引 0 处的元素包含该数组。

3.2. Map

Map工厂方法的签名是:

static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3)

和用法:

Map<String, String> map = Map.of("foo", "a", "bar", "b", "baz", "c");

ListSet类似,*of(…)*方法被重载为具有 0 到 10 个键值对。

对于Map,对于超过 10 个键值对有不同的方法:

static <K,V> Map<K,V> ofEntries(Map.Entry<? extends K,? extends V>... entries)

它的用法:

Map<String, String> map = Map.ofEntries(
  new AbstractMap.SimpleEntry<>("foo", "a"),
  new AbstractMap.SimpleEntry<>("bar", "b"),
  new AbstractMap.SimpleEntry<>("baz", "c"));

传递 Key 的重复值会抛出IllegalArgumentException

@Test(expected = IllegalArgumentException.class)
public void givenDuplicateKeys_ifIllegalArgExp_thenSuccess() {
    Map.of("foo", "a", "foo", "b");
}

同样,在Map的情况下,原始类型也是自动装箱的。

4. 实施说明

使用工厂方法创建的集合不是常用的实现。 例如,List不是ArrayList并且Map不是HashMap。这些是 Java 9 中引入的不同实现。这些实现是内部的,它们的构造函数具有受限的访问权限。 在本节中,我们将看到所有三种类型的集合共有的一些重要的实现差异。

4.1. 不可变

使用工厂方法创建的集合是不可变的,更改元素、添加新元素或删除元素会抛出UnsupportedOperationException

@Test(expected = UnsupportedOperationException.class)
public void onElemAdd_ifUnSupportedOpExpnThrown_thenSuccess() {
    Set<String> set = Set.of("foo", "bar");
    set.add("baz");
}
@Test(expected = UnsupportedOperationException.class)
public void onElemModify_ifUnSupportedOpExpnThrown_thenSuccess() {
    List<String> list = List.of("foo", "bar");
    list.set(0, "baz");
}
@Test(expected = UnsupportedOperationException.class)
public void onElemRemove_ifUnSupportedOpExpnThrown_thenSuccess() {
    Map<String, String> map = Map.of("foo", "a", "bar", "b");
    map.remove("foo");
}

4.2. 不允许空元素

对于ListSet,任何元素都不能为null。对于Map,键和值都不能为null。传递null参数会引发NullPointerException

@Test(expected = NullPointerException.class)
public void onNullElem_ifNullPtrExpnThrown_thenSuccess() {
    List.of("foo", "bar", null);
}

List.of 不同,Arrays.asList 方法接受 null 值。

4.3. 基于值的实例

工厂方法创建的实例是基于值的。这意味着工厂可以自由地创建新实例或返回现有实例。 因此,如果我们创建具有相同值的列表,它们可能会或可能不会引用堆上的相同对象:

List<String> list1 = List.of("foo", "bar");
List<String> list2 = List.of("foo", "bar");

在这种情况下,list1 == list2可能会或可能不会评估为true,具体取决于 JVM。

4.4. 序列化

如果集合的元素是可序列化的,则从工厂方法创建的集合是可序列化的。