简介
在《Java8 Stream 入门篇》讲述了Stream的基本使用与工作流程。在这篇中,我们将由浅入深探讨Stream的原理。
参考资料
Stream 原理
由一段简单的代码开始:
List<Integer> collect = list.stream()
.map(item -> Integer.parseInt(item)) //转换数据类型
.filter(item -> item < 4) //过滤数据
.map(item -> item + 1 ) //增加1
.collect(Collectors.toList()); //收集结果
创建Stream
在调用stream方法时发生了呢?
如上代码:集合通过StreamSupport.stream方法返回实现了Stream接口的Head对象。并调用集合的spliterator()生成一个Spliterator对象,将Spliterator对象存放在Head对象,使得集合作为Stream的数据源
Spliterator是1.8中集合框架中新增加的接口。作用是用于遍历集合中的数据。
与Iterator不同的是:使用Iterator是在集合外部遍历集合,使用Spliterator是在集合内部遍历。
啥意思呢?就是使用Iterator,开发人员需要在程序中使用循环代码如:for、foreach、while去遍历集合。Spliterator是集合内部实现了遍历,开发人员只需要关注处理元素的逻辑。如下:
好处是啥?隐藏低层细节
- 开发人员只关注处理元素的逻辑
- 性能问题,交由底层代码去优化
链式调用
Stream是如何实现链式调用的?类似的如StringBuffer的append方法实现链式调用是通过返回自身类型。Stream是否也是一样呢?看map方法和filter方法(在ReferencePipeline.java中):
代码中看出map中间操作是通过返回实现Stream接口的StatelessOp对象来完成链式调用。
Stream类图如下:(引用深入理解Java8中Stream的实现原理中的类图)
延迟执行
储存操作
Stream具有延迟执行的特性,要实现延迟执行,是不是应该把中间操作存储起来呢?Stream是如何储存操作的呢?
继续看map方法:(在ReferencePipeline.java中)
Stream并没有把map中间操作存放在一个数组或者集合中。而是放在StatelessOp对象中,用Sink接口对mapper操作进行封装,调用opWrapSink方法即可获取封装好mapper操作的Sink接口对象。
并不是所有的中间操作都放在StatelessOp对象中。在ReferencePipeline中对于操作的区分如下:(引用深入理解Java8中Stream的实现原理中的类图)
为了简述原理,接下来只对StatelessOp进行讲解。
关联操作
Stream没有用数组或集合去储存中间操作,是用StatelessOp对象储存的,那么这些StatelessOp对象是怎么关联的呢?
看StatelessOp对象的构造方法(在ReferencePipeline.java中)
由以下代码可以看出,在创建新的SatelessOp对象时,把当前AbstractPipeline对象的引用,作为新AbstractPipeline对象的上一个节点,并把当前AbstractPipeline对象中的下一个节点指向新创建的AbstractPipeline对象
如下图:Stream中的AbstractPipeline实现双向链表来关联中间操作,每次调用map、filter等方法就是在这个双向链表的尾部增加一个节点,StatelessOp是AbstractPipeline的子类
从上面我们可以得知,以AbstractPipeline为中心的类,是为了储存数据源与中间操作,如下图:
启动Stream
Stream在配置好结束操作后,开始整个流程的运行。那么Stream是怎么开始执行操作呢?执行操作后的结果怎么处理呢?
执行中间操作
在调用结束操作时,Stream做了哪些处理呢?以collect方法为例,如下:
示例代码是串行执行,所以会执行如上红圈中的代码。并行原理不在这篇文章中讲述
上图中的代码,在调用collect方法时,生成一个TerminalOp对象。
TerminialOp是一个接口。用于标识结束操作,由实现TerminialOp接口的对象发起对PipelineHelper(储存中间操作的对象)流程的执行
下图中,ReduceOps.makeRef方法返回了实现TerminialOp接口的ReduceOp对象,由ReduceOp对象调用PipelineHelper对象中的wrapAndCopyInto方法开始流程执行。
AbbstractPipeline类继承PiplelineHelper,并实现了wrapAndCopyInto方法。
上图代码中,在Stream启动时,做了两个件事。
-
获取中间操作
-
从AbstractPipleline对象形成的双向链表尾部向链表头部遍历
-
调用opWrapSink方法获取封装中间操作的Sink对象
-
opWrapSink方法在创建Sink对象时,把当前的Sink对象做为下一个Sink对象的下游(downStream)
-
循环遍历储放中间操作的双向链表,一直到Head节点停止遍历
Head节点的depth=0,且Head是存放数据源的地方,不保存中间操作。
-
遍历完成后,封装中间操作的Sink对象关联如下:
-
-
执行中间操作
-
Sink关联完成后,返回单向链表的第一个元素Sink1对象
-
spliterator对象是Stream的数据源,调用forEachReamining方法遍历数据源中元素,并将元素传给Sink1对象进行处理
-
元素从Sink1开始处理一直到Sink4,Sink中的accpet方法当自身处理完成后,会传给下一个Sink
-
收集结果
执行中间操作后的结果怎么收集呢?数据源中的元素经过处理后,**collect(Collectors.toList())**方法是怎么收集的?
Collectors.toList()方法返回的是一个CollectorImpl对象,包含了ArrayList对象,add方法。
继续看ReduceOps.makeRef方法,在发起PipelineHelper的wrapAndCopyInto调用时,ReduceOp生成了一个ReducingSink对象。并将ReducingSink作为Sink单向链表的最后一个节点。
在ReducingSink中,使用supplier对象获取到存放在Collector中的ArrayList对象,并存放到state属性中。
当流程执行中,元素从第一个Sink处理完后,并传到最后的ReducingSink,ReducingSink调用accumulator的accept方法,交由ArrayList对象的add方法向ArrayList对象中存放元素。
List::add方法只有一个参数,但Biconsuner接口中的accept方法有两个参数,这是因为编译器会去自动去推导方法类型匹配。当accept的第一个参数是List时,使用方法引用的List::add与accept的第一个参数类型List相同,所以accept可以忽略第一个参数,只去匹配剩余的参数数量与参数类型。
流程执行完后,获取结果
wrapAndCopyInto方法返回的是最后一个Sink对象。也就是ReducingSink对象,并调用ReducingSink对象的get方法,获取state属性,也就一开始设置在ReducingSink中的ArrayList对象。
至此,Stream流程就执行完了。并返回了执行后的结果。
操作之间调用(Sink接口协议)
Sink的作用:Sink接口是用来操作之间调用时的协议,统一的操作(中间操作与结束操作)调用的规范。
主要的方法:begin、end、accpet、cancellationRequested
begin : 在流程执行前,当前Sink要处理的内容
如:ReducingSink中获取Collector中的ArrayList对象,并存放在state属性中
end : 在流程执行后,当前Sink要处理的内容
如:RefSortingSink中将排序后的结果,继续遍历传给下游的Sink对象。因为,排序需要获取所有的元素,才能进行排序。
accpet : 在流程执行中,当前Sink要处理的内容
如:map操作与filter操作,元素在经过处理后,再传给下游。
cancellationRequested : 在每一次遍历后,判断是否停止流程
FindSink,找到第一个元素后,cancellationRequested方法将返回true,并停止流程。
有cancellationRequested的结束操作会走,下图的流程
总结
- 数据源和中间操作储存在ReferencePipeline对象形成的双向链表中
- 数据源存放在Head中
- 中间操作分别存放在StatelessOp(无状态)和StatefulOp(有状态)对象中
- 结束操作负责发起整个Stream的执行。启动过程中负责两件事
- 从ReferencePipeline双向链表中获取中间操作与数据源,并执行中间操作
- 生成结束操作的Sink,从链表尾部遍历,获取封装中间操作的Sink,并形成Sink单向链表
- 开启数据源遍历,并从第一个Sink的accept方法开始调用处理数据源中的元素
- 收集处理好的结果
- 生成的结束操作Sink用于接收中间操作处理完的结果
- 可以是用一个容器收集,也可以遍历结果给外部消费
- 如:collect(Collector.toList())、forEach(Consumer)
- 从ReferencePipeline双向链表中获取中间操作与数据源,并执行中间操作