Stream深入分析

关于jdk8的重大改进,其中java对函数式编程的重视程度看看加入函数式编程后,扩充了多少功能,重写了多少基础类库可见一斑。引入Lambda表达式和Stream也是工作中应用最广泛的。关于Stream推荐参考《Java8函数式编程》。

一、Stream初始化

       stream初始化的方式主要有以下几种:

  • 调用Collection.stream()或者Collection.parallelStream()方法
  • 调用Arrays.stream(T[] array)方法

虽然Stream是通过  Collection.stream()初始化的但是和Collections有以下不同之处:

  • 无存储stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
  • 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream
  • 惰式执行stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  • 可消费性stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

    下面具体了解一下Stream流初始化的源码:

图一:Collection.stream()

 最终都是调用到 StreamSupport.stream这个方法。我们看到这里第一个参数是获取一个Spliterator的实例,它表示从数据源中获取元素的方式。相当于升级版的Iterator。第二个参数是是否并行。大概介绍一下Spliterator接口方法。

  • boolean tryAdvance(Consumer action); 该方法会处理每个元素,如果没有元素处理,则应该返回false,否则返回true。
  • default void forEachRemaining(Consumer action) 对每个剩余元素执行给定对动作,依次处理知道所有元素被处理或者异常终止,默认方法调用tryAdvance方法。
  • Spliterator trySplit(); 将一个Spliterator分割成多个Spliterator。分割的Spliterator被用于每个子线程进行处理,从而达到并发处理的效果返回一个新对Spliterator迭代器。
  • long estimateSize();用于估算还剩下多少个元素需要遍历。
  • int characteristics(); 给出stream流具有的特性,不同的特性,不仅是会对流的计算有优化作用,更可能对计算结果会产生影响。如:Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED。
    • default Comparator getComparator() 对sorted的流,给出比较器。如果Spliterator对list是通过Comparator排序对,则返回Comparator。如果list是自然排序返回null,其他情况下抛错。

这里主要讲Stream关于Spliterator内部实现大家可以通过源码了解一下,这里不赘述。

>>>继续进入StreamSupport类:

图二:StreamSupport.stream()

我们可以看到最终调用对是ReferencePipeLine.Head,从图四可以看到ReferencePipeLine和InitPipeLine  LongPipeLine  DoublePipeLine 都继承AbstractPipeline和ReferencePipeLine时并行关系。之所以为三种基本类型定制,主要用于频繁拆装箱。AbstractPipeline时流水线核心抽象类,用于构建和管理流水线。

>>>AbstractPipeline:

AbstractPipeline中定义类三个变量:sourceStage(源阶段),previousStage(上游pipeline,前一阶段),nextStage(下一阶段)。

图三:AbstractPipeline

 

 

图四:Stream对主要接口类对关系图

 

 

讲完Stream对初始化,下面我们来了解一下Stream对中间操作,Stream对操作类型分外中间操作和结束操作

图五:Stream操作类型

Stream操作分类              

二、中间操作(Intermediate operations)
1、返回结果是stream的非静态方法
2、中间操作是一种lazy操作可以多个,实际上并没有执行,而是等最终操作时执行。这也是Stream在迭代大集合时高效等原因之一。
3、中间操作分为无状态(Stateless)操作和有状态(Stateful)操作,无状态是指不依赖前面元素的影响。而有状态必须等前面执行完成之后才能知道结果。

通常一个完整的Stream操作是由:数据源  操作  回调函数组成的双向链表结构。如图六

图六:Stream流水线双向链表结构

 

通过Collection.Head初始化,后面调用一系列调中间操作不断产生新调Stream。这些Stream以双向链表调方式组织起来,形成一个流水线。由于每个stage记录了前一次操作调回调函数,这样就建立起对数据源对所有操作。

前面我们也提到了这种方式可以提高大集合迭代的高效那么stream是做的呢?

>>>Stream叠加操作

Stream通过Sink接口来协调相邻Stage之间的调用关系。sink接口包含下面几个方法:

  • void begin(Long size);开始遍历元素之前调用该方法,通知Sink做好准备。
  • void end();所有元素遍历完成之后调用。
  • boolean cancellationReauested();是否可以结束操作,可以让短路操作尽早结束。
  • void accept(T t);遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。

   通过Sink接口相邻Stage之间调用就很清晰了,每个Stage都将自己都操作封装到一个Sink里,前一个Stage调用后一个Stage到accept()方法。我们来看一下Sink.map的源码。

 

Sink封装了Stream的每一步操作,但是我们前面提到Stream的中间操作是Lazy操作,真正启动这些操作的是结束操作来执行的。

三、结束操作(Terminal operations)
1、只能有一个最终操作,不一定有返回结果。
2、分为非短路操作和短路操作。非短路操作所有元素处理完,短路操作不用处理全部元素就可以返回结果

以ReferencePipeline.forEachOrder为例,PS:简单提一下Stream.forEach不能保证顺序,forEachOrder能保证顺序但效率不高。直接上源码:

 

ForEachOps是用户创建TerminalOp实例的工厂类。TerminalOp是终止操作最顶层的一个接口。TerminalOp接口的实现类有ForEachOp, ReduceOp,FindOp, MatchOp。
先看ForEachOps.makeRef()方法:

OfRef是引用流的默认实现类,这里新建了一个OfRef的实例,构造方法如下:

 

将我们实现的Consumer函数式接口赋值给成员变量。回到evaluate方法:

 

跟到串行流的实现,实现在ForEachOp中:

 

 

PipelineHelper类型其实是AbstractPipeline的父类,而AbstractPipeline又是ReferencePipeline的父类。再跟进helper.wrapAndCopyInto方法,是现在AbstractPipeline中:

 

可以看到通过ReferencePipeline的双向链表,从最后一个操作(也就是终止操作)往前遍历,将所有的操作都串联起来,最终返回一个指向第一个操作的Sink引用。

对于Stream操作性能:

  • 对于复杂操作,Stream串行API性能可以和手动实现的效果匹敌,在并行执行时Stream API效果远超手动实现。
  • 对于简单操作,比如最简单的遍历,Stream串行API性能明显差于显示迭代,但并行的Stream API能够发挥多核特性。

所以,如果出于性能考虑,1. 对于简单操作推荐使用外部迭代手动实现,2. 对于复杂操作,推荐使用Stream API, 3. 在多核情况下,推荐使用并行Stream API来发挥多核优势,4.单核情况下不建议使用并行Stream API。

如果出于代码简洁性考虑,使用Stream API能够写出更短的代码。即使是从性能方面说,尽可能的使用Stream API也另外一个优势,那就是只要Java Stream类库做了升级优化,代码不用做任何修改就能享受到升级带来的好处。

参考资料:

https://www.cnblogs.com/CarpenterLee/p/6637118.html

https://www.cnblogs.com/Dorae/p/7779246.html

https://www.cnblogs.com/CarpenterLee/p/6675568.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章