拆箱会拖慢Java Stream吗?为何带拆箱的Stream求最小值更快?
为什么带拆箱的Stream求最小值反而更快?
这结果看起来有点反直觉对吧?明明多了一步拆箱操作,却比直接用包装类型的Stream快了一倍!其实这背后是Java Stream对原生类型流的专门优化,以及泛型流和原生流底层实现的差异导致的,咱们来慢慢捋清楚:
先看你的测试代码和结果
首先贴一下你的核心代码和测试输出,方便理解场景:
public final class App { private App() { } public static void main(String[] args) { Stopwatch stopwatch = Stopwatch.createStarted(); new App().main(); System.out.println(((double) stopwatch.elapsed(TimeUnit.MICROSECONDS) * 1_000_000) + " seconds!"); } private void main() { List<Integer> list = new ArrayList<>(); for (int i = 0; i < 1000000; i++) { list.add(ThreadLocalRandom.current().nextInt(1000)); } // 多次测试逻辑 Stopwatch noUnboxing = Stopwatch.createStarted(); for (int i = 0; i < 1000; i++) { minNoUnboxing(list); } System.out.println((double) noUnboxing.elapsed(TimeUnit.MILLISECONDS) / 1000 + " no unboxing seconds"); Stopwatch withUnboxing = Stopwatch.createStarted(); for (int i = 0; i < 1000; i++) { minWithUnboxing(list); } System.out.println((double) withUnboxing.elapsed(TimeUnit.MILLISECONDS) / 1000 + " with unboxing seconds"); } private Integer minNoUnboxing(List<Integer> list) { return list.stream().min(Integer::compareTo).orElse(-1); } private Integer minWithUnboxing(List<Integer> list) { return list.stream().mapToInt(x -> x).min().orElse(-1); } }
测试输出:
4.166 no unboxing seconds 1.922 with unboxing seconds
核心原因:原生流的底层效率优势
1. 泛型Stream的min操作开销
minNoUnboxing方法里的list.stream().min(Integer::compareTo)是基于泛型Stream<T>的操作:
- 遍历过程中始终处理
Integer包装对象,每次比较都要调用Integer::compareTo方法(本质是a.compareTo(b),内部还要再调用Integer.compare(a.intValue(), b.intValue()),多了一层方法调用) - 泛型
min方法依赖Comparator接口,即使是方法引用,JVM也要处理接口方法的调用分发,相比原生操作有额外开销 - 整个过程涉及包装对象的字段访问(获取
intValue),不如直接操作原生int高效
2. IntStream的min操作是原生优化的
而minWithUnboxing里的mapToInt(x -> x).min()则利用了原生类型流IntStream的专属优化:
mapToInt(x -> x)的拆箱操作看起来有成本,但JIT编译器会把这个简单的拆箱逻辑(等价于x.intValue())直接内联到代码里,几乎消除了额外开销IntStream.min()的底层实现是直接针对原生int值的比较,逻辑类似手写的原生int遍历:int minVal = Integer.MAX_VALUE; for (int num : ints) { if (num < minVal) minVal = num; },完全是机器码级别的原生整数比较,没有包装对象的访问和方法调用开销- 原生流避免了泛型类型擦除带来的间接性,JIT可以对这段代码做更深度的优化,比如循环展开、边界检查消除等
JIT优化的放大效应
你的测试是循环1000次,属于热点代码,JIT会对这段代码进行深度编译优化:
- 对于
IntStream的操作,JIT可以生成几乎没有额外开销的原生机器码 - 而泛型
Stream的Comparator调用,即使经过优化,还是会比原生操作多一点点开销,1000次循环累积下来,就形成了一倍的性能差距
简单来说:拆箱的那点开销,和原生流比较操作节省的开销比起来,完全不值一提——这就是为什么带拆箱的方法反而更快。
内容的提问来源于stack exchange,提问作者Ivan Dimitrov
相关产品推荐
相关产品推荐

