Stream原理与执行流程探析

简介: 本文简单讲述了Stream原理,并以一段比较简单常见的stream操作代码为例进行讲解。

大家都知道可以将Collection类转化成流(Stream)进行操作,代码变得简约流畅,写起来一气呵成。为什么流能支持这种流水线式工作模式,用以替代for循环呢?接下来让我们来简单探究下。


下面是一段比较简单常见的stream操作代码,经过映射与过滤操作后,最后得到的endList=["ab"],下文讲解都会以此代码为例。

List<String> startlist = Lists.newArrayList("a","b","c");
List<String> endList = startlist.stream().map(r->r+"b").filter(r->r.startsWith("a")).collect(Collectors.toList());

流的三大特点


当我们查阅资料时,得到了流有三大特点:


1.流并不存储元素。这些元素可能存储在底层的集合中,或者是按需生成。

2.流的操作不会修改其数据元素,而是生成一个新的流。

3.流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。

我们可从中捕捉到若干关键词,包括“不存储元素”,“惰性执行”等,这些含义希望读者看完流的运行流程后能找到答案。


流的运行流程


一段Stream代码的运行包括以下三部分:


1.搭建流水线,定义各阶段功能。

2.从终结点反向索引,生成操作实例Sink。

3.数据源送入流水线,经过各阶段处理后,生成结果。


整体类图介绍

image.png

在对原理进行介绍前,先对Stream整体类图进行介绍,帮助后续代码理解。


Stream是一个接口,它定义了对Stream的操作,主要可分为中间操作与终结操作,中间操作对流进行转化,比如对数据元素进行映射/过滤/排序等行为。终结操作启动流水线,获取结果数据。


AbstractPipline是一个抽象类,定义了流水线节点的常用属性,sourceStage指向流水线首节点,previousStage指向本节点上层节点,nextStage指向本节点下层节点,depth代表本节点处于流水线第几层(从0开始计数),sourceSpliterator指向数据源。


ReferencePipline实现Stream接口,继承AbstractPipline类,它主要对Stream中的各个操作进行实现,此外,它还定义了三个内部类Head/StatelessOp/StatefulOp。Head为流水线首节点,在集合转为流后,生成Head节点。StatelessOp为无状态操作,StatefulOp为有状态操作。无状态操作只对当前元素进行作用,比如filter操作只需判断“a”元素符不符合“startWith("a")”这个要求,无需在对“a”进行判断时关注数据源其他元素(“b”,“c”)的状态。有状态操作需要关注数据源中其他元素的状态,比如sorted操作要保留数据源其他元素,然后进行排序,生成新流。

image.png



搭建流水线

首先需要区分一个概念,Stream(流)并不是一个容器,不存储数据,它更像是一个个具有不同功能的流水线节点,可相互串联,容许数据源挨个通过,最后随着终结操作生成结果。Stream流水线搭建包括三个阶段:


1.创建一个流,如通过stream()产生Head,Head就是初始流,数据存储在Spliterator。

2.将初始流转换成其他流的中间操作,可能包含多个步骤,比如上面map与filter操作。

3.终止操作,用于产生结果,终结操作后,流也就走到了终点。

定义输入源HEAD

只有实现了Collection接口的类才能创建流,所以Map并不能创建流,List与Set这种单列集合才可创建流。上述代码使用stream()方法创建流,也可使用Stream.of()创建任何数量引元的流,或是Array.stream(array,from,to)从数组中from到to的位置创建输入源。

stream()运行结果

示例代码中使用stream()方法生成流,让我们看看生成的流中有哪些内容:

Stream<String> headStream =startlist.stream();

image.png

从运行结果来看,stream()方法生成了ReferencPipeline$Head类,ReferencPipeline是Stream的实现类,Head是ReferencePipline的内部类。其中sourceStage指向实例本身,depth=0代表Head是流水线首层,sourceSpliterator指向底层存储数据的集合,其中list即初始数据源。


stream()源码分析

image.png

spliterator()将“调用stream()方法的对象本身startlist”传入构造函数,生成Spliterator类,传入StreamSupport.stream()方法。

image.png

StreamSupport.stream()返回了ReferencPipeline$Head类。

image.png

一路追溯至AbstractPipline中,可看到使用sourceSpliterator指向数据源,sourceStage为Head实例本身,深度depth=0。

image.png

定义流水线中间节点

Map

map()运行结果

对数据进行映射,对每个元素后接"b"。

Stream<String> mapStream =startlist.stream().map(r->r+"b");

image.png

此时sourceStage与previousStage皆指向Head节点,depth变为1,表示为流水线第二节点,由于代码后续没接其他操作,所以nextStage为null。其中mapper代表函数式接口,指向lambda代码块,即“r->r+"b"”这个操作。

map()源码分析

image.png

map()方法是在ReferencePipline中被实现的,返回一个无状态操作StatelessOp,定义opWrapSink方法,运行时会将lambda代码块的内容替换apply方法,对数据元素u进行操作。opWrapSink方法将返回Sink对象,其用处将在下文讲解。downstream为opWrapSink的入参sink。

Filter

filter()运行结果

filter对元素进行过滤,只留存以“a”开头的数据元素。

Stream<String> filterStream =startlist.stream().map(r->r+"b").filter(r->r.startsWith("a"));

image.png

Filter阶段的depth再次+1,sourceStage指向Head,predict指向代码块:

“r->r.startsWith("a")”,previousStage指向前序Map节点,同时可见到Map节点中的nextStage开始指向Filter,形成双向链表。

filter()源码分析

image.png

filter()也是在ReferencePipline中被实现,返回一个无状态操作StatelessOp,实现opWrapSink方法,也是返回一个Sink,其中accept方法中的predicate.test="r->r.startsWith("a")",用以过滤符合要求的元素。downstream等于opWrapSink入参Sink。StatelessOp的基类AbstractPipline中有个构造方法帮助构造了双向链表。

image.png

定义终结操作

collect()运行结果

List<String> endList = startlist.stream().map(r->r+"b").filter(r->r.startsWith("a")).collect(Collectors.toList());

image.png

经过终结操作后,生成最终结果[“ab”]。

collect()源码分析

image.png

同样的,collect终结操作也在ReferencePipline中被实现。由于不是并行操作,只要关注evaluate()方法即可。


image.png


makeRef()方法中也有个类似opWrapSink一样返回Sink的方法,不过没有以其他Sink为输入,而是直接new一个ReducingSInk对象。


至此,我们可以根据源码绘出下图,使用双向链表连接各个流水线节点,并将每个阶段的lambda代码块存入Sink类中。数据源使用sourceSpliterator引用。

image.png

灰色表示还未生成Sink实例。


反向回溯生成操作实例

还记得前面说的“惰性执行”么,在一层一层搭建中间节点时,并未有任何结果产生,而在终结操作collect之后,生成最终结果endList,终结节点到底有什么魔力,让我们探究一下collect()方法中的evaluate方法。

image.png

这里调用了Collect中定义的makeSink()方法,输入终结节点生成的sink与数据源spliterator。

image.png

先来看wrapSink方法,在这个方法里,中间节点的opWrapSink方法将发挥大作用,它利用previousStage反向索引,后一个节点的sink送入前序节点的opWrapSink方法中做入参,也就是downstream,生成当前sink,再索引向前,生成套娃Sink。

image.png

最后索引到depth=1的Map节点,生成的结果Sink包含了depth2节点Filter与终结节点Collect的Sink。

image.png

红色框图表示Map节点的Sink,包含当前Stream与downstream(Filter节点Sink),黄色代表Filter节点Sink,downstream指向Collect节点。Sink被反向套娃实例化,一步步索引到Map节点,可以对图2进行完善。

image.png


启动流水线

一切准备就绪,就差把数据源冲入流水线。卷起来!在wrapSink方法套娃生成Sink之后,copyInto方法将数据源送入了流水线。

image.png

先是调用Sink中已定义好的begin方法,做些前序处理,Sink中的begin方法会不断调用下一个Sink的begin方法。随后对数据源中各个元素进行遍历,调用Sink中定义好的accept方法处理数据元素。accept执行的就是咱在每一节点定义的lambda代码块。

image.png

随后调用end方法做后序扫尾工作。

image.png

一个简单Stream整体关联图如上所示,最后调用get()方法生成结果。



总结


本文简单讲述了Stream原理,可以看到Stream流水线就是使用双向链表将各个节点串联而成,当最终节点不为终结操作,则不会产生任何数据结果,只有遇到终结操作,才会产生数据,这就是“惰性执行”。每跟随一个节点,则会产生一个新的Stream对象,这就是“流的操作不会改变原容器中的数据元素,而是产生新流”。


参考链接:


1.书籍:《Java核心技术卷II》


2.原来你是这样的Stream:https://zhuanlan.zhihu.com/p/47478339




来源  |  阿里云开发者公众号
作者  |  
始信




相关文章
|
3月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
143 29
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
3月前
|
监控 Java 测试技术
技术分享:设计依赖双父任务的子任务执行流程
在复杂的工作流和项目管理中,任务之间的依赖关系至关重要。当一个子任务需要等待两个或多个父任务同时完成后才能执行时,合理的设计和实现这一流程对于确保项目顺利推进至关重要。以下,我将从设计思路、技术实现、以及优化策略三个方面,分享如何在工作学习中有效处理这种依赖关系。
57 2
|
7月前
|
存储 安全 Java
【深度挖掘Java并发编程底层源码】「底层技术原理体系」带你零基础认识和分析学习相关的异步任务提交机制FutureTask的底层原理
【深度挖掘Java并发编程底层源码】「底层技术原理体系」带你零基础认识和分析学习相关的异步任务提交机制FutureTask的底层原理
48 0
|
5月前
|
Java Spring 容器
Java中套路和实现问题之基于SPI机制的套路有哪些关键点
Java中套路和实现问题之基于SPI机制的套路有哪些关键点
|
5月前
|
缓存 Java
浅析JAVA日志中的性能实践与原理解释问题之AsyncAppender的配置方式的问题是如何解决的
浅析JAVA日志中的性能实践与原理解释问题之AsyncAppender的配置方式的问题是如何解决的
|
7月前
|
前端开发 网络协议 Java
Netty | 工作流程图分析 & 核心组件说明 & 代码案例实践
Netty | 工作流程图分析 & 核心组件说明 & 代码案例实践
361 0
|
存储 算法 Java
JUC并发编程学习(十三)-学习Stream流的使用
JUC并发编程学习(十三)-学习Stream流的使用
JUC并发编程学习(十三)-学习Stream流的使用
|
算法 Java
【Java技术指南】「并发编程专题」Fork/Join框架基本使用和原理探究(原理篇)
【Java技术指南】「并发编程专题」Fork/Join框架基本使用和原理探究(原理篇)
163 0
【Java技术指南】「并发编程专题」Fork/Join框架基本使用和原理探究(原理篇)
|
NoSQL 网络协议 安全
0基础也看得懂的 I/O 多路复用解析(超详细案例)
IO多路复用目前在大厂的面试中,一般在两个地方可能会被问到,一个是在问到网络这一块的时候,另一个是在问到 Redis 这一块的时候,因为 Redis 底层也是使用了IO多路复用,所以整体来说 IO多路复用,也算是一道比较高频的一个面试题,所以今天跟大家来分享一下。
328 0
0基础也看得懂的 I/O 多路复用解析(超详细案例)
|
算法 Java Android开发
抽丝剥茧聊Kotlin协程之Job是如何建立结构化并发的双向传播机制关系的
抽丝剥茧聊Kotlin协程之Job是如何建立结构化并发的双向传播机制关系的
抽丝剥茧聊Kotlin协程之Job是如何建立结构化并发的双向传播机制关系的