定义
作为Jdk8的新特性,stream流提供了对集合的各种聚合操作,可以理解为更高级的迭代器,配合lambda表达式,是程序更加简洁易懂。
看懂本文需要会使用lambda表达式做为基础,如果不会可以先看下对于lambda式的介绍。
自顶向下的代码环节
//把list的所有元素都加100并排序,生成一个result集合
List<Integer> result = list.stream().map((a)-> a+100).sorted().collect(Collectors.toList());
从这句简单的代码出发,逐步拆解。
list.stream()
public interface Collection<E> extends Iterable<E> {
//省略上面...
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}
其实就是返回一个Stream类的对象。Stream类有很多子类,我们接下来在介绍。
jdk8中在Collection接口中新增了几个默认方法。默认方法也是jdk8的新特性,即在接口里定义实现好的方法。我们使用的stream() 会调用StreamSupport.stream(spliterator(), false)。我们来解释下这个两个参数的作用。
spliterator()
StreamSupport.stream()的第一个参数
底层其实就是返回一个Spliterator对象,翻译一下就是分割器。可以理解为一种多线程并发迭代的迭代器iterator,即把数组分割成多段由多线程进行操作,线程安全,最大程度利用cpu的性能。
并行设计也是Jdk8的一大特性,像ConcurrentHashMap的并发扩容设计就是和这个Spliterator有关。
parallel true||false
StreamSupport.stream()的第二个参数
就是一个布尔值。表示是否需要多线程处理,即利用刚才提到的spliterator。
那么这里就要返回到刚才的list.stream()了。我源码有列出新增的两个默认方法,parallelStream()和stream()。他们的区别就是底层的StreamSupport.stream() 方法第二个参数是否为true,即一个并行,一个串行。
StreamSupport.stream(Spliterator,parallel)
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
//判定是否null
Objects.requireNonNull(spliterator);
return new ReferencePipeline.Head<>(spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}
我们再来看这个StreamSupport.stream()的源码。除去一个校验,实际就是返回了一个ReferencePipeline的Head类对象,这个对象其实就是Stream的子类,这个是比较复杂的,需要从架构设计上去理解什么是Head类。
ReferencePipeline
先介绍下ReferencePipeline抽象类
首先先看下这个类的类图。
ReferencePipeline是一个抽象类,实现了stream接口,继承了pipelineHelper类。他最重要的是有三个静态内部类,重点来了!
- Head 头
- StatefulOp 有状态
- StatelessOp 无状态
要想解释这几个东西是干嘛的,要自顶向下的看。来看最开始写的实例代码。
Stream的操作分类
//把list的所有元素都加100并排序,生成一个result集合
List<Integer> result = list.stream().map((a)-> a+100).sorted().collect(Collectors.toList());
我们通常把Stream的操作分为两大类,(即stream()后面的那些操作)。中间操作和终结操作。
中间操作只是对操作进行了记录,像map(),也称为惰性求值。终结操作才会触发计算逻辑,像collect()。
。终结操作还可分为短路操作和非短路操作。
-
终结操作
- 短路
- 非短路
中间操作
中间操作还可以细分为无状态(Stateless)操作和有状态(Stateful)操作
- 无状态 Stateless
- 有状态 Stateful
看到这个无状态和有状态的英文,对应上面的内部类,你应该就有点头绪了把,但我们先介绍完。
无状态操作指的是指元素的处理不受之前元素的影响,可以一个一个即时处理。像map(),filter()这种
有状态操作就是不可以一个一个处理,需要拿到全部元素才能继续。例如sort()。排序是需要获取全部元素的。
终结操作
终结操作还可以细分为短路操作和非短路操作,前者是指遇到某些符合条件的元素就可以得到最终结果;而后者是指必须处理所有元素才能得到最终结果
操作结构
每一个操作都被封装成一个节点,也叫做Stage,然后通过前后指针连接起来,形成了一条双向链表。那么第一个节点就是Head节点,接下来的中间操作就是无状态或有状态节点了。这就对应到了上文的三个内部类。最后的终结操作会生成一个TerminalOp节点。
map()
我们接着看源码,以map() 为切入点来看。
//把list的所有元素都加100并排序,生成一个result集合
List<Integer> result = list.stream().map((a)-> a+100).sorted().collect(Collectors.toList());
由上文可知这边stream()已经返回了一个双向链表的Head结点,即stream对象(stream是Head的父类),接着执行map()。
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));
}
};
}
};
}
本质上也是返回一个Stream对象,也就是StatelessOp内部类。但是在返回这个内部类时,重写了opWrapSink() 。这里我们就要说下上面提到的那些内部类的作用了,就是记录操作,在终结操作时再执行,那么他记录的方式就是通过Sink对象记录。我们接下来讲一下Sink。
Sink
Sink是一个接口,有四个重要的方法。
// 开始遍历元素之前调用该方法,通知Sink做好准备
default void begin(long size)
// 所有元素遍历完成之后调用,通知Sink没有更多的元素了
default void end()
// 是否可以结束操作,可以让短路操作尽早结束
default boolean cancellationRequested()
// Stage 把自己包含的回调方法封装到该方法里,前一个Stage只需要调用当前 Stage.accept(T t)方法就行了
default void accept(int value)
那些节点的操作重写进accept()方法里,终结操作处理数据时只需要从head节点开始对数据执行每个节点的sink的accept()方法就可以了。对照下刚才重写的代码看一下,就明白了。
对于有状态的操作,Sink的begin()和end()方法是必须实现的。例如Stream.sorted(),begin()方法创建了一个存储元素的容器,accept()方法负责将元素添加到该容器,最后end()实现对容器中的元素进行排序,并决定了下游 Sink 如何执行
collect()
小结一下,每个操作都对应一个在双向链表的stage节点,里面通过sink记录下了操作,等待终结操作时从head节点开始执行。所以我们来看下执行终结操作的流程,我这里举例为串行的collect()。
public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {
A container;
//省略并行的内容
container = evaluate(ReduceOps.makeRef(collector));
return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)
? (R) container
: collector.finisher().apply(container);
}
核心就只有一个evaluate(ReduceOps.makeRef(collector))。
里面的ReduceOps.makeRef(collector)就是生成返回一个ReduceOps(就是TerminalOP终结节点的子类)。然后再来看evaluate方法。
evaluate()
final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
return terminalOp.evaluateSequential(this,sourceSpliterator(terminalOp.getOpFlags()));
}
执行终结节点的evaluateSequential方法,这个evaluateSequential方法对于不同的节点类型例如foreachOp,reduceOP等等,也是不同的,但是里面都会有相同的一个方法,就是wrapAndCopyInto。
public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
Spliterator<P_IN> spliterator) {
return helper.wrapAndCopyInto(makeSink(), spliterator).get();
}
makesink()就是创建这个终结节点的sink对象
wrapAndCopyInto() 重点
@Override
final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
return sink;
}
调了这么多方法,终于到了关键点。两个重要的方法
- copyInto() 把链表里的所有的sink总头到尾执行一遍
- wrapSink() 返回了一个实现了sink接口方法的sink
wrapSink()
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;
}
我们看到了熟悉的方法,opWrapSink(),这个就是我在map()那里重写的方法,忘记的同学可以回去看一下,也就是创建并返回了一个实现了accpet()方法的sink。我们之前sink对象一直没有创建,是在这里创建sink对象。
是一个循环,从尾结点开始遍历到头,执行opWrapSink()。这个方法的作用就是
- 使用当前Sink包装的回调函数处理u
- 将处理结果传递给流水线下游的Sink
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重写的方法,new Sink.ChainedReference构造器的意思是创建一个sink节点,这个节点的downstream(下一个节点)就是sink参数。在循环的第一步,这里的sink参数就是终结节点。表示创建一个sink节点,sink的downstream为下一个sink节点。所以这个循环执行完最后会得到一个sink对象。这个对象里包含了下一个,再下一个,再再下一个...的sink节点,有点类似于链表。
也就是说从尾部出发,创建重写了方法的sink对象的同时,并把它们连接起来,这就是这个方法的作用。
copyInto()
wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();
执行begin和end方法在开始和结束的时候。中间执行了forEachRemaining方法,其实就是类似于arraylist的foreach方法,每个元素都会调用sink的accept方法,由上文可知,accept执行之后还会执行下一个sink的方法,通过这种方式遍历。
流程总结
上面说了很多,我在这里总结一下流程。还是以代码为例
List<Integer> result = list.stream().map((a)-> a+100).collect(Collectors.toList());
- stream() 创建stream对象,一个Head头结点
- map() 一个中间操作的无状态操作,生成一个无状态结点,是stream类的子类,与head形成双向链表。这个无状态结点重写了一个opWrapSink方法。
- collect() 一个终结操作,执行到这就会触发计算逻辑。从尾节点开始向头结点遍历,依次执行opWrapSink方法,生成sink对象并形成链。然后从头sink对象开始执行sink的accept()方法。
小结
每个操作的底层都是不一样的,但是有着共同点,大体的思路就是这样。其中jdk1.8又引入了很多并行的设计,底层也就是使用了线程池进行多线程调用,这个地方就不重点介绍了。如果有什么问题或者错误的地方可以评论我一下。雀氏是这样的,我都说雀氏了。