为何IntStream.mapToObj映射在Stream.count时未被执行?
问题拆解:为什么IntStream.mapToObj后count不执行映射操作?
这是个非常典型的Stream优化陷阱,我来给你把底层逻辑说透:
核心原因:JVM对无元素数量变化的Stream做了短路优化
Java的Stream API在设计时会尽可能做性能优化,尤其是当终端操作是count()的时候。如果中间操作不会改变流的元素数量,而且原始流的大小是确定的,JVM就会直接跳过中间操作的执行,直接返回原始流的大小。
放到你的例子里:
IntStream.range(1000,1010)是一个大小固定的原始流,元素数量明明白白是10个,JVM完全知道这一点;mapToObj(this::mapToString)只是把每个int转成String,属于无状态、不会增减元素的中间操作——不管怎么映射,元素数量还是10个;- 当你调用
count()时,JVM觉得:既然映射操作不会改变元素数量,那何必浪费时间去执行mapToString里的代码?直接返回原始流的10就完事儿了。这就是为什么你看到count返回了10,但mapToString里的打印和db.add根本没执行。
为什么加filter/用collect/forEach就正常?
这几种情况都是因为JVM无法再做这种优化:
- 加
filter(Objects::nonNull)后,JVM没法确定过滤后剩下多少元素——万一映射返回了null,filter就会把它去掉,元素数量可能减少。所以必须执行mapToObj和filter才能准确计算count,你的映射方法自然就被调用了; collect需要实际处理每个元素来生成集合,不可能跳过映射操作;forEach本身就是为了执行副作用操作设计的,必须遍历每个元素并执行对应逻辑。
额外提醒:Stream操作要尽量避免副作用
顺便提一句,你的mapToString方法里修改外部db集合的操作属于副作用,这其实不符合Stream的最佳实践。Stream的中间操作应该尽量是纯函数(输入相同输出相同,不修改外部状态),副作用应该放在终端操作里(比如forEach)。
正确的写法示例
如果你的目标是初始化数据库记录,更合适的写法是直接用forEach执行副作用:
@Test public void mapToStringAndCount() { IntStream.range(1000, 1010) .forEach(this::mapToString); long count = countString(); Assert.assertEquals(10L, count); }
或者如果你需要保留映射后的结果,用collect来收集:
@Test public void mapToStringAndCount() { List<String> strings = IntStream.range(1000, 1010) .mapToObj(this::mapToString) .collect(Collectors.toList()); Assert.assertEquals(10L, strings.size()); Assert.assertEquals(strings.size(), countString()); }
内容的提问来源于stack exchange,提问作者A4L
相关产品推荐
相关产品推荐

