关于Java8 流“只遍历一次”的理解

关于Java8 流“只遍历一次”的理解

前言

先贴上一篇整理的很到位的对流的基本介绍,没有接触过流的同学可以通过下文快速地对流有一个概念,在此就不再做多余的文字搬运工作
链接:Java8新特性——StreamAPI(一) - 大闲人柴毛毛 - CSDN博客

问题的产生

当时对流的特性之一:只遍历一次理解不够深入,导致在项目中出现了与预期相左的结果,现结合代码进行分析:

List<Map<String, Object>> test = publishList.stream().filter(it -> it != null
                && it.getChannelIds() != null
                && it.getChannelIds().contains(channel.getId()))
                .map(noticeRecord -> {
                    //执行了一些处理逻辑
                    System.out.println(noticeRecord.getId());
                }).filter(it -> it != null && !it.isEmpty()).findFirst().orElse(new ArrayList<>());

当时在对项目组曾经的旧项目重构代码写单元测试时发现了上述的一个方法,按照最开始我的理解,其流程为:将publishList中符合条件的元素通过filter筛选出来再通过map批量执行相关的操作逻辑(map中的具体逻辑不深究),最后再经过一遍filter,若结果集不为空则返回第一个,否则返回一个新的ArrayList避免空指针。
读懂了代码后开始写单元测试,可发现即使在第一个filter中允许有多个元素通过,但其在map中并不会都执行相应的逻辑,反复测试发现均只有一个元素做完了map中的方法。

当时真的是各种打断点,打log看是哪个逻辑出了问题,无果

最后把我能想到的风险点都排除了,问题依然没有解决,无论怎么设定数据,在map中始终只有一个数据得到处理,于是又想到为什么偏偏只有一个,或许和findFirst方法有关?bingo

关于“只遍历一次”的理解

下面对流“只遍历一次”的实现原理进行阐述:

一般来说Stream可分为三个部分:源source、中间操作Intermediate和终止操作Terminal。流的源可以是一个数组、一个集合、一个生成器方法,一个I/O通道等等。
一个流可以有零个和或者多个中间操作,每一个中间操作都会返回一个新的流,供下一个操作使用一个流只会有一个终止操作。中间操作都是惰性的,也就是说仅仅调用流的中间操作,其实并没有真正开始流的源的遍历
一个流只能有一个终止操作,它必定是流的最后一个操作。只有调用了流的终止操作,流的源的元素才会真正的开始遍历,并且会生成一个结果返回或者产生一个副作用(side-effect)。另外,每一个流只能被使用一次(即调用中间操作或者终止操作)。

从表面上来看,好像流在执行了多个中间操作和一个终止操作之后,对于每一个操作,流中的元素都会遍历执行,也就是有几个操作,流中的元素就会进行几次遍历。这种观点是大错特错的

流的实际执行流程是这样的,在遇到中间操作的时候,其实只是构建了一个Pipeline对象,而该对象是一个双向链表的数据结构,只有在遇到终止操作的时候,那些中间操作和终止操作会被封装成链表的数据结构链接起来,而流中每一个元素只会按照顺序链接的去执行这些操作,也就是说,流中的元素最终只会在遇到终止操作后遍历一次,而每个元素会将所有操作按顺序执行一遍

问题的解决

具体回到项目中遇见的问题, 当流遇上终止操作findFirst()时,流真正开始执行,不像我们想当然的所有元素先一起经过filter再一起经过map,而是每个元素像水流一样逐个地走完整个流程,当第一个元素成功到达终止操作findFirst时,流即被截断(findFirst:找到第一个元素。找到了就直接返回,不在遍历后面元素)。所以之前无论流中还有多少符合filter的数据,其都不会执行map方法。

这次找了许久的bug归根到底是自己对jdk8新特性底层的不熟悉,反思反思



参考资料:
Java8新特性——StreamAPI(一) - 大闲人柴毛毛 - CSDN博客
Java8的流Stream与收集器Collector详解 - zw19910924的专栏 - CSDN博客

你可能感兴趣的:(关于Java8 流“只遍历一次”的理解)