详解Java 8中Stream类型的“懒”加载
在进入正题之前,我们需要先引入Java 8中Stream类型的两个很重要的操作:
中间和终结操作(Intermediate and Terminal Operation)
Stream类型有两种类型的方法:
- 中间操作(Intermediate Operation)
- 终结操作(Terminal Operation)
官方文档给出的描述为[不想看字母的请直接跳过]:
Stream operations are divided into intermediate and terminal operations, and are combined to form stream pipelines. A stream pipeline consists of a source (such as a Collection, an array, a generator function, or an I/O channel); followed by zero or more intermediate operations such as Stream.filter or Stream.map; and a terminal operation such as Stream.forEach or Stream.reduce. Intermediate operations return a new stream. They are always lazy; executing an intermediate operation such as filter() does not actually perform any filtering, but instead creates a new stream that, when traversed, contains the elements of the initial stream that match the given predicate. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed. Terminal operations, such as Stream.forEach or IntStream.sum, may traverse the stream to produce a result or a side-effect. After the terminal operation is performed, the stream pipeline is considered consumed, and can no longer be used; if you need to traverse the same data source again, you must return to the data source to get a new stream. In almost all cases, terminal operations are eager, completing their traversal of the data source and processing of the pipeline before returning. Only the terminal operations iterator() and spliterator() are not; these are provided as an "escape hatch" to enable arbitrary client-controlled pipeline traversals in the event that the existing operations are not sufficient to the task. Processing streams lazily allows for significant efficiencies; in a pipeline such as the filter-map-sum example above, filtering, mapping, and summing can be fused into a single pass on the data, with minimal intermediate state. Laziness also allows avoiding examining all the data when it is not necessary; for operations such as "find the first string longer than 1000 characters", it is only necessary to examine just enough strings to find one that has the desired characteristics without examining all of the strings available from the source. (This behavior becomes even more important when the input stream is infinite and not merely large.)
其实看完这个官方文档,撸主整个人是很蒙圈的,给大家讲讲官方文档这段话到底说了些什么:
第一段:流操作分为中间操作和终结操作(我就这么翻译了啊),这两种操作外加数据源就构成了所谓的pipeline,处理管道。
第二段:说中间操作会返回一个流;中间操作是懒的(lazy,究竟怎么个懒法,我们后面会讲到);还拿filter举了个例子说,执行中间操作filter的时候实际上并没有进行任何的过滤操作,而是创建了一个新的流,这个新流包含啥呢?包含的是在遍历原来流(initial stream)过程中符合筛选条件的元素(很奇怪哎,这不明显是一个过滤操作吗?怎么说没有呢);要注意的是:中间操作在pipeline执行到终结操作之前是不会开始执行的(这将在我们后面的内容中讲到);
第三段:人家说了,终结操作是eager的,也就是说,执行到终结操作的时候我就要开始遍历数据源并且执行中间操作这个过程了,不会再去等谁了。而且一旦pipeline中的终结操作完成了,那么这个pipeline的使命就完成了,如果你还有新的终结操作,那么对不起,这个旧的pipeline就用不了了,你得新建一个stream,然后在造一遍轮子。这里有一句话我实在没弄明白什么意思啊,"
Only the terminal operations iterator() and spliterator() are not; these are provided as an "escape hatch" to enable arbitrary client-controlled pipeline traversals in the event that the existing operations are not sufficient to the task.
",还希望道友们帮忙解释一下,感激不尽!
第四段:夸了一下stream“懒”执行的好处:效率高。将中间操作融合在一起,使操作对对象的状态改变最小化;而且还能使我们避免一些没必要的工作,给了个例子:在一堆字符串里要找出第一个含超过1000个字符的字符串,通过stream operation的laziness那么我们就不用遍历全部元素了,只需执行能找出满足条件的元素的操作就行(其实这个需求不通过stream pipeline也能做到不是吗?);其实最重要的还是当面对一个无限数据源的操作时,它的不可替代性才体现了出来,因为经典java中collection是finite的,当然这个不是我们今天的目标,这里就不拓展开讲了。
愿文档后面还有一点内容,讲了中间操作有的是持有状态的(stateful),有的是无状态的(stateless),他们在对原数据的遍历上也有一些不同感兴趣的同学可自己去研究研究,我们今天主要还是看看中间操作是怎么个“懒”法以及这个“懒”的过程是怎么样的。
Stream之所以“懒”的秘密也在于每次在使用Stream时,都会连接多个中间操作,并在最后附上一个结束操作。 像map()和filter()这样的方法是中间操作,在调用它们时,会立即返回另一个Stream对象。而对于reduce()及findFirst()这样的方法,它们是终结操作,在调用它们时才会执行真正的操作来获取需要的值。
从一个例子出发:
比如,当我们需要打印出第一个长度为3的大写名字时:
public class LazyStreams {
private static int length(final String name) {
System.out.println("getting length for " + name);
return name.length();
}
private static String toUpper(final String name ) {
System.out.println("converting to uppercase: " + name);
return name.toUpperCase();
}
public static void main(final String[] args) {
List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe", "Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson"); final String firstNameWith3Letters = names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name))
.findFirst()
.get(); System.out.println(firstNameWith3Letters);
}
}
你可能认为以上的代码会对names集合进行很多操作,比如首先遍历一次集合得到长度为3的所有名字,再遍历一次filter得到的集合,将名字转换为大写。最后再从大写名字的集合中找到第一个并返回。这也是经典情况下Java Eager处理的角度。此时的处理顺序是这样的
对于Stream操作,更好的代码阅读顺序是从右到左,或者从下到上。每一个操作都只会做到恰到好处。如果以Eager的视角来阅读上述代码,它也许会执行15步操作:
可是实际情况并不是这样,不要忘了Stream可是非常“懒”的,它不会执行任何多余的操作。实际上,只有当findFirst方法被调用时,filter和map方法才会被真正触发。而filter也不会一口气对整个集合实现过滤,它会一个个的过滤,如果发现了符合条件的元素,会将该元素置入到下一个中间操作,也就是map方法中。所以实际的情况是这样的:
\
控制台的输出是这样的:
getting length for Brad
getting length for Kate
getting length for Kim
converting to uppercase: Kim
KIM
为了更好理解上述过程,我们将Lambda表达式换为经典的Java写法,即匿名内部类的形式:
final String firstNameWith3Letters = names.stream()
.filter(new Predicate<String>{
public boolean test(String name){
return length(name)==3;
}
})
.map(new Function<String,String>{
public String apply(String name){
return toUpper(name);
}
})
.findFirst()
.get();
执行的见下图:
很容易得出之前的结论:只有当findFirst方法被调用时,filter和map方法才会被真正触发。而filter也不会一口气对整个集合实现过滤,它会一个个的过滤,如果发现了符合条件的元素,会将该元素置入到下一个中间操作,也就是map方法中。
当终结操作获得了它需要的答案时,整个计算过程就结束了。如果没有获得到答案,那么它会要求中间操作对更多的集合元素进行计算,直到找到答案或者整个集合被处理完毕。
JDK会将所有的中间操作合并成一个,这个过程被称为熔断操作(Fusing Operation)。因此,在最坏的情况下(即集合中没有符合要求的元素),集合也只会被遍历一次,而不会像我们想象的那样执行了多次遍历,也许这就回答了官方文档中为什么说"Processing streams lazily allows for significant efficiencies"了。
为了看清楚在底层发生的事情,我们可以将以上对Stream的操作按照类型进行分割:
Stream<String> namesWith3Letters = names.stream()
.filter(name -> length(name) == 3)
.map(name -> toUpper(name)); System.out.println("Stream created, filtered, mapped...");
System.out.println("ready to call findFirst..."); final String firstNameWith3Letters = namesWith3Letters.findFirst().get(); System.out.println(firstNameWith3Letters);
// 输出结果
// Stream created, filtered, mapped...
// ready to call findFirst...
// getting length for Brad
// getting length for Kate
// getting length for Kim
// converting to uppercase: Kim
// KIM
根据输出的结果,我们可以发现在声明了Strema对象上的中间操作之后,中间操作并没有被执行。只有当真正发生了findFirst()调用之后,才会执行中间操作。
参考资料:
撸主比较懒,上文中的例子和前两张图来自于 CSDN 博主 dm_vincent 的博客《 [Java 8] (7) 利用Stream类型的"懒"操作 》
详解Java 8中Stream类型的“懒”加载的更多相关文章
- Java类中各种静态变量的加载顺序的学习
最近在补<thinking in java>...有一节提到了加载类需要做的一些准备...我照着书本敲了一下代码...同时稍微修改了一下书本上的代码.... package charpte ...
- Rest风格中关于JPA使用懒加载的坑
公司最近使用的ORM框架是JPA实现产品使用的是hibernate,曾经看过一篇博客上面说的是如果团队里面没有一个精通hibernate的人,那么最好不要使用它,我现在是深刻的体会到了.但是使用什么框 ...
- hibernate 中 fetch=FetchType.LAZY 懒加载失败处理
对这种懒加载问题,最后的做法是利用Spring提供的一个针对Hibernate的一个支持类,其主要意思是在发起一个页面请求时打开Hibernate的Session,一直保持这个Session,使得Hi ...
- 对于富文本编辑器中使用lazyload图片懒加载
使用lazyload.js图片懒加载的作用是给用户一个好的浏览体验,同时对服务器减轻了压力,当用户浏览到该图片的时候再对图片进行加载,项目中使用lazyload的时候需要将图片加入data-orgin ...
- 一文详解 Java 的八大基本类型
自从Java发布以来,基本数据类型就是Java语言中重要的一部分,本文就来详细介绍下每种基本类型的具体使用方法和限制. 作者 | Jeremy Grifski 译者 | 弯月,责编 | 郭芮 出品 | ...
- 详解 Java 17 中新推出的密封类
Java 17推出的新特性Sealed Classes经历了2个Preview版本(JDK 15中的JEP 360.JDK 16中的JEP 397),最终定稿于JDK 17中的JEP 409.Seal ...
- Java类中的各种成员的加载顺序
//执行顺序:(优先级从高到低.)静态代码块>mian方法>构造代码块>构造方法. 其中静态代码块只执行一次.构造代码块在每次创建对象是都会执行. 1 普通代码块 1 //普通代码块 ...
- 产品列表中使用v-lazyload插件懒加载img图片,但是当产品列表重新排序(人气,销量,价格...),产品info信息改变,但是 img 图片没有发生变化;
1.控制台查看 DOM 结构,发现 DOM 绑定的图片链接也没有发生变化: 2.查阅资料找到解决方法,只需要在 img 标签中增加 :key='imgUrl',即可实现 img 图片随数据排序的改变动 ...
- ios 懒加载详解
iOS开发之懒加载 在iOS开发中几乎经常用到懒加载技术,比如我们存放网络数据的数组,控制器的view,控件的自定义,复杂的运算逻辑等等情况下都会用到懒加载技术,那么什么是懒加载呢?? 他又有什么样的 ...
随机推荐
- mybatis generator.xml 配置 自动生成model,dao,mapping
generator.xml文件: <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE gener ...
- Ajax方式上传文件
用到两个对象 第一个对象:FormData 第二个对象:XMLHttpRequest 目前新版的Firefox 与 Chrome 等支持HTML5的浏览器完美的支持这两个对象,但IE9尚未支持 For ...
- 12月5日PHPCMS替换主页
cms替换主页的步骤 1.先做好静态页面: 2.在D:\wamp\www\phpcms\install_package\phpcms\templates文件夹下建新的文件夹tianqiwangluo( ...
- Python:基本语法1
I.Python中的转义符注意情况 如果'本身是一个字符,则可将其用" "括起来: 如果字符串内部既有',又有",则可用转义字符\,比如: 'I\'m\"OK\ ...
- 视图控制器的View整体上移问题
最近我朋友代码出现一个问题,我看了下,发现已经是适配iOS那时候的问题了 如果你准备将你的老的 iOS 6 app 迁移到 iOS 7 上,那么你必须注意了.当你的老的 app 在 iOS 7 设备上 ...
- JSP页面元素构成
1.page指令 <%@ page 属性1="" 属性2="" 属性3=""> <%@ page language=&qu ...
- PHP中::、->、self、$this操作符的区别
在访问PHP类中的成员变量或方法时,如果被引用的变量或者方法被声明成const(定义常量)或者static(声明静态),那么就必须使用操作符::,反之如果被引用的变量或者方法没有被声明成const或者 ...
- Network
App Icon: http://www.easyicon.net/
- easyui Datagrid查询报错Uncaught TypeError:Cannot read property 'length' of undefined
1.问题描述 easyui中datagrid执行loadData方法出现如下异常:Cannot read property 'length' of undefined 2.一开始怀疑是js或者页面的问 ...
- CSS 常用基本功能整理
1.文字访问特权 点击前.点击时.点击后.下划线.跳转. <!DOCTYPE html> <html> <head> <meta charset=" ...