归约(reduce),也称约简,顾名思义,是把一个流(Stream)中的元素聚合成一个值,能实现对集合求和、求乘积和求最值操作。实际上,终止操作sum()、max()、min()、count()等都是reduce操作,它们底层都是由reduce()实现的,将他们单独设为函数只是因为常用。
本文详解Java函数式编程中终止操作reduce()方法。
reduce()方法概述
reduce()方法可以实现从一组元素中归约生成一个值的操作。实际上,sum()、max()、min()、count()等都是reduce操作,将他们单独设为函数只是因为常用。reduce()的方法定义有三种重写形式。
一、reduce()方法的三种重载方法
在使用Stream的reduce方法时,发现该方法有三个重载方法,可分为: 一个参数、两个参数、三个参数的三种。
// 一个参数
Optional<T> reduce(BinaryOperator<T> accumulator);
// 两个参数
T reduce(T identity, BinaryOperator<T> accumulator);
// 三个参数
<U> U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner);
虽然方法定义一个比一个长,但其语义是一样的。后两个方法,第一个参数只是指定了初始值(参数identity);第三个参数指定了一个组合器(参数combiner),其作用是在并行处理场景对多个中间结果进行归并操作。
本文将对这三个重载方法的区别和使用场景进行探讨。
二、与reduce()方法的输入参数相关的函数接口介绍
这三个重载形式相关的两个函数接口:
1、双参数的BiFunction函数接口:可以输入2个参数,输出一个参数。
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
2、双参数的BinaryOperator 函数接口:
具备BiFunction 接口功能,还有2个静态方法 minBy 和 maxBy ,看方法名称作用是:获取最大值和最小值
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
}
public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
Objects.requireNonNull(comparator);
return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
}
}
三、stream的reduce()方法的三种重载方法的用法和使用实例
reduce()方法的功能是进行归约(聚合)操作。 最常用的是 累加、累减,求取最大值、最小值。
Stream类中有三种reduce,分别接受1个参数,2个参数,和3个参数,首先来看一个参数的情况:
- 一个参数的reduce 方法
一个参数reduce的方法签名:
Optional<T> reduce(BinaryOperator<T> accumulator);
该方法接受一个BinaryOperator参数accumulator,BinaryOperator是一个函数式接口,需要实现方法:
R apply(T t, U u);
但通常会用Lambda表达式或方法引用来代替参数accumulator。
Java中的Optional reduce(BinaryOperator accumulator)方法的作用是将流中的元素进行归约操作,最终返回一个Optional对象,表示可能存在的结果。
方法定义和参数
reduce(BinaryOperator accumulator)方法接受一个BinaryOperator类型的参数accumulator,该参数定义了元素之间的归约操作。这个方法没有初始值,它将流中的元素依次进行二元操作,最终返回一个Optional对象。
使用场景和示例
这个方法通常用于处理流中的元素,当流可能为空时,使用Optional来处理可避免空指针异常。例如:
单个参数示例:
// 求和
List<Integer> list = Arrays.asList(1,2,3);
Optional<Integer> result1=list.stream().reduce(Integer::sum);
// 求长度最长的单词
Stream<String> stream = Stream.of("World", "me", "you");
Optional<String> word = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional<String> word = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println("最长的单词:"+word.get());
单参数reduce(Integer::sum)的好处是:当列表list为空列表时,也能处理,不会出现异常。
- 两个参数的reduce的方法签名:
T reduce(T identity, BinaryOperator<T> accumulator);
这个方法接收两个参数:identity和accumulator。其中,参数identity是reduce的初始化值。
两个参数reduce的初始化值identity的规则: 两个参数reduce的初始化值identity有一定规则的,根据Java文档说明:
identity必须是accumulator函数的一个identity,
即必须满足条件:对于所有的t,都必须满足 accumulator.apply(identity, t) == t
只有满足此条件,顺序串行流与并行流才会有相同的归约计算结果。则stream和parallelStream计算出的结果是一致的。这就是identity的真正意义。
例如下所示:
List<Integer> list = Arrays.asList(1,2,3);
Integer result2= list.stream().reduce(0, Integer::sum);
当列表list有数据时,此式其处理结果与上面的单参数的结果是一样的。但当列表list为空时,会出现异常。
Reduce()归约操作与循环求和操作比较测试:
public static void loopAndReduce() {
// 函数式编程中规约操作reduce处理方式
int sum = IntStream.of(1,2,3,4,5,6,7,8).reduce(0, (v1, v2) -> v1 + v2);
System.out.println("reduce处理的数列之和: " + sum);
// 面向对象编程中循环求和的处理方式
sum = 0;
for (int i = 1; i < 9; i++) {
sum = sum + i;
}
System.out.println("循环处理的数列之和: " + sum);
}
其测试结果完全相同,只是处理方式不同。
更多的示例:
public static void streamReduceTwoTest() {
final List<Integer> list = Arrays.asList(1, 3, 5, 2, 4);
System.out.println("列表中元素:");
list.forEach( e-> System.out.print(" "+e) );
System.out.println();
// 2个参数,初值为0,一起累加 , 0+1+3+5+2+4
Integer reduce = list.stream().reduce(0, (x, y) -> x + y);
System.out.println("初始化0,reduce x+y ==>" + reduce);
// 累乘,初值为10, 10*1*3*5*2*4
Integer reduce1 = list.stream().reduce(10, (x, y) -> x * y);
System.out.println("【reduce 2个参数,初值为10,累积乘法】: " + reduce1);
// 最大值,初值为0
Integer reduce3 = list.stream().reduce(0, BinaryOperator.maxBy((x, y) -> x - y));
System.out.println("【reduce 2个参数,初值为0,最大值】: " +reduce3);
}
// 求单词长度之和
Stream<String> stream = Stream.of("World", "me", "you");
Integer len = stream.reduce(0,
(sum, str) -> sum+str.length(),
(a, b) -> a+b);
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println("单词长度之和:"+len);
在“求单词长度之和”示例中,累加器(sum, str) -> sum+str.length(), 实现了两个功能,1,计算单词长度;2,累加操作。如果想要使用map()和sum()组合来达到上述目的也可行:int lengthSum = stream.mapToInt(str -> str.length()).sum();
使用reduce()函数将这两步合二为一,有助于提升性能。
说明: Java的函数式编程,有串行流(顺序流,stream() )和并行流(parallelStream())两种模式。单参数和两个参数的reduce()方法,是顺序串行流专用的。
- 叁个参数的reduce的方法签名
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
和前面的方法不同的是,多了一个combiner。这个combiner是组合器,是使用并行流(parallelStream())时用来合并计算结果的。
注意: 当使用顺序流(又称串行流,stream() )计算时,combiner组合器是不会发挥作用的。
叁个参数reduce的初始化值identity的规则: 叁个参数reduce的初始化值identity 也有一定规则,根据Java文档说明:
同样地,identity需要满足 combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
大家可能注意到了,为什么accumulator的类型是BiFunction而,而combiner的类型是BinaryOperator?
public interface BinaryOperator<T> extends BiFunction<T,T,T>
BinaryOperator是BiFunction的子接口。BiFunction中定义了要实现的apply方法。
实际上,这两种类型的函数接口BiFunction和BinaryOperator非常类似,可能是为了区分,下面这个示例,我们用了同一个Lambda表达式。
List<Integer> list = Arrays.asList(1, 3, 5, 2, 4);
// 使用 并行流
Integer reduce2 = list.parallelStream().reduce(0, (x, y) -> x + y, (x, y) -> x + y);
System.out.println("Reduce三参数,并行行流:"+reduce2);
并行流parallelStream()叁参数reduce()方法处理示意图:
图中的“汇聚方式1”就是累加器accumulator,“汇聚方式2”则是组合器combiner。组合器combiner只有在并行流(多线程)中才有用。
reduce()方法使用综合实例
下面再来看几个示例
一个参数reduce的方法使用场景和示例
这个方法通常用于处理流中的元素,当流可能为空时,使用Optional来处理可能的空值情况。例如:
/***求最大值***/
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println); // 输出: 5
/***求累加值***/
List<Integer> list = Arrays.asList(2,3,5,8);
Optional<Integer> optional = list.stream().reduce((a, b) -> a + b);
System.out.println(optional.orElse(0));
Optional<Integer> optional1 = list.stream().reduce(Integer::sum);
System.out.println(optional1.orElse(0));
叁个参数reduce的方法在顺序流和并行流比较的示例
public static void streamReduceParallelTest() {
List<Integer> list = Arrays.asList(1, 3, 5, 2, 4);
System.out.println("列表中元素:");
list.forEach( e-> System.out.print(" "+e) );
System.out.println();
// 使用 顺序串行流
Integer reduce = list.stream().reduce(0, (x, y) -> x + y, (x, y) -> x * y);
System.out.println("Reduce三参数,顺序串行流:"+reduce);
// 使用 并行流
Integer reduce2 = list.parallelStream().reduce(0, (x, y) -> x + y, (x, y) -> x + y);
System.out.println("Reduce三参数,并行行流:"+reduce2);
}
上面的顺序串行流中使用了叁个参数reduce的方法,第三个参数combiner组合器是用不到的,实际上是被忽略的,只要能骗过编译器怎么写都行。这儿随便写了一个Lambda表达式:(x, y) -> x * y 实际并未发生作用。
reduce()方法使用的疑难问题:
前面提到:叁参数reduce()方法“当使用顺序流(又称串行流,stream() )计算时,combiner组合器是不会发挥作用的。”,那么是否据此可以得出结论:叁个参数reduce()方法是并行流专用的呢? 问题没有这么简单,我们来研究一个示例:
//示例 第一部分
List<Integer> list = Arrays.asList(1, 3, 5, 2, 4);
// 2个参数reduce(),初值为0 ,求和,方式一。
Integer reduce = list.stream().reduce(0, (x, y) -> x + y);
// 2个参数reduce(),初值为0 ,求和,方式二。
Integer reduce2 = list.stream().reduce(0, Integer::sum);
//示例 第二部分
//下面这个演示。是复杂性所在
List<String> strList = Arrays.asList("Hello", "Ok", "Java");
Integer lenSum1 = strList.stream().reduce(0, (sum,str)-> sum += str.length(),Integer::sum);
/***下面这行写法,无法通过编译***/
Integer lenSum2 = strList.stream().reduce(0, (sum,str)-> sum += str.length());
疑惑之处: 示例使用的都是顺序(串行)流(stream())。示例的第一部分符合常理,使用两个参数的reduce()。但是,示例的第二部分就有点复杂了,使用两个参数的reduce()无法通过编译,使用叁个参数的reduce()可正常运行。令人疑惑的是,顺序(串行)流(stream())中好像确实用不到第三个参数(combiner组合器),我们可以修改第三个参数,把“Integer::sum”改为“Integer::max”,程序结果不变。
参考文档: