Java8 Stream 原理篇

890 阅读7分钟

简介

在《Java8 Stream 入门篇》讲述了Stream的基本使用与工作流程。在这篇中,我们将由浅入深探讨Stream的原理。

参考资料

  1. 深入理解Java8中Stream的实现原理

Stream 原理

image-20210628134720553.png

由一段简单的代码开始:

        List<Integer> collect =  list.stream()
                .map(item -> Integer.parseInt(item))  //转换数据类型
                .filter(item -> item < 4)             //过滤数据
                .map(item -> item + 1 )	              //增加1
                .collect(Collectors.toList());        //收集结果

创建Stream

在调用stream方法时发生了呢?

image-20210722103808221.png

image-20210722103723121.png

如上代码:集合通过StreamSupport.stream方法返回实现了Stream接口的Head对象。并调用集合的spliterator()生成一个Spliterator对象,将Spliterator对象存放在Head对象,使得集合作为Stream数据源

Spliterator是1.8中集合框架中新增加的接口。作用是用于遍历集合中的数据。

与Iterator不同的是:使用Iterator是在集合外部遍历集合,使用Spliterator是在集合内部遍历。

啥意思呢?就是使用Iterator,开发人员需要在程序中使用循环代码如:for、foreach、while去遍历集合。Spliterator是集合内部实现了遍历,开发人员只需要关注处理元素的逻辑。如下:

image-20210722105130941.png

好处是啥?隐藏低层细节

  1. 开发人员只关注处理元素的逻辑
  2. 性能问题,交由底层代码去优化

链式调用

Stream是如何实现链式调用的?类似的如StringBuffer的append方法实现链式调用是通过返回自身类型。Stream是否也是一样呢?看map方法和filter方法(在ReferencePipeline.java中): image-20210722110840275.png 代码中看出map中间操作是通过返回实现Stream接口的StatelessOp对象来完成链式调用。

Stream类图如下:(引用深入理解Java8中Stream的实现原理中的类图)

image.png

延迟执行

储存操作

Stream具有延迟执行的特性,要实现延迟执行,是不是应该把中间操作存储起来呢?Stream是如何储存操作的呢?

继续看map方法:(在ReferencePipeline.java中)

image-20210712164226404.png

Stream并没有把map中间操作存放在一个数组或者集合中。而是放在StatelessOp对象中,用Sink接口mapper操作进行封装,调用opWrapSink方法即可获取封装好mapper操作Sink接口对象。

并不是所有的中间操作都放在StatelessOp对象中。在ReferencePipeline中对于操作的区分如下:(引用深入理解Java8中Stream的实现原理中的类图)

image.png 为了简述原理,接下来只对StatelessOp进行讲解。

关联操作

Stream没有用数组或集合去储存中间操作,是用StatelessOp对象储存的,那么这些StatelessOp对象是怎么关联的呢?

StatelessOp对象的构造方法(在ReferencePipeline.java中) image-20210722072531804.png

由以下代码可以看出,在创建新的SatelessOp对象时,把当前AbstractPipeline对象的引用,作为新AbstractPipeline对象的上一个节点,并把当前AbstractPipeline对象中的下一个节点指向新创建的AbstractPipeline对象

image-20210729113442307.png

如下图:Stream中的AbstractPipeline实现双向链表来关联中间操作,每次调用map、filter等方法就是在这个双向链表的尾部增加一个节点,StatelessOpAbstractPipeline的子类

image-20210723094822587.png

从上面我们可以得知,以AbstractPipeline为中心的类,是为了储存数据源中间操作,如下图:

image.png

启动Stream

Stream在配置好结束操作后,开始整个流程的运行。那么Stream是怎么开始执行操作呢?执行操作后的结果怎么处理呢?

执行中间操作

在调用结束操作时,Stream做了哪些处理呢?以collect方法为例,如下:

示例代码是串行执行,所以会执行如上红圈中的代码。并行原理不在这篇文章中讲述

image-20210723104546848.png 上图中的代码,在调用collect方法时,生成一个TerminalOp对象

TerminialOp是一个接口。用于标识结束操作,由实现TerminialOp接口的对象发起对PipelineHelper(储存中间操作的对象)流程的执行

image-20210723111202767.png

下图中,ReduceOps.makeRef方法返回了实现TerminialOp接口ReduceOp对象,由ReduceOp对象调用PipelineHelper对象中的wrapAndCopyInto方法开始流程执行。

image-20210723110507892.png

AbbstractPipeline类继承PiplelineHelper,并实现了wrapAndCopyInto方法。

image-20210723111848290.png 上图代码中,在Stream启动时,做了两个件事。

  1. 获取中间操作

    1. AbstractPipleline对象形成的双向链表尾部链表头部遍历

    2. 调用opWrapSink方法获取封装中间操作Sink对象

    3. opWrapSink方法在创建Sink对象时,把当前的Sink对象做为下一个Sink对象的下游(downStream)

    4. 循环遍历储放中间操作的双向链表,一直到Head节点停止遍历

      Head节点的depth=0,且Head是存放数据源的地方,不保存中间操作。

    5. 遍历完成后,封装中间操作Sink对象关联如下: image-20210729111539291.png

  2. 执行中间操作

    1. Sink关联完成后,返回单向链表的第一个元素Sink1对象

    2. spliterator对象是Stream的数据源,调用forEachReamining方法遍历数据源中元素,并将元素传给Sink1对象进行处理

    3. 元素从Sink1开始处理一直到Sink4,Sink中的accpet方法当自身处理完成后,会传给下一个Sink image-20210729111917608.png image-20210729112451054.png

收集结果

执行中间操作后的结果怎么收集呢?数据源中的元素经过处理后,**collect(Collectors.toList())**方法是怎么收集的?

Collectors.toList()方法返回的是一个CollectorImpl对象,包含了ArrayList对象,add方法。

image-20210802093505545.png

继续看ReduceOps.makeRef方法,在发起PipelineHelperwrapAndCopyInto调用时,ReduceOp生成了一个ReducingSink对象。并将ReducingSink作为Sink单向链表的最后一个节点。

image-20210802065427086.png

ReducingSink中,使用supplier对象获取到存放在Collector中的ArrayList对象,并存放到state属性中。

当流程执行中,元素从第一个Sink处理完后,并传到最后的ReducingSinkReducingSink调用accumulatoraccept方法,交由ArrayList对象的add方法向ArrayList对象中存放元素。

List::add方法只有一个参数,但Biconsuner接口中的accept方法有两个参数,这是因为编译器会去自动去推导方法类型匹配。当accept的第一个参数是List时,使用方法引用的List::add与accept的第一个参数类型List相同,所以accept可以忽略第一个参数,只去匹配剩余的参数数量与参数类型。

流程执行完后,获取结果 image-20210802102825824.png

wrapAndCopyInto方法返回的是最后一个Sink对象。也就是ReducingSink对象,并调用ReducingSink对象的get方法,获取state属性,也就一开始设置在ReducingSink中的ArrayList对象。

image-20210802103214747.png 至此,Stream流程就执行完了。并返回了执行后的结果。

操作之间调用(Sink接口协议)

Sink的作用:Sink接口是用来操作之间调用时的协议,统一的操作(中间操作与结束操作)调用的规范。

主要的方法:begin、end、accpet、cancellationRequested

begin : 在流程执行前,当前Sink要处理的内容

如:ReducingSink中获取Collector中的ArrayList对象,并存放在state属性中

image-20210802113932955.png

end : 在流程执行后,当前Sink要处理的内容

如:RefSortingSink中将排序后的结果,继续遍历传给下游的Sink对象。因为,排序需要获取所有的元素,才能进行排序。

image-20210802132724394.png

accpet : 在流程执行中,当前Sink要处理的内容

如:map操作与filter操作,元素在经过处理后,再传给下游。

image-20210802133157780.png

cancellationRequested : 在每一次遍历后,判断是否停止流程

FindSink,找到第一个元素后,cancellationRequested方法将返回true,并停止流程。

image-20210802132155383.png

有cancellationRequested的结束操作会走,下图的流程

image-20210802132116172.png

总结

  1. 数据源和中间操作储存在ReferencePipeline对象形成的双向链表中
    1. 数据源存放在Head中
    2. 中间操作分别存放在StatelessOp(无状态)和StatefulOp(有状态)对象中
  2. 结束操作负责发起整个Stream的执行。启动过程中负责两件事
    1. 从ReferencePipeline双向链表中获取中间操作与数据源,并执行中间操作
      1. 生成结束操作的Sink,从链表尾部遍历,获取封装中间操作的Sink,并形成Sink单向链表
      2. 开启数据源遍历,并从第一个Sink的accept方法开始调用处理数据源中的元素
    2. 收集处理好的结果
      1. 生成的结束操作Sink用于接收中间操作处理完的结果
      2. 可以是用一个容器收集,也可以遍历结果给外部消费
        1. 如:collect(Collector.toList())、forEach(Consumer)