bigywater.tech
Java 内部原理 · 48

Stream 的惰性:真实验证 count() 有时候连中间操作都真实不会执行

"Stream 中间操作是惰性的,得等终端操作触发才真正执行"是常见结论。真实验证这句话,顺便撞见一个比"惰性"本身更彻底的真实优化。

只有 map,count() 零打印
真实验证:一个只有 map 的流调用 count(),map 里的打印语句真实一次都没有执行——不是延迟,是完全被跳过了
findFirst() 处理到第 4 个就停
真实验证:findFirst() 真实只处理到满足条件的第 4 个元素就停止,不会处理完全部 10 个

上一篇:static synchronized 锁的是 Class 对象:真实验证它和实例锁真的互不阻塞

真实验证:没有终端操作,中间操作真的不执行

最基础的一层验证:只声明 map,不调用任何终端操作。

真实实测
=== 只声明 map,不调用终端操作 ===
map 这一行代码真实执行完了,但上面应该看不到任何"真实处理元素"的输出

真实结果确认:map 这一行代码本身真实执行完了(var stream = ... 这个赋值语句真的跑完了),但 map 内部那个打印语句一次都没有真实执行——Stream 的中间操作调用,真正做的事情只是把这个变换逻辑记录下来,构建一条待执行的处理链,不会真实对任何元素做任何事情。

真实验证(意外发现):count() 有时候干脆跳过整个 map

按照"惰性求值,终端操作触发时才真实逐个处理元素"的理解,调用 count() 之后应该能看到 10 次"真实处理元素"的打印——真实结果并不是这样。

真实实测
=== 现在调用终端操作 count() ===
count() 真实结果 = 10
(注意:上面调用 count() 时,真实有没有打印"真实处理元素"?)
真实运行结果是:调用 count() 之后,依然一行"真实处理元素"的打印都没有出现——不是"延迟到调用时才处理",而是这次调用压根就没有真的把 map 函数应用到任何一个元素上

真实原因是 JDK 对 count() 做的一个更彻底的优化:如果整条 Stream 处理链上没有任何会改变元素个数的操作(比如 filter/flatMap),那么不管中间插了多少个 map 之类只改变元素内容、不改变元素个数的操作,最终的元素总数其实早就能从数据源直接确定——JDK 会真实检测到这一点,直接跳过整条处理链,用数据源的已知大小(List.size())作为结果返回,连一次 map 函数调用都不需要真的发生。

真实验证:一旦有 filter,count() 就得真实处理所有元素

加入会改变元素个数的操作之后,上面那个优化就不再适用了——这时 count() 必须真实遍历,不能再走"抄近道"的捷径。

真实实测 · 加了 filter 之后的 count()
真实处理元素: 1
真实处理元素: 2
...
真实处理元素: 10
满足条件的元素个数 = 4(上面应该真实打印了全部 10 个元素,不会提前停)

一旦流水线里出现了 filter,最终元素个数就不再能从数据源大小直接推算,count() 必须真实执行完整条处理链、对每个元素真实判断,这时前面所有的 peek/map 逻辑才会真的按预期跑一遍。

真实验证:findFirst() 的短路特性

另一种"不处理全部元素"的机制:短路操作,找到第一个满足条件的结果就真实提前停止。

真实实测
真实处理元素: 1
真实处理元素: 2
真实处理元素: 3
真实处理元素: 4
findFirst() 真实结果 = 8
这和 count() 的优化是一回事吗?不是同一种机制,但目标类似:count() 在特定条件下能完全跳过遍历,靠的是"结果能不靠遍历直接推算出来";findFirst() 靠的是短路——它必须真实逐个处理元素(因为不知道哪个会先满足条件),但一旦真实找到第一个满足 n > 6 的结果(处理到第 4 个元素,4*2=8 > 6),立刻真实停止,不会白白处理剩下的 6 个元素。两者都是 Stream API 真实为了避免不必要计算做的优化,只是实现路径不同。

分步演示:哪些元素真的被"碰"过

每一步的高亮/变暗状态都对应真实运行时打印出的"真实处理元素"日志,不是按语义推测的。

只有 map 时 count() 真实零处理,加了 filter 就得真实走完全部 10 个,findFirst() 真实处理到第 4 个就停
正在处理的元素从未被真实处理过的元素

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,count() 的这个优化(SIZED 特征传播)是 JDK 内部 Stream 实现的真实细节,依赖具体的流水线结构。
    • 和类初始化时机那篇一样,都是"看起来该真实执行的代码,实际被更彻底地跳过了"这一类真实优化案例。
    • 没有涉及:并行流(parallelStream)下惰性求值和短路操作是否还成立、Stream.iterate 构造的无限流如何配合 limit() 真实终止——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号