前几天刷LeetCode题,看到评论区有人问了一个有意思的问题,代码如下:
public static void main(String[] args) {
int a[] = {1, 2, 3, 4, 5};
int num = 3;
a[num - 1] = a[num-- - 1];
for (int i : a) {
System.out.println(i);
}
}
问输出结果应该是1、2、3、4、5,还是1、2、2、4、5?
那如果把num--变成--num,结果又是什么?
一般上学认真听讲的可能都知道结果,num--的时候输出1、2、3、4、5。
--num的时候,输出1、2、2、4、5。
从上学刚学编程,老师就教过,i--是先取i的值,然后i再减1。--i是先减1,再取i的值。
本着刨根问底的精神,特别想知道这两个操作虚拟机到底是怎么执行的。我们javap -c看一下字节码是什么样的。
先看--num时候:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_5
1: newarray int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: iconst_2
10: iastore
11: dup
12: iconst_2
13: iconst_3
14: iastore
15: dup
16: iconst_3
17: iconst_4
18: iastore
19: dup
20: iconst_4
21: iconst_5
22: iastore
23: astore_1
24: iconst_3
25: istore_2
26: aload_1
27: iload_2
28: iconst_1
29: isub
30: aload_1
31: iinc 2, -1
34: iload_2
35: iconst_1
36: isub
37: iaload
38: iastore
39: aload_1
40: astore_3
41: aload_3
42: arraylength
43: istore 4
45: iconst_0
46: istore 5
48: iload 5
50: iload 4
52: if_icmpge 75
55: aload_3
56: iload 5
58: iaload
59: istore 6
61: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
64: iload 6
66: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
69: iinc 5, 1
72: goto 48
75: return
}
我们从头开始逐行分析:
0:iconst_5:表示常量5入栈
1:newarray int:分配int类型数组,需要弹出栈顶数据作为数组长度
3:dup:复制栈顶内容,这里就是复制了一份数组
接下来4到22行,都是数据赋值,我们只看一组4到7行:
4:iconst_0:常量0入栈
5:iconst_1:常量1入栈
6:iastore:将栈顶int数据存入指定数组的指定索引位置,需要出栈3次,所以每次赋值后都要dup复制栈顶元素。
数据初始化完,从23行继续看:
23:astore_1:将栈顶元素存入第二个本地变量,在java方法中局部变量数组中按顺序要放入:this(static方法没有)、参数、局部变量。所以这里第二个变量就是我们声明的数组a。
此时栈分布是空的:
栈 |
---|
接下来24、25行:
24:iconst_3:常量3入栈
栈 |
---|
3 |
25:istore_2:将栈顶元素保存到第三个本地变量,就是给我们声明的num赋值3
栈 |
---|
接下来就是取值了:
26:aload_1:将第二个引用类型入栈,这里就是数组a入栈
27:iload_2:将第三个整数类型入栈,这里就是num入栈
28:iconst_1:常量1入栈
栈 |
---|
1 |
num=3 |
a |
29:isub:栈顶两个元素减法,结果入栈。就是num-1结果入栈
栈 |
2 |
a |
30:aload_1:将第二个引用类型入栈,这里就是数组a入栈
栈 |
a |
2 |
a |
31:iinc 2,-1:将第三个本地变量与-1相加,此时num变为2
34:iload_2:将第三个整数类型入栈,这里就是num入栈
35:iconst_1:常量1入栈
栈 |
1 |
num=2 |
a |
2 |
a |
36:isub:栈顶两个元素减法,结果入栈,即num-1
栈 |
1 |
a |
2 |
a |
37:iaload:int类型数组栈顶位置索引的数值入栈,这里就是a[num-1]入栈
栈 |
a[1]=2 |
2 |
a |
38:iastore:将栈顶int类型数据存储到int类型数组指定索引位置。
这里也就是a[2]=a[1],所以打印结果是1、2、2、4、5。
我们再看下num--情况下的字节码:
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_5
1: newarray int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: iconst_2
10: iastore
11: dup
12: iconst_2
13: iconst_3
14: iastore
15: dup
16: iconst_3
17: iconst_4
18: iastore
19: dup
20: iconst_4
21: iconst_5
22: iastore
23: astore_1
24: iconst_3
25: istore_2
26: aload_1
27: iload_2
28: iconst_1
29: isub
30: aload_1
31: iload_2
32: iinc 2, -1
35: iconst_1
36: isub
37: iaload
38: iastore
39: aload_1
40: astore_3
41: aload_3
42: arraylength
43: istore 4
45: iconst_0
46: istore 5
48: iload 5
50: iload 4
52: if_icmpge 75
55: aload_3
56: iload 5
58: iaload
59: istore 6
61: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
64: iload 6
66: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
69: iinc 5, 1
72: goto 48
75: return
}
唯一的区别就在于31行,在num--的情况下,执行iinc 2,-1前,先执行了iload_2,即对局部变量修改前,提前把局部变量值入栈。
至此,分析完毕。
通过分析字节码追溯该问题的根源,还是很有意思的。
字节码其实在编码中还是会常用到的,比如虚拟机是怎么做try...catch的等等,从字节码上可以很清楚的理解。