JAVA 1.8 Steam 流—— 源码简单解读

接触并使用了java8 特性的大家伙儿,对于集合的一些操作估计都已经得心应手了。那就是使用集合操作的相关的Stream的api。Stream是什么呢?在api中说,她是支持对元素进行并行或者顺序操作的一个序列。

我们直接上源码:

List words = Arrays.asList("i", "love", "you", "my", "friend", "and", "thanks", "!");
        words.stream()
                .filter(str -> str.length() < 4)
                .map(str -> str + "**")
                .forEach(System.out::println);

这段代码是说,先把words使用stream()方法(在Collection类中,默认实现了该方法,返回的是一个Stream实例),转变为一个Stream。然后,使用filter()方法把长度小于4的字符串过滤出来,接着使用map()方法,在每个字符串后边添加两个*号,最后,使用forEach方法,循环输出字符串。最终结果,大家都能猜到,这里就先不贴了。

看到了这种神奇的效果,好奇的我们怎么不想知道它内部是怎么实现的呢?让我们一步一步来跟着源码分析一下。

在分析源码之前,我们需要先知道,stream的操作有两种,中间操作和结束操作,也就是说,只有调用结束操作时,流才会真正的流动起来,这也叫做惰性求值。比如例子中的代码,如果没有forEach方法,那么流是永远都不会动起来。

下边,首先我们来看一下例子中调用的stream()方法,我们跟进去,逻辑如下所示:

    default Stream stream() {
        return StreamSupport.stream(spliterator(), false);
    }

将spliterator()作为参数,返回一个Stream的实例,我们接着跟下去:

JAVA 1.8 Steam 流—— 源码简单解读_第1张图片

发现返回的是一个ReferencePipeline.Head的类,其中,ReferencePipeline继承自Stream,Head是ReferencePipeline的静态内部类,并且继承ReferencePipeline。我们看到传入两个参数:一个是spliterator,这是我们的集合;一个是parallel,false说明流是顺序的,true说明流是并行的。我们直接跟进到Head的构造方法,并一路跟踪super的构造方法,最终我们看到:

JAVA 1.8 Steam 流—— 源码简单解读_第2张图片

最终实例化了一个AbstractPipeline对象,这就是Stream进行流式操作的一个默认实现,继承自PipelineHelper。所谓helper,我的理解就是帮助流,一步一步向下流动的。

再继续向下看源码之前,我们需要先关注一下AbstractPipeline中的几个属性。在属性声明部分我们可以看到:sourceStage代表的是当前链接的头部,如果当前是第一步,那么就是self;previousStage代表的是上一步的操作,如果为null,那么这一步就是第一步;nextStage代表的是下一步的操作,如果为null,那么这一步就是最后一步。我们先记住这三个参数。

言归正传,我们接着往下走,来到了filter方法,因为上一步我们返回的是个Head对象,而Head继承自ReferencePipeline,我们可以从这两个地方,或者他们的父类,来找寻一下filter方法。不出意料,我们在ReferencePipeline类中找到了filter方法,我们索性先看一下这个类中所有的方法吧。JAVA 1.8 Steam 流—— 源码简单解读_第3张图片

看吧,我们对集合的所有操作,这里几乎都有实现,每个方法其实都是大同小异的,我们还是继续说一下我们的filter方法。

JAVA 1.8 Steam 流—— 源码简单解读_第4张图片

可以看到,该方法返回的是一个StatelessOp对象,我们看一下这个对象,发现其实它是ReferencePipeline的静态内部类,并继承了ReferencePipeline,也就是,filter方法返回了一个ReferencePipeline,也就是他的祖父PipelineHelper,跟stream方法一样。大胆的猜测,其实所有的中间操作都返回一个PipelineHelper类型的实例。大概浏览一下源码,发现,确实如此。

我们再观察一下,在StatelessOp的构造方法中,传入了三个参数,我们这里只说第一个参数,this,也就是当前的PipelineHelper对象的引用。我们跟踪构造方法,一路到super的顶端,如下所示:

JAVA 1.8 Steam 流—— 源码简单解读_第5张图片

发现,其实stream流是一个双向链表的形式。传进来的PipelineHelper,被指定了下一个步骤,就是当前步骤;当前步骤指定了上一个步骤,就是传入的步骤;链表头,则被复用。看depth,具体的作用没有细查,就目前来看,好像是代表着流的深度,希望知道的大牛留言解答。

我们接着看filter方法,因为在AbstractPipeline中有一个未实现的abstract方法:opWrapSink()。在filter中对其进行了实现。返回的是一个Sink对象,我们可以先看一下Sink对象,继承自Consumer,有一个为实现的accept方法,我们着重看一下accept方法,一会儿要用到。我们注意到,在Sink对象的构造方法中,传入了两个参数:一个是flags,另一个是另一个Sink对象。作用我们后续会说到。

我们接着向下看map()方法,同样可以在ReferencePipeline类中找到他的实现:

JAVA 1.8 Steam 流—— 源码简单解读_第6张图片

可以看到,同filter方法一样,StatelessOp也是PipelineHelper类型的子孙,这样map方法也加入到了双向链表中了。

到目前为止,只是准备好了一个双向链表而已,其中的逻辑功能,都没有进行实际的调用,这就是我们所说的中间操作啦。下边就是结束操作了,他是怎么调动前边准备好的所有的中间操作的呢,我们一起来看。

同理,forEach()方法,在ReferencePipeline也可以看到具体的实现:

JAVA 1.8 Steam 流—— 源码简单解读_第7张图片

先看一下ForEachOps.makeRef()方法,参数是我们forEach要执行的action,也就是Consumer函数。跟进去:

JAVA 1.8 Steam 流—— 源码简单解读_第8张图片

返回了ForEachOp.OfRef类型的一个对象,深入跟进我们发现,其实这也是一个继承自Sink的对象。这里暂且保留不表,接着回到forEach方法,这次我们进入到evaluate()方法,参数就是我们刚刚得到的ForEachOp.OfRef对象。

JAVA 1.8 Steam 流—— 源码简单解读_第9张图片

首先判断linkedOrConsumed是否为true,如果为true则表示当前流已经被一个结束操作使用过了,会抛出异常,告诉我们一个流不应该被重复消费。我们是顺序流,不是并行流,所以,我们着重看一下红框中的部分,也就是evaluateSequential方法,这个方法第一个参数是this。

this是什么?就是当前forEach所代表的PipelineHelper啊。通过前边,我们知道每一步操作都会生成一个PipelineHelper,然后,通过它再去调用下一个步骤,并将他自己传入下一个PipelineHelper构造方法中,使他们以双向链表的方式链接起来。那这里的这个结束操作的this(也就是PipelineHelper)传进去,是要做什么呢?

我们要来到的是ForEachOp.OfRef的evaluateSequential()方法,也就是其父类ForEachOp的evaluateSequential()方法。其使用传入的PipelineHelper,并调用了 PipelineHelper的wrapAndCopyInto方法,在wrapAndCopyInto方法中,将ForEachOp.OfRef,也就是self传递了进去,上边已经分析,其实也是一个Sink。继续往下跟,来到AbstractPipeline类的wrapAndCopyInto方法,如下图所示:

我们先看wrapSink方法:

JAVA 1.8 Steam 流—— 源码简单解读_第10张图片

这段代码很容易看出,这里将前边准备好的所有PipelineHelper向前遍历,这里的depth证明了我们的想法,他确实代表着双向链表的长度。遍历之后,调用每个步骤中的Sink实现类的opWrapSink()方法,我们可以借用贴图回想一下。

JAVA 1.8 Steam 流—— 源码简单解读_第11张图片

我们进入ChainedReference内部类中看一下:

JAVA 1.8 Steam 流—— 源码简单解读_第12张图片

即:下游流就是传入的Sink。那么我们就可以这样理解:wrapSink方法从后向前,实现了一个单向链表,总是将后一个步骤挂载前一个步骤的下游字段上,最终,根据源码我们得知,wrapSink方法,返回的是第一个Sink,但此时,他已经是一个一步一步连接了所有后续操作的单向链表了。

拿到Sink链表后,我们接着向后看,进入copyInto方法。

JAVA 1.8 Steam 流—— 源码简单解读_第13张图片

可以看到,先执行begin方法,在执行forEachRemaining方法,最后执行end方法,begin方法是重置接收器状态,已接收新的数据。end方法说明所有数据处理完毕,要恢复到初始状态了。在spliterator.forEachRemaining(wrappedSink)方法中,就会调用定义的accept方法,也就是我们自定的一些处理逻辑。

需要说明的是,流的这几个方法是一个一个流过的,也就是说,第一个步骤的begin步骤执行完,会调用下一个步骤的begin步骤,所有的begin步骤都执行后,才会继续向下执行,后续步骤,与此相同。

到这里,我的分析就完成了,希望可以启发大家一下,共同学习,共同进步。

你可能感兴趣的:(JDK源码解读)