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. 说明与使用
为List、Set和Map接口提供了静态方法,它们将元素作为参数并分别返回List、Set和Map的实例。 对于所有三个接口,此方法都命名为of(…)。
3.1. List和Set
List和Set工厂方法的签名和特性是相同的:
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");
与List和Set类似,*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. 不允许空元素
对于List和Set,任何元素都不能为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. 序列化
如果集合的元素是可序列化的,则从工厂方法创建的集合是可序列化的。