大家好,我是小水珠。
上一讲中,我们讲List集合类,那我想你一定也知道集合的顶端接口Collecion。在Java8中,Collection新增了两个流方法,分别是stream()和paralleStream()。 通过英文名不难猜测,这两个方法肯定和Stream有关,那进一步猜测,是不是和我们熟悉的InputStream和OutputStream也有关系呢?集合类中新增的两个Stream方法到底有什么作用?今天,我们就来深入了解下Stream。
一 什么是Stream
我们用一个简单的列子来体验下Stream的简洁与强大
1.串行实现
2.并行实现
二 Stream如何优化遍历
1.Stream操作分类
2.Stream源码实现
在了解Stream如何工作之前,我们先来了解下Stream包是由哪些结构类组合而成的,各个类的职责是什么,参照下图:
BaseStream和Stream为最顶端的接口类。BaseStream主要定义了流的基本接口方法,例如:spliterator,isParallel等;Stream则定义了一些流的常用操作方法,例如:map,filter等。
ReferencePipline是一个结构类,它通过定义内部类组装了各种操作流。它定义了Head,StatelessOp,StatefulOp三个内部类,实现了BaseStream与Stream的接口方法。
Sink接口是定义每个Stream操作之间关系的协议,它包含begin(),and(),cancellationRequested(),accept()四个方法。ReferencePipline最终会将整个Stream流操作组装成一个调用链,而这条调用链上的各个Stream操作的上线关系就是通过Sink()接口协议来定义实现的。
3.Stream操作叠加
我们知道,一个Stream的各个操作是由处理管道组装,并统一完成数据处理的。在JDK中每次的中断操作会以使用阶段(stage)命名。
管道结构通常是由ReferencePipline类实现的,前面讲Stream包结构时,我提到过ReferencePipline包含了Head,StatelessOp,StatefulOp三种内部类。
Head类主要用于定义数据源操作,在我们初次调用names.stream()方法时,会初次加载Head对象,此时为加载数据源操作;接着加载的是中间操作,分别为无状态中间操作StatelessOp对象和有状态中间操作StatefulOp对象,此时的Stage并没有执行,而是通过AbstractPipline生成了一个中间操作Stage链表;当我们调用终结操作时,会生成一个最终的Stage,通过这个Stage触发之前的中间操作,从最后一个Stage开始,递归产生一个Sink链。如下图所示:
我们再来通过一个例子来感受下Stream的操作分类是如何实现高效迭代大数据集合的。
首先因为names是ArrayList集合,所以names.stream()方法将会调用集合类基础接口Collection的Stream方法:
然后,Stream方法就会调用StreamSupport类的stream()方法,方法中初始化了一个ReferencePipline的Head内部类对象:
再调用filter和map方法,这两个方法都是无状态中间操作,所以执行filter和map操作时,并没有进行任何的操作,而是分别创建了一个Stage来标识用户的每一次操作。
而通常情况下Stream的操作又需要一个回调函数,所以一个完整的Stage是由数据来源,操作,回调函数组成的三元组来标识。如下图所示,分别是ReferencePipeline的filter方法和map方法。
new StatelessOp将会调用父类AbstractPipline的构造函数,这个构造函数将前后的Stage联系起来,生成一个Stage链表。
因为在创建每一个Stage时,都会包含一个opWrapSink(),该方法会把一个操作的具体实现封装在Sink类中,Sink采用(处理->转发)的模式来叠加操作。
当执行max方法时,会调用ReferencePipline的max方法,此时由于max方法时终结操作,所以会创建一个TerminalOp操作,同时创建一个ReducingSink,并且将操作封装在sink类中。
最后,调用AbstractPipline的wrapSink方法,该方法会调用opWrapSink生成一个Sink链表,Sink链表中的每一个Sink都封装了一个操作的具体实现。
当Sink链表生完后,Stream开始执行,通过spliterator迭代集合,执行Sink链表中的操作。
Java8中的Spliteraor的forEachRemaining会迭代集合,每迭代一次,都会执行一次filter操作,如果filter操作通过,就会触发map操作,然后将结果放入到临时数组object中,再进行下一次的迭代。完成中间操作后,就会触发终结操作max。
这就是串行处理方式了,那么Stream的另一种处理方式又是怎么操作的呢?
4.Stream并行处理
Stream处理数据的方式有两种,串行处理和并行处理。要实现并行处理,我们只需要在例子中的代码中新增一个parallel()方法,代码如下所有:
Stream的并行处理在执行终结操作之前,跟串行处理的实现是一样的。而在调用终结方法之后,实现的方式就有点不太一样,会调用TerminalOp的evaluateParallel方法进行并行处理。
三 合理使用Stream
我们对常规的迭代,Stream串行迭代以及Stream并行迭代进行性能测试对比,迭代循环中,我们对数据进行过滤,分组等操作。分别进行一下几组测试:
- 多核CPU服务器配置配置环境下,对比长度100的int数组的性能;
- 多核CPU服务器配置配置环境下,对比长度1.00E+8的int数组的性能;
- 多核CPU服务器配置配置环境下,对比长度1.00E+8的对象数组过滤分组的性能;
- 单核CPU服务器配置配置环境下,对比长度1.00E+8的对象数组过滤分组的性能;
通过以上测试,我统计的测试结果如下(迭代使用时间):
- 常规的迭代;
- Stream并行迭代;
- Stream并行迭代;
- 常规的迭代;
总结
纵观Stream的设计实现,非常值得我们学习。
1.在串行处理操作中
Stream在执行每一步中间操作时,并不会做实际的数据处理,而是将这些中间操作串联起来,最终由终结操作触发,生成一个数据处理链表,通过Java8中的Spliterator迭代器进行数据处理;此时,每执行一次迭代,就对所有的无状态中间操作进行数据处理,而对有状态的中间操作,就需要迭代处理完所有的数据,再进行处理操作;最后就是进行终结操作的数据处理。
2.在并行处理操作中
Stream对中间操作基本跟串行处理方式是一样的,但在终结操作中,Stream将结合ForkJoin框架对集合进行切片处理,ForkJoin框架将每个切片的处理结果Join合并起来。