jdk8 stream底层源码深入剖析,我都说深入了你耳隆啊!

576 阅读8分钟

定义

作为Jdk8的新特性,stream流提供了对集合的各种聚合操作,可以理解为更高级的迭代器,配合lambda表达式,是程序更加简洁易懂。

看懂本文需要会使用lambda表达式做为基础,如果不会可以先看下对于lambda式的介绍。

image.png

自顶向下的代码环节

//把list的所有元素都加100并排序,生成一个result集合   
List<Integer> result = list.stream().map((a)-> a+100).sorted().collect(Collectors.toList());

从这句简单的代码出发,逐步拆解。

list.stream()

public interface Collection<E> extends Iterable<E> {
    //省略上面...
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

其实就是返回一个Stream类的对象。Stream类有很多子类,我们接下来在介绍。

jdk8中在Collection接口中新增了几个默认方法。默认方法也是jdk8的新特性,即在接口里定义实现好的方法。我们使用的stream() 会调用StreamSupport.stream(spliterator(), false)。我们来解释下这个两个参数的作用。

spliterator()

StreamSupport.stream()的第一个参数

底层其实就是返回一个Spliterator对象,翻译一下就是分割器。可以理解为一种多线程并发迭代的迭代器iterator,即把数组分割成多段由多线程进行操作,线程安全,最大程度利用cpu的性能。

并行设计也是Jdk8的一大特性,像ConcurrentHashMap的并发扩容设计就是和这个Spliterator有关。

parallel true||false

StreamSupport.stream()的第二个参数

就是一个布尔值。表示是否需要多线程处理,即利用刚才提到的spliterator。

那么这里就要返回到刚才的list.stream()了。我源码有列出新增的两个默认方法,parallelStream()和stream()。他们的区别就是底层的StreamSupport.stream() 方法第二个参数是否为true,即一个并行,一个串行。

StreamSupport.stream(Spliterator,parallel)

    public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
        //判定是否null
        Objects.requireNonNull(spliterator);
        return new ReferencePipeline.Head<>(spliterator,
                                            StreamOpFlag.fromCharacteristics(spliterator),
                                            parallel);
    }

我们再来看这个StreamSupport.stream()的源码。除去一个校验,实际就是返回了一个ReferencePipelineHead类对象,这个对象其实就是Stream的子类,这个是比较复杂的,需要从架构设计上去理解什么是Head类。

ReferencePipeline

先介绍下ReferencePipeline抽象类

在这里插入图片描述

首先先看下这个类的类图

ReferencePipeline是一个抽象类,实现了stream接口,继承了pipelineHelper类。他最重要的是有三个静态内部类,重点来了!

  • Head 头
  • StatefulOp 有状态
  • StatelessOp 无状态

要想解释这几个东西是干嘛的,要自顶向下的看。来看最开始写的实例代码。

Stream的操作分类

//把list的所有元素都加100并排序,生成一个result集合   
List<Integer> result = list.stream().map((a)-> a+100).sorted().collect(Collectors.toList());

我们通常把Stream的操作分为两大类,(即stream()后面的那些操作)。中间操作终结操作

中间操作只是对操作进行了记录,像map(),也称为惰性求值。终结操作才会触发计算逻辑,像collect()。

。终结操作还可分为短路操作非短路操作

  • 终结操作

    • 短路
    • 非短路

中间操作

中间操作还可以细分为无状态(Stateless)操作和有状态(Stateful)操作

  • 无状态 Stateless
  • 有状态 Stateful

看到这个无状态和有状态的英文,对应上面的内部类,你应该就有点头绪了把,但我们先介绍完。

无状态操作指的是指元素的处理不受之前元素的影响,可以一个一个即时处理。像map(),filter()这种

有状态操作就是不可以一个一个处理,需要拿到全部元素才能继续。例如sort()。排序是需要获取全部元素的。

终结操作

终结操作还可以细分为短路操作非短路操作,前者是指遇到某些符合条件的元素就可以得到最终结果;而后者是指必须处理所有元素才能得到最终结果

操作结构

每一个操作都被封装成一个节点,也叫做Stage,然后通过前后指针连接起来,形成了一条双向链表。那么第一个节点就是Head节点,接下来的中间操作就是无状态或有状态节点了。这就对应到了上文的三个内部类。最后的终结操作会生成一个TerminalOp节点。

map()

我们接着看源码,以map() 为切入点来看。

//把list的所有元素都加100并排序,生成一个result集合   
List<Integer> result = list.stream().map((a)-> a+100).sorted().collect(Collectors.toList());

由上文可知这边stream()已经返回了一个双向链表的Head结点,即stream对象(stream是Head的父类),接着执行map()。

map()源码

    public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<P_OUT, R>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        downstream.accept(mapper.apply(u));
                    }
                };
            }
        };
    }

本质上也是返回一个Stream对象,也就是StatelessOp内部类。但是在返回这个内部类时,重写了opWrapSink() 。这里我们就要说下上面提到的那些内部类的作用了,就是记录操作,在终结操作时再执行,那么他记录的方式就是通过Sink对象记录。我们接下来讲一下Sink。

Sink

在这里插入图片描述

Sink是一个接口,有四个重要的方法。

// 开始遍历元素之前调用该方法,通知Sink做好准备
default void begin(long size)
// 所有元素遍历完成之后调用,通知Sink没有更多的元素了
default void end()
// 是否可以结束操作,可以让短路操作尽早结束
default boolean cancellationRequested() 
// Stage 把自己包含的回调方法封装到该方法里,前一个Stage只需要调用当前 Stage.accept(T t)方法就行了
default void accept(int value)

那些节点的操作重写进accept()方法里,终结操作处理数据时只需要从head节点开始对数据执行每个节点的sink的accept()方法就可以了。对照下刚才重写的代码看一下,就明白了。

对于有状态的操作,Sink的begin()和end()方法是必须实现的。例如Stream.sorted(),begin()方法创建了一个存储元素的容器,accept()方法负责将元素添加到该容器,最后end()实现对容器中的元素进行排序,并决定了下游 Sink 如何执行

collect()

小结一下,每个操作都对应一个在双向链表的stage节点,里面通过sink记录下了操作,等待终结操作时从head节点开始执行。所以我们来看下执行终结操作的流程,我这里举例为串行的collect()。

    public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {
        A container;
        //省略并行的内容
        container = evaluate(ReduceOps.makeRef(collector));
        return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)
               ? (R) container
               : collector.finisher().apply(container);
    }

核心就只有一个evaluate(ReduceOps.makeRef(collector))。

里面的ReduceOps.makeRef(collector)就是生成返回一个ReduceOps(就是TerminalOP终结节点的子类)。然后再来看evaluate方法。

evaluate()

    final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
        return  terminalOp.evaluateSequential(this,sourceSpliterator(terminalOp.getOpFlags()));
    }

执行终结节点的evaluateSequential方法,这个evaluateSequential方法对于不同的节点类型例如foreachOp,reduceOP等等,也是不同的,但是里面都会有相同的一个方法,就是wrapAndCopyInto

        public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
                                           Spliterator<P_IN> spliterator) {
            return helper.wrapAndCopyInto(makeSink(), spliterator).get();
        }

makesink()就是创建这个终结节点的sink对象

wrapAndCopyInto() 重点

@Override
final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
    copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
    return sink;
}

调了这么多方法,终于到了关键点。两个重要的方法

  • copyInto() 把链表里的所有的sink总头到尾执行一遍
  • wrapSink() 返回了一个实现了sink接口方法的sink

wrapSink()

    final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
        Objects.requireNonNull(sink);
​
        for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
            sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
        }
        return (Sink<P_IN>) sink;
    }

我们看到了熟悉的方法,opWrapSink(),这个就是我在map()那里重写的方法,忘记的同学可以回去看一下,也就是创建并返回了一个实现了accpet()方法的sink。我们之前sink对象一直没有创建,是在这里创建sink对象。

是一个循环,从尾结点开始遍历到头,执行opWrapSink()。这个方法的作用就是

  • 使用当前Sink包装的回调函数处理u
  • 将处理结果传递给流水线下游的Sink
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
    return new Sink.ChainedReference<P_OUT, R>(sink) {
        @Override
        public void accept(P_OUT u) {
            downstream.accept(mapper.apply(u));
        }
    };
      

看下之前map重写的方法,new Sink.ChainedReference构造器的意思是创建一个sink节点,这个节点的downstream(下一个节点)就是sink参数。在循环的第一步,这里的sink参数就是终结节点。表示创建一个sink节点,sink的downstream为下一个sink节点。所以这个循环执行完最后会得到一个sink对象。这个对象里包含了下一个,再下一个,再再下一个...的sink节点,有点类似于链表。

也就是说从尾部出发,创建重写了方法的sink对象的同时,并把它们连接起来,这就是这个方法的作用。

copyInto()

            wrappedSink.begin(spliterator.getExactSizeIfKnown());
            spliterator.forEachRemaining(wrappedSink);
            wrappedSink.end();

执行begin和end方法在开始和结束的时候。中间执行了forEachRemaining方法,其实就是类似于arraylist的foreach方法,每个元素都会调用sink的accept方法,由上文可知,accept执行之后还会执行下一个sink的方法,通过这种方式遍历。

流程总结

上面说了很多,我在这里总结一下流程。还是以代码为例

List<Integer> result = list.stream().map((a)-> a+100).collect(Collectors.toList());
  • stream() 创建stream对象,一个Head头结点
  • map() 一个中间操作的无状态操作,生成一个无状态结点,是stream类的子类,与head形成双向链表。这个无状态结点重写了一个opWrapSink方法。
  • collect() 一个终结操作,执行到这就会触发计算逻辑。从尾节点开始向头结点遍历,依次执行opWrapSink方法,生成sink对象并形成链。然后从头sink对象开始执行sink的accept()方法。

小结

每个操作的底层都是不一样的,但是有着共同点,大体的思路就是这样。其中jdk1.8又引入了很多并行的设计,底层也就是使用了线程池进行多线程调用,这个地方就不重点介绍了。如果有什么问题或者错误的地方可以评论我一下。雀氏是这样的,我都说雀氏了。

image.png