在 Java8 中,Stream API 提供了一系列聚合操作(aggregation operations),这些操作允许你对流中的元素执行计算并返回一个结果。聚合操作是终端操作(terminal operations),因为它们会消耗流并返回一个非流的结果。
本文将介绍 Java8 的 reduce 方法,它是 Stream API 中一个强大的归约操作,它允许你对流中的元素进行累积操作,并返回单一的结果。
reduce 方法有三种重载形式,每种形式都有其特定的用途:
(1)只带有累加器的 reduce:
Optional<T> reduce(BinaryOperator<T> accumulator)
这个方法只接受一个二元操作符 accumulator,它没有初始值。它将流中的元素依次进行二元操作,最终返回一个 Optional 对象,表示可能存在的结果(当流为空时,结果为空)。
(2)带有初始值和累加器的 reduce:
T reduce(T identity, BinaryOperator<T> accumulator)
这个方法接受一个初始值 identity 和一个二元操作符 accumulator。它使用初始值和流中的元素依次进行二元操作,将结果累积到最终的结果中,并返回最终结果。
(3)带有初始值、累加器和组合器的 reduce:
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
这个方法用于并行流的归约操作。它接受一个初始值 identity、一个累加器函数 accumulator 和一个组合器函数 combiner。在并行流中,流被分成多个子流并行处理,然后使用组合器函数将各个子流的结果合并成最终结果。
如果你希望对元素求和,或者以其他方式将流中的元素组合为一个值,你可以使用聚合方法。最简单的方式是使用一个二元函数,从流的前两个元素开始,不断将它应用到流中的其他元素上。为简单起见,我们以求和为例来看一下:
package com.hxstrive.jdk8.stream_api; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.stream.Stream; /** * reduce 方法 * @author hxstrive.com */ public class StreamReduceDemo { public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 获取流 Stream<Integer> stream = list.stream(); // 求和计算 Optional<Integer> sum = stream.reduce((x, y) -> x + y); sum.ifPresent(System.out::println); //输出: //55 } }
在这个例子中,聚合方法会计算 list[0]+ list[1] + list[2] + ... + list[n]。如果流为空,就无法产生有效的结果,那么该方法就会返回一个 Optional 值。
注意,上面例中,你可以使用 stream.reduce(Integer::sum) 来代替 stream.reduce((x,y) -> x+y),例如:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 获取流 Stream<Integer> stream = list.stream(); // 求和计算 Optional<Integer> sum = stream.reduce(Integer::sum); sum.ifPresent(System.out::println); //输出: //55
一般来说,如果聚合方法有一个聚合操作 op,那么该聚合会产生 v0 op v1 op v2 op ...,其中 v₁ op v+1 就表示我们编写的函数调用 op(v1, v+1)。该操作应该是联合的,即与你组合元素的顺序无关。在数学定义中,(x op y) op z = x op (v op z)。这样就允许通过并行流来进行有效的聚合。
在实践中,许多联合操作可能会变得非常有用,例如求和、求积、字符串追加、求最大值和最小值、求并集和交集等。注意,减法是一个非联合操作的例子,例如 (6 - 3) - 2 ≠ 6 - (3 - 2)。
通常,如果有一个标识 e 使得 e op x = x,那么你就可以使用该元素作为计算的起点。例如,对于相加来说,0 就是这个标识(0 + x = x)。因此聚合操作的第二种形式就是:
package com.hxstrive.jdk8.stream_api; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; /** * reduce 方法 * @author hxstrive.com */ public class StreamReduceDemo3 { public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 获取流 Stream<Integer> stream = list.stream(); // 求和计算 Integer sum = stream.reduce(0, (x,y) -> x + y); System.out.println(sum); //输出: //55 } }
上面例子中,当流为空时会返回标识值 0,这样你就不再需要处理 Optional 类了。
现在假设你有一个包含多个对象的流,并且希望对它们的某个属性进行求和,例如求一个流中所有字符串的总长度。这时,你无法使用聚合方法的简单形式,因为它需要一个函数 (T,T) -> T,其中参数和结果的类型要求是一样的。然而,当前这两个类型是不同的。流中元素的类型是 String,但是累加结果的类型是 Integer。对于这种情况,你应该使用聚合方法的另一种形式。
首先,你要提供一个“累加器”函数 (total, word) -> total + word.length()。该函数会被重复调用,形成累加值。但是当并行计算时,会产生多个累加值,因此你需要将它们再累加起来。为了做到这一点,你需要提供第二个函数。完整的调用应当如下所示:
package com.hxstrive.jdk8.stream_api; import java.util.Arrays; import java.util.List; import java.util.stream.Stream; /** * reduce 方法 * @author hxstrive.com */ public class StreamReduceDemo4 { public static void main(String[] args) { List<String> list = Arrays.asList("Alice", "Bob", "Charlie", "David"); // 获取流 Stream<String> stream = list.stream(); // 求和计算 Integer sum = stream.reduce(0, // 累加器,计算元素的累加值,即 "Alice".length() + "Bob".length() + ... (total, word) -> total + word.length(), // 如果是并行处理,将多个累加器的结果再次进行累加 (total1, total2) -> total1 + total2); System.out.println(sum); //输出: //20 } }
注意:在实际中,你可能不会大量地使用聚合方法。通常来说,更简单的方法是映射到一个数字流上,并使用它的方法之一来计算总和、最大值或者最小值。在这个特殊的示例中,你可以调用 words.mapToInt(String::length).sum(),由于它不会调用自动封箱和拆箱,所以效率更高,而且更简单。例如:
List<String> list = Arrays.asList("Alice", "Bob", "Charlie", "David"); // 获取流 Stream<String> stream = list.stream(); // 求和计算 IntStream intStream = stream.mapToInt(s -> s.length()); System.out.println(intStream.sum()); //输出: //20
注意:
(1)在并行流中使用 reduce 方法时,要注意线程安全性。确保你的累加器和组合器函数是线程安全的,或者在必要时使用同步机制来确保线程安全。
(2)如果你在并行流中使用带有初始值、累加器和组合器的 reduce 方法,确保你的组合器函数能够正确地将各个子流的结果合并成一个最终结果。组合器函数必须满足结合律,以确保无论子流如何组合,最终结果都是一致的。
(3)对于大数据集,归约操作可能会非常耗时。在使用 reduce 方法时,要考虑性能因素,并可能需要对数据进行适当的分区或批处理以提高性能。