要点
- 创建
Stream时,会生成Head类的实例 - 对
Stream执行中间操作(intermediate operation)时,会生成对应的StatelessOp实例或StatefulOp实例,这些实例以及Head类的实例会组成一个双链表 - 对
Stream执行终止操作(terminal operation)时,会生成一系列的Sink实例,这些Sink实例会组成一个单链表,Head实例中的元素会通过这个单链表来进行传递
例子及分析
我们在使用 Stream 时,典型的步骤是
- 创建
Stream - 执行若干(可以是
0个)中间操作 - 执行终止操作
下面举一个具体的例子。
假设现在需要计算小于 10 的所有正偶数的立方和,即
可以用如下的代码来计算 ⬇️
import java.util.stream.IntStream;
public class SimpleIntStream {
public static void main(String[] args) {
// Suppose that we need to calculate the cubic sum of positive even numbers that are less than 10
int result = IntStream.range(1, 10) // Step 1
.filter(n -> (n & 1) == 0) // Step 2
.map(n -> n * n * n) // Step 3
.sum(); // Step 4
System.out.println("The result of 2**3 + 4**3 + 6**3 + 8**3 is: " + result);
}
}
上面的代码中和 IntStream 有直接关系的代码共有 4 步,代码的注释中也标出来了。
| 类型 | 做了什么 | |
|---|---|---|
Step 1 | 创建 IntStream 的操作 | 创建 IntStream |
Step 2 | 中间操作(intermediate operation) | 调用 IntStream.filter(...) 方法,生成一个新的 IntStream |
Step 3 | 中间操作(intermediate operation) | 调用 IntStream.map(...) 方法,生成一个新的 IntStream |
Step 4 | 终止操作(terminal operation) | 调用 IntStream.sum() 方法,得到结果。 |
其中 Step 2 和 Step 3 都是中间操作,Step 4 是终止操作。
我们来看看这 4 个 step 的背后发生了什么。
Step 1: 创建 IntStream
分析
Step 1 就是 IntStream.range(1, 10),range(1, 10) 会返回 IntStream 的一个实例,我们把这个实例称为 。
借助 Intellij IDEA,可以看到 的精确类型是 IntPipeline.Head (全限定类名是 java.util.stream.IntPipeline.Head)⬇️
下图展示了 Step 1 的逻辑 ⬇️
上图中,灰色背景的节点表示 Step 1 的代码,
蓝色背景的节点表示 IntPipeline.Head 的实例 (完整的类名太长了,在图中就简称为 Head 了)。
图中有一条名为 sourceStage 的绿色边,这个 sourceStage 字段来自 AbstractPipeline 类。我画了简要的类图(所有的泛型在图中都省略了) ⬇️
classDiagram
AutoCloseable <|-- BaseStream
<<interface>> AutoCloseable
BaseStream <|-- IntStream
<<interface>> BaseStream
IntStream <|.. IntPipeline
<<interface>> IntStream
PipelineHelper <|-- AbstractPipeline
<<Abstract>> AbstractPipeline
<<Abstract>> PipelineHelper
AbstractPipeline <|-- IntPipeline
IntPipeline <|-- `IntPipeline.Head`
IntPipeline <|-- `IntPipeline.StatelessOp`
IntPipeline <|-- `IntPipeline.StatefulOp`
<<Abstract>> IntPipeline
AbstractPipeline: private final AbstractPipeline sourceStage
AbstractPipeline: protected final AbstractPipeline previousStage
AbstractPipeline: private AbstractPipeline nextStage
AbstractPipeline: private int depth
上方类图中的类/接口所在包的包名普遍比较长,我把这些类/接口与 fully qualified name 之间的对应关系列在下表中了 ⬇️
| 类名/接口名 | fully qualified name |
|---|---|
AutoCloseable | java.lang.AutoCloseable |
BaseStream | java.util.stream.BaseStream |
IntStream | java.util.stream.IntStream |
PipelineHelper | java.util.stream.PipelineHelper |
AbstractPipeline | java.util.stream.AbstractPipeline |
IntPipeline | java.util.stream.IntPipeline |
IntPipeline.Head | java.util.stream.IntPipeline.Head |
IntPipeline.StatelessOp | java.util.stream.IntPipeline.StatelessOp |
IntPipeline.StatefulOp | java.util.stream.IntPipeline.StatefulOp |
小结
Step 1 创建了 IntPipeline.Head 类的实例 。
Step 2: 执行 filter 操作得到新的 IntStream
分析
Step 1 创建了 IntStream 的一个实例 ,
Step 2 通过 调用 IntStream.filter(...) 方法,生成 IntStream 的又一个实例。我们称这个新实例为 。
是 java.util.stream.IntPipeline.StatelessOp 的一个 匿名子类 的实例 ⬇️
注意: 的精确类型是
java.util.stream.IntPipeline$10,由于这是一个匿名内部类,我们不必关心这个类的具体名称。匿名内部类有点像做了好事而不愿留名的人,对这样的人来说,只要好事做完了就可以了,别人不必知道自己的名字。
下图展示了 Step 2 的逻辑 ⬇️
灰色背景的节点表示 Step 2 的代码,
蓝色背景的两个节点表示 IntStream 的两个实例,即 和 。
- 图中有名为
previousStage的黄色边,这个previousStage字段来自AbstractPipeline类。 - 图中有名为
nextStage的蓝色边,这个nextStage字段来自AbstractPipeline类。
小结
Step 2创建了IntPipeline.StatelessOp类的(子类的)实例- , 通过
previousStage字段和nextStage字段组成了一个双向链表。
Step 3: 执行 map 操作得到新的 IntStream
分析
Step 2 创建了 IntStream 的实例 ,
Step 3 通过 调用 IntStream.map(...) 方法,生成 IntStream 的一个实例。我们称这个新实例为 。
是 java.util.stream.IntPipeline.StatelessOp 的一个 匿名子类 的实例 ⬇️
注意: 的精确类型是 java.util.stream.IntPipeline$4,由于这是一个匿名内部类,我们不必关心这个类的具体名称。
下图展示了 Step 3 的逻辑 ⬇️
灰色背景的节点表示 Step 3 的代码,
蓝色背景的三个节点表示 IntStream 的三个实例,即 , , 。
小结
Step 3创建了IntPipeline.StatelessOp类的(子类的)实例- , , 组成了一个双向链表。
nextStage用于指向链表中的下一个节点。如果用 表示nextStage,那么这三个节点的关系可以表示为previousStage用于指向链表中的上一个节点。如果用 表示previousStage,那么这三个节点的关系可以表示为
我把 , , 的重要信息列在下方的表格中(其中 字段上文没有提到,不过它的含义比较直观)。
IntPipeline.Head | |||||
IntPipeline.StatelessOp 的一个子类 | |||||
IntPipeline.StatelessOp 的一个子类 |
我们只关心 和 这两个字段,把 Step 3 的图简化一下,得到如下的结果 ⬇️
Step 4: 调用 sum() 方法
Step 1 到 Step 3 都是在创建 IntStream 的实例,而真正的计算逻辑是在 Step 4 完成的。
sum() 方法的逻辑如下
@Override
public final int sum() {
return reduce(0, Integer::sum);
}
我们再去查看 reduce(...) 方法 ⬇️
@Override
public final int reduce(int identity, IntBinaryOperator op) {
return evaluate(ReduceOps.makeInt(identity, op));
}
ReduceOps.makeInt(identity, op) 返回的是 TerminalOp 的一个实例,我们将这个实例称为 (就本文而言 TerminalOp 只有这一个实例,但为了风格的统一,还是给它加了个下标)
evaluate(...) 方法的代码如下 ⬇️
通过打断点可以确认,我们的代码对应的 isParallel() 值为 false,所以会运行到下面这一行。
terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()))
这个 evaluateSequential(...) 方法定义在 java.util.Spliterator.TerminalOp 接口中。
我们需要去 TerminalOp 接口的实现类中查看实际运行的逻辑。
刚才这一段代码绕来绕去,容易绕晕,下方有张时序图 ⬇️ 可以帮助我们再理理思路。
sequenceDiagram
participant M as the main method
participant Stage3 as stage₃
participant ReduceOps
%%participant Sink1 as sink₁
participant T as terminal₁
M->>+Stage3: sum()
Stage3->>+ReduceOps: makeInt(identity, op)
ReduceOps-->>-Stage3: return terminal₁
Stage3->>+T: evaluateSequential(...)
请注意
ReduceOps.makeInt(identity, op)是静态方法ReduceOps.makeInt(identity, op)方法返回
TerminalOp 接口中的 evaluateSequential(...) 方法
是在下图红框位置生成的 ⬇️
ReduceOps.makeInt(...) 这个静态方法的逻辑如下 ⬇️
看来 ReduceOps.makeInt(int identity, IntBinaryOperator operator) 方法会返回一个匿名内部类,且这个匿名内部类是 ReduceOp 类的子类(这个匿名内部类的全限定类名是 java.util.stream.ReduceOps$6)。
就我们的代码而言,
identity参数是0operator参数是对Integer中的静态方法sum(int, int)的方法引用
下方的类图展示了这几个类的关系 ⬇️
classDiagram
TerminalOp <|.. ReduceOp
<<interface>> TerminalOp
<<Abstract>> ReduceOp
ReduceOp <|-- ReduceOp 的匿名子类
class TerminalOp {
evaluateSequential(...)
}
class ReduceOp {
evaluateSequential(...)
}
上方类图中的类/接口所在包的包名普遍比较长,我把这些类/接口与 fully qualified name 之间的对应关系列在下表中了 ⬇️
| 类名/接口名 | fully qualified name |
|---|---|
TerminalOp | java.util.stream.TerminalOp |
ReduceOp | java.util.stream.ReduceOps.ReduceOp |
ReduceOp 的匿名子类 | 略(对匿名类而言,它的具体名称不重要) |
由于 ReduceOp 的匿名子类(即 java.util.stream.ReduceOps$6)中没有 override evaluateSequential(...) 方法,
所以最终调用的是 ReduceOp 类里的 evaluateSequential(...) 方法。
ReduceOp 类里的 evaluateSequential(...) 方法是这样的 ⬇️
@Override
public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
Spliterator<P_IN> spliterator) {
return helper.wrapAndCopyInto(makeSink(), spliterator).get();
}
所以需要看以下两处逻辑
-
makeSink() -
helper.wrapAndCopyInto(...)(helper其实就是 )
我们先猜测一下这两个方法的作用,然后一边看一边验证。
makeSink()方法听起来是要制作sink。helper.wrapAndCopyInto(...)方法听起来像是要对sink做包装,然后把stream里的元素 copy 到sink里去。
先看 makeSink()。
makeSink() 方法
从 Step 1 到 Step 3,我们生成了 3 个 IntStream 的实例,即 , , 。
我们可以把这些实例想象成水流, 那么水流要流到哪里去呢?要流到 sink 去。我去 Google 了一下 ⬇️
看来厨房和卫生间的水槽都可以称为 sink。水流从真实的 sink 流走, 而 IntStream 则从 java 的 sink 流走。
通过打断点可以看到 makeSink() 中只有以下一句代码
return new ReducingSink();
这句代码返回了 ReducingSink 类的实例,我们称这个实例为 。
ReducingSink 类是局部内部类,它的全限定类名是 java.util.stream.ReduceOps$5ReducingSink(我们不用关心它的具体名称)。
简要的类图如下(有些方法/字段略去了,所有和泛型相关的内容都已略去) ⬇️
classDiagram
Consumer <|-- Sink
Sink <|-- TerminalSink
Supplier <|-- TerminalSink
<<interface>> Consumer
<<interface>> Sink
<<interface>> Supplier
TerminalSink <|-- AccumulatingSink
<<interface>> TerminalSink
AccumulatingSink <|.. ReducingSink
`Sink.OfInt` <|.. ReducingSink
<<interface>> AccumulatingSink
<<interface>> `Sink.OfInt`
Sink <|-- `Sink.OfInt`
IntConsumer <|-- `Sink.OfInt`
<<interface>> IntConsumer
Consumer: accept(...)
Supplier: get()
IntConsumer: accept(int)
上方类图中的类/接口所在包的包名普遍比较长,我把这些类/接口与 fully qualified name 之间的对应关系列在下表中了 ⬇️
| 类名/接口名 | fully qualified name |
|---|---|
Consumer | java.util.function.Consumer |
Sink | java.util.stream.Sink |
Supplier | java.util.function.Supplier |
IntConsumer | java.util.function.IntConsumer |
TerminalSink | java.util.stream.TerminalSink |
Sink.OfInt | java.util.stream.Sink.OfInt |
AccumulatingSink | java.util.stream.ReduceOps.AccumulatingSink |
ReducingSink | 略(因为它是局部内部类,不必关心它的完整类名) |
这么说来 makeSink() 方法的作用确实是制作一个 sink。
那么 makeSink() 就看完了,更新一下任务列表 ⬇️
-
makeSink() -
helper.wrapAndCopyInto(...)(helper其实就是 )
然后再看 helper.wrapAndCopyInto(...) 方法。
helper.wrapAndCopyInto(...) 方法
wrapAndCopyInto(...) 方法定义在 PipelineHelper 这个抽象类中 ⬇️
AbstractPipeline 类中 override 了这个方法 ⬇️
// 以下代码是从 AbstractPipeline.java 里 copy 过来的
@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;
}
其中的主要逻辑是 wrapSink(...) 和 copyInto(...),我们将任务列表更新如下 ⬇️
-
makeSink() -
helper.wrapAndCopyInto(...)(helper其实就是 )-
wrapSink(...) -
copyInto(...)
-
我们还是先猜猜这些方法的作用,然后一边看代码一边验证。
wrapSink(...)方法听起来是对sink做包装copyInto(...)方法听起来会把元素从什么地方 copy 到什么地方去,照理说应该是从 copy 到sink去。
先看 wrapSink(...)。
它的 javadoc 如下 ⬇️ 看来确实是在做包装的工作。
其代码如下
// 以下代码是从 AbstractPipeline.java 里 copy 过来的
@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;
}
主要逻辑的时序图如下 ⬇️
sequenceDiagram
participant T as terminal₁
participant S3 as stage₃
participant S2 as stage₂
T->>+S3: opWrapSink(..., sink₁)
S3-->>-T: return sink₂
T->>+S2: opWrapSink(..., sink₂)
S2-->>-T: return sink₃
这段代码的作用是将现有的 sink 包装成新的 sink,
为了便于描述,我们按照时间顺序把新生成的 依次称为 和 。
那么这段代码的主要逻辑是
- 把 包装成
- 把 包装成
请注意
3个stage节点是从Head的实例算起的,所以 从上到下 依次是 , , (Head实例是 )3个sink节点是从ReducingSink的实例算起的,所以 从下到上 依次是 , , (ReducingSink实例是 ),这3个sink节点通过downstream字段构成了一个单链表
wrapSink(...) 方法的返回值就是上图的
wrapSink(...) 方法看完了,更新后的任务列表如下 ⬇️
-
makeSink() -
helper.wrapAndCopyInto(...)(helper其实就是 )-
wrapSink(...) -
copyInto(...)
-
然后我们再去看 copyInto(...) 方法。
这个方法的 javadoc 如下 ⬇️ 看来它会把 IntStream 中的元素 copy 到 里去。
打断点后,会看到我们的代码将运行到 copyInto(...) 方法里的 if 分支 ⬇️
上图中的 wrappedSink 就是
wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();
看来又有新的代码要看了,更新后的任务列表如下 ⬇️
-
makeSink() -
helper.wrapAndCopyInto(...)(helper其实就是 )-
wrapSink(...) -
copyInto(...)-
begin(long)method inSinkinterface -
forEachRemaining(Consumer)method inSpliteratorinterface -
end()method inSinkinterface
-
-
先猜测一下这三个方法的作用 ⬇️
begin(long)方法听起来像是在真正的处理前,做准备工作的步骤forEachRemaining(Consumer)方法听起来像是真正干活的步骤end()方法听起来像是干完活后,处理善后事宜的步骤
begin(long) 和 end() 都是 Sink 接口中定义的方法,我把这两个方法的 javadoc 复制到下方 ⬇️
// 下方的代码是从 Sink.java 中 copy 的
/**
* Resets the sink state to receive a fresh data set. This must be called
* before sending any data to the sink. After calling {@link #end()},
* you may call this method to reset the sink for another calculation.
* @param size The exact size of the data to be pushed downstream, if
* known or {@code -1} if unknown or infinite.
*
* <p>Prior to this call, the sink must be in the initial state, and after
* this call it is in the active state.
*/
default void begin(long size) {}
/**
* Indicates that all elements have been pushed. If the {@code Sink} is
* stateful, it should send any stored state downstream at this time, and
* should clear any accumulated state (and associated resources).
*
* <p>Prior to this call, the sink must be in the active state, and after
* this call it is returned to the initial state.
*/
default void end() {}
forEachRemaining(Consumer) 来自 Spliterator 接口,它的 javadoc 如下 ⬇️
// 下方的代码是从 Spliterator.java 中 copy 的
/**
* Performs the given action for each remaining element, sequentially in
* the current thread, until all elements have been processed or the action
* throws an exception. If this Spliterator is {@link #ORDERED}, actions
* are performed in encounter order. Exceptions thrown by the action
* are relayed to the caller.
* <p>
* Subsequent behavior of a spliterator is unspecified if the action throws
* an exception.
*
* @implSpec
* The default implementation repeatedly invokes {@link #tryAdvance} until
* it returns {@code false}. It should be overridden whenever possible.
*
* @param action The action
* @throws NullPointerException if the specified action is null
*/
default void forEachRemaining(Consumer<? super T> action) {
do { } while (tryAdvance(action));
}
但通过打断点,可以看到,我们的代码实际上执行的并不是上面这个方法,Spliterator.OfInt 这个嵌套类(其实它是个接口,但是如果叫“嵌套接口”的话,感觉有点拗口)中 override 了 forEachRemaining(Consumer) 方法。我们的代码会执行这个 override 的方法 ⬇️
通过打断点,我们会发现,上图的 if 分支成立。
这个 if 分支里只有下面一行代码
forEachRemaining((IntConsumer) action);
上面的这个 forEachRemaining(IntConsumer) 方法在 Spliterator.OfInt 类中。
打断点后,会发现 java.util.stream.Streams.RangeIntSpliterator 类 override 了 forEachRemaining(IntConsumer) 方法。
这里有点绕,我们可以参考下方的类图来辅助理解。
打断点后,可以看到 i 会从 1 遍历到 10 (不包含 10)⬇️
图中的 consumer 就是上文提到的 。 和 都是 Sink.ChainedInt 的子类的实例。
下方的表格展示了 , , 是如何通过 字段连接起来的 ⬇️
Sink.ChainedInt 的一个匿名子类 | filter 的逻辑,所以有的元素不会传递给 | ||
Sink.ChainedInt 的一个匿名子类 | mapper 会执行计算立方的操作 | ||
ReducingSink ⬅️ 它是一个局部内部类 | 没有 字段 | operator 和 Integer 中的静态方法 sum(int, int) 对应 |
至于 begin(long) 和 end(),它们也是沿着 的方向来处理的。
为了方便理解,我画了对应的表格 ⬇️
| code for | code for | |||
|---|---|---|---|---|
Sink.ChainedInt 的一个匿名子类 | ||||
Sink.ChainedInt 的一个匿名子类 | override 方法,所以这里的 方法来自 Sink.ChainedInt | |||
ReducingSink ⬅️ 它是一个局部内部类 | 没有 字段 |
相关时序图
begin(long) 方法的时序图
sequenceDiagram
participant S as stage₃
participant B as sink₃
participant C as sink₂
participant D as sink₁
S->>+B: begin(9)
B->>+C: begin(-1)
C->>+D: begin(-1)
D-->>-C: return
C-->>-B: return
B-->>-S: return
forEachRemaining(Consumer) 方法的时序图
完整的例子会涉及从 1 到 9 的 9 种情况,全画出来的话会比较繁琐且没有必要,所以下图中只展示了 的情况。
sequenceDiagram
participant A as Spliterator for stage₁
participant B as sink₃
participant C as sink₂
participant D as sink₁
A->>+B: accept(1)
B-->>-A: return
A->>+B: accept(2)
B->>+C: accept(2)
C->>+D: accept(8)
D-->>-C: return
C-->>-B: return
B-->>-A: return
A->>+B: accept(3)
B-->>-A: return
A->>+B: accept(4)
B->>+C: accept(4)
C->>+D: accept(64)
D-->>-C: return
C-->>-B: return
B-->>-A: return
注意:
- 由于 中有过滤逻辑,在 遇到奇数元素时,它会直接返回
- 中会计算入参的立方,所以它的入参和
downstream(即 )的入参不同 - 中的
accept(int)方法被调用时,会计算累积的立方和,上图中略去了这个过程
end() 方法的时序图
sequenceDiagram
participant S as stage₃
participant B as sink₃
participant C as sink₂
participant D as sink₁
S->>+B: end()
B->>+C: end()
C->>+D: end()
D-->>-C: return
C-->>-B: return
B-->>-S: return
就我们的例子而言, end() 方法中没有特别的逻辑,所以对应的时序图比较简单 ⬆️
Step 4 我们也看完了,任务列表更新后如下 ⬇️
-
makeSink(): 生成 -
helper.wrapAndCopyInto(...)(helper其实就是 )-
wrapSink(...): 将 包装成 ,再将 包装成 ,构成 这样的单链表 -
copyInto(...): 将 中的元素 copy 到 中-
begin(long)method inSinkinterface -
forEachRemaining(Consumer)method inSpliteratorinterface -
end()method inSinkinterface
-
-