写点什么

从原理剖析带你理解 Stream

  • 2022 年 9 月 01 日
    广东
  • 本文字数:2020 字

    阅读完需:约 7 分钟

从原理剖析带你理解Stream

本文分享自华为云社区《深入理解Stream之原理剖析》,作者: 李哥技术 。


Stream 是 jdk1.8 给我们提供的新特性,主要就是允许我们采用声明式的方式处理数据集合,我们要知道在项目中我们集合就是我们最常用的数据存储结构,有时后我们需要对集合内的元素做一些过滤或者其他的操作我们一般是采用 for 循环的方式。

流操作分类



Stream 中的操作可以分为两大类:中间操作与结束操作。


中间操作只会进行操作记录,只有结束操作才会触发实际的计算,可以理解为懒加载,这也是 Stream 在操作大对象迭代计算的时候如此高效的原因之一。


中间操作分为有状态操作与无状态操作,无状态是指元素的处理不受之前元素的影响,有状态是指该操作只有拿到所有元素之后才能继续下去。这也比较好理解,比如有状态的 distinct()去重方法,你说他能不关心其他值吗?当然不能,他必须拿到所有元素才知道当前迭代的元素是否被重复。


结束操作可以分为短路与非短路操作,这个应该很好理解,短路是指遇到某些符合条件的元素就可以得到最终结果;而非短路是指必须处理所有元素才能得到最终结果。


之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。

Stream 结构分析


让我们先简单看看下面一段代码:


 List<String> list = new ArrayList<>(); // 获取stream1 Stream<String> stream1 = list.stream(); // stream1通过filter后得到stream2 Stream<String> stream2 = stream1.filter("lige"::equals); // stream1与stream2是同一个对象吗? System.out.println("stream1.equals(stream2) = " + stream1.equals(stream2)); System.out.println("stream1.classTypeName = " + stream1.getClass().getTypeName()); System.out.println("stream2.classTypeName = " + stream2.getClass().getTypeName()); // 结果 // stream1.equals(stream2) = false // stream1.classTypeName = java.util.stream.ReferencePipeline$Head // stream1.classTypeName = java.util.stream.ReferencePipeline$2
复制代码


很明显,stream1 与 stream2 不是同一个对象,并且他们不是同一个实现类。stream1 的实现类为 ReferencePipeline$Head,而 stream2 的实现类为一个匿名内部类,让我们进步一分析其源码,所谓源码之下,无所遁形。





让我们再看看 stream2:








通过分析我们可以发现,stream2 的实现类是 StatelessOp,所以就形成了这样一个结构。



每一次中间操作都会生成一个新的 Stream,如果是无状态操作则实现类是 StatelessOp,如果是有状态操作则实现类是 StatefulOp。


让我们再来看一下他们之间的继承关系。



再聊核心 Sink


实际上 Stream API 内部实现的的本质,就是如何重载 Sink 的这四个接口方法。


我还是从一个示例开始:


List<String> list = new ArrayList<>();list.add("zhangsan");list.add("ligeligeligeligeligeligeligeligeligelige");list.add("lisilisilisilisilisilisilisilisi");list.add("wangwu");list.add("ligejishuligejishuligejishuligejishuligejishuligejishuligejishu");List<String> resultList = list.stream() .filter(it -> it.contains("li"))// 1. 只要包含li的数据 .filter(it -> it.contains("lige"))// 2. 只要包含lige的数据 .map(String::toUpperCase)// 3. 对符合的数据作进一步加工,转换大写 .map(String::toLowerCase)// 4. 对符合的数据作进一步加工,转换小写 .collect(Collectors.toList());resultList.forEach(System.out::println);
复制代码


不管是 filter 方法,还是 map 方法,还是其他的方法,我们进入到源码层面,返回了一个 StatelessOp 对象或 StatefulOp 对象。


所以便产生了这样一个结构:



但是和 Sink 有什么关系呢?我们再反过来看 filter 或者 map 源码:



直接返回一个匿名 StatelessOp 对象,实现 opWrapSink 方法,opWrapSink 方法是传入一个 sink 对象,返回另一个 sink 对象。而新的 sink 对象拥有传入 sink 对象的引用。


但是,这个代码有什么用?什么时候触发的呢?


别着急,让我们从 collect(Collectors.toList())方法开始一步一步深入研究。





这里我们需要知道传入 xx 方法的终端对象是 ReduceOp,并且这个 ReduceOp 对象在 makeSink 的时候返回了一个匿名内部类 ReducingSink 对象。





这里的 makeSink 我们提到过,返回一个匿名内部类 ReducingSink 对象。



先执行 warpSink,再执行 copyInto。直白一点就是先对 Sink 进行包装成链式 Sink,再遍历 Sink 链进行 copy 到结果对象里。这里的两个步骤都很核心。


先看 warpSink:



  1. 首次进入时,this 为最后的 Stream 对象,从尾部向头部遍历

  2. 每次遍历时,得到一个新的 Stream 对象,一般为 StatelessOp 对象或 StatefulOp 对象

  3. 执行操作对象的 opWrapSink 方法,这就是匿名实现了。

  4. 在每一个 opWrapSink 实现方法中,传入了上一个 sink,最终得到一个 sink 链表



最后,返回 Sink 链的头节点,内部称之为包装好的 sink,命名 wrapped,随后,准备进行执行 begin,forEachRemaining,end 方法。



forEachRemaning 最终调用接受方法。


动画理解 Stream 执行流程


 

点击关注,第一时间了解华为云新鲜技术~

发布于: 刚刚阅读数: 3
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
从原理剖析带你理解Stream_开发_华为云开发者联盟_InfoQ写作社区