[Java] 以 IntStream 为例,浅析 Stream 的实现

158 阅读6分钟

要点

  • 创建 Stream 时,会生成 Head 类的实例
  • Stream 执行中间操作(intermediate operation)时,会生成对应的 StatelessOp 实例或 StatefulOp 实例,这些实例以及 Head 类的实例会组成一个双链表
  • Stream 执行终止操作(terminal operation)时,会生成一系列的 Sink 实例,这些 Sink 实例会组成一个单链表,Head 实例中的元素会通过这个单链表来进行传递

例子及分析

我们在使用 Stream 时,典型的步骤是

  1. 创建 Stream
  2. 执行若干(可以是 0 个)中间操作
  3. 执行终止操作

下面举一个具体的例子。 假设现在需要计算小于 10 的所有正偶数的立方和,即

23+43+63+832^3 + 4^3 + 6^3 + 8^3

可以用如下的代码来计算 ⬇️

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 2Step 3 都是中间操作,Step 4 是终止操作。

我们来看看这 4step 的背后发生了什么。

Step 1: 创建 IntStream

分析

Step 1 就是 IntStream.range(1, 10)range(1, 10) 会返回 IntStream 的一个实例,我们把这个实例称为 stage1stage_1

借助 Intellij IDEA,可以看到 stage1stage_1 的精确类型是 IntPipeline.Head (全限定类名是 java.util.stream.IntPipeline.Head)⬇️

image.png

下图展示了 Step 1 的逻辑 ⬇️

Step 1

上图中,灰色背景的节点表示 Step 1 的代码, 蓝色背景的节点表示 IntPipeline.Head 的实例 stage1stage_1(完整的类名太长了,在图中就简称为 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
AutoCloseablejava.lang.AutoCloseable
BaseStreamjava.util.stream.BaseStream
IntStreamjava.util.stream.IntStream
PipelineHelperjava.util.stream.PipelineHelper
AbstractPipelinejava.util.stream.AbstractPipeline
IntPipelinejava.util.stream.IntPipeline
IntPipeline.Headjava.util.stream.IntPipeline.Head
IntPipeline.StatelessOpjava.util.stream.IntPipeline.StatelessOp
IntPipeline.StatefulOpjava.util.stream.IntPipeline.StatefulOp

小结

Step 1 创建了 IntPipeline.Head 类的实例 stage1stage_1

Step 2: 执行 filter 操作得到新的 IntStream

分析

Step 1 创建了 IntStream 的一个实例 stage1stage_1Step 2 通过 stage1stage_1 调用 IntStream.filter(...) 方法,生成 IntStream 的又一个实例。我们称这个新实例为 stage2stage_2

stage2stage_2java.util.stream.IntPipeline.StatelessOp 的一个 匿名子类 的实例 ⬇️ image.png 注意:stage2stage_2 的精确类型是 java.util.stream.IntPipeline$10,由于这是一个匿名内部类,我们不必关心这个类的具体名称。匿名内部类有点像做了好事而不愿留名的人,对这样的人来说,只要好事做完了就可以了,别人不必知道自己的名字。

下图展示了 Step 2 的逻辑 ⬇️

Step 2

灰色背景的节点表示 Step 2 的代码, 蓝色背景的两个节点表示 IntStream 的两个实例,即 stage1stage_1stage2stage_2

  • 图中有名为 previousStage 的黄色边,这个 previousStage 字段来自 AbstractPipeline 类。
  • 图中有名为 nextStage 的蓝色边,这个 nextStage 字段来自 AbstractPipeline 类。

小结

  • Step 2 创建了 IntPipeline.StatelessOp 类的(子类的)实例 stage2stage_2
  • stage1stage_1, stage2stage_2 通过 previousStage 字段和 nextStage 字段组成了一个双向链表。

Step 3: 执行 map 操作得到新的 IntStream

分析

Step 2 创建了 IntStream 的实例 stage2stage_2Step 3 通过 stage2stage_2 调用 IntStream.map(...) 方法,生成 IntStream 的一个实例。我们称这个新实例为 stage3stage_3

stage3stage_3java.util.stream.IntPipeline.StatelessOp 的一个 匿名子类 的实例 ⬇️

image.png

注意:stage3stage_3 的精确类型是 java.util.stream.IntPipeline$4,由于这是一个匿名内部类,我们不必关心这个类的具体名称。

下图展示了 Step 3 的逻辑 ⬇️

Step 3

灰色背景的节点表示 Step 3 的代码, 蓝色背景的三个节点表示 IntStream 的三个实例,即 stage1stage_1, stage2stage_2, stage3stage_3

小结

  • Step 3 创建了 IntPipeline.StatelessOp 类的(子类的)实例 stage3stage_3
  • stage1stage_1, stage2stage_2, stage3stage_3 组成了一个双向链表。
    • nextStage 用于指向链表中的下一个节点。如果用 \rightarrow 表示 nextStage ,那么这三个节点的关系可以表示为 stage1stage_1 \rightarrow stage2stage_2 \rightarrow stage3stage_3
    • previousStage 用于指向链表中的上一个节点。如果用 \leftarrow 表示 previousStage ,那么这三个节点的关系可以表示为 stage1stage_1 \leftarrow stage2stage_2 \leftarrow stage3stage_3

我把 stage1stage_1, stage2stage_2, stage3stage_3 的重要信息列在下方的表格中(其中 depthdepth 字段上文没有提到,不过它的含义比较直观)。

classclasssourceStagesourceStagenextStagenextStagepreviousStagepreviousStagedepthdepth
stage1stage_1IntPipeline.Headstage1stage_1stage2stage_2nullnull00
stage2stage_2IntPipeline.StatelessOp 的一个子类stage1stage_1stage3stage_3stage1stage_111
stage3stage_3IntPipeline.StatelessOp 的一个子类stage1stage_1nullnullstage2stage_222

我们只关心 nextStagenextStagepreviousStagepreviousStage 这两个字段,把 Step 3 的图简化一下,得到如下的结果 ⬇️

Step 3

Step 4: 调用 sum() 方法

Step 1Step 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 的一个实例,我们将这个实例称为 terminal1terminal_1 (就本文而言 TerminalOp 只有这一个实例,但为了风格的统一,还是给它加了个下标)

evaluate(...) 方法的代码如下 ⬇️

image.png

通过打断点可以确认,我们的代码对应的 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) 方法返回 terminal1terminal_1

TerminalOp 接口中的 evaluateSequential(...) 方法

terminal1terminal_1 是在下图红框位置生成的 ⬇️ image.png

ReduceOps.makeInt(...) 这个静态方法的逻辑如下 ⬇️

image.png

看来 ReduceOps.makeInt(int identity, IntBinaryOperator operator) 方法会返回一个匿名内部类,且这个匿名内部类是 ReduceOp 类的子类(这个匿名内部类的全限定类名是 java.util.stream.ReduceOps$6)。

就我们的代码而言,

  • identity 参数是 0
  • operator 参数是对 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
TerminalOpjava.util.stream.TerminalOp
ReduceOpjava.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 其实就是 stage3stage_3

我们先猜测一下这两个方法的作用,然后一边看一边验证。

  • makeSink() 方法听起来是要制作 sink
  • helper.wrapAndCopyInto(...) 方法听起来像是要对 sink 做包装,然后把 stream 里的元素 copy 到 sink 里去。

先看 makeSink()

makeSink() 方法

Step 1Step 3,我们生成了 3IntStream 的实例,即 stage1stage_1, stage2stage_2, stage3stage_3。 我们可以把这些实例想象成水流, 那么水流要流到哪里去呢?要流到 sink 去。我去 Google 了一下 ⬇️

image.png

看来厨房和卫生间的水槽都可以称为 sink。水流从真实的 sink 流走, 而 IntStream 则从 javasink 流走。

image.png

通过打断点可以看到 makeSink() 中只有以下一句代码

return new ReducingSink();

这句代码返回了 ReducingSink 类的实例,我们称这个实例为 sink1sink_1

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
Consumerjava.util.function.Consumer
Sinkjava.util.stream.Sink
Supplierjava.util.function.Supplier
IntConsumerjava.util.function.IntConsumer
TerminalSinkjava.util.stream.TerminalSink
Sink.OfIntjava.util.stream.Sink.OfInt
AccumulatingSinkjava.util.stream.ReduceOps.AccumulatingSink
ReducingSink略(因为它是局部内部类,不必关心它的完整类名)

这么说来 makeSink() 方法的作用确实是制作一个 sink。 那么 makeSink() 就看完了,更新一下任务列表 ⬇️

  • makeSink()
  • helper.wrapAndCopyInto(...)helper 其实就是 stage3stage_3

然后再看 helper.wrapAndCopyInto(...) 方法。

helper.wrapAndCopyInto(...) 方法

wrapAndCopyInto(...) 方法定义在 PipelineHelper 这个抽象类中 ⬇️

image.png

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 其实就是 stage3stage_3
    • wrapSink(...)
    • copyInto(...)

我们还是先猜猜这些方法的作用,然后一边看代码一边验证。

  • wrapSink(...) 方法听起来是对 sink 做包装
  • copyInto(...) 方法听起来会把元素从什么地方 copy 到什么地方去,照理说应该是从 stage1stage_1 copy 到 sink 去。

先看 wrapSink(...)。 它的 javadoc 如下 ⬇️ 看来确实是在做包装的工作。 image.png

其代码如下

// 以下代码是从 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, 为了便于描述,我们按照时间顺序把新生成的 sinksink 依次称为 sink2sink_2sink3sink_3

那么这段代码的主要逻辑是

  • stage3stage_3sink1sink_1 包装成 sink2sink_2
  • stage2stage_2sink2sink_2 包装成 sink3sink_3

image.png

请注意

  • 3stage 节点是从 Head 的实例算起的,所以 从上到下 依次是 stage1stage_1, stage2stage_2, stage3stage_3Head 实例是 stage1stage_1
  • 3sink 节点是从 ReducingSink 的实例算起的,所以 从下到上 依次是 sink1sink_1, sink2sink_2, sink3sink_3ReducingSink 实例是 sink1sink_1),这 3sink 节点通过 downstream 字段构成了一个单链表 sink3sink_3 \rightarrow sink2sink_2 \rightarrow sink1sink_1

wrapSink(...) 方法的返回值就是上图的 sink3sink_3

wrapSink(...) 方法看完了,更新后的任务列表如下 ⬇️

  • makeSink()
  • helper.wrapAndCopyInto(...)helper 其实就是 stage3stage_3
    • wrapSink(...)
    • copyInto(...)

然后我们再去看 copyInto(...) 方法。

这个方法的 javadoc 如下 ⬇️ 看来它会把 IntStream 中的元素 copy 到 sink3sink_3 里去。

image.png

打断点后,会看到我们的代码将运行到 copyInto(...) 方法里的 if 分支 ⬇️

image.png

上图中的 wrappedSink 就是 sink3sink_3

wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();

看来又有新的代码要看了,更新后的任务列表如下 ⬇️

  • makeSink()
  • helper.wrapAndCopyInto(...)helper 其实就是 stage3stage_3
    • wrapSink(...)
    • copyInto(...)
      • begin(long) method in Sink interface
      • forEachRemaining(Consumer) method in Spliterator interface
      • end() method in Sink interface

先猜测一下这三个方法的作用 ⬇️

  • 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 这个嵌套类(其实它是个接口,但是如果叫“嵌套接口”的话,感觉有点拗口)中 overrideforEachRemaining(Consumer) 方法。我们的代码会执行这个 override 的方法 ⬇️

image.png

通过打断点,我们会发现,上图的 if 分支成立。 这个 if 分支里只有下面一行代码

forEachRemaining((IntConsumer) action);

上面的这个 forEachRemaining(IntConsumer) 方法在 Spliterator.OfInt 类中。 打断点后,会发现 java.util.stream.Streams.RangeIntSpliteratoroverrideforEachRemaining(IntConsumer) 方法。

这里有点绕,我们可以参考下方的类图来辅助理解。

image.png

打断点后,可以看到 i 会从 1 遍历到 10 (不包含 10)⬇️

image.png

图中的 consumer 就是上文提到的 sink3sink_3sink3sink_3sink2sink_2 都是 Sink.ChainedInt 的子类的实例。

下方的表格展示了 sink3sink_3, sink2sink_2, sink1sink_1 是如何通过 downstreamdownstream 字段连接起来的 ⬇️

classclassdownstreamdownstreamcodecode
sink3sink_3Sink.ChainedInt 的一个匿名子类sink2sink_2image.png 注意:因为有 filter 的逻辑,所以有的元素不会传递给 downstreamdownstream
sink2sink_2Sink.ChainedInt 的一个匿名子类sink1sink_1image.png 注意:图中的 mapper 会执行计算立方的操作
sink1sink_1ReducingSink ⬅️ 它是一个局部内部类没有 downstreamdownstream 字段image.png 注意:图中的 operatorInteger 中的静态方法 sum(int, int) 对应

至于 begin(long)end(),它们也是沿着 sink3sink2sink1sink_3 \rightarrow sink_2 \rightarrow sink_1 的方向来处理的。

为了方便理解,我画了对应的表格 ⬇️

classclassdownstreamdownstreamcode for begin(long)begin(long)code for end()end()
sink3sink_3Sink.ChainedInt 的一个匿名子类sink2sink_2image.pngimage.png
sink2sink_2Sink.ChainedInt 的一个匿名子类sink1sink_1image.png 注意:由于匿名子类没有 override begin(long)begin(long) 方法,所以这里的 begin(long)begin(long) 方法来自 Sink.ChainedIntimage.png 注意:其实本图和上一行的图对应的代码在同一个位置
sink1sink_1ReducingSink ⬅️ 它是一个局部内部类没有 downstreamdownstream 字段image.pngimage.png
相关时序图
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) 方法的时序图

完整的例子会涉及从 199 种情况,全画出来的话会比较繁琐且没有必要,所以下图中只展示了 i=1,2,3,4i=1,2,3,4 的情况。

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

注意:

  • 由于 sink3sink_3 中有过滤逻辑,在 sink3sink_3 遇到奇数元素时,它会直接返回
  • sink2sink_2 中会计算入参的立方,所以它的入参和 downstream (即 sink1sink_1)的入参不同
  • sink1sink_1 中的 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(): 生成 sink1sink_1
  • helper.wrapAndCopyInto(...)helper 其实就是 stage3stage_3
    • wrapSink(...): 将 sink1sink_1 包装成 sink2sink_2,再将 sink2sink_2 包装成 sink3sink_3,构成 sink3sink2sink1sink_3 \rightarrow sink_2 \rightarrow sink_1 这样的单链表
    • copyInto(...): 将 stage1stage_1 中的元素 copy 到 sink3sink_3
      • begin(long) method in Sink interface
      • forEachRemaining(Consumer) method in Spliterator interface
      • end() method in Sink interface

参考资料