前幾天刷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的等等,從字節碼上可以很清楚的理解。