JAVA基础8-Stream原理学习笔记

1. 概念理解

首先理解几个概念。
中间操作: 只是一种标记,只有结束操作才会触发实际计算。比如map、filter、sort操作
结束操作: 最终开始做计算操作,回调接口。比如collect、max、anyMatch操作
无状态操作: 指元素的处理不受前面元素的影响。比如map操作
有状态操作: 必须等到所有元素处理之后才知道最终结果。比如sort操作
非短路操作: 指需要处理全部元素才可以返回结果。比如collect、max操作
短路操作: 指不用处理全部元素就可以返回结果,比如找到第一个满足条件的元素。比如anyMatch、noneMatch。

2. 流程理解

2.1 操作记录

中间操作只是一个标记,那么只需要记录操作即可。很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是<数据来源,操作,回调函数>构成的三元组。Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线。
在Stream中,使用ReferencePipeline(当然还有IntPipeline, LongPipeline, DoublePipeline处理基本类型int、long和double的,这里就以普遍性来将流程)来存储每个stage的操作。
ReferencePipeline的子类有三种:Head、StatelessOp和StatefulOp。Head代表头部操作(Stream.of等生成stream的方法);StatelessOp代表无状态操作,StatefulOp代表有状态操作。结构图如下:
在这里插入图片描述

2.2 流程操作

通过操作记录,我们可以推出Stream的流程操作如下图:
在这里插入图片描述
图中通过Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作 。这就是Stream记录操作的方式。

2.3 操作叠加

这里有一个问题,每个stage只记录自己操作,并不知道下一个操作是什么。所以需要一个规范来定义stage之间如何调用,那么Sink接口就诞生。此接口自定义4个接口:

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

这样,每个stage都封装在Sink下面,按照Sink的流程来使用即可。Sink的调用流程是:先执行begin()方法,再执行accept()方法,再执行cancellationRequested()方法,最后执行end()方法。对于有状态的操作来说,必须实现begin()和end()方法。对于短路操作来说,必须实现cancellationRequested()方法
每个stage如何包装成一个Sink呢?是使用AbstractPipeline的opWrapSink()方法,每个实现AbstractPipeline的类都需要实现这个方法,这个方法就是将每个stage封装为一个Sink。关键代码在于AbstractPipeline的wrapSink()方法,此方法循环调用流水线的opWrapSink()方法,得到流水线上最开始那个操作的Sink,这个Sink包含流水线从头到尾的操作,所以只需这个Sink就执行整个流水线操作,执行代码在AbstractPipeline的copyInto()方法中。代码如下:

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;
}

final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
        Objects.requireNonNull(wrappedSink);
        if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
            wrappedSink.begin(spliterator.getExactSizeIfKnown());
            spliterator.forEachRemaining(wrappedSink);
            wrappedSink.end();
        }
        else {
            copyIntoWithCancel(wrappedSink, spliterator);
        }
}

用示例图可以如下:
在这里插入图片描述
下面通过例子来解释不同操作如何实现opWrapSink()方法,以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));
                    }
                };
            }
        };
}

map函数就是返回一个StatelessOp操作,而这个StatelessOp操作需要实现抽象类AbstractPipeline的opWrapSink()方法,map函数实现的opWrapSink()方法就是将mapper函数封装进入Sink的accept。因为map是无状态操作,所以只需要实现accept即可。我们可以看到accept方法就是调用mapper的apply,然后将元素传给下一个操作的accept。

我们再看另外一个方法sort。可以看到sort里面调用SortedOps.makeRef(this),通过这个方法创建一个OfRef(该类也是继承StatefulOp,StatefulOp也继承至AbstractPipeline),所有需要实现opWrapSink方法。opWrapSink方法通过判断Stream类型来创建不同的Sink,下面代码就是创建的RefSortingSink方法:

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
        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
        public void end() {
            list.sort(comparator);
            downstream.begin(list.size());
            if (!cancellationWasRequested) {
                list.forEach(downstream::accept);
            }
            else {
                for (T t : list) {
                    if (downstream.cancellationRequested()) break;
                    downstream.accept(t);
                }
            }
            downstream.end();
            list = null;
        }
        @Override
        public void accept(T t) {
            list.add(t);
        }
    }

由于sort是一个有状态操作,所以除了实现accept方法外,还需要实现begin和end方法。

  1. begin方法是将告诉这次操作总共有多少个元素;
  2. accept方法是将上游传过来的元素放入一个list中,循环操作;
  3. end方法是执行排序,并调用下一个操作。
  4. 可以看end的方法是如何执行下一个操作,先是调用begin操作告诉下一个操作有多少个元素,然后判断是否为短路操作。如果是非短路操作,循环调用下一个操作的accept方法;如果是短路操作,判断可中断,随时中断操作。最后调用下一个操作的end方法。

2.4 结果存储

结束操作需要返回数据,不同结束方法返回数据类型不一样,可分为以下几类:

返回类型 对应的结束操作
boolean anyMatch() allMatch() noneMatch()
Optional findFirst() findAny()
归约结果 reduce() collect()
数组 toArray()

上面几种类型的结束操作都是实现TerminalOp接口,各自存储数据,通过evaluateSequential(同步)或者evaluateParallel(并行)返回结果。

2.5 并行操作

并行操作实际就是多个线程一起完成任务。在Stream不是所有操作都可以并行操作,有状态中间操作就必须等到所有元素就位才能操作,比如sort操作。所以如果一个流水线存在一个有状态操作,并且使用并行,那么就会生成2条流水线,有状态操作之前的流水线和有状态操作之后的流水线。
在源码中,AbstractPipeline的evaluate ()方法,执行时判断使用哪种模式。

final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
        assert getOutputShape() == terminalOp.inputShape();
        if (linkedOrConsumed)
            throw new IllegalStateException(MSG_STREAM_LINKED);
        linkedOrConsumed = true;
        return isParallel()? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags())) : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}

Stream的并行处理是基于ForkJoin框架的。

3. Stream的“坑”

3.1 并行操作的“坑”

并行是多线程,如果你想在处理过程中保存集合(List、Map、Set等)时,一定是创建线程安全的集合。不然可能出现情况包括:结果不正确(非线程安全加入删除元素可能出现不正确)、程序报错(使用非线程线程安全操作)

3.2 Stream.of(T)的“坑”

此方法只能作为引用类型数据转换为Stream,如果是基本类型(int、long等)则会转换为一个数组对象。以下代码输出的不是1、2、3、4、5,而是数组的地址。

int[] array = {1, 2, 3, 4, 5};
 //Stream.of
 Stream<int[]> stream2 = Stream.of(array);
stream2.forEach(x -> System.out.println(x));

猜你喜欢

转载自blog.csdn.net/linwu_2006_2006/article/details/95032894
今日推荐