Java函数式编程【三】【Stream终止操作】【中】之【reduce()方法】用法详解与疑惑

归约(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”,程序结果不变。

参考文档:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值