Bootstrap

Stream双链原理解析

一、简介

Stream是java8加入的新特性,它提供了一种类SQL化的方式来访问集合数据,将对集合的操作清晰明了的展现出来,特别是配合Lambda表达式,能写出非常优雅的代码。

顾名思义,Stream操作过程可以看做数据流过一个管道,入口处集合转换为数据流(Collection.stream()),在管道上设置有一个一个的过滤点(filter、sorted、map等等),数据流过时将想要的数据过滤出来,在出口处将数据流转换为想要的数据类型(forEach、collect、count、max等等)。

二、概念

先简单介绍流计算过程中的几个概念:

Head:入口,生成流的操作,主要通过StreamSupport.stream()方法生成,Collection.stream()也是调用的该方法。

中间操作(intermediate operations):过滤点上的操作(filter、map等),共有两种类型,有状态操作(StatefulOp)和无状态操作(StatelessOp)。

结束操作(terminal operation):出口处的操作(forEach、collect等),分为短路操作和非短路操作。

stage:入口操作和中间操作的过程被称为一个stage阶段,Head看做stage0,中间操作按顺序排开stage1、stage2...。结束操作不属于stage。

无状态操作(StatelessOp):中间操作的一种类型,无状态是指这一stage的操作结果跟下一stage没有关系,可随意蹂躏,如filter、map。

有状态操作(StatefulOp):中间操作的一种类型,有状态是指这一stage的操作结果会影响下一stage的操作,如sorted,后面的操作跟排序结果有关。

Sink:封装了具体的操作,就是真正干活的人,中间操作和结束操作都会包装(创建)一个Sink,Head不会包装Sink,因为他只生成流,不做操作。

三、原理

例子:

list.stream()
    .filter(entity -> entity.getValue() > 900)
    .sorted(Comparator.comparingInt(Entity::getValue)) 
    .map(entity -> entity.getValue()) 
    .limit(10) 
    .distinct() 
    .collect(Collectors.toList());

1、简述

Stream是怎么实现上述例子简洁而又强大的API的呢?每次计算是否会导致一次迭代呢?下面来介绍下Steam的原理。

总的来说,Stream流计算分为三步,第一步生成流,即入口;第二步对流中的数据进行操作,也就是中间操作,第三步返回数据,也就是结束操作。每个流至少有一个入口和一个结束操作,可以有零个或多个中间操作。需要说明的是第二步中的中间操作不是实时进行,是滞后的,等遇到结束操作的时候会执行,因此stream是惰性的,所有中间操作都是由结束操作触发的。

再说一个容易被误导的问题,跟命名相关。Stream源码中将其顶级类命名为PipelineHelper,网上很多资料也都用pipeline定义Stream,并翻译为流水线。但是将stream的执行过程形容为流水线的方式却是有些不妥,流水线更强调的是操作的并行,而stream的执行过程中的操作都是串行进行的,比如例子中的filter、sorted、map、limit。。。都是按顺序依次执行的,不存在并行情况(并行流parallelStream中并行的也是数据,不是操作)。因此pipeline翻译为管道流较为合适。

2、类图

Stream的类图

从集合到流,感觉好神秘的样子,好像做了什么不可告人的事。其实说到底就是类,实现了stream接口的类,Head和中间操作都是类图中的ReferencePipeline类(应为AbstractPipeline,这里只为叙述原理)。Head和中间操作都继承自ReferencePipeline,中间操作只是定义了对数据的action。集合数据在最初生成流(Head)的时候,把数据交给了Head,后面对数据的蹂躏都通过Head来发起了。结束操作是把流重新汇总起来,不具备继续往下流的性质了,因此结束操作不属于stream。结束操作是对具体问题的具体处理,共有四种类型:FindOp(返回Optional)、ForEachOp(无返回值,消耗型)、MatchOp(返回Boolean)、ReduceOp(返回集合等)。

由图可知,Head和中间操作(StatelessOp、StatefulOp)都实现了Stream接口,而Stream接口定义了所有的中间操作和结束操作的方法。入口和中间操作的返回类型还是Stream,因此能够形成链式调用。

3、过程

Stream的执行过程是这样的:第一步,创建一个流,方式有多种,但是最后生成的流只有一种,就是Head类,Head持有数据,在遇到结束操作之前,数据是不会流动的,一直呆在Head这里;第二步,定义中间操作链,不会对数据进行计算,只是定义了对数据的action,每次中间操作都会生成StatelessOp或StatefulOp类。调用Head的中间操作(比如filter)生成一个新的StatelessOp对象,然后又调用该对象的中间操作(比如sorted)又生成一个新的StatefulOp对象,依次类推。。。,将这些生成的对象通过链表的形式串联起来;第三步,触发结束操作,结束操作一定是Head对象或最后一个中间操作对象调用的,调用结束操作时,会根据链表指向(previousStage)一直往前寻找到第一个中间操作,同时每个中间操作都会包装一个sink,形成一个sink套娃;最后,迭代Head的数据,执行sink套娃,返回需要的结果。

由过程可知,Stream需要解决几个问题:

1、怎么生成流?

2、数据怎么流动?

3、操作怎么串联起来?

4、串联后的操作怎么执行?

5、结果怎么返回?

上面几个问题的解决,Stream是通过两个链条来实现的,第一个pipeline链,将所有的stage链接起来,链接的方式是通过previousStage属性。pipeline链从前往后执行,(例子中)Head告诉filter,你的previousStage是我,filter告诉sorted,你的previousStage是我,依次往下直到distinct。pipeline链解决了1、2问题。第二个是sink链,将所有的操作链接起来,链接的方式是通过downstream属性。sink链从后往前寻找,直到找到第一个中间操作,(例子中)collect告诉distinct,你的downstream是我,distinct告诉limit,你的downstream是我,依次往上直到filter。sink链解决了3、4、5问题。

4、双链

pipeline链

pipeline链连接的是stage,每个stage就是一个ReferencePipeline类,ReferencePipeline类除了实现把stage串联起来以外,都还封装了一个opWrapSink方法,该方法包装(创建)了一个Sink,供sink链调用。pipeline链的实现方式是这样的:

pipeline链的第一个节点就是Head,也就是流是怎么生成的,生成流的方式有多种,最常用的是list.stream(),还有Arrays.stream、Stream.of等,通过查看源码可知,所有生成流的方法最后都是调用StreamSupport.stream方法。而该方法返回的就是是Head类。

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

Head是流的入口,有类图可知Head实现了stream接口,定义了所有的中间操作和结束操作,下面是Head的构造函数,可见Head没有前置节点

     /**
     * Constructor for the head of a stream pipeline.
     *
     * @param source {@code Supplier<Spliterator>} describing the stream source
     * @param sourceFlags The source flags for the stream source, described in
     * {@link StreamOpFlag}
     * @param parallel True if the pipeline is parallel
     */
    AbstractPipeline(Supplier<? extends Spliterator<?>> source,
                     int sourceFlags, boolean parallel) {
        this.previousStage = null;  // 没有前置stage
        this.sourceSupplier = source;  // 数据源
        this.sourceStage = this;
        this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK;
        // The following is an optimization of:
        // StreamOpFlag.combineOpFlags(sourceOrOpFlags, StreamOpFlag.INITIAL_OPS_VALUE);
        this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
        this.depth = 0;  // 链表层级
        this.parallel = parallel;
    }

从例子出发,下一个操作是filter,即调用Head类的filter方法,从源码可以看到,filter返回了一个新的stream,这个stream是一个无状态的中间操作StatelessOp

    @Override
    public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
        Objects.requireNonNull(predicate);
        // new了一个新的对象,即一个新的stream,将this传递给了StatelessOp的构造函数
        return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SIZED) {
            @Override
            // 包装Sink的方法,后续会用到
            Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
                return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                    @Override
                    public void begin(long size) {
                        downstream.begin(-1);
                    }

                    @Override
                    public void accept(P_OUT u) {
                        if (predicate.test(u))
                            downstream.accept(u);
                    }
                };
            }
        };
    }

依次往上找到StatelessOp的构造函数,虽然和Head同继承自AbstractPipeline,但是调用了不同的构造函数,由构造函数可知,filter通过previousStage跟Head链接了起来

    /**
     * Constructor for appending an intermediate operation stage onto an
     * existing pipeline.
     *
     * @param previousStage the upstream pipeline stage
     * @param opFlags the operation flags for the new stage, described in
     * {@link StreamOpFlag}
     */
    AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
        if (previousStage.linkedOrConsumed)
            throw new IllegalStateException(MSG_STREAM_LINKED);
        previousStage.linkedOrConsumed = true;
        previousStage.nextStage = this;
        // previousStage即前一个stage,通过该属性将前后两个stage链接起来
        this.previousStage = previousStage;
        this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
        this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
        this.sourceStage = previousStage.sourceStage;
        if (opIsStateful())
            sourceStage.sourceAnyStateful = true;
        this.depth = previousStage.depth + 1;
    }

下一个操作是sorted,sorted方法返回了如下的一个类,可见该类继承自ReferencePipeline.StatefulOp,因此sorted属于StatefulOp操作。通过向上追溯,可知该操作与filter调用了同一构造函数,因此也通过previousStage跟filter链接了起来

    /**
     * Specialized subtype for sorting reference streams
     */
    private static final class OfRef<T> extends ReferencePipeline.StatefulOp<T, T> {
        ......其他代码以省略
        /**
         * Sort using natural order of {@literal <T>} which must be
         * {@code Comparable}.
         */
        OfRef(AbstractPipeline<?, T, ?> upstream) {
            // 与filter调用同一构造函数
            super(upstream, StreamShape.REFERENCE,
                  StreamOpFlag.IS_ORDERED | StreamOpFlag.IS_SORTED);
            this.isNaturalSort = true;
            // Will throw CCE when we try to sort if T is not Comparable
            @SuppressWarnings("unchecked")
            Comparator<? super T> comp = (Comparator<? super T>) Comparator.naturalOrder();
            this.comparator = comp;
        }
        ......其他代码以省略
   }

往下的map、limit、distinct以及所有其他中间操作都是类似。

这样pipeline链形成,stream的每一步操作都生成一个新的stream对象,这些对象通过previousStage连接了起来。steam的链式调用有点类似于builder模式,只不过builder每次调用返回的自己本身。

sink链

其实有了前面的pipeline链,对于stream的操作,可以从Head开始,一步一步沿着pipeline链执行就行了,没错是这样的。只不过每个stage的操作不一样,前面的stage也不知道后面的stage到底要做什么,不知道该调用下一个stage的什么方法,因此需要一个协议来规范stage的操作形式,以便相邻stage的调用。这个协议就是Sink。其实Sink是加强版的Consumer,Sink继承自Consumer,同时增加了三个方法begin、end、cancellationRequested,形成了sink协议。单纯从类的关系来说,Sink跟Stream没有半毛钱关系。

void begin(long size)

通知sink准备接收数据,必须在任何数据到来之前调用。

void end()

在处理完所有数据之后调用。

void accept(T t)

处理数据,并将数据流到下游

boolean cancellationRequested()

短路处理,如find、match等操作,不需要遍历完所有数据,可提前结束循环。

 

 

 

 

 

sink协议的四个方法相互协作完成本stage需要的操作,通常的执行顺序是这样的,首先执行begin方法,通知sink准备接收数据;然后循环数据执行accept方法,对数据进行处理,如果cancellationRequested为true则退出循环;最后执行end方法。有了sink协议,每个stage要做的就是实现这四个方法,这也是Stream API最本质的东西,pipeline链中的每个stage的包装Sink的方法来实现sink协议的。前一个stage按照此顺序调用下一个stage的Sink的方法,就可以把所有stage的操作串联起来。

来看一个Sink的例子,filter操作,重写了begin和accept方法,filter是无状态的操作不需要重写cancellationRequested和end方法。

    @Override
    public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
        Objects.requireNonNull(predicate);
        return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
                                     StreamOpFlag.NOT_SIZED) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
                // filter定义的sink
                return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
                    @Override
                    // 重写begin,调用下一stage的begin
                    public void begin(long size) {                        
                        downstream.begin(-1);
                    }

                    @Override
                    // 重写accept方法
                    public void accept(P_OUT u) {
                        // 当前stage的操作,过滤数据
                        if (predicate.test(u))
                            // 调用下一个stage的accept方法,数据流到下游stage
                            downstream.accept(u);
                    }
                };
            }
        };
    }

另一个Sink,sorted操作,该Sink完整的体现sink协议执行过程。排序动作需要全部数据,也就是需要截流,等所有数据都流到后,进行排序,然后再按顺序放到下游去。

  • bigin:只是定义了一个list,用于存放要排序的数据。
  • accept:仅仅是把数据放到list中,并没有让数据流到下游去。
  • end:对数据进行排序,并将数据流到下游Sink去,即调用下游Sink的begin、accept、end方法,有一点如果下游Sink是短路操作,当满足短路条件时退出即可,数据不用再往下流了。
    /**
     * {@link Sink} for implementing sort on reference streams.
     */
    private static final class RefSortingSink<T> extends AbstractRefSortingSink<T> {
        private ArrayList<T> list;

        RefSortingSink(Sink<? super T> sink, Comparator<? super T> comparator) {
            super(sink, comparator);
        }

        @Override
        // 1、本stage的准备工作
        public void begin(long size) {
            if (size >= Nodes.MAX_ARRAY_SIZE)
                throw new IllegalArgumentException(Nodes.BAD_SIZE);
                // 定义一个存放数据的容器
            list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
        }

        @Override
        // 3、本stage的结束处理
        public void end() {
            // 排序
            list.sort(comparator);
            // 1、下游stage的准备工作
            downstream.begin(list.size());
            // 2、下游stage的数据处理
            if (!cancellationWasRequested) {
                list.forEach(downstream::accept);
            }
            else {
                for (T t : list) {
                    // 短路操作退出
                    if (downstream.cancellationRequested()) break;
                    downstream.accept(t);
                }
            }
            // 3、下游stage的结束处理
            downstream.end();
            list = null;
        }

        @Override
        // 2、本stage的数据处理
        public void accept(T t) {
            // 仅仅是放入列表
            list.add(t);
        }
    }

有了Sink之后,事情就简单了,每个stage把自己的操作都封装到Sink中,等到适当的时候把这些Sink串联起来执行即可。这些Sink是怎么串联的呢?其实很简单,就是在Sink再加一个属性downstream,这个属性赋值就是下一个stage的Sink。这个适当的时候又是什么时候呢?就是结束操作。结束操作也会把自己的操作也封装成一个Sink,但是这个Sink没有下游了,是sink链的终点。结束操作从自己的Sink出发,沿着pipeline链从后往前,依次执行每个stage的Sink包装方法,并把自己赋值给上一个Sink的downstream,这样就把Sink串联了起来。来看结束操作串联Sink的源码:

    @Override
    @SuppressWarnings("unchecked")
    // 包装sink
    final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
        // sink为结束操作的sink
        Objects.requireNonNull(sink);
        // 结束操作是由最后一个中间操作触发的,this是最后一个stage,沿着pipeline链依次往前
        for (AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
            // 调用每个stage的包装Sink的方法,通过查看源码可知,每个包装方法都会创建一个新的Sink实例,因此每个新Sink都是包含了自己操作和下游Sink的。。。套娃
            sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
        }
        // 返回Sink套娃
        return (Sink<P_IN>) sink;
    }

到此sink链生成,执行第一个Sink就相当于执行了整个stream。

5、执行

直接看源码

    @Override
    final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
        // 包装好的Sink套娃
        Objects.requireNonNull(wrappedSink);

        if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
             // 1、准备
             wrappedSink.begin(spliterator.getExactSizeIfKnown());
             // 2、迭代
            spliterator.forEachRemaining(wrappedSink);
            // 3、结束
            wrappedSink.end();
        }
        else {
            copyIntoWithCancel(wrappedSink, spliterator);
        }
    }

这么简单、这么清晰。

6、返回

管道流执行的结果,其实也就是结束操作的执行结果,结束操作是对具体问题的具体处理,一共有四种类型:FindOp(返回Optional)、ForEachOp(无返回值,消耗型)、MatchOp(返回Boolean)、ReduceOp(返回集合等)。可见有的流没有返回值,只是产生一些副作用(就是这么翻译的),有的有返回值。

7、几个点

Head不包装Sink,Head没有操作,只负责生成流。

pipeline链从前往后生成,sink链从后往前生成,pipeline为sink而生。

sink套娃的执行顺序有点弯弯,伪代码的样子应该是这样的:

sink1.begin();
sink2.begin();
...
sinkn.begin();
for(var item : list){
    sink1.accept(item);
    sink1.accept(item);
    ...
    sinkn.accept(item);
}
sink1.end();
sink2.end();
...
sinkn.end();

四、问题

1、每执行一个操作都会循环一次数据?

不会。从上面的分析可以看出,再遇到结束操作前,前面的所有操作只是new了几个类而已。结束操作会触发循环,截流操作也会触发循环,就我目前所知,只有sorted操作又循环了一次,其他操作都没有完成的再次循环。

2、stream的性能是不是很差?

不一定。steam会有生成环境的额外消耗,如果只是简单的一个求和什么的操作,stream确实会慢,对于复杂的多级操作,或循环对象是实体类,stream的额外消耗可以忽略不计。另外并行stream相对于自己写并发程序更容易实现。

3、什么时候用stream?

我觉得绝大部分循环场景都可以用stream,优雅、易懂,现在的机器性能相对于哪一点额外的消耗,不值一提。更何况自己写的实现会有bug。。。

4、stream有多少种操作?

stream的操作其实并不多,也就是常用的哪几种。

Stream操作分类

中间操作(Intermediate operations)

无状态(Stateless)

unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek()

有状态(Stateful)

distinct() sorted() sorted() limit() skip()

结束操作(Terminal operations)

非短路操作

forEach() forEachOrdered() toArray() reduce() collect() max() min() count()

短路操作(short-circuiting)

anyMatch() allMatch() noneMatch() findFirst() findAny()

 


 

 

 

;