1. 概述
在这个教程中,我们将探讨从流(Stream)获取列表的不同方法,并讨论它们之间的差异以及何时使用每种方法。
2. 将流元素收集到列表中
从流中获取列表是流管道中最常用的终止操作。在 Java 16 之前,我们通常会调用 Stream.collect()
方法,并传入一个 Collector
作为参数来收集元素到列表中。Collector
是通过调用 Collectors.toList()
方法创建的。
然而,对于直接从流实例获取列表的请求已有变更提案(JDK-8256441)。随着 Java 16 的发布,我们现在可以直接在流上调用 toList()
,这是一个新的方法,用于获取列表。像 StreamEx 这样的库也为直接从流获取列表提供了方便的方式。
我们可以使用以下方法将流元素积累到列表中:
-
Stream.collect(Collectors.toList())
:自 Java 8 起 -
Stream.collect([Collectors.toUnmodifiableList()](/java-stream-immutable-collection#1-using-javas-tounmodifiablelist))
:自 Java 10 起 -
Stream.toList()
:自 Java 16 起
我们将按照这些方法发布的顺序进行操作。
3. 分析列表
首先,我们将使用前一节中描述的方法创建列表,然后分析它们的属性。
我们将使用以下国家代码流作为所有示例的基础:
Stream.of(Locale.getISOCountries());
3.1. 创建列表
现在,我们将使用不同方法从给定的国家代码流创建一个列表。
首先,我们将使用 Collectors:toList()
创建一个列表:
List<String> result = Stream.of(Locale.getISOCountries()).collect(Collectors.toList());
然后,我们使用 Collectors.toUnmodifiableList()
进行收集:
List<String> result = Stream.of(Locale.getISOCountries()).collect(Collectors.toUnmodifiableList());
在这两种方法中,我们通过 Collector
接口将流积累到列表中,这会导致额外的分配和复制,因为我们不直接与流交互。
接下来,我们再次使用 Stream.toList()
进行收集:
List<String> result = Stream.of(Locale.getISOCountries()).toList();
在这里,我们直接从流中获取列表,从而避免了额外的分配和复制。
因此,直接在流上使用 toList()
相比其他两种调用方式更加简洁、整洁、方便且优化。
3.2. 检查累积的列表
首先,让我们检查我们创建的列表类型。
Collectors.toList()
将流元素收集到 ArrayList
中:
java.util.ArrayList
Collectors.toUnmodifiableList()
将流元素收集到不可变列表中:
java.util.ImmutableCollections.ListN
Stream.toList()
将元素收集到不可变列表中:
java.util.ImmutableCollections.ListN
尽管 Collectors.toList()
当前的实现会创建可变列表,但该方法的规范本身并未保证列表的类型、可变性、序列化或线程安全性。
另一方面,Collectors.toUnmodifiableList()
和 Stream.toList()
都会产生不可变列表。
这意味着我们可以对 Collectors.toList()
的元素执行添加和排序操作,但不能对 Collectors.toUnmodifiableList()
和 Stream.toList()
的元素执行此类操作。
3.3. 允许列表中的 null 元素
虽然 Stream.toList()
产生的列表是不可变的,但这并不等同于 Collectors.toUnmodifiableList()
。这是因为 Stream.toList()
允许 null 元素,而 Collectors.toUnmodifiableList()
不允许。然而,Collectors.toList()
是允许 null 元素的。
Collectors.toList()
在收集包含 null 元素的流时不会抛出异常:
Assertions.assertDoesNotThrow(() -> {
Stream.of(null,null).collect(Collectors.toList());
});
当收集包含 null 元素的流时,Collectors.toUnmodifiableList()
会抛出 NullPointerException
:
Assertions.assertThrows(NullPointerException.class, () -> {
Stream.of(null,null).collect(Collectors.toUnmodifiableList());
});
Stream.toList()
在尝试收集包含 null 元素的流时不会抛出 NullPointerException
:
Assertions.assertDoesNotThrow(() -> {
Stream.of(null,null).toList();
});
因此,在将代码从 Java 8 迁移到 Java 10 或 Java 16 时,这一点需要注意。我们不能盲目地用 Stream.toList()
替换 Collectors.toList()
或 Collectors.toUnmodifiableList()
。
3.4. 分析总结
下表总结了我们的分析结果:
4. 如何使用不同的 toList()
方法
添加 Stream.toList()
的主要目的是减少 Collector
API 的冗长性。
如前所见,使用 Collectors
方法获取列表非常冗长。相比之下,使用 Stream.toList()
方法可以使代码更简洁。
然而,如前几节所述,Stream.toList()
不能作为 Collectors.toList()
或 Collectors.toUnmodifiableList()
的快捷方式。
其次,Stream.toList()
使用更少的内存,因为其实现独立于 Collector
接口。它直接将流元素积累到列表中。所以如果我们知道流的大小,那么使用 Stream.toList()
将是最优选择。
我们也知道,流 API 只提供了 toList()
方法的实现。它并没有类似的方法来获取映射或集合。因此,如果我们想要统一的方式来获取任何转换器,如列表、映射或集合,我们将继续使用 Collector
API。这也将保持一致性并避免混淆。
最后,如果我们在 Java 16 以下的版本,我们必须继续使用 Collectors
方法。
下表总结了给定方法的最佳使用方式:
5. 结论
在这篇文章中,我们分析了从流获取列表的三种最流行方法。然后我们探讨了主要的差异和相似之处,并讨论了如何以及何时使用这些方法。
如往常一样,本文使用的示例代码可在 GitHub 上找到。