.java
.測試
.spa
.code
.blog
今天有同事和我探討在羣裏看到的一道有趣的題目,在探討的過程當中讓我搞清楚了一些曾經模糊的概念,特此記錄下來。內存
題目給出以下代碼,問運行後打印的結果是什麼。get
1 public static void main(String []args) { 2 System.out.println(fun()); 3 } 4 public static int fun () { 5 int x = 0; 6 try { 7 x = 1; 8 } finally { 9 ++x; 10 } 11 try { 12 return x; 13 } finally { 14 ++x; 15 } 16 }
嘗試運行,結果以下:(輸出 2)io
1 >$ javac -g No1.java 2 >$ javac No1 3 2 4 >$
爲什麼輸出是 2 而不是 3 呢,這個可能讓不少小夥伴有所疑惑,咱們經過 javap 指令查看字節碼來解釋這個疑問。table
首先來看一些前置知識——java 字節碼指令的 iconst_n、iload_n、istore_n 和 iinc。ast
指令 | 意義 |
iconst_n | 表示將整型常量 n 推入棧頂,n 的範圍是 -1 ≤ n ≤ 5。 |
iload_n | 表示將局部變量表中第 n 個槽的整型變量加載到操做數棧頂。 |
istore_n | 表示將操做數棧頂的整型值彈出,並存儲到局部變量表的第 n 個槽中。 |
iinc a b | 表示將局部變量表第 a 個槽中的值增長 b,並將結果存回 a 槽。 |
好,有了上面四個指令的基礎就夠了,接下來咱們看一下上面代碼的字節碼。
>$ javap -c -l No1 public static int fun(); Code: 0: iconst_0 // 常量 0 入棧頂 1: istore_0 // 將棧頂的 0 彈出並存入局部變量表的第 0 個槽中 2: iconst_1 // 常量 1 入棧頂 3: istore_0 // 彈出棧頂的 1 並存入局部變量表的第 0 個槽中,覆蓋原值 4: iinc 0, 1 // 將局部變量表第 0 個槽中的值加 1,並將結果存回局部變量表第 0 個槽中,覆蓋原值 7: goto 16 // 跳轉到 16 10: astore_1 11: iinc 0, 1 14: aload_1 15: athrow // 下面兩行是重點,將局部變量表第 0 個槽中的值拷貝了一個副本到局部變量表第 1 個槽中 16: iload_0 // 將局部變量表第 0 個槽中的值加載到棧頂 17: istore_1 // 彈出棧頂的值並存儲到局部變量表第 1 個槽中 18: iinc 0, 1 // 將局部變量表第 0 個槽中的值加 1,並將結果存回局部變量表第 0 個槽中,覆蓋原值 21: iload_1 // 重點:將局部變量表中第 1 個槽中的值加載到棧頂(並無加載第 0 個槽中的值) 22: ireturn // 返回棧頂的值 23: astore_2 24: iinc 0, 1 27: aload_2 28: athrow Exception table: from to target type 2 4 10 any 16 18 23 any
上面的文字太抽象,能夠對照着 圖1 的內容來理解。
圖1 執行過程內存圖例
在 圖1 中,紅色的字表示字節碼的行號,黑色的字表示執行此行字節碼以後,對應的內存中的值的變化。
不難看出,在字節碼第 1六、17 行,將 x 的值從局部變量表的第 0 個槽中拷貝了一個副本,保存在局部變量表的第 1 個槽中,而最後執行 ireturn 指令以前,將此副本加載到了棧頂,所以返回的值是在 finally 運算以前就肯定下來了的,此後 finally 中再次對 x 的運算都只是在局部變量表的第 0 個槽中作的,因此並不會影響到 ireturn 指令返回的值。
總結:
通過 LZ 幾番測試發現,不管在 try 中 return 的變量是否參與了後面在 finally 中的計算,都會被拷貝一個副本出來。
而 return 沒有在 try 塊中時,被 return 的變量則不會被拷貝副本。
因而可知,當 try 遇到 return 時,變量被「特殊照顧」了一下。
*注意,以上測試僅使用了 int 類型(基本數據類型),沒有測試 return 引用類型的狀況。