一、简介
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() |