Java函数式编程之【Stream终止操作】【中】【通用约简reduce】
一、归约 reduce 概述
归约reduce()方法是一种比较通用的终止操作,又称约简,顾名思义,是把一个流(Stream)中的元素聚合成一个值,能实现对集合求和、求乘积和求最值操作。约简reduce操作也可看作复杂的终止操作。
实际上,终止操作max()和min()都是reduce操作,其底层都是由reduce()实现的,将他们单独设为函数只是因为比较常用。
Java 8中Stream类的终止操作max()方法和min()方法的具体实现在java\util\stream\ReferencePipeline类中,它们是reduce()方法的特例之一。以下是它们的实现源代码:
@Overridepublic final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {return reduce(BinaryOperator.maxBy(comparator));} //max()方法的实现代码。@Overridepublic final Optional<P_OUT> min(Comparator<? super P_OUT> comparator) {return reduce(BinaryOperator.minBy(comparator));} //min()方法的实现代码。
reduce()约简操作是一种对Stream中元素进行迭代约简累积操作的算法,最终返回一个累积的结果。它有三种重载形式,可分为一个参数、两个参数和三个参数三种版本。下面分别进行介绍:
二、reduce()方法的三种重载方法
在使用Stream的reduce方法时,发现该方法有三个重载方法,可分为: 一个参数、两个参数、三个参数的三种。
- reduce(BinaryOperator): 单个参数约简方法,它有一个二元操作符参数作为累加器,从流中取元素进行累加,初始时从流中取第一个元素运算,然后用中间结果与后续取得的元素累加,最后返回结果用Optional类型进行包装,可避免空指针异常。其方法签名是:
Optional<T> reduce(BinaryOperator<T> accumulator)
它的方法体,实现的伪代码如下:
boolean foundAny = false;T result = null;for (T element : this stream) {if (!foundAny) {foundAny = true;result = element;} else result = accumulator.apply(result, element);}return foundAny ? Optional.of(result) : Optional.empty();
单参数的reduce()最后返回的结果用Optional<T>包装。如果约简结果为空值(null),此方法将返回Optional.empty()即空值,表示没有任何有效的值。
- reduce(identity, accumulator): 两个参数约简方法,第一个参数identity作为累加器的初值,从流中取元素进行累加,然后用中间结果与后续取得的元素累加,最后返回类型为T的累加结果。其方法签名是:
T reduce(T identity, BinaryOperator<T> accumulator)
它的方法体实现的伪代码如下:
T result = identity;for (T element : this stream)result = accumulator.apply(result, element)return result;
- reduce(identity,accumulator,combiner): 方法有三个参数,第一个参数identity作为累加器的初值,第二个参数是累加器accumulator,第三个参数是组合器combiner。三参数约简格式适用于顺序流及并行流,组合器参数combiner,其作用是在并行处理场景对多个中间结果进行归并操作。其方法签名是:
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
三种重载方法,虽然方法定义一个比一个长,但其语义是一样的。后两个方法,第一个参数只是指定了初始值(参数identity);第三个参数指定了一个组合器(参数combiner),其作用是在并行处理场景对多个中间结果进行归并操作。
reduce()方法初始化值identity必须遵守如下规则
在Java函数式编程中,reduce()方法的初始化值identity必须满足特定的数学性质,以确保并行计算和顺序计算能得到相同的结果。identity初始值,并非随意可以设定,它必须遵守以下规则:
1,两个参数初始值identity的规则:初始值identity 对于所有的 t 都必须满足以下条件
accumulator.apply(identity, t) ==t
2,叁个参数初始值identity的规则: 初始值identity 对于所有的 t 和 u,必须满足以下条件
//此规则基于accumulator.apply(identity, t) == t 且combiner是可结合的操作。combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
请看一个示例:整数 sum.apply(identity, t) == t; 其identity只能是0;
int identity=0,t=6;BinaryOperator<Integer> sum = (x, y) -> x + y;int result = sum.apply(identity, t);System.out.println("result="+result); //打印结果:result=6
reduce()方法中用到了两个函数接口BiFunction和BinaryOperator。
有关函数接口BiFunction和BinaryOperator的相关知识可参见:
Java 函数接口UnaryOperator和BinaryOperator介绍与示例【函数式编程】
Java 函数接口Function和BiFunction详解与示例【函数式编程】
Java 函数接口BiFunction与BinaryOperator简介与示例【函数式编程】
BiFunction接口有一个名为apply的抽象方法,该方法接受两个类型为T和U的参数,并返回一个类型为R的结果。其定义如下:
@FunctionalInterfacepublic interface BiFunction<T, U, R> {R apply(T t, U u);}
BinaryOperator是BiFunction<T,T,T> 的特例,接受两个相同类型的参数并返回相同类型的结果。
BinaryOperator是可结合的操作。实际应用算法中有很多可结合的操作,例如:求和、乘积、字符串连接、求最大值和最小值、求集的并与交等。
但是,减法操作是不可结合的。示例:(9-5)-2 ≠ 9-(5-2) 是不可结合的。
三、归约 reduce()方法的使用实例
【例程10-21】通用约简reduce()终止操作的演示例程
来看一些例程,由于要用到Student类,我们在上一篇博文“Java函数式编程之【Stream终止操作】【上】【简单约简】”的例程StreamTerminal.java基础上,可增加静态方法来演示通用约简reduce()终止操作的用法:
public static Stream<Student> crtStream() { return students.stream(); } //新增方法/***一个参数格式:reduce(BinaryOperator) 应用示例***/public static void reduceOneTest() { //单个参数reduceList<Integer> intList = Arrays.asList(1,2,3,4,5,6);Optional<Integer> sumOpn = intList.stream().reduce((x,y)->x+y);System.out.println("reduce((x,y)->x+y)="+sumOpn.orElse(0));sumOpn = intList.stream().reduce(Integer::sum); //用方法引用System.out.println("reduce( Integer::sum)="+sumOpn.orElse(0));// BigDecimal 类型,累加求和List<Integer> list = Arrays.asList(1,2,3,4,5,6);Optional<BigDecimal> d = list.stream().map(BigDecimal::new).reduce(BigDecimal::add);//字符串的"+"操作 //字符串拼接List<String> names = Arrays.asList("Bob","John","Mary");Optional<String> str = names.stream().reduce((s1,s2)->s1+s2);System.out.println("拼接="+str.orElse(""));/***学生分数汇总***/Optional<Integer> scoreSum = crtStream().map(Student::getScore).reduce(Integer::sum);System.out.println("学生分数汇总="+scoreSum.orElse(0));} //reduceOneTest()方法源码结束处。/***二个参数格式:reduce(t, BinaryOperator) 应用示例***/public static void reduceTwoTest() { List<Integer> iList = Arrays.asList(1,2,3,4,5,6);Integer sum = iList.stream().reduce(0, Integer::sum);System.out.println("reduce(0, Integer::sum)="+sum);// 求最大值,初值为0,Integer max1 = iList.stream().reduce(0, Integer::max);System.out.println("【reduce 2个参数,初值为0,最大值max1】: " + max1);/***字符串拼接处理演示,拼接email邮箱地址***/String emailStr = Stream.of("163", "com").reduce("qiugen@", (x, y) -> x.concat(".").concat(y));System.out.println("【email邮箱地址】: " + emailStr);} //reduceTwoTest()方法源码结束处。/***三个参数格式:reduce(u,accumulator,combiner)应用示例***/public static void reduceThreeTest() {/***字符串拼接处理演示***/List<String> sList = Arrays.asList("Hello", "Ok", "Java");String str = sList.stream().reduce("@", (x, y) -> x.concat("+").concat(y), (x, y) -> x);System.out.println("【初值'@',连接符'+' 】:"+str);/**格式reduce(u,accumulator,combiner) 常用于对象流、并行流,应用示例**/List<Integer> intList = Arrays.asList(1,2,3,4,5,6);int sum = intList.parallelStream().reduce(0, Integer::sum, Integer::sum);System.out.println("reduce(0, Integer::sum, Integer::sum)="+sum);} //reduceThreeTest()方法源码结束处。
大家可能注意到了,为什么reduce()方法的累加器 accumulator 的类型是BiFunction而,而组合器 combiner 的类型是BinaryOperator?
BinaryOperator是BiFunction<T,T,T> 的特例,接受两个相同类型的参数并返回相同类型的结果。
public interface BinaryOperator<T> extends BiFunction<T,T,T>
BinaryOperator是BiFunction的子接口。BiFunction中定义了需要实现的 apply() 方法。
实际上,这两种类型的函数接口BiFunction和BinaryOperator非常类似,但 BiFunction 应用范围更广。我们先来看一个简单示例,在本例中这二个接口好像没有区别,我们都用了同一个Lambda表达式。后面会有专门的例程讨论两者的区别。
List<Integer> list = Arrays.asList(1, 3, 5, 2, 4);// 使用 并行流Integer total = list.parallelStream().reduce(0, (x, y) -> x + y, (x, y) -> x + y);System.out.println("Reduce三参数,并行行流:"+ total);
并行流parallelStream()叁参数reduce()方法处理示意图:
图中的“汇聚方式1”就是表示累加器 accumulator,“汇聚方式2”则是表示组合器 combiner。组合器 combiner 一般只有在并行流(多线程)中才会用到。
说明:reduce()约简操作,也可用其他操作组合来实现。例如:对于“求所有单词长度之和”例子,我们使用两种不同实现方式来演示:(第二种方式用reduce()归约方法来收集结果,只需 1 次循环,效率更高。)
//使用map()和sum()组合实现(内循环需要2次)int lengthSum = Stream.of("World", "me", "you").mapToInt(str -> str.length()).sum();//使用reduce()终止方法实现,可合二为一(内循环只需1次),有助于提高效率Integer len = Stream.of("World", "me", "you").reduce(0, (sum, str) -> sum+str.length(), Integer::sum);
再来看一个例程
【例程10-22】使用约简reduce()操作实现员工最高工资演示例程MaxSalary
下面的员工最高工资演示例程MaxSalary.java也演示了两种不同处理方式:
例程中用到的Person类定义
package test; // 例程中用到的Person类定义
public class Person {private String name; // 姓名private int salary; // 薪资 public Person(String name, int salary) {this.name = name;this.salary = salary;} public int getSalary() { return salary; }
} // Person定义结束。
员工最高工资例程 MaxSalary的主程序
package test;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**** @author QiuGen* @description 员工最高工资例程 MaxSalary* @date 2025/5/12* ***/
public class MaxSalary {public static void main(String[] args) {List<Person> personList = new ArrayList<>();personList.add(new Person("Tom", 8900));personList.add(new Person("Jack", 6000));personList.add(new Person("Lily", 5800));// 求最高工资:Integer maxSalary = personList.stream().map(Person::getSalary).reduce(Integer::max).orElse(0);// 求最高工资2://BiFunction<Integer, Person, Integer> acc=(max, p) -> max > p.getSalary() ? max : p.getSalary();Integer maxSalary2 = personList.stream().reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(),Integer::max);System.out.println("最高工资:" + maxSalary + "," + maxSalary2);// 求工资之和方式1:Optional<Integer> sumSalary = personList.stream().map(Person::getSalary).reduce(Integer::sum);// 求工资之和方式2:Integer sumSalary2 = personList.stream().reduce(0, (sum, p) -> sum += p.getSalary(),Integer::sum);System.out.println("工资之和:" + sumSalary.get() + "," + sumSalary2 );}
} // 最高工资例程MaxSalary结束。
再来看一个借助reduce()生成谓词逻辑的花式 filter 过滤例程:
【例程10-23】使用约简reduce()操作实现花式过滤例程FancyPredocate
package stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**** @author QiuGen* @description 使用reduce()实现花式过滤例程FancyPredocate* @date 2025/6/18* ***/
public class FancyPredocate {public static void main(String[] args) {List<Predicate<String>> predList = new ArrayList<>();predList.add(str -> str.startsWith("A"));predList.add(str -> str.contains("a")); predList.add(str -> str.length() > 4);List<String> names = Arrays.asList("Adam","Alexander","Maryann");List<String> result = names.stream().filter(predList.stream().reduce(x->true, Predicate::and)).collect(Collectors.toList());result.forEach(System.out::println);}
}
叁参数reduce()方法的第三个参数组合器 combiner在顺序流中的作用
叁参数reduce()方法“当应用于顺序流stream()约简时,combiner组合器是不会发挥作用的。” 请看一个示例代码,例如:
//(x,y)->x);Integer len = Stream.of("World", "me", "you").reduce(0, (sum, str) -> sum+str.length(),Integer::sum );
上面这个代码的第三个参数只要参数的类型符合要求,第三个参数无论是Integer::sum 还是 (x,y)->x ,其约简结果都是相同的。combiner组合器好像没有发挥作用。
BinaryOperator是BiFunction<T,T,T> 的特例,接受两个相同类型的参数并返回相同类型的结果。
但 BiFunction应用范围更广,适用于数据转换、对象合并和复杂计算。有时候在顺序流中使用叁个参数reduce()方法,可实现两个参数reduce()方法无法解决的问题。我们来研究一个示例:
【例程10-24】约简reduce()叁参数和两参数用法比较例程ReduceProblem
package stream;
import java.util.stream.Stream;
/**** @author QiuGen* @description reduce疑难问题例程ReduceProblem* @date 2025/4/20* ***/
public class ReduceProblem { //reduce疑难问题例程public static void main(String[] args) {//功能一:字符串长度累加的两参数版本无法通过编译/******Integer totalLen = Stream.of("World", "me", "you").reduce(0, (sum,str)-> sum + str.length());******//***功能一:字符串长度累加。叁参数版本可通过编译***/ Integer len = Stream.of("World", "me", "you").reduce(0, (sum, str) -> sum+str.length(),Integer::sum ); //(x,y)->x);System.out.println("单词长度之和:"+len);/***功能二:字符串拼接处理。下面两个版本,都可通过编译***/String reduce_str = Stream.of("Mery", "Tom", "Bob").reduce("@", (x, y) -> x.concat(" @").concat(y));String reduceStr = Stream.of("Mery", "Tom", "Bob").reduce("@", (x, y) -> x.concat(" @").concat(y), (x, y) -> x+y);}
}
例程ReduceProblem分别用叁参数和两参数reduce演示实现两种功能。功能二的两个版本都能正常编译运行。但功能一的两参数版本(下面这行代码)编译无法通过。
Integer totalLen = Stream.of("World", "me", "you").reduce(0, (sum,str)-> sum + str.length());
编译出错原因: Stream类型中两参数的reduce只支持reduce(String identity, BinaryOperator accumulator),其不支持重载形式reduce(int identity, BinaryOperator accumulator)。因而导致源代码参数不正确(不匹配)错误。
而下面的reduce叁参数版本能正常编译执行。这说明 BiFunction应用范围更广,适用于数据转换、对象合并和复杂计算。
/***功能一:字符串长度累加。叁参数版本可通过编译***/ Integer len = Stream.of("World", "me", "you").reduce(0, (sum, str) -> sum+str.length(),Integer::sum ); //(x,y)->x);System.out.println("单词长度之和:"+len);