先看这段样例代码:
java
var al = new ArrayList<String>();
al.add("Hello");
al.add("world");
al.stream().map(String::toUpperCase).forEach(System.out::println);
1 创建流
样例代码创建一个 ArrayList 实例,并将其转化为流进行数据处理。stream() 方法源自 Collection 接口。自 Java 8 起,该接口为适配 Stream 新增了几个转换方法。
代码1-1:
java
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
// 这是继承自 Iterable 接口的方法
@Override
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
为保持向后兼容,这些新增方法均提供了默认实现。java.util.stream.StreamSupport 是操作流的工具类,该类在 Stream 内部代码中被频繁使用,但通常的业务代码无需直接调用。至于其中调用的 spliterator() 方法,则引入了一个新概念。
1.1 Spliterator
"拆分器"(本文使用译名)以接口形式出现,java.util.Spliterator。官方文档对此接口说明为:
用于遍历和分割源元素的对象。Spliterator 所覆盖元素的源可以是数组、集合、IO 通道或生成函数。
拆分器可逐个遍历元素(
tryAdvance() ),也可批量顺序遍历(forEachRemaining())。
主要方法有,代码1-2:
java
public interface Spliterator<T> {
// 消费拆分器中一个元素
boolean tryAdvance(Consumer<? super T> action);
// 消费拆分器中剩余所有元素
default void forEachRemaining(Consumer<? super T> action) {
do { } while (tryAdvance(action));
}
// 分出自身部分元素到一个新实例
Spliterator<T> trySplit();
// 当前拆分器中预估元素数量
long estimateSize();
default long getExactSizeIfKnown() {
return (characteristics() & SIZED) == 0 ? -1L : estimateSize();
}
// 当前拆分器的 特质
int characteristics();
}
特别地,拆分器的特质是通过一组二进制位(如 ORDERED、DISTINCT、SORTED 等)来表示当前数据集的一些特性,这部分计算较为复杂,本文不作深入探讨。
Iterable 接口自 Java 8 起新增了 spliterator() 方法,实现了从"迭代器"到"拆分器"的转换。其默认实现如代码1-3所示:
java
default Spliterator<T> spliterator() {
return Spliterators.spliteratorUnknownSize(iterator(), 0);
}
Spliterators 是一个用于操作或创建拆分器的工具类,通常业务代码中不会直接使用。尽管接口提供了默认实现,但 JDK 中每个具体的集合类型都会根据自身特点重写此方法。这里以最熟悉的 ArrayList 为例,观察其拆分器的工作方式,如代码1-4所示:
java
public Spliterator<E> spliterator() {
return new ArrayListSpliterator(0, -1, 0);
}
final class ArrayListSpliterator implements Spliterator<E> {
private int index; // current index, modified on advance/split
private int fence; // -1 until used; then one past last index
private int expectedModCount; // initialized when fence set
ArrayListSpliterator(int origin, int fence, int expectedModCount) {
this.index = origin;
this.fence = fence;
this.expectedModCount = expectedModCount;
}
private int getFence() { // initialize fence to size on first use
int hi; // (a specialized variant appears in method forEach)
if ((hi = fence) < 0) {
expectedModCount = modCount;
hi = fence = size;
}
return hi;
}
public ArrayListSpliterator trySplit() {
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
return (lo >= mid) ? null : // divide range in half unless too small
new ArrayListSpliterator(lo, index = mid, expectedModCount);
}
public boolean tryAdvance(Consumer<? super E> action) {
if (action == null)
throw new NullPointerException();
int hi = getFence(), i = index;
if (i < hi) {
index = i + 1;
@SuppressWarnings("unchecked") E e = (E)elementData[i];
action.accept(e);
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
return false;
}
}
ArrayListSpliterator 类的实现基于二分查找的思想。index 可理解为起始位置索引,fence 可理解为结束位置索引,expectedModCount 则记录了 ArrayList 中用于快速失败检测的 modCount 变量的值。getFence 方法的作用是初始化 fence 和 expectedModCount。
trySplit 方法对指定范围内的元素进行二分。具体过程可通过以下代码演示:
java
var al = new ArrayList<Integer>(List.of(1, 2, 3, 4, 5));
var s0 = al.spliterator(); // 3 4 5
var s1 = s0.trySplit(); // 2
var s2 = s1.trySplit(); // 1
s0 最初从 al 中获取全部元素,调用一次 trySplit 方法后,将元素 1, 2 分配给了变量 s1。变量 s1 再次调用 trySplit 方法,将元素 1 分配给了变量 s2。对于单元大小的拆分器(如 s1、s2),再次调用 trySplit 方法将返回 null。
ArrayList 的 forEachRemaining 方法基于数组索引实现,其效果与接口的默认实现一致,此处不再赘述。
!IMPORTANT\] ❗ 注意 拆分器只能遍历一次,即读指针只能向前走,没重置读指针位置的方法。
后续看到Spliterator相关的代码,基本可以简单理解为"数据源",例如在样例代码中它就代表ArrayList实例。
2 操作流水线
在开头的样例代码中,我们使用 map(String::toUpperCase) 来操作数据。String::toUpperCase 是 Java 中的方法引用,作用是将字符串转换为大写形式。而 map 方法则是 Stream 接收数据处理逻辑的入口。接下来,我们将探究数据的"操作"是如何被处理的。
至于为什么选择这四个类型,官方的解释是:Java 中的其他类型都可以转化为这四个类型,从而在效率与代码优雅之间取得平衡。换句话说,如果一个接口需要为所有类型都编写独立的实现,那得写9份看起来有点🤪。
对于样例代码来说,java.util.stream.Stream#map 方法的实现可以在ReferencePipeline类中找到。
2.1 ReferencePipeline 特化类
ReferencePipeline 类对应引用类型的特化实现,通常业务代码中也是这个类运行得最多。其 map 方法的实现如代码2-1所示:
java
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));
}
};
}
};
}
可以看到,我们传入的"操作"逻辑被包装进一个 StatelessOp 类实例并返回。
2.1.1 StatelessOp类
StatelessOp 类,顾名思义,代表无状态操作,它是 ReferencePipeline 的一个内部类。很容易联想到,应该还有表示有状态操作的类:StatefulOp。有状态计算较为复杂,本文不会涉及,主要关注无状态计算的情况。
!TIP\] 无状态:元素处理互不依赖;有状态:需要知道其他元素的信息,如`sorted`。
StatelessOp 类的继承关系,图2-1:
这个继承关系很重要,后面需要不时回顾。
StatelessOp 类的声明,代码2-2:
java
abstract static class StatelessOp<E_IN, E_OUT>
extends ReferencePipeline<E_IN, E_OUT> {
StatelessOp(AbstractPipeline<?, E_IN, ?> upstream,
StreamShape inputShape,
int opFlags) {
super(upstream, opFlags);
assert upstream.getOutputShape() == inputShape;
}
@Override
final boolean opIsStateful() {
return false;
}
}
StreamShape 是一个枚举,列举了前面提到的四种数据类型,用于表示当前数据流中的元素类型。StreamOpFlag 也是一个枚举(其注释远比代码丰富),用于表示数据流的各种特质。至于代码2-1中实现的 opWrapSink 方法,则定义在其父类 AbstractPipeline 中,如代码2-3所示:
java
abstract Sink<E_IN> opWrapSink(int flags, Sink<E_OUT> sink);
此方法的具体作用我们稍后再谈。接下来,让我们看看 Sink.ChainedReference 类。
2.2 Sink
Sink这个词数据处理相关的程序中拿来作术语(flink中也有),动词形式有"下沉; 坐下; 减弱; 挖掘; 使受挫; 击球入洞; 灌"等意思;名词形式常为"洗涤池"之意。在数据处理中,这个词代表的概念常是数据处理的最后一步。
java.util.stream.Sink接口是增强版的Consumer(消费者),负责流中"消费"或"处理"数据,代码2-4:
java
interface Sink<T> extends Consumer<T> {
// 在开始处理数据元素之前被调用。用于进行一些初始化工作(例如,提前分配一个合适大小的数组)
// -1 表示无法预估大小
default void begin(long size) {}
// 在所有数据元素都被处理完毕之后被调用。用于执行最终的清理或计算工作。
default void end() {}
// 这是一个短路判断机制。在处理每个元素之前,流框架会询问这个 Sink:"是否需要取消后续处理?"
default boolean cancellationRequested() {
return false;
}
// ...
}
Java Stream中所有对数据的操作几乎都与此接口相关。它内部也有对四个类型实现的内部类,上文用到的Sink.ChainedReference类就是其中之一。
Sink.ChainedReference类部分代码如下,代码2-5:
java
interface Sink<T> extends Consumer<T> {
// ...
abstract static class ChainedReference<T, E_OUT> implements Sink<T> {
protected final Sink<? super E_OUT> downstream;
public ChainedReference(Sink<? super E_OUT> downstream) {
this.downstream = Objects.requireNonNull(downstream);
}
@Override
public void begin(long size) {
downstream.begin(size);
}
@Override
public void end() {
downstream.end();
}
@Override
public boolean cancellationRequested() {
return downstream.cancellationRequested();
}
}
}
结合代码2-1来看,我们传入的操作逻辑(即 String::toUpperCase 方法)被放置在了 Consumer#accept 方法的实现内部,将在后续的某个时刻被触发调用。
2.3 流水线的构成
结合代码2-1和代码2-2,当前(ReferencePipeline类)实例由构方法传入到父构造器中,通过层层调用最后回到AbstractPipeline类(回顾图2-1)。涉及此类的两个构造器方法,代码2-5:
java
// Constructor for the head of a stream pipeline.
AbstractPipeline(Spliterator<?> source,
int sourceFlags, boolean parallel) {
this.previousStage = null;
this.sourceSpliterator = 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;
}
// Constructor for appending an intermediate operation stage onto an existing pipeline.
AbstractPipeline(AbstractPipeline<?, E_IN, ?> previousStage, int opFlags) {
if (previousStage.linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
previousStage.linkedOrConsumed = true;
previousStage.nextStage = this;
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;
}
忽略其他操作,可以看出这是双向链表的构建方法。
depth:链表的"深度",或者为当前节的序号,从0开始,依次加1。sourceStage:头节点的指针。nextStage:下一个节点的指针。previousStage:上一个节点的指针。
从代码1-1中可以知道,流是调用StreamSupport.stream(spliterator(), false)方法创建的。它的具体实现是这样的,代码2-6:
java
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
Objects.requireNonNull(spliterator);
return new ReferencePipeline.Head<>(spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}
这个ReferencePipeline.Head类构造器最后调用的就是代码2-5中头节点构造器。
对于样例代码中Stream#map方法的实现(代码2-1),return new StatelessOp<P_OUT, R>(this, ...) 这行代码中的this是ReferencePipeline.Head实例。但这行StatelessOp的构造方法最终会调用到它的父类AbstractPipeline构造方法(代码2-5),当运行至previousStage.nextStage = this;行时,此时的this则是当前StatelessOp类实例。后续节点调用此方法时,这两个this将会依次后移,链表就这样构造出来了。形成类似于下图的逻辑结构,图2-2:
在看源码时需注意代码中this指向的节点。
3 流的计算
前面分析了数据和操作流水线的构建,接下来就是流最终是如何进行运算的。
3.1 流的执行
在样例代码中,最后调用一个forEach的终止操作来结束流的定义。终止操作是流计算和获取结果的方法,它们都定义在java.util.stream.Stream接口中,实现在ReferencePipeline类中。部分终止方法实现,代码3-1:
java
@Override
public void forEach(Consumer<? super P_OUT> action) {
evaluate(ForEachOps.makeRef(action, false));
}
@Override
public final boolean allMatch(Predicate<? super P_OUT> predicate) {
return evaluate(MatchOps.makeRef(predicate, MatchOps.MatchKind.ALL));
}
@Override
public final Optional<P_OUT> findAny() {
return evaluate(FindOps.makeRef(false));
}
这就是流的终止操作的特殊性,每个终止操作都由一个JDK内部XxxOps类包装后使用。
evaluate方法是执行流计算并获取结果的方法,为 ReferencePipeline 的父类 AbstractPipeline 中的final方法,代码3-2:
java
final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
assert getOutputShape() == terminalOp.inputShape();
if (linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
linkedOrConsumed = true;
return isParallel()
? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
: terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}
此方法的主要作用是分派计算方法:对于串行计算调用 evaluateSequential 方法,对于并发计算则调用 evaluateParallel 方法。具体的计算方案由 terminalOp 这个终止操作类来实现。并发计算涉及的内容较为复杂,本文不会涉及。
注意代码中this的指向。就样例代码而言,当前terminalOp.evaluateSequential 方法中的this指向Stream#map方法构建出的ReferencePipeline实例。
sourceSpliterator方法可简单理解为"取出数据"操作,不过会有一些校验等,此处不展开。
此时terminalOp变量为java.util.stream.ForEachOps实例,它的部分代码如下,代码3-3:
java
final class ForEachOps {
private ForEachOps() { }
public static <T> TerminalOp<T, Void> makeRef(Consumer<? super T> action,
boolean ordered) {
Objects.requireNonNull(action);
return new ForEachOp.OfRef<>(action, ordered);
}
// ...
abstract static class ForEachOp<T>
implements TerminalOp<T, Void>, TerminalSink<T, Void> {
// ...
static final class OfRef<T> extends ForEachOp<T> {
final Consumer<? super T> consumer;
OfRef(Consumer<? super T> consumer, boolean ordered) {
super(ordered);
this.consumer = consumer;
}
@Override
public void accept(T t) {
consumer.accept(t);
}
}
@Override
public <S> Void evaluateSequential(PipelineHelper<T> helper,
Spliterator<S> spliterator) {
return helper.wrapAndCopyInto(this, spliterator).get();
}
@Override
public Void get() {
return null;
}
// ...
}
// ...
}
可以看到它的evaluateSequential方法是执行计算并获取结果的地方,但forEach的终止操作不产出数据所以get()方法直接返空。下面有返回值的终止操作例子(不知道为什么Java Stream模块内部没怎么用Optional类):
java
final class FindOps {
// ...
static final class OfInt extends FindSink<Integer, OptionalInt>
implements Sink.OfInt {
// ...
public OptionalInt get() {
return hasValue ? OptionalInt.of(value) : null;
}
}
PipelineHelper类看名字像个工具类,但它是ReferencePipeline类的父类,回看图2-1,此处helper参数应当视为AbstractPipeline类实例或者理解为"流水线"节点。
结合代码3-2,样例代码会运行到helper.wrapAndCopyInto(this, spliterator).get()行。此时this指向ForEachOps实例,helper参数更确切点说为Stream#map方法构建出的ReferencePipeline实例。wrapAndCopyInto方法定义在PipelineHelper中,实现在AbstractPipeline类里,如下,代码3-4:
java
@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;
}
@Override
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);
}
}
wrapAndCopyInto 方法名虽未提及计算,但对于串行流而言,计算基本上就是在此处执行的,终止操作主要负责获取最终结果。
在当前样例代码的上下文中,由于没有使用 filter 等可能引发短路计算的中间操作,因此会执行非短路分支内的代码。
java
wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();
在执行计算之前,还需要调用 wrapSink(Sink<E_OUT> sink) 方法。在了解此方法之前,我们先补充两个相关的接口定义,如代码3-5所示:
java
interface TerminalOp<E_IN, R> {
// ...
}
interface TerminalSink<T, R> extends Sink<T>, Supplier<R> { }
第一个接口定义了终止操作的公共行为(主要是evaluateParallel和evaluateSequential方法),第二接口则是拓展了Sink接口。结合代码3-3来看,ForEachOp.OfRef是针对引用类数据的实现,也间接实现了Sink接口。此时wrapSink(Sink<E_OUT> sink)方法的入参为ForEachOp.OfRef实例。
接下来再看java.util.stream.AbstractPipeline#wrapSink方法,代码3-6:
java
@Override
@SuppressWarnings("unchecked")
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;
}
这里要注意代码中this的指向,它是指向上一级AbstractPipeline节点。对于样例代码,此处为Stream#map方法构建出的AbstractPipeline节点。而sink参数则传入的ForEachOp.OfRef实例。
以样例代码来讲:
java
ForEachOp.OfRef实例 传入 -> wrapSink(Sink<E_OUT> sink) 方法
AbstractPipeline.this 拿出 Stream#map 方法构建出的AbstractPipeline实例
p.opWrapSink(p.previousStage.combinedFlags, sink)的调用展开为:
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) { // ForEachOp.OfRef实例 -> sink
return new Sink.ChainedReference<P_OUT, R>(sink) {
@Override
public void accept(P_OUT u) {
downstream.accept(mapper.apply(u));
}
};
}
返回Sink.ChainedReference实例
p指针的已移动到头节点,depth 为 0 ,退出循环
结合代码2-5中的
java
public ChainedReference(Sink<? super E_OUT> downstream) {
this.downstream = Objects.requireNonNull(downstream);
}
各个Sink类中的downstream变量确实指向下一级的Sink实例。
综上,流计算的前期步骤已经完成。接下来就看看数据是如何计算的。
3.2 数据的运算
之前分析说过,串行流在wrapAndCopyInto方法就执行了计算。对于我们的样例代码来说计算就发生这三行代码中:
java
wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();
wrappedSink变量代表着流第一个操作,在本例中为map(String::toUpperCase)。
spliterator代表数据源,本例中它为ArrayList。
wrappedSink.begin流运算开始前的准备工作。大部分操作并不需要做什么准备,通常使用默认实现downstream.begin(size);通知下游操作;对于filter之类的操作,常常实现为downstream.begin(-1);表示未知大小。
spliterator.forEachRemaining语义为把数据源中的所有元素逐一遍历,ArrayList实现为使用索引遍历具体此处略。这里需要注意数据流的形成。先看forEachRemaining方法类似的实现:
java
public void forEachRemaining(Consumer<? super E> action) { // 传入的是map(String::toUpperCase)方法构成的Sink实例
for (i = lo; i < hi; ++i) {
E e = (E) a[i];
action.accept(e);
}
}
结合代码2-1中的片段
java
public void accept(P_OUT u) {
downstream.accept(mapper.apply(u)); // mapper为map(String::toUpperCase)方法构成的Sink实例
}
downstream变量结合之前所讲它为ForEachOps实例,即为打印元素的语句。
这里也表明上一级操作结果就是下一级操作的输入,数据流动起来了。
wrappedSink.end方法通常也只是通知下游操作。
4 总结
本文从一个简单的例子出发,展示了Java Stream运行的基本而完整流程,其中也涉及到重要概念与类设计。
限于篇幅与能力,本文略过有状态计算、并行计算等复杂部分;如有错漏,欢迎指出。