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 上找到。