Stream 流水线:惰性、短路与并行边界
学完你要能自己回答
- Stream 的惰性是怎么实现的?为什么没有终止操作就什么都不发生?
- Spliterator 的切分与 characteristics 如何影响并行?
- 短路在什么条件下生效?
sorted为什么会破坏短路? - parallel stream 的收益与风险分别是什么?
按元素垂直流动,而不是按阶段水平执行01
filter 不是先把整个集合过滤完再交给 map,而是每个元素依次走完所有阶段。这样 findFirst 在拿到第一个匹配元素后就不再向上游要数据。
| 阶段 | 例子 | 是否立即执行 |
|---|---|---|
| 来源 | collection.stream() / Stream.of | 否 |
| 中间操作 | filter / map / sorted | 否,只建流水线 |
| 终止操作 | collect / forEach / count | 是,触发执行 |
| 短路终止 | findFirst / anyMatch | 是,可能提前停止 |
sorted 是有状态中间操作,必须等所有元素到齐;它会破坏短路,除非上游有限且你确实需要全局排序。
惰性:没有终止操作就没有执行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 是为调试设计的;它的执行次数和顺序依赖流水线优化,不要用它做业务副作用。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 可跳过 |
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
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的下游收集器。
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。