前言
在Java8中另外一个比较大且非常重要的改动就是Stream。它规范及简化了程序的统计过程。举个例子:
//过程式的解决方案
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(), 保留最长的长度
}
}
//stream流的解决方案
int longestStringLengthStartingWithA
= strings.stream()
.filter(s -> s.startsWith("A"))
.mapToInt(String::length)
.max();
stream流的解决方案解开了代码细节和业务逻辑的耦合,类似于sql语句,表达的是"要做什么"而不是"如何去做",使程序员可以更加专注于业务逻辑,写出易于理解和维护的代码。
Stream-操作分类
作为一计算框架,其基础是操作拆解,归类。常用操作分类:
Stream上的所有操作分为两类:中间操作和结束操作。
- 中间操作只是一种标记(只有结束操作才会触发实际计算)。中间操作又可以分为无状态的(Stateless)和有状态的(Stateful)。
- 无状态中间操作是指元素的处理不受前面元素的影响,
- 有状态的中间操作必须等到所有元素处理之后才知道最终结果,
- 结束操作,会触发实际计算(带动中间操作).其又可分为短路操作和非短路操作。
- 短路操作,是指不用处理全部元素就可以返回结果.
- 非短路操作,要全部执行完成.
有如此精细的划分,底层可以对每一种情况的处理方式不同。
Stream-执行计划的构建
当仅仅当“结束操作”的时,才会进行整个流程的计算。Stream是如果保存串联“中间操作”?
Stream完整的中间操作定义成<数据来源,操作,回调函数>的三元组,双用双向链表的方式构建。
图中通过Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是Stream记录操作的方式。
操作叠加
针对无状态和有状态及断路操作,JDK在回调函数sink上进行了优化。对Sink接口包含的方法如下表所示:
方法名 | 作用 |
---|---|
void begin(long size) | 开始遍历元素之前调用该方法,通知Sink做好准备 |
void end() | 所有元素遍历完成之后调用,通知Sink没有更多的元素了 |
boolean cancellationRequested() | 是否可以结束操作,可以让短路操作尽早结束 |
void accept(T t) | 遍历元素时调用,接受一个待处理元素,并对元素进行处理 |
- 对于filter,直接在accept中把符合条件的数据传给下一个sink
- 对于sorted,begin时创建数组,accept插入排序,end时触发下一个
- 对于findAny,直接让cancellationRequested为true,结束传递
操作执行
Sink完美封装了Stream每一步操作,并给出了[处理->转发]的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。
//ReferencePipeline::collect
//AbstractPipeline::evaluate
//ReduceOp::evaluateSequential
//AbstractPipeline::wrapAndCopyInto
//AbstractPipeline::wrapSink
/**
* @param sink 表示Terminal Operation的sink
* return 表示第一个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;
}
//ReferencePipeline::collect
//AbstractPipeline::evaluate
//ReduceOp::evaluateSequential
//AbstractPipeline::wrapAndCopyInto
//AbstractPipeline::copyInto
/**
* @param wrappedSink 表示第一个sink
* @param spliterator 数据源
**/
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);
}
}
上述代码首先调用wrappedSink.begin()方法告诉Sink数据即将到来,然后调用spliterator.forEachRemaining()方法对数据进行迭代Spliterator,最后调用wrappedSink.end()方法通知Sink数据处理结束
Spliterator 是容器的一种迭代器
结果收集
根据需要的结果不同,通过构建不同Terminal Operation的sink来实现的
返回类型 | 对应的结束操作 |
---|---|
boolean | anyMatch() allMatch() noneMatch() |
Optional | findFirst() findAny() |
归约结果 | reduce() collect() |
数组 | toArray() |
- 对于表中返回boolean或者Optional的操作(Optional是存放 一个 值的容器)的操作,由于值返回一个值,只需要在对应的Sink中记录这个值,等到执行结束时返回就可以了。
- 对于归约操作,最终结果放在用户调用时指定的容器中(容器类型通过收集器指定)。collect(), reduce(), max(), min()都是归约操作,虽然max()和min()也是返回一个Optional,但事实上底层是通过调用reduce()方法实现的。
- 对于返回是数组的情况,毫无疑问的结果会放在数组当中。这么说当然是对的,但在最终返回数组之前,结果其实是存储在一种叫做Node的数据结构中的。Node是一种多叉树结构sink,元素存储在树的叶子当中,并且一个叶子节点可以存放多个元素。这样做是为了并行执行方便.
并行原理
以上讲的是都是基于串行的。Java Stream还提供了并行的支持。相比串行操作,并行执行中的难度为:有状态中间操作。串行操作可以很方面的利用sink的begin和end方法汇聚数据,执行。而并行呢?Java Stream则将整个链路以“有状态中间操作” 进行split.
//ReferencePipeline::collect
//AbstractPipeline::evaluate
//AbstractPipeline::sourceSpliterator
private Spliterator<?> sourceSpliterator(int terminalFlags) {
//如果是并行流并且有stage包含stateful操作
if (isParallel() && sourceStage.sourceAnyStateful) {
int depth = 1;
//那么就会依次遍历stage,直到遇到stateful stage时
for (AbstractPipeline u = sourceStage, p = sourceStage.nextStage, e = this;
u != e;
u = p, p = p.nextStage) {
//p是有状态操作
if (p.opIsStateful()) {
depth = 0;
//尽量以惰性求值的方式进行操作,将前面一半执行得出结果
spliterator = p.opEvaluateParallelLazy(u, spliterator);
}
p.depth = depth++;
}
}
return spliterator;
}
任务如何拆分和存储
借助于Fork/Join框架,把任务按以下规则进行拆分。
每个任务产生的数据都将成为(Node是一种多叉树结构sink)的一个child,最后通过合并child成最终的结果。
主要参考
《深入理解Java Stream流水线》
《Java8中Spliterator详解》
《java8Stream原理深度解析》