JDK JDK 学习手册8 → 27 · JDK 专用学习站
DEEPDIVE · COLLECTIONS · JEP 107

Stream 流水线:惰性、短路与并行边界

JDK 8 · 深钻主题 02 / 03 · 预计 55 分钟
深入四步深钻JEP 107
一句话Stream 的价值不只是写法短,而是把「遍历策略」从业务代码里抽走:来源决定怎么切分,中间操作决定怎么处理,终止操作决定何时停止。
本质:一条 Stream 是一次性的惰性流水线:Spliterator 负责产出与切分,Sink 链负责逐元素传递,终止操作负责拉动并在短路时停止。

学完你要能自己回答

  1. Stream 的惰性是怎么实现的?为什么没有终止操作就什么都不发生?
  2. Spliterator 的切分与 characteristics 如何影响并行?
  3. 短路在什么条件下生效?sorted 为什么会破坏短路?
  4. parallel stream 的收益与风险分别是什么?
01 按元素垂直流动,而不是按阶段水平执行02 惰性:没有终止操作就没有执行

按元素垂直流动,而不是按阶段水平执行01

filter 不是先把整个集合过滤完再交给 map,而是每个元素依次走完所有阶段。这样 findFirst 在拿到第一个匹配元素后就不再向上游要数据。

阶段例子是否立即执行
来源collection.stream() / Stream.of否
中间操作filter / map / sorted否,只建流水线
终止操作collect / forEach / count是,触发执行
短路终止findFirst / anyMatch是,可能提前停止

sorted 是有状态中间操作,必须等所有元素到齐;它会破坏短路,除非上游有限且你确实需要全局排序。

把 Stream 想成「传送带」而不是「集合上的批处理」:元素一个个经过工位,终止操作是最后拉动传送带的人。

惰性:没有终止操作就没有执行02

中间操作只是把 Sink 串起来,不会主动拉数据。终止操作开始后,才由最下游的 Sink 向上游 tryAdvance。

List<String> log = new ArrayList<>();

Stream<String> pipeline = Stream.of("a", "bb", "ccc")
        .filter(s -> { log.add("filter:" + s); return s.length() > 1; })
        .map(s -> { log.add("map:" + s); return s.toUpperCase(); });
System.out.println("before=" + log);

Optional<String> first = pipeline.findFirst();
System.out.println("first=" + first.get());
System.out.println("log=" + log);
before=[]
first=BB
log=[filter:a, filter:bb, map:bb]

从输出可以看到:a 被 filter 拒绝,bb 通过 filter 和 map,ccc 根本没被处理——findFirst 在拿到 BB 后就短路了。

peek 是为调试设计的;它的执行次数和顺序依赖流水线优化,不要用它做业务副作用。
01 Spliterator:产出、切分与特征02 Sink 链与终止操作03 并行:切分、commonPool 与合并

Spliterator:产出、切分与特征01

Spliterator 是 Stream 的来源协议:tryAdvance 消费一个元素,trySplit 把剩余元素切一半给另一个线程,estimateSize 给出剩余估算,characteristics 描述 ORDERED/SIZED/DISTINCT/SORTED 等特征。

Spliterator<Integer> sp = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8).spliterator();
System.out.println("size=" + sp.estimateSize());

Spliterator<Integer> half = sp.trySplit();
System.out.println("split=" + half.estimateSize() + "/" + sp.estimateSize());
System.out.println("ordered=" + sp.hasCharacteristics(Spliterator.ORDERED));
size=8
split=4/4
ordered=true

ArrayList 的 Spliterator 是 SIZED + SUBSIZED + ORDERED,所以可以精确对半切分;HashSet 没有 ORDERED,TreeSet 有 SORTED。特征决定哪些优化合法,例如 SIZED 让 count() 能获知精确大小;但 JDK 8 仍会执行中间操作(本机用 peek 可观察),跳过中间操作的 count 优化是后续版本才有的。

特征含义对流水线的影响
ORDERED有遇到顺序并行时保留顺序有成本
SIZED能给出精确大小为 count 提供大小信息
SUBSIZED切分后大小仍精确并行切分更可控
DISTINCT元素不重复distinct 可跳过
SORTED按比较器有序sorted 可跳过
自定义 Spliterator 时,trySplit 必须把一部分元素交给新 Spliterator,且不能重复或丢失;否则并行结果会错。

Sink 链与终止操作02

每个中间操作都包装一个 Sink:filter 在 accept 里判断是否向下传递,map 先转换再传递,sorted 先把元素收进数组再统一发出。终止操作的 Sink 负责收集或短路。

collect(Collector) 把收集逻辑拆成 supplier / accumulator / combiner / finisher。顺序流只用 accumulator;并行流用 combiner 合并各段结果,所以 Collector 必须满足结合律与身份值约束。

List<String> out = Stream.of("a", "bb", "ccc")
        .filter(s -> s.length() > 1)
        .map(String::toUpperCase)
        .collect(Collectors.toList());
System.out.println(out);
[BB, CCC]
有状态操作会把「逐元素」变成「先缓冲」;在无限流上使用 sorted/distinct 可能永远不结束。

并行:切分、commonPool 与合并03

并行流用 trySplit 把来源切成子任务,提交到 ForkJoinPool.commonPool(),每个子任务跑同一套 Sink 链,最后用 combiner 合并。默认并行度是 CPU 核数减一。

long sum = IntStream.rangeClosed(1, 100)
        .parallel()
        .filter(i -> i % 2 == 0)
        .sum();
System.out.println("parallelSum=" + sum);
parallelSum=2550
  • 只有数据量大、每个元素计算重、操作无状态且满足结合律时,收益才可能覆盖切分与合并成本。
  • findFirst 在并行流上要保留遇到顺序,通常不如 findAny 快;forEach 不保证顺序。
  • commonPool 是 JVM 共享的;阻塞 I/O 会占住工作线程,拖慢其他并行流。
  • 并行流里修改共享 ArrayList 会丢元素;用 collect、ConcurrentHashMap 或 reduce。
并行不是「更高级的顺序」;它是用复杂度换吞吐。多数业务流水线顺序执行已经足够。
01 收集器实战与常见异常02 排障:流只能消费一次

收集器实战与常见异常01

toMap 的重复 key 默认抛 IllegalStateException,可以传 merge 函数决定保留哪一个;toMap 的 value 为 null 时会抛 NPE,因为内部用了 Map.merge。

try {
    Stream.of("Ada", "Alan")
          .collect(Collectors.toMap(s -> s.charAt(0), s -> s));
} catch (IllegalStateException e) {
    System.out.println(e.getMessage());
}
Duplicate key Ada
  • 需要有序 Map 时传 TreeMap::new 或 LinkedHashMap::new 作为 mapFactory。
  • groupingBy 的 key 不能为 null(JDK 8 实测抛 NPE);需要保留 null 分组时先映射成哨兵值。
  • partitioningBy 只分 true/false 两组,比 groupingBy 更明确。
  • counting、summingInt、averagingInt 可以作为 groupingBy 的下游收集器。
JDK 8 的 toList() 返回可变 ArrayList;不要把它当作不可变集合直接暴露。不可变收集器是 JDK 10 才有的。

排障:流只能消费一次02

Stream 保存了「已消费」状态;第二次调用终止操作会抛 IllegalStateException。需要重复使用就重新创建流,或先把结果收集到集合。

Stream<String> once = Stream.of("a");
once.count();
try {
    once.count();
} catch (IllegalStateException e) {
    System.out.println(e.getMessage());
}
stream has already been operated upon or closed
  • 用 peek 观察元素时,注意它可能被优化掉;不要依赖其次数。
  • 调试并行流时先用 sequential() 复现,再开并行比较结果。
  • 大量装箱时改用 mapToInt/IntStream;summaryStatistics 一次拿到 count/sum/min/max/average。
  • 不要在 Lambda 里抛受检异常;用自定义函数式接口或包装成运行时异常,并保留 cause。
把 Stream 传给方法时,约定「谁创建谁消费」;返回 Stream 的公共 API 很容易造成二次消费。
01 先猜再看02 能否讲清因果

先猜再看01

先猜再看没有终止操作的 Stream 会执行 filter 吗?
不会。中间操作只构建流水线,终止操作才拉取元素;没有终止操作,什么都不会发生。
先猜再看parallel 一定比 sequential 快吗?
不一定。小数据量、I/O 密集、强顺序依赖或共享可变状态时,并行可能更慢甚至出错;收益来自大数据量、CPU 密集且可并行的计算。
先猜再看sorted 后 findFirst 还能短路吗?
排序本身必须先收齐所有元素,所以短路只能发生在排序之后;如果上游是无限流,sorted 永远等不到结果。

能否讲清因果02

01. Stream 的惰性由什么保证?
中间操作只把 Sink 链串起来,不读取元素;终止操作调用 tryAdvance 从 Spliterator 拉取数据,元素才逐个经过各阶段。
02. Spliterator 的 trySplit 对并行流意味着什么?
它把来源切分成可独立处理的子任务,交给 ForkJoinPool 执行;切分质量决定并行度与负载均衡,切分必须不重不漏。
03. 为什么 sorted 是有状态操作?
它必须看到所有元素才能产生第一个有序结果,因此需要缓冲;这破坏了逐元素流动和短路能力。
04. collect 在并行流里如何合并结果?
Collector 提供 supplier 创建容器、accumulator 累加、combiner 合并两个容器、finisher 做最终转换;并行时每段用自己的容器,最后 combiner 合并。
05. 并行流为什么可能丢元素?
多个线程同时修改非线程安全的 ArrayList 或普通计数器,会发生竞态;应该用 collect/reduce 让每个线程有独立中间结果,或使用线程安全结构。
能说清「Spliterator 切分、Sink 传递、终止操作拉动」三件事,并解释 sorted 为什么破坏短路,这一节就过关了。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。