参考资料:
https://www.cnblogs.com/CarpenterLee/archive/2017/03/28/6637118.html
1、关键问题
Stream流API是如何实现的呢?
Pipeline是怎么执行的,每次方法调用都会导致一次迭代吗?
自动并行又是怎么做到的,线程个数是多少?
2、流水线
Stream上的所有操作分为两类:中间操作和结束操作。
中间操作根据是否受到前面的元素影响,又可以分为无状态的(Stateless)和有状态的(Stateful),比如排序是有状态操作,在读取所有元素之前并不能确定排序结果;
结束操作又可以分为短路操作和非短路操作,短路操作是指不用处理全部元素就可以返回结果.
之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。
流水线的实现方式:
java 7
java 8
int longest = 0;
for(String str : strings){
if(str.startsWith("A")){// 1. filter(), 保留以A开头的字符串
int len = str.length();// 2. mapToInt(), 转换成长度
longest = Math.max(len, longest);// 3. max(), 保留最长的长度
}
}
int longestStringLengthStartingWithA
= strings.stream()
.filter(s -> s.startsWith("A"))
.mapToInt(String::length)
.max();
Stream类库设计在的问题在于并不知道用户的意图具体是什么,所以Stream流水线方案要解决的问题有:
用户操作如何记录?
操作如何叠加?
叠加之后的操作如何执行?
执行后的结果(如果有)在哪里?
2.1、操作如何记录
Stream使用阶段(Stage)的概念来描述完整的操作<数据来源,操作,回调函数>,跟Stream相关类和接口的继承关系图示
这里的PipelineHelper的实例代表了Stage,Head表示地一个Stage,即将数据源转换为Stream流。(从StreamSupport类作为入口,可以找到上图中的类,都位于java.util.stream包内)
Stream流水线组织结构示意图如下:这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。
2.2、操作如何叠加
由于每个Stage只知道自己所要执行的动作,所以需要Sink接口(在ReferencePipeline类中)在协调相邻Stage之间的调用关系。
Sink接口包含的方法
方法名
作用
void begin(long size)
开始遍历元素之前调用该方法,通知Sink做好准备。
void end()
所有元素遍历完成之后调用,通知Sink没有更多的元素了。
boolean cancellationRequested()
是否可以结束操作,可以让短路操作尽早结束。
void accept(T t)
遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。
当然对于有状态的操作,Sink的begin()和end()方法也是必须重写的。比如Stream.sorted(),它是一个有状态的中间操作。
对于短路操作,Sink.cancellationRequested()也是必须重写的。比如Stream.findFirst(),它是短路操作。
每个Stage都会把自己的操作封装到Sink中,Sink采用[处理->转发]的模式来叠加操作。
Stream的中间操作是如何将自身的操作包装成Sink以及Sink是如何将处理结果转发给下一个Sink
// Stream.map(),调用该方法将产生一个新的Stream
public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
...
return new StatelessOp<P_OUT, R>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
@Override /*opWripSink()方法返回由回调函数包装而成Sink*/
Sink<P_OUT> opWrapSink(int flags, Sink<R> downstream) {
return new Sink.ChainedReference<P_OUT, R>(downstream) {
@Override
public void accept(P_OUT u) {
R r = mapper.apply(u);// 1. 使用当前Sink包装的回调函数mapper处理u
downstream.accept(r);// 2. 将处理结果传递给流水线下游的Sink
}
};
}
};
}
这里的处理都是延迟加载的。
-
Stream.sorted()方法的示例,这是一个有状态的中间操作
// Stream.sort()方法用到的Sink实现
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
private ArrayList<T> list;// 存放用于排序的元素
RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
super(downstream, comparator);
}
@Override
public void begin(long 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) {// 下游Sink不包含短路操作
list.forEach(downstream::accept);// 2. 将处理结果传递给流水线下游的Sink
}
else {// 下游Sink包含短路操作
for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。
if (downstream.cancellationRequested()) break;
downstream.accept(t);// 2. 将处理结果传递给流水线下游的Sink
}
}
downstream.end();
list = null;
}
@Override
public void accept(T t) {
list.add(t);// 1. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
}
}
1、首先beging()方法告诉Sink参与排序的元素个数,方便确定中间结果容器的的大小;
2、之后通过accept()方法将元素添加到中间结果当中,最终执行时调用者会不断调用该方法,直到遍历所有元素;
3、最后end()方法告诉Sink所有元素遍历完毕,启动排序步骤,排序完成后将结果传递给下游的Sink;
4、如果下游的Sink是短路操作,将结果传递给下游时不断询问下游cancellationRequested()是否可以结束处理。
2.3、叠加之后操作如何执行
调用终止(Terminal)操作启动流水线。
终止操作会创建一个包装了自己操作的Sink,这个Sink只需要处理数据而不需要将结果传递给下游的Sink。
上游的Sink是如何找到下游Sink的?
使用opWrapSink()可以将当前操作与下游Sink(上文中的downstream参数)结合成新Sink。
// AbstractPipeline.wrapSink()
// 从下游向上游不断包装Sink。如果最初传入的sink代表结束操作,
// 函数返回时就可以得到一个代表了流水线上所有操作的Sink。
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
...
for (AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
return (Sink<P_IN>) sink;
}
现在流水线上从开始到结束的所有的操作都被包装到了一个Sink(相当与头节点)里,执行这个Sink就相当于执行整个流水线。
每个Sink的执行代码如下:
// AbstractPipeline.copyInto(), 对spliterator代表的数据执行wrappedSink代表的操作。
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
...
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历
spliterator.forEachRemaining(wrappedSink);// 迭代处理所有数据
wrappedSink.end();// 通知遍历结束
}
...
}
2.4、执行的结果在哪里
对于表中返回boolean或者Optional的操作(Optional是存放 一个 值的容器)的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。
对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()和min()也是返回一个Optional,但事实上底层是通过调用reduce()方法实现的。
对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便。关于Node的具体结构,我们会在下一节探究Stream如何并行执行时给出详细说明。