前言
上文Java8 Stream实现原理,源码浅析(上)我们介绍了流的创建和中间操作,下面讲一讲Stream的终止操作
三、Stream的终止
终止操作的分类与共性
终止操作又分为「短路操作」和「非短路操作」。
之后又根据不同作用分类。
下面列出一些常用API,读者可以尝试观察它们的共同点和不同点。
短路操作:可以提前终止的操作
如果是短路操作,那么不一定需要完整的遍历整个Stream的元素,在某些条件下,可以提前得到结果,提前结束遍历过程。短路操作有MatchOp和FindOp两个TerminalOp实现,分别表示匹配和查找。
cancellationRequested():如果返回true,表示sink不再处理Stream中后续的元素,用于短路操作。
匹配操作:MatchOp
anyMatch
有一个匹配就立刻返回true
java
public final boolean anyMatch(Predicate<? super P_OUT> predicate) {
return evaluate(MatchOps.makeRef(predicate, MatchOps.MatchKind.ANY));
}
allMatch
有一个不匹配就立刻返回false
java
public final boolean allMatch(Predicate<? super P_OUT> predicate) {
return evaluate(MatchOps.makeRef(predicate, MatchOps.MatchKind.ALL));
}
查找操作:FindOp
findFirst:返回第一个元素
虽然名字叫find但findAny在并行流就是随便返回集合内一个元素,在串行流,就是返回集合的首元素,挺没用的,不支持自定义过滤。要自己匹配还得用match。
java
@Override
public final Optional<P_OUT> findFirst() {
return evaluate(FindOps.makeRef(true));
}
findAny在串行流与findFirst完全相同,如果是并行流,不一定是第一个。
非短路操作
遍历:ForEachOp
forEeah
java
public void forEach(Consumer<? super P_OUT> action) {
evaluate(ForEachOps.makeRef(action, false));
}
forEachOrdered :在串行流中,与forEeah的结果没区别。这个是为了保证并行流对元素的处理顺序是从前往后
聚合:ReduceOp
reduce:聚合
java
@Override
public final Optional<P_OUT> reduce(BinaryOperator<P_OUT> accumulator) {
return evaluate(ReduceOps.makeRef(accumulator));
}
max/min:求最大最小元素
java
public final Optional<P_OUT> max(Comparator<? super P_OUT> comparator) {
return reduce(BinaryOperator.maxBy(comparator));
}
也就是max/min实际调用的是上面这个reduce方法
collect:将流转化为集合
java
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);
}
终止操作的流程🚩
- 所有方法都根据作用分类成了不同的Op,它们的父类都是TerminalOp。调用了对应的xxxOps工厂类的makeRef方法得到一个TerminalOp的实现类。
- 执行evaluate方法。
简单说,就是利用工厂类,根据你的特点,生成一个终止操作:TerminalOp,然后evaluate。
1、TerminalOp:终止操作
这里插一嘴,TerminalOp不像前面都继承自pipeline。TerminalOp自己就是个顶层的父接口。
并且TerminalOp并不会像其他中间操作一样仍返回一个Stream。
java
interface TerminalOp<E_IN, R> {
default StreamShape inputShape() { return StreamShape.REFERENCE; }
default int getOpFlags() { return 0; }
// 并行 evaluate
default <P_IN> R evaluateParallel(PipelineHelper<E_IN> helper,
Spliterator<P_IN> spliterator) {
// 简略了一下
return evaluateSequential(helper, spliterator);
}
// 串行 evaluate 抽象方法
<P_IN> R evaluateSequential(PipelineHelper<E_IN> helper,
Spliterator<P_IN> spliterator);
}
2、evaluate():执行操作
java
final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
// 忽略校验
return isParallel()
? // 并行
terminalOp.evaluateParallel(this,sourceSpliterator(terminalOp.getOpFlags()))
: // 串行
terminalOp.evaluateSequential(this,sourceSpliterator(terminalOp.getOpFlags()));
}
也就是真正的执行是terminalOp.evaluateSequential方法,是写在terminalOp内部的
那么核心点就在于terminalOp了。
四大TerminalOp架构/共同点
- 都是工厂模式,由xxxOps.makeRef方法创建对象。
- 都重写了evaluateXXX方法,即一个Stream流语句的最终执行
- 都实现了TerminalOp接口
- evaluateXXX方法最终都是调用pipeline的wrapAndCopyInto方法
ForEachOp
java
static abstract class ForEachOp<T>
implements TerminalOp<T, Void>, TerminalSink<T, Void> {
private final boolean ordered;
}
TerminalOp接口:前文介绍过,主要是提供了evaluate方法的具体实现
TerminalSink接口:继承了Sink和Supplier,Sink继承自Consumer,有消费能力的同时提供了begin/end。Supplier也是lambda表达式,无参有返回值。
重点是重写了evaluate方法
java
// 因为foreach没有返回值,所以get返回null this就是当前的TerminalOp
public <S> Void evaluateSequential(PipelineHelper<T> helper,
Spliterator<S> spliterator) {
return helper.wrapAndCopyInto(this, spliterator).get();
}
ReduceOp
java
private static abstract class ReduceOp<T, R, S extends AccumulatingSink<T, R, S>>
implements TerminalOp<T, R> {
// makeSink方法抽象
public abstract S makeSink();
// 不关注并行了。实现了evaluateSequential方法
@Override
public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
Spliterator<P_IN> spliterator) {
return helper.wrapAndCopyInto(makeSink(), spliterator).get();
}
}
FindOp
java
private static final class FindOp<T, O> implements TerminalOp<T, O> {
// 核心属性
private final StreamShape shape;
final boolean mustFindFirst;
final O emptyValue;
final Predicate<O> presentPredicate;
final Supplier<TerminalSink<T, O>> sinkSupplier;
// 方法
public <S> O evaluateSequential(PipelineHelper<T> helper,Spliterator<S> spliterator) {
O result = helper.wrapAndCopyInto(sinkSupplier.get(), spliterator).get();
return result != null ? result : emptyValue;
}
}
可以看到也是wrapAndCopyInto方法。
MatchOp
java
private static final class MatchOp<T> implements TerminalOp<T, Boolean> {
private final StreamShape inputShape;
final MatchKind matchKind;
final Supplier<BooleanTerminalSink<T>> sinkSupplier;
public <S> Boolean evaluateSequential(PipelineHelper<T> helper,
Spliterator<S> spliterator) {
return helper.wrapAndCopyInto(sinkSupplier.get(), spliterator).getAndClearState();
}
}
wrapAndCopyInto方法
四大Op的evaluateSequential方法都调用了pipeline的wrapAndCopyInto方法。
正如方法名:
- wrap包装sink
- 执行CopyInto方法
wrapAndCopyInto方法的实现在AbstractPipeline
java
final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
return sink;
}
1、封装Sink
这里首先有一个wrapSink方法,对sink的封装
java
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;
}
可以看到,往回遍历双向链表然后调用opWrapSink方法,仍然返回一个sink。
opWrapSink方法还记得吗?是所有不管有无状态的Op即中间操作必须重写的方法。
这里回顾一下map的:
java
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
// 返回 Sink.ChainedReference
return new Sink.ChainedReference<P_OUT, R>(sink) {
@Override
public void accept(P_OUT u) {
// 这个mapper就是 map(mapper) lambda表达式,比如 映射到Id就是 User::getId
downstream.accept(mapper.apply(u));
}
};
}
ChainedReference有一个核心属性:
java
protected final Sink<? super E_OUT> downstream;
// 即下一个的Sink,从而实现Sink的相连
又重写了accept方法,即当前Op先apply结束以后,再调用下一个的Op,从而实现了所有Op的相连,返回了一个最终的Sink。
至此确定了copyInto的wrappedSink参数。
2、执行copyInto:遍历处理
java
// wrapAndCopyInto 调用 copyInto
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();
}
else {
copyIntoWithCancel(wrappedSink, spliterator);
}
}
SHORT_CIRCUIT是短路的意思,因此如果是短路操作,进入 copyIntoWithCancel;否则forEachRemaining
2.1、若短路操作
java
final <P_IN> void copyIntoWithCancel(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
// 链表尾
AbstractPipeline p = AbstractPipeline.this;
while (p.depth > 0) {
p = p.previousStage;
}
wrappedSink.begin(spliterator.getExactSizeIfKnown());
// 依次消费所有元素
p.forEachWithCancel(spliterator, wrappedSink);
wrappedSink.end();
}
final void forEachWithCancel(Spliterator<P_OUT> spliterator, Sink<P_OUT> sink) {
do { } while (!sink.cancellationRequested() && spliterator.tryAdvance(sink));
}
// tryAdvance就是,用lambda表达式的accept方法消费一个元素,sink就是consumer的子类,有accept方法
2.2、若非短路操作
非短路操作:
- 先begin
- 后spliterator.forEachRemaining(wrappedSink);
- 最后end
begin和end都被StatexxxOp重写,而forEachRemaining又被各种spliterator重写
这里以ArrayListSpliterator为例:
java
public void forEachRemaining(Consumer<? super E> action) {
int i, hi, mc; // hoist accesses and checks from loop
ArrayList<E> lst; Object[] a;
if ((lst = list) != null && (a = lst.elementData) != null) {
if ((hi = fence) < 0) {
mc = lst.modCount;
hi = lst.size;
}
else
mc = expectedModCount;
if ((i = index) >= 0 && (index = hi) <= a.length) {
for (; i < hi; ++i) {
E e = (E) a[i];
action.accept(e);
}
if (lst.modCount == mc)
return;
}
}
throw new ConcurrentModificationException();
}
就是遍历处理。
在evaluateSequential方法的最后,通常还有一个get方法,用于获取返回值。
至此,终止操作分析完毕。
小结
终止操作:大概是创建一个TerminalOp,然后调用evaluate方法处理。evaluate最终依赖wrapAndCopyInto,会遍历双向链表来封装一个链式Sink,然后遍历元素调用lambda表达式即sink来处理。
中间操作是如何生效的
非短路下:先begin方法。再accpet。最后end。
一般都是链式调用,我处理完了,给我下一个处理。但是不乏特殊的,比如sorted:
java
public void begin(long size) {
list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
}
public void end() {
// 排序
list.sort(comparator);
downstream.begin(list.size());
// 非短路 全部处理
if (!cancellationWasRequested) {
list.forEach(downstream::accept);
}
else {
// 短路 随时可能会break
for (T t : list) {
if (downstream.cancellationRequested()) break;
downstream.accept(t);
}
}
downstream.end();
list = null;
}
public void accept(T t) {
// 并没有向下传递
list.add(t);
}
begin,accept方法并不是链式调用,相反,begin直接停住了new一个ArrayList,accept是往自己的ArrayList添加元素。那么我们知道后面的accept方法就不会被调用到了,于是开始end。此时又会让下游begin,accept,end走一套。
collect方法的参数:Collector
collect方法与别的终止操作有些不一样。
Collectors.toList()返回一个Collector接口,这个方法做了些什么呢?
Collector接口
java
public interface Collector<T, A, R> {
// 四个lambda方法
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
// Characteristics枚举类 : CONCURRENT / UNORDERED / IDENTITY_FINISH
Set<Characteristics> characteristics();
}
Collector唯一实现:CollectorImpl
java
static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
// 五个属性 分别对应五个方法
private final Supplier<A> supplier;
private final BiConsumer<A, T> accumulator;
private final BinaryOperator<A> combiner;
private final Function<A, R> finisher;
private final Set<Characteristics> characteristics;
}
实际上,这也是Collector的最大的作用了。
Collector仅仅起到了封装数据(一些lambda表达式)的作用。
Collectors工具类
toList():返回ArrayList
java
public static <T>Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>(
1 (Supplier<List<T>>) ArrayList::new,
2 List::add,
3 (left, right) -> { left.addAll(right); return left; },
4 CH_ID // 这个可以不用管
);
}
// 上面调用的是这个构造方法
// 1 Supplier 无参,有返回值
// 2 BiConsumer 两个参数,没有返回值
// 3 BinaryOperator 两个入参,有返回值,三者类型相同,在toList这里的类型是ArrayList
// 猜测:这里用 BinaryOperator 应该是为了让并行也能用吧...
CollectorImpl(Supplier<A> supplier,
BiConsumer<A, T> accumulator,
BinaryOperator<A> combiner,
Set<Characteristics> characteristics) {
// 依次赋值 finisher比较特殊一点,是 finisher = castingIdentity()
}
Collectors.toList()方法生成了一个CollectorImpl实例,实现了Collector接口。传递了三个lambda方法
- new 一个ArrayList
- 向ArrayList add元素
- 向ArrayList addAll另一个ArrayList 的元素
toList的terminalOp
java
public static <T, I> TerminalOp<T, I> makeRef(Collector<? super T, I, ?> collector) {
// 返回一个 TerminalOp
// supplier 是 ArrayList::new
Supplier<I> supplier = Objects.requireNonNull(collector).supplier();
// accumulator 是 List::add
BiConsumer<I, ? super T> accumulator = collector.accumulator();
// combiner 是 (left, right) -> { left.addAll(right); return left; }
BinaryOperator<I> combiner = collector.combiner();
// ---------------------
class ReducingSink extends Box<I>
implements AccumulatingSink<T, I, ReducingSink> {
// 就是new一个ArrayList
@Override
public void begin(long size) {
state = supplier.get();
}
// 向ArrayList add一个元素
@Override
public void accept(T t) {
accumulator.accept(state, t);
}
// 向ArrayList add 另一个ArrayList的所有元素
@Override
public void combine(ReducingSink other) {
state = combiner.apply(state, other.state);
}
}
// ---------------------
// 以上可以概括为取出CollectorImpl的几个重要的表达式
return new ReduceOp<T, I, ReducingSink>(StreamShape.REFERENCE) {
@Override
public ReducingSink makeSink() {
return new ReducingSink();
}
// 省略 getOpFlags
};
}
至此,得到一个ReduceOp实例,并重写了makeSink方法。makeSink用作wrapAndCopyInto方法的入参。
在evaluate时,ReduceOp也会被当成和普通的StatexxxOp一样,被调用begin,end,accept方法,他的sink在整个sinke链条的最后。因此实现了将结果转化为一个ArrayList。
joining():字符串拼接
java
String result = Stream.of("java", "scala", "go", "python")
.collect(Collectors.joining(","));
System.out.println(result);
// 输出 : java,scala,go,python
// 支持无参直接拼接,也支持分隔符
通过分析toList我们已经知道,Collector只是封装几个函数,因此不同方法只是几个lambda表达式方法和别人的不同,因此后文只关注这些方法。
joining()就是:
java
public static Collector<CharSequence, ?, String> joining() {
return new CollectorImpl<CharSequence, StringBuilder, String>(
StringBuilder::new, // 1
StringBuilder::append, // 2
(r1, r2) -> { r1.append(r2); return r1; },// 3
StringBuilder::toString, // 4
CH_NOID);
}
多写了一个方法:将StringBuilder转化为String,多指定了finisher方法,这在ArrayList没有。
toSet():返回HashSet
java
public static <T>
Collector<T, ?, Set<T>> toSet() {
return new CollectorImpl<>(
(Supplier<Set<T>>) HashSet::new,
Set::add,
(left, right) -> { left.addAll(right); return left; },
CH_UNORDERED_ID);
}
toMap():返回HashMap
java
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),// 处理k
valueMapper.apply(element), // 处理v
mergeFunction);
// merge方法可以简单理解为 add 只不过可以自定义key相同的情况,如何处理,下面可以看到源码
return new CollectorImpl<>(mapSupplier, // new
accumulator, // merge
mapMerger(mergeFunction),
CH_ID);
}
toMap()要稍微特殊一点,可以看到他有个mergeFunction的函数,这个是处理两个key一模一样的情况的。
java
// merge是map接口下的方法 主要是利用remappingFunction 决定key相同时,用哪个value
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if(newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
除了上述这些内容,Collectors还提供了max/minBy,reuducing,averaging,summing等等方法,就不一一介绍了。
特殊的终止:toArray
这个就不关心实现了,但它和别人确实都不太一样。
java
public final Object[] toArray() {
return toArray(Object[]::new);
}
public final <A> A[] toArray(IntFunction<A[]> generator) {
return (A[]) Nodes.flatten(evaluateToArrayNode(generator), generator)
.asArray(generator);
}
小结:Stream语句的原理
第一步,肯定是创建Stream流对象 ,众所周知Stream是个接口。因此我们创建的流的实体类是:Pipeline。用的最多的是ReferencePipeline,他又有三个内部类:Head头节点,无状态操作,和有状态的操作。这两种状态的操作我又理解成普通节点。因为它是已双向链表的形式组织起来所有的Option的,头节点是特殊的Head,其余节点就是这两种Option。创建对象呢又依赖StreamSupport.stream方法。这里又有三种创建流对象的方法底层全都依赖StreamSupport,会返回一个Head头节点。
第二步,就是很多中间操作,比如我们常用的map映射,然后sorted排序这些。这些方法的实现都在AbstractPipeline,是ReferencePipeline的父类。也并不复杂,通常是创建一个节点,就是无状态操作,和有状态操作中的一个,然后插入到链表尾部。然后StatelessOp这也是抽象类,虽然Stream是个惰性流,但我们也要存储你的lambda表达式,所以我们要重写opWrapSink方法,这个方法很重要,会在终止Stream的时候使用。
最后一步,也就是终止操作 。终止操作又有很多种比如collect,foreach这些。这些方法会创建一个TerminalOp对象。TerminalOp又有很多子类,这里我拿collect举例吧,就是ReduceOp,利用ReduceOps的makeRef方法创建对象。如果是collect(Collections.toList())的话他就会向最后这个ReduceOp存储三个lambda方法,就是创建ArrayList,以及ArrayList的add和addAll方法。然后这个ReduceOp还会重写一个方法叫makeSink。这个Sink是一个Consumer的子类,就也是一个lambda表达式。最后呢我们执行evaluate方法,会先调用这个makeSink方法取出TerminalOp的Sink,然后对它做一个封装,就是第二步中间操作提到的重写的opWrapSink方法,我们会从后往前遍历双向链表,依次调用这个opWrapSink方法,那这个方法的逻辑是取决于你的Option的,比如map是返回一个Sink.ChainedReference,首先它内部有一个它后续的Sink,其次重写它的accpet方法就是lambda表达式嘛,会先执行当前的lambda式以后,调用后续Sink的accept方法,以此实现对所有的lambda表达式的依次执行,执行完以后就返回了。
整个流程大概就是这样。