整合stream

本文深入探讨了Java StreamAPI中的整合操作,特别是reduce()方法,强调了终端操作的重要性以及并行计算的注意事项。通过示例展示了如何使用BinaryOperator进行求和和求最大值,并解释了二元运算符需满足的结合律和标识元素属性。此外,还介绍了StreamAPI中不同重载的reduce()方法,包括如何处理没有标识元素的情况。
摘要由CSDN通过智能技术生成

整合stream

到现在为止,在本教程中看到的包括聚合数据在内的整合stream看起来像sql语言所做的一样。在示例中,你已使用了collect(Collectors.toList())来聚合元素。所有的这些操作称为终端操作,包括整合stream。
在整合stream 时有两点需要注意:

  • 没有终端操作的流不会处理任何数据,如果你的代码中使用了这样的操作,将会是一个bug。
  • 一个流有且只有一个中间调用或终端调用,如果你重用一个流,将会抛出IllegalStateException异常。

使用Binary Operator 整合流

stream接口有三个重载的reduce()方法。都是BinaryOperator
作为参数。
让我们看一个示例,假设有一列整数需要求和,使用传统的处理方式如下:

List<Integer> ints = List.of(3, 6, 2, 1);

int sum = ints.get(0);
for (int index = 1; index < ints.size(); index++) {
    sum += ints.get(index);
}
System.out.println("sum = " + sum);

输出:

sum = 12

这段代码所做的是如下的事情:

  • 取开始的两个元素并求和
  • 然后取下一个元素,把它加到你计算的部分和。
  • 重复这个步骤,直到取到列表的末尾
    这种计算的方式可以总结在下图中。
    求和stream元素
    如果你仔细查看代码,可以发现使用SUM 与一个 binary operator操作可以达到相同的结果。
List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;

int result = ints.get(0);
for (int index = 1; index < ints.size(); index++) {
    result = sum.apply(result, ints.get(index));
}
System.out.println("sum = " + result);

现在可以看到这段代码只依赖于binary operator。假设你需要去计算最大值,所要做的只是提供一个binary operator。

List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> max = (a, b) -> a > b ? a: b;

int result = ints.get(0);
for (int index = 1; index < ints.size(); index++) {
    result = max.apply(result, ints.get(index));
}
System.out.println("max = " + result);

由此得出的结论是,只要提供一个只对两个元素进行运算的binary operator,就可以进行整合运算。这就是reduce()方法在Stream API中的工作方式。

选择一个可以并行使用的binary operator

不过,有两点需要注意。让我们把第一个放在这里,第二个放在下一节。
第一个是流可以并行计算。这一点稍后将在本教程中详细讨论,但我们现在需要讨论它,因为它对这个二元运算符有影响。
这里会介绍Stream API是如何实现并行处理的。你的数据会被分成两个部分,每个部分独立处理。每个处理都是与之前使用的处理流程一样,使用的是binary operator。然后,当每个部分都处理好后,两个部分的额数据会使用binary operator合并。
这是这个计算如何处理的:
在stream中并行处理
处理一个数据流非常简单:只需在给定的stream上调用parallel()。
让我们检查一下底层是如何工作的,为此,您可以编写以下代码。你只是在模拟如何并行地进行计算。这当然是一个过于简化的平行流版本,只是为了解释事情是如何工作的。
创建一个以一个binary operator为参数的reduce()方法。使用它来整合一列整数。

int reduce(List<Integer> ints, BinaryOperator<Integer> sum) {
    int result = ints.get(0);
    for (int index = 1; index < ints.size(); index++) {
        result = sum.apply(result, ints.get(index));
    }
    return result;
}

List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;

int result1 = reduce(ints.subList(0, 2), sum);
int result2 = reduce(ints.subList(2, 4), sum);

int result = sum.apply(result1, result2);
System.out.println("sum = " + result);

为了明确起见,我们将源数据分成两部分,并将它们分别简化为两个整数:reduce1和reduce2。然后我们用相同的二元运算符合并这些结果。这基本上就是并行流的工作方式。
这段代码是非常简化的,它只是为了显示二进制运算符应该具有的一个非常特殊的属性。如何分割流中的元素不应该影响计算结果。所有以下的分割应该会给你相同的结果:

  • 3 + (6 + 2 + 1)
  • (3 + 6) + (2 + 1)
  • (3 + 6 + 2) + 1

这表明二元运算符应该具有一个众所周知的性质,即结合律。传递给reduce()方法的二元操作符应该是关联的。
Stream API中reduce()方法重载版本的JavaDoc API文档指出,作为参数提供的二进制操作符必须是关联的。
如果不是,会发生什么?这正是问题所在:它不会被编译器或Java运行时检测到。这样你的数据就不会有明显的错误。你可能得到正确的结果,也可能没有;这取决于数据在内部处理的方式。事实上,如果您多次运行代码,你可能会得到不同的结果。这是你需要注意的非常重要的一点。
怎么检测你的binary operator是否是关联的。在有些情况下可能是非常简单的,如SUM, MIN, MAX都是关联操作。在有些情况下,可能会变得很复杂。一种方法去检查那个属性就是使用binary operator运行随机数,并检验是否得到是相同的结果。如果不是,那么你的binary operator就不是关联的。如果是这样,那么,不幸的是,你不能得出可靠的结论。

管理具有任何标识元素的二进制操作符

第二个是二元运算符的结合律的结果。
这个结合律不该受到分隔方式的影响。如果你把一个集合a分成两个子集B和C,那么减少a应该会得到与减少B和减少C相同的结果。
可以将前面的属性写入更一般的表达式:
A = B ⋃ C ⇒ Red(A) = Red(Red(B), Red©)
结果是,它导致了另一个后果。假设代码运行的不顺利,B实际上是空的。在这种情况下,C = a。前面的表达式变成了:
Red(A) = Red(Red(∅), Red(A))
当且仅当空集的整合运算(∅)是整合运算操作的标识元素时为真。
这是数据处理中的一个一般的属性:空集的整合是整合操作的标识元素。
这确实是数据处理中的一个问题,尤其是在并行数据处理中,因为一些非常传统的整合二元运算符没有单位元素,如MIN和MAX。空集的最小元素没有定义,因为MIN操作没有单位元素。
这个问题必须处理,因为在流中会遇到处理空流的情况。你注意到可以创建空流,并且看到调用filter()可以过滤正在处理的流元素,因此需要返回一个没有任何内容需要处理的流。
在Stream API中所做的选择如下。标识元素未知的reduce(要么不存在,要么未提供)返回Optional类的实例。我们将在本教程的后面详细介绍这个类。此时你需要知道的是,这个Optional类是一个可以为空的包装类。每次在没有已知标识元素的流上调用终端操作时,stream API都会将结果包装到该对象中。如果您处理的流是空的,那么这个可选项也将是空的,并且将由您和您的应用程序决定如何处理这种情况。

探索Stream API的整合方法

正如前面提到的,stream Api有三个重载的reduce()方法。

使用单位元素整合方法

第一个函数接受一个identity元素和一个BinaryOperator实例作为参数。因为已知提供的第一个参数是二元运算符的标识元素,所以实现的时候可以使用它来简化计算。与使用流的前两个元素来开始处理流程不同,它不使用任何元素,而是从这个标识元素开始。使用的算法有以下形式。

List<Integer> ints = List.of(3, 6, 2, 1);
BinaryOperator<Integer> sum = (a, b) -> a + b;
int identity = 0;

int result = identity;
for (int i: ints) {
    result = sum.apply(result, i);
}

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

可以看到,即使需要处理的列表是空的,这种编写内容的方式也能很好地工作。在这种情况下,它将返回identity元素,这正是所需要的。
API不会检查您提供的元素是否确实是二进制操作符的identity元素。如果提供的元素不是这样的,将返回一个错误的结果。
看如下的代码:

Stream<Integer> ints = Stream.of(0, 0, 0, 0);

int sum = ints.reduce(10, (a, b) -> a + b);
System.out.println("sum = " + sum);

您可能希望这段代码在控制台上打印值0。因为reduce()方法调用的第一个参数不是二元操作符的标识元素,所以结果实际上是错误的。运行此代码将在控制台上打印以下内容。

sum = 10

下面这个是正确的做法:

Stream<Integer> ints = Stream.of(0, 0, 0, 0);

int sum = ints.reduce(0, (a, b) -> a + b);
System.out.println("sum = " + sum);

这个示例向您展示,在编译或运行代码时,传递错误的标识元素不会触发任何错误或异常。要确保传递的对象确实是二元运算符的identity元素,这实际由开发人员决定。
测试此属性的方法与测试关联属性的方法相同。将候选的标识元素与尽可能多的值结合起来。如果你发现一个因组合而改变的候选标识元素,那么你的候选标识元素就不是正确的。不幸的是,如果你找不到任何错误的组合,并不一定意味着你的候选标识元素是正确的。

不用标识元素的整合

reduce()方法的第二个重载只接受一个BinaryOperator实例,没有标识元素。正如预期的那样,它返回一个Optional 对象,包装整合的结果。对于Optional 对象你可以做的最简单的事情就是打开看看是否有内容在里面。
示例如下:

Stream<Integer> ints = Stream.of(2, 8, 1, 5, 3);
Optional<Integer> optional = ints.reduce((i1, i2) -> i1 > i2 ? i1: i2);

if (optional.isPresent()) {
    System.out.println("result = " + optional.orElseThrow());
} else {
    System.out.println("No result could be computed");
}

输出:

result = 8

注意,这段代码使用orElseThrow()方法打开Optional ,现在这是首选的方法。Java SE 10中添加了这个模式,以取代最初在Java SE 8中引入的更传统的get()方法。
这个get()方法的问题是,在Optional 为空的情况下,它可能抛出NoSuchElementException。将此方法命名为orElseThrow()比get()更合适,因为它会提醒您,如果您试图打开一个空的可选选项,将会抛出一个异常。
使用Optional 还可以完成更多的事情,您将在本教程的后面了解到这一点。

在一个方法中融合映射和整合

第三个比较复杂。它结合了一个内部映射和一个带有几个参数的整合。
让我们检查一下这种方法的签名。

<U> U reduce(U identity,
             BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner);

该方法使用的一个参数类型为U,该类型在该方法的局部定义并由二元运算符使用。二元操作符的工作方式与reduce()方法前面的重载方法相同,只是它不应用于流中的元素,而仅仅应用于它们的映射版本。
这个映射和reduce本身实际上合并为一个操作:累加器。在本部分开始时,您看到整合是以增量方式进行的,并且每次消费一个元素。在每一点上,整合操作的第一个参数是对到目前为止所消耗的所有元素的整合。
标识元素是结合器的标识元素。
假设你有一个string实例的stream,需要计算所有string的长度之和。
结合器结合两个整数:到目前为止字符串的长度的部分和。所以标识元素是0.
累加器从stream中取一个元素,将它映射为整数,然后加入到目前已经计算的额部分和中。
融合映射和整合

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

BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;
BiFunction<Integer, String, Integer> accumulator =
        (partialReduction, element) -> partialReduction + element.length();

int result = strings.reduce(0, accumulator, combiner);
System.out.println("sum = " + result);

输出:

sum = 15

上面的映射可以简写为:

Function<String, Integer> mapper = String::length;

根据这种模式可以重写累加器。

Function<String, Integer> mapper = String::length;
BinaryOperator<Integer> combiner = (length1, length2) -> length1 + length2;

BiFunction<Integer, String, Integer> accumulator =
        (partialReduction, element) -> partialReduction + mapper.apply(element);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值