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方法。
- begin方法是将告诉这次操作总共有多少个元素;
- accept方法是将上游传过来的元素放入一个list中,循环操作;
- end方法是执行排序,并调用下一个操作。
- 可以看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));