"Stream 中间操作是惰性的,得等终端操作触发才真正执行"是常见结论。真实验证这句话,顺便撞见一个比"惰性"本身更彻底的真实优化。
上一篇:static synchronized 锁的是 Class 对象:真实验证它和实例锁真的互不阻塞
最基础的一层验证:只声明 map,不调用任何终端操作。
=== 只声明 map,不调用终端操作 === map 这一行代码真实执行完了,但上面应该看不到任何"真实处理元素"的输出
真实结果确认:map 这一行代码本身真实执行完了(var stream = ... 这个赋值语句真的跑完了),但 map 内部那个打印语句一次都没有真实执行——Stream 的中间操作调用,真正做的事情只是把这个变换逻辑记录下来,构建一条待执行的处理链,不会真实对任何元素做任何事情。
按照"惰性求值,终端操作触发时才真实逐个处理元素"的理解,调用 count() 之后应该能看到 10 次"真实处理元素"的打印——真实结果并不是这样。
=== 现在调用终端操作 count() === count() 真实结果 = 10 (注意:上面调用 count() 时,真实有没有打印"真实处理元素"?)真实运行结果是:调用 count() 之后,依然一行"真实处理元素"的打印都没有出现——不是"延迟到调用时才处理",而是这次调用压根就没有真的把 map 函数应用到任何一个元素上
真实原因是 JDK 对 count() 做的一个更彻底的优化:如果整条 Stream 处理链上没有任何会改变元素个数的操作(比如 filter/flatMap),那么不管中间插了多少个 map 之类只改变元素内容、不改变元素个数的操作,最终的元素总数其实早就能从数据源直接确定——JDK 会真实检测到这一点,直接跳过整条处理链,用数据源的已知大小(List.size())作为结果返回,连一次 map 函数调用都不需要真的发生。
加入会改变元素个数的操作之后,上面那个优化就不再适用了——这时 count() 必须真实遍历,不能再走"抄近道"的捷径。
真实处理元素: 1 真实处理元素: 2 ... 真实处理元素: 10 满足条件的元素个数 = 4(上面应该真实打印了全部 10 个元素,不会提前停)
一旦流水线里出现了 filter,最终元素个数就不再能从数据源大小直接推算,count() 必须真实执行完整条处理链、对每个元素真实判断,这时前面所有的 peek/map 逻辑才会真的按预期跑一遍。
另一种"不处理全部元素"的机制:短路操作,找到第一个满足条件的结果就真实提前停止。
真实处理元素: 1 真实处理元素: 2 真实处理元素: 3 真实处理元素: 4 findFirst() 真实结果 = 8
每一步的高亮/变暗状态都对应真实运行时打印出的"真实处理元素"日志,不是按语义推测的。