系列中的上一篇
当前教程
在流上添加终止操作
系列中的下一篇

系列中的上一篇: 减少流

系列中的下一篇: 查找流的特征

在流上添加终止操作

 

避免使用 Reduce 方法

如果流没有以终止操作结束,它就不会处理任何数据。我们已经介绍了终止操作 reduce(),并且您在其他示例中看到了几个终止操作。现在让我们介绍您可以在流上使用的其他终止操作。

使用 reduce() 方法不是减少流的最简单方法。您需要确保提供的二元运算符是关联的,然后您需要知道它是否具有标识元素。您需要检查许多点以确保您的代码正确并产生您期望的结果。如果您能避免使用 reduce() 方法,那么您绝对应该这样做,因为它很容易出错。

幸运的是,Stream API 为您提供了许多其他减少流的方法:sum()min()max(),我们在介绍数字的专用流时介绍过,是您可以用来代替等效的 reduce() 调用的便捷方法。我们将在本部分介绍更多方法,您应该了解这些方法,以避免使用 reduce() 方法。实际上,您应该将此 reduce() 方法作为最后的手段,只有在没有其他解决方案时才使用。

 

计算流处理的元素

count() 方法存在于所有流接口中:在专用流和对象流中。它只是返回该流处理的元素数量,以 long 形式表示。这个数字可能很大,实际上大于 Integer.MAX_VALUE,因为它是一个 long。因此,流可以计算比您可以放入 ArrayList 中的更多对象。

您可能想知道为什么需要这么大的数字。实际上,您可以使用许多来源创建流,包括可以产生大量元素的来源,这些元素大于 Integer.MAX_VALUE。即使不是这种情况,也很容易创建一个中间操作,它会将流处理的元素数量乘以。我们之前在本教程中介绍过的 flatMap() 方法可以做到这一点。有很多方法会导致您最终处理的元素数量超过 Integer.MAX_VALUE。这就是 Stream API 支持它的原因。

以下是一个 count() 方法实际应用的示例。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");

long count =
        strings.stream()
                .filter(s -> s.length() == 3)
                .count();
System.out.println("count = " + count);

运行此代码将产生以下结果。

count = 4

 

逐个消费每个元素

Stream API 的 forEach() 方法允许您将流的每个元素传递给 Consumer 接口的实例。此方法对于打印流处理的元素非常方便。以下代码就是这么做的。

Stream<String> strings = Stream.of("one", "two", "three", "four");
strings.filter(s -> s.length() == 3)
       .map(String::toUpperCase)
       .forEach(System.out::println);

运行此代码将打印出以下内容。

ONE
TWO

此方法非常简单,您可能会倾向于在错误的用例中使用它。

请记住,您编写的 lambda 表达式应避免更改其外部范围。有时,更改外部状态称为进行副作用。消费者的案例很特殊,因为没有副作用的消费者不会为您做太多事情。实际上,调用 System.out.println() 会对应用程序的控制台产生副作用。

让我们考虑以下示例。

Stream<String> strings = Stream.of("one", "two", "three", "four");
List<String> result = new ArrayList<>();

strings.filter(s -> s.length() == 3)
       .map(String::toUpperCase)
       .forEach(result::add);

System.out.println("result = " + result);

运行前面的代码将打印出以下内容。

result = [ONE, TWO]

因此,您可能会倾向于使用此代码,因为它很简单,而且“可以正常工作”。好吧,此代码做了一些错误的事情。让我们逐一分析。

调用 result::add 会将该流处理的所有元素添加到外部 result 列表中,方法是在流内部更改该列表。此消费者正在对流范围之外的变量产生副作用。

访问这样的变量会使您的 lambda 表达式成为捕获 lambda 表达式。创建这样的 lambda 表达式是完全合法的;您只需要知道这样做会带来重大的性能损失。如果性能在您的应用程序中很重要,那么您应该避免编写捕获 lambda。

此外,这种编写方式会阻止您将此流并行化。此外,如果您尝试将此流并行化,这种消费元素的方式会有问题。如果您这样做,那么您将有多个线程同时访问您的结果列表。此列表是 ArrayList 的实例,而不是专门用于处理并发访问的类。

您有两种模式可以将流的元素存储在列表中。以下示例演示了第一种模式,它使用集合对象。第二种模式使用 Collector 对象,将在后面介绍。

Stream<String> strings = Stream.of("one", "two", "three", "four");

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toList());

此收集器创建 ArrayList 的实例,并将流处理的元素添加到其中。因此,此模式不会产生任何副作用,因此不会造成性能损失。

并行性和并发性由 Collector API 本身处理,因此您可以安全地将此流并行化。

此模式代码与前面的代码一样简单易读。它没有在消费者对象中创建副作用的任何缺点。这绝对是您应该在代码中使用的模式。

从 Java SE 16 开始,您有了第二种更简单的模式。

Stream<String> strings = Stream.of("one", "two", "three", "four");

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .toList();

此模式会生成 List 的特殊实例,该实例是不可修改的。如果您需要的是可修改的列表,则应坚持使用第一个收集器模式。它也可能比将流收集到 ArrayList 的实例中性能更好。这一点将在下一段中介绍。

 

将流元素收集到集合或数组中

Stream API 为您提供了多种将流处理的所有元素收集到集合中的方法。您在上一节中已经初识了其中两种模式。让我们看看其他模式。

在选择需要哪种模式之前,您需要问自己几个问题。

  • 您需要构建一个不可变的列表吗?
  • 您是否对 ArrayList 的实例感到满意?或者您更喜欢 LinkedList 的实例?
  • 您是否对流要处理的元素数量有精确的了解?
  • 您是否需要将元素收集到一个精确的、可能是第三方或自制的 List 实现中?

Stream API 可以处理所有这些情况。

收集到普通 ArrayList 中

您已经在之前的示例中使用过这种模式。它是您可以使用的最简单的模式,它将元素返回到 ArrayList 的实例中。

以下是一个此类模式在实际操作中的示例。

Stream<String> strings = Stream.of("one", "two", "three", "four");

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toList());

此模式创建一个简单的 ArrayList 实例,并在其中累积流中的元素。如果元素过多,以至于 ArrayList 的内部数组无法存储它们,那么当前数组将被复制到一个更大的数组中,并由垃圾收集器处理。

如果您想避免这种情况,并且您知道流将产生的元素数量,那么您可以使用 Collectors.toCollection() 收集器,它接受一个供应商作为参数来创建您将收集处理后的元素的集合。以下代码使用此模式来创建一个初始容量为 10,000 的 ArrayList 实例。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toCollection(() -> new ArrayList<>(10_000)));

收集到不可变列表中

在某些情况下,您需要将元素累积到一个不可变列表中。这听起来可能很矛盾,因为收集包括将元素添加到必须可变的容器中。实际上,这就是 Collector API 的工作方式,您将在本教程的后面部分详细了解。在此累积操作结束时,Collector API 可以继续进行最后一个可选操作,在本例中,该操作包括在返回列表之前对其进行密封。

为此,您只需要使用以下模式。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toUnmodifiableList()));

在此示例中,result 是一个不可变列表。

从 Java SE 16 开始,有一种更好的方法可以将数据收集到不可变列表中,在某些情况下它可能更有效。模式如下。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .toList();

它如何才能更有效?第一个模式建立在使用收集器的基础上,首先将元素收集到一个普通的 ArrayList 中,然后在处理完成后对其进行密封以使其不可变。您的代码看到的只是从这个 ArrayList 构建的不可变列表。

如您所知,ArrayList 的实例建立在一个具有固定大小的内部数组上。此数组可能会填满。在这种情况下,ArrayList 实现会检测到它并将它复制到一个更大的数组中。这种机制对客户端是透明的,但它会带来开销:复制此数组需要一些时间。

在某些情况下,Stream API 可以跟踪在消耗整个流之前要处理的元素数量。在这种情况下,创建正确大小的内部数组效率更高,因为它避免了将小数组复制到更大数组的开销。

这种优化已在 Stream.toList() 方法中实现,该方法已添加到 Java SE 16 中。如果您需要的是一个不可变列表,那么您应该使用此模式。

收集到自制列表中

如果您需要将数据收集到您自己的列表或 JDK 之外的第三方列表中,那么您可以使用 Collectors.toCollection() 模式。您用来调整 ArrayList 实例的初始大小的供应商也可以用来构建任何 Collection 实现,包括不在 JDK 中的实现。您只需要提供一个供应商。在以下示例中,我们提供了一个供应商来创建一个 LinkedList 实例。

Stream<String> strings = ...;

List<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toCollection(LinkedList::new));

收集到集合中

由于 Set 接口是 Collection 接口的扩展,您可以使用模式 Collectors.toCollection(HashSet::new) 将数据收集到 Set 的实例中。这很好,但 Collector API 仍然为您提供了一个更简洁的模式来做到这一点:Collectors.toSet()

Stream<String> strings = ...;

Set<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toSet());

您可能想知道这两种模式之间是否存在任何区别。答案是肯定的,存在细微的差别,您将在本教程的后面部分看到。

如果您需要的是一个不可变集合,Collector API 为您提供了另一种模式:Collectors.toUnmodifiableSet()

Stream<String> strings = ...;

Set<String> result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .collect(Collectors.toUnmodifiableSet());

收集到数组中

Stream API 还有一套自己的 toArray() 方法重载。其中有两个。

第一个是一个普通的 toArray() 方法,它返回一个 Object[] 实例。如果您的流的精确类型已知,那么如果您使用此模式,则该类型将丢失。

第二个接受一个类型为 IntFunction<A[]> 的参数。这种类型乍一看可能很吓人,但实际上编写此函数的实现非常容易。如果您需要构建一个字符字符串数组,那么此函数的实现是 String[]::new

Stream<String> strings = ...;

String[] result = 
    strings.filter(s -> s.length() == 3)
           .map(String::toUpperCase)
           .toArray(String[]::new);

System.out.println("result = " + Arrays.toString(result));

运行此代码将产生以下结果。

result = [ONE, TWO]

 

提取流的最大值和最小值

Stream API 为您提供了多种方法,具体取决于您当前正在使用的流。

我们已经介绍了来自专门数字流的 max()min() 方法:IntStreamLongStreamDoubleStream。您知道这些操作没有标识元素,因此您不应该对发现它们都返回可选对象感到惊讶。

顺便说一句,来自相同数字流的 average() 方法也返回一个可选对象,因为平均操作也没有标识元素。

Stream 接口还具有两个方法 max()min(),它们也返回一个可选对象。与对象流的区别在于,Stream 的元素实际上可以是任何类型。为了能够计算最大值或最小值,实现需要比较这些对象。这就是您需要为这些方法提供比较器的原因。

以下是在实际操作中的 max() 方法。

Stream<String> strings = Stream.of("one", "two", "three", "four");
String longest =
     strings.max(Comparator.comparing(String::length))
            .orElseThrow();
System.out.println("longest = " + longest);

它将在您的控制台中打印以下内容。

longest = three

请记住,尝试打开一个为空的可选对象会抛出一个NoSuchElementException,这在您的应用程序中是不希望看到的。这种情况只会在您的流没有要处理的数据时发生。在这个简单的例子中,您有一个流来处理多个字符字符串,没有过滤操作。这个流不可能为空,所以您可以安全地打开这个可选对象。

 

在流中查找元素

Stream API 提供了两种终端操作来查找元素:findFirst()findAny()。这两种方法都不接受任何参数,并返回流中的单个元素。为了正确处理空流的情况,此元素被包装在一个可选对象中。如果您的流为空,那么这个可选对象也是空的。

要理解返回哪个元素,您需要了解流可能是有序的。有序流只是一个元素顺序很重要并且由 Stream API 保持的流。默认情况下,在任何有序源(例如 List 接口的实现)上创建的流本身就是有序的。

在这样的流上,有第一个、第二个或第三个元素是有意义的。因此,查找此类流的第一个元素也是有意义的。

如果您的流不是有序的,或者如果顺序在您的流处理中丢失了,那么查找第一个元素是未定义的,调用 findFirst() 实际上会返回流中的任何元素。您将在本教程的后面看到有关有序流的更多详细信息。

请注意,调用 findFirst() 会在流实现中触发一些检查,以确保如果您流是有序的,您将获得该流的第一个元素。如果您的流是并行流,这可能很昂贵。在许多情况下,获取第一个找到的元素并不相关,包括您的流只处理单个元素的情况。在所有这些情况下,您应该使用 findAny() 而不是 findFirst()

让我们看看 findFirst() 的实际应用。

Collection<String> strings =
        List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");

String first =
    strings.stream()
           // .unordered()
           // .parallel()
           .filter(s -> s.length() == 3)
           .findFirst()
           .orElseThrow();

System.out.println("first = " + first);

此流是在 List 的实例上创建的,这使其成为一个有序流。请注意,两行 unordered()parallel() 在此第一个版本中已注释掉。

多次运行此代码将始终为您提供相同的结果。

first = one

中间方法调用 unordered() 将您的有序流变成一个无序流。在这种情况下,它没有任何区别,因为您的流是顺序处理的。您的数据是从一个始终以相同顺序遍历其元素的列表中提取的。用 findAny() 方法调用替换 findFirst() 方法调用也不会产生任何区别,原因相同。

您可以对这段代码进行的第一个修改是取消注释 parallel() 方法调用。现在您有一个有序流,以并行方式处理。多次运行此代码将始终为您提供相同的结果:one。这是因为您的流是有序的,所以第一个元素是定义的,无论您的流是如何处理的。

要使此流无序,您可以取消注释 unordered() 方法调用,或者用 Set.of() 替换 List.of()。在这两种情况下,用 findFirst() 终止您的流将从该并行流中返回一个随机元素。并行流的处理方式就是这样。

您可以对这段代码进行的第二个修改是,用 Set.of() 替换 List.of()。现在这个源不再是有序的了。此外,由 Set.of() 返回的实现使得集合元素的遍历以随机顺序发生。多次运行此代码表明,findFirst()findAny() 都返回一个随机的字符字符串,即使 unordered()parallel() 都已注释掉。查找无序源的第一个元素是未定义的,结果是随机的。

从这些例子中,您可以推断出在并行流的实现中采取了一些预防措施来跟踪哪个元素是第一个。这构成了开销,因此在这种情况下,您应该只在确实需要时才调用 findFirst()

 

检查流的元素是否与谓词匹配

在某些情况下,在流中查找元素或无法在流中找到任何元素可能是您真正需要做的。您找到的元素与您的应用程序无关;重要的是这个元素存在。

以下代码将用于检查给定元素是否存在。

boolean exists =
    strings.stream()
           .filter(s -> s.length() == 3)
           .findFirst()
           .isPresent();

实际上,这段代码检查返回的可选对象是否为空。

之前的模式运行良好,但 Stream API 提供了更有效的方式来做到这一点。实际上,构建这个可选对象是一种开销,如果您使用以下三种方法之一,您就不会支付这种开销。这三种方法都接受一个谓词作为参数。

让我们看看这些方法的实际应用。

Collection<String> strings =
    List.of("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten");

boolean noBlank  = 
        strings.stream()
               .allMatch(Predicate.not(String::isBlank));
boolean oneGT3   = 
        strings.stream()
               .anyMatch(s -> s.length() == 3);
boolean allLT10  = 
        strings.stream()
               .noneMatch(s -> s.length() > 10);
        
System.out.println("noBlank = " + noBlank);
System.out.println("oneGT3  = " + oneGT3);
System.out.println("allLT10 = " + allLT10);

运行此代码将产生以下结果。

noBlank = true
oneGT3  = true
allLT10 = true

 

短路处理流

您可能已经注意到我们在这里介绍的不同终端操作之间的一个重要区别。

其中一些需要处理您的流消耗的所有数据。这是COUNTMAXMINAVERAGE 操作以及 forEach()toList()toArray() 方法调用的情况。

我们介绍的最后一个终端操作并非如此。 findFirst()findAny() 方法将在找到元素后立即停止处理您的数据,无论还有多少元素要处理。 anyMatch()allMatch()noneMatch() 也是如此:它们可能会在没有消耗源可以产生的所有元素的情况下,以结果中断流的处理。

仍然有一些情况是这些最后的方法需要处理所有元素

  • 对于 findFirst()findAny(),只有在处理完所有元素后才能返回空可选对象。
  • 对于 anyMatch(),返回 false 也需要处理流中的所有元素。
  • 对于 allMatch()noneMatch(),返回 true 也需要处理流中的所有元素。

这些方法在 Stream API 中被称为短路方法,因为它们可以在不必处理流的所有元素的情况下产生结果。


最后更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
在流上添加终止操作
系列中的下一篇

系列中的上一篇: 减少流

系列中的下一篇: 查找流的特征