记录一些 Java 代码中非常细节的点。有时候看看就行,没必要专门去记这个。
遍历列表的几种写法
遍历列表是一个非常频繁的操作,有很多种写法,但是不同写法之间还是有细微差异的。
常见写法:
public static int sum1(List<Integer> list) {
int res = 0;
for (int i = 0; i < list.size(); i++)
res += list.get(i);
return res;
}
iMax 写法:
public static int sum2(List<Integer> list) {
int res = 0;
for (int i = 0, iMax = list.size(); i < iMax; i++)
res += list.get(i);
return res;
}
看起来第一种相当于每一轮循环都会重新获取列表的大小,字节码的反映上确实是这样子的,这个是 sum1 的字节码:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: aload_0
6: invokeinterface #2, 1 // InterfaceMethod java/util/List.size:()I
11: if_icmpge 36
14: iload_1
15: aload_0
16: iload_2
17: invokeinterface #3, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
22: checkcast #4 // class java/lang/Integer
25: invokevirtual #5 // Method java/lang/Integer.intValue:()I
28: iadd
29: istore_1
30: iinc 2, 1
33: goto 4 // 重点
36: iload_1
37: ireturn
字节码显示每一轮循环结束都会跳转到第 4 条指令,而第 6 条指令负责调用 size() 方法获取列表大小,因此每一轮循环都要重新获取终止条件。
for-loop 写法:
public void sum3(Blackhole blackhole) {
int sum = 0;
for (Integer integer : list)
sum += integer;
blackhole.consume(sum);
}
我们看一下 for-loop 写法编译后反编译的代码:
public void sum3(Blackhole blackhole) {
int sum = 0;
Integer integer;
for(Iterator var3 = this.list.iterator(); var3.hasNext(); sum += integer)
integer = (Integer)var3.next();
blackhole.consume(sum);
}
可以看出,for-loop 写法实际上就是一个语法糖,本质上是循环的迭代器写法。使用迭代器的循环消耗肯定比前两种要高,但是好处是迭代过程中可以增删列表的元素而不会报错。
虽然普通写法的字节码显示会重复调用 List::size 方法,但是实际上数据量小时执行得要比其他写法快,应该是 JIT 的优化导致的。数据量大时 for-loop 写法要快些,估计也是 JIT 优化的原因。
# 10000 数据量
Benchmark Mode Cnt Score Error Units
LoopBenchmark.sum1 avgt 10 8.954 ± 0.465 us/op
LoopBenchmark.sum2 avgt 10 9.656 ± 0.369 us/op
LoopBenchmark.sum3 avgt 10 10.304 ± 0.513 us/op
# 100000 数据量
Benchmark Mode Cnt Score Error Units
LoopBenchmark.sum1 avgt 10 127.279 ± 39.001 us/op
LoopBenchmark.sum2 avgt 10 119.146 ± 15.773 us/op
LoopBenchmark.sum3 avgt 10 110.528 ± 2.728 us/op