深度解读Java8-归约器Collector

本文详细介绍了Java8 Stream API中的归约操作,通过一个累加的例子,逐步剖析了Collector接口的设计原理,包括Supplier、BiConsumer、BinaryOperator和Function在归约过程中的作用,帮助读者理解如何利用Collector实现数据流的高效处理和转换。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java8提供的Stream API,将对数据流的所有操作,仅用三个步骤概括全了-过滤、转化、归约。其中,过滤、转化还比较容易理解,但是归约就是一个非常高级的抽象接口了,这篇博客从一个简单的累加例子出发,管中窥豹,带你彻底理解归约器。


何谓归约

归约,就是对中间操作(过滤,转换等)的结果进行收集归一化的步骤,当然也可以对归约结果进行再归约,这就是归约的嵌套了。中间操作不消耗流,归约会消耗流,而且只能消费一次,就像......把流都吃掉了。对于刚接触Stream API的人来说,这样的描述可能太抽象了,请看下面的例子
public class TestStream {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
        int[] sum = numbers.stream()
                .filter(i -> i > 0)
                .collect(() -> new int[]{0}, (a, b) -> a[0] += b, (s1, s2) -> s1[0] += s2[0]);
        System.out.println(sum[0]);
    }
}
这是一个简单的先过滤非正整数,然后对剩余元素求和的例子(这个例子纯粹是为了说明Collector原理所写,等你熟悉了Stream,你会有更好的实现方法)。你看,那么长一串数字流最后变成了一个数—被归约了,是不是很形象~~

Collector接口

collect里面需要传进去的是一个Collector接口,这就是我们今天的主角-归约器了,来看它的源码定义

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();

    Set<Characteristics> characteristics();
}
先强行记住上面四个方法(最后一个先不管),根据他们的名字(其实都很形象),看能不能与我下面的描述对应起来


由特殊到一般

我们来从头开始梳理计算从1-9累加和的步骤

1.首先你得提供一个用来接收每一步累加结果的变量,我们用A表示

2.你得确定你的累加变量的初始值是什么。如果我们把计算范围看作一个变量,那这一步就非常有必要了,我

现在给你的计算区间是[1,100),那如果我给你一个[0,0)这样一个区间呢,这个数据流是空的,但你同样要有一个

输出

3.确定你的操作。为了理解这个高级抽象接口的参数意义,我们不得不尽可能把一切都看成可变的,这里这个累加

的操作也是可变的,比如说我想求阶乘了

4.纯逻辑上的抽象已经我们已经做到极致了,但你还可以做的更完美,让我们上升到物理层面上去思考。假如

我想把这个工作分给三台计算机去做,另外一台计算机专门负责收集计算结果。先假设每一台计算机的累加结果

都用A表示?那么负责合并的计算机该怎么把所有的结果A合并起来,这也是个可变的操作

5.想一想还能有什么会是变化的。让我们接着上面的思路,汇总计算机把所有的计算结果都汇总好了,汇总的

结果还是一个A类型的。假设是累加的例子,那么它就是一个int,现在我想要的结果不是一个int了,我想知道

这个值是不是大于5000,那么结果就是一个boolean类型,所以我们还可以抽象出一个结果转换器,来对累加

结果进行转换,转换成我们想要的最终结果


你把上面的步骤,与API中的抽象方法都对应起来了吗?下面开始划重点啦(敲黑板^^^)

  • 对于步骤1和2,我们需要一个Supplier<A>,它可以创建一个用于归约过程的累加器,这个累加器的类型是A。再回到开头的那个例子,()->new int[]{0},这个lambda表达式就是一个累加器啦,它创建出一个数组用来接收累加的结果,并且给定初始值为0
  • 对于步骤3,我们需要一个BiConsumer<A,T>,这是一个二操作数的消费器,A是累加器,T是数据流中的每个元素的数据类型,可以看作,A把T累加到自己头上,这个T就是被消费了。(a,b)->a[0]+=b,就是一个BiConsumer
  • 对于步骤4,我们需要一个BinaryOperator<A>,它其实是一个BiFunction<T,U,R>,后者的作用是把T和U合并为一个R,所以BinaryOperator<A>的作用是,把两个A合并为一个A。(s1,s2)->s1[0]+=s2[0],就是一个BinaryOperator
  • 对于步骤5,我们需要一个Function<A,R>,这就是Stream API中最基础的一个转换器,它的作用是把A转换为R。开头的那个例子没有体现出来,这个Function可能是这样的,a[0]->a[0]>5000。可以把这个Function包装到Collector中,传递给collect方法,就可以实现转化了

至于API中的最后一个Set<Characteristics>是干嘛的,它定义了归约器的一些其他行为,比如流是否可以并行归约,是否可以直接把累加器类型作为最终返回值返回等


思维发散

你以为抽象到这里就结束了吗?其实中间还有更多细节是可以抽象的

比如说我想有个触发动作(一开始写成了触发器,但事实上触发器是指触发触发动作的触发条件),它的作用是,在累加的过程中,一旦发现有一个数据异常,就发出一封告警邮件。虽然事实上这个功能我们可以用现有的框架来实现,先对异常数据进行partitioningBy,然后交给下一个Stream去处理。但是如果我是想直接停止整个计算流程呢,这就还是得通过触发动作来做。虽然可能不会有这么无理的需求,但可能会一个无理的架构师根据需求只提出了这么一个无理的设计方案,作为执行者的你只能这样去执行

再比如说,我把工作分摊到每一台计算机的策略是什么,是平均分摊?还是先获取每一台计算机的硬件数据,根据它们的计算能力决定它们分配任务的多少?(这个可以放到底层实现去做,放到这个接口里似乎不合适,这里只是提供一个发散思维的思考方向)

PS:上面所说的细节抽象只是个人想法,不是标准做法,目的是为了激发读者的抽象思维,去理解这个东西为什么要这样抽象。你所需要的大部分需求,都能使用Java8提供的现有的Stream API去实现。除非不得已,否则不要进行盲目的优化,你可能会遭其反噬
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值