详解Java 8中Stream类型的“懒”加载

详解Java 8中Stream类型的“懒”加载

在进入正题之前,我们需要先引入Java 8中Stream类型的两个很重要的操作:

中间和终结操作(Intermediate and Terminal Operation)
Stream类型有两种类型的方法:

中间操作(Intermediate Operation)
终结操作(Terminal Operation)
官方文档给出的描述为[不想看字母的请直接跳过]:

复制代码


```java
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的大写名字时:

复制代码


```java
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类型的"懒"操作 》
https://www.cnblogs.com/heimianshusheng/p/5674372.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 8 引入了一种新的抽象概念 Stream(流),它使得对数据的处理变得更加简便和高效。Stream 是一种来自数据源的元素队列并支持聚合操作。 Stream API 借助于lambda表达式,极大的提高了Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用fork/join并行方式来拆分任务和加速处理过程。 Stream 的特性可以归纳为: - 不是数据结构 - 没有存储元素 - 支持延迟计算 - 支持并行处理 Stream 的操作分为间操作和终止操作。间操作会返回一个新的 Stream,我们可以对这个新的 Stream 进行下一步的操作。终止操作则会返回一个最终的结果。 Stream 操作可以分为以下几种: - Intermediate(间)操作:一个流可以后面跟随零个或多个Intermediate操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。 - Terminal(终止)操作:一个流只能有一个 Terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。 Stream API 提供了大量的方法,可以用来完成各种不同的操作,如过滤、映射、筛选、查找、归约等等。这些方法可以分为以下几类: - 转换操作:map、flatMap、filter、distinct、sorted、peek、limit、skip - 聚合操作:forEach、reduce、collect - 匹配操作:allMatch、anyMatch、noneMatch - 查找操作:findFirst、findAny - 统计操作:count、min、max、average、sum Stream API 的使用可以大大简化代码,增加可读性和可维护性。同时,由于它的并行特性,可以有效地提升程序的性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值