一道經典筆試題
下面是一道經典的面試題:try {}裡有一個return語句,那麼緊跟在這個try後的finally {}裡的code會不會被執行,什麼時候被執行,在return前還是後?
很多人回答return在後,但是所給的理由不夠充分;或者有些說return在前如http://bbs.csdn.net/topics/60474475;還有的說在return中間執行等等;
首先說一下,我的答案如下:finally在return返回之前執行。在執行finally之前,程式碰到return,首先計算機return後面的表示式,並將結果儲存到一個臨時變數,然後開始執行finally中的語句,但此時的語句已經無法影響到需要返回的臨時變數,執行完finally之後,取出臨時變數的值返回;下面用程式證明我的觀點:
int testtry() {
int x = 5;
try {
return x=2;
} finally {
x=x+2;
}
}
該函式的返回值應該為1;使用bytecodeview可以檢視編譯後的位元組碼檔案:
0 iconst_5 1 istore_1 //將5存入區域性變數1 2 iconst_2 //將常量2存入棧 3 dup //複製棧頂 4 istore_1 //將2存入區域性變數1 5 istore_3 //將2存入區域性變數3 6 iinc 1 by 2 //對區域性變數1自增2,可見對區域性變數3沒有影響 9 iload_3 //載入區域性變數3 10 ireturn //返回區域性變數3,即為2 11 astore_2 12 iinc 1 by 2 15 aload_2 16 athrow
由於JVM虛擬機器對程式首先檢查優化使用的標準優化技術有:程式碼提升,公共的子表示式清除、迴圈展開(unrolling)、範圍檢測清除、死程式碼清除、資料流分析,還有各種在靜態編譯語言中不實用的優化技術,例如虛方法呼叫的聚合內聯,所以首先計算return後面的表示式,為了證明這點,我對程式碼稍作修改如下:
int testtry() {
int x = 5;
try {
return x=(x+3); //證明先計算表示式
} finally {
x=x+2;
}
}
得到的位元組碼如下:
0 iconst_5 1 istore_1 2 iinc 1 by 3 //可見首先計算表示式 5 iload_1 //結果存於區域性變數1即x中 6 istore_3 7 iinc 1 by 2 10 iload_3 11 ireturn 12 astore_2 13 iinc 1 by 2 16 aload_2 17 athrow
由上可以證明我的觀點,對於有return 返回值的try finally結構來說,fianlly先執行,但是在finally裡面的操作區域性變數不再對返回值造成影響,返回的結果是原來計算儲存在臨時變數的結果,其實這裡只是延遲return返回而已。
對於try finally結構帶有return語句的問題,可以參考Java虛擬機器規範中相關章節,下面對其中的一部分做一下引用說明:(書中以jdk 版本號小於50的為例):
java虛擬機器規範(SE7)4.10.2.5 異常和 finally 寫道 為了實現 try-finally 結構,在版本號小於或等於 50.0 的 Java 語言編譯器中,可以使用將兩種特殊指令:jsr( “跳轉到程式子片段” )和 ret( “程式子片段返回” )組合的方式來生成try-finally 結構的 Java 虛擬機器程式碼。當使用 jsr 指令來呼叫程式子片段時,該指令會把程式子片段結束後應返回的地址值壓入運算元棧中, 以便在 jsr 之後的指令能被正確執行。這個地址值會作為 returnAddress 型別資料存放於運算元棧上。程式子片段的程式碼中會把返回地址存放在區域性變數中,在程式子片段執行結束時,ret 指令從區域性變數中取回返回地址並將執行的控制權交給返回地址處的指令。如果 try 語句中遇到了 return,程式碼的行為如下:
1. 如果有返回值的話,將返回值儲存在區域性變數中。
2. 執行 jsr 指令將控制權轉到給 finally 語句中。
3. 在 finally 執行完成後,返回事先儲存在區域性變數中的值。
下面再說明另外一個例子:
public int get() {
try {
return 1;
} finally {
return 2;
}
}
答案很是奇怪,竟然是2!下面是位元組碼:
0 goto 4 (+4)
3 pop
4 iconst_2
5 ireturn
這裡有兩個return語句,如果按照上面的話第二個return的效果就不會起作用,但是返回的確實是第二個的2!由上面的位元組碼可以看出,程式直接跳到第二個常量2並返回。
java虛擬機器規範在“finally 語句中的程式碼也給驗證器帶來了一些特殊的問題”這樣寫道:
驗證 finally 語句中的程式碼是很複雜的,但幾個基本的思路如下: 每個保持追蹤 jsr 目標的指令都需要能到達那個目標指令。對於大部分程式碼來說,這個列表是空的。對於 finally 語句中的程式碼來說,列表的長度應該是 1。對於多級嵌入finally 程式碼來說,列表的長度應該大於 1。
對於每條指令及每條 jsr 指令將要轉向到那條指令,在 jsr 指令執行後,就有一個位向量(Bit Vector)記錄著所有對區域性變數的訪問及修改。
執行 ret 指令就意味著從程式子片段中返回,這應該是唯一的一條從程式子片段中返回的路徑。兩個不同的程式子片段是不同將 ret 指令的執行結果歸併到一起。
為了對 ret 指令實施資料流分析,需要進行一些特殊處理。因為驗證器知道程式子片段中將從哪些指令中返回,所以它可以找出呼叫程式子片段的所有的 jsr 指令,並將它們對應的 ret 指令的運算元棧和區域性變量表狀態合併。對於合併區域性變量表時使用的特殊值定義如下:
如果位向量 (前面定義過)表明區域性變數在程式子片段中被訪問或修改過,那麼就使用執行 ret 時區域性變數的值的型別。
對於其它區域性變數,使用執行 jsr 指令之前的區域性變數的型別。
可以看出在java編譯對程式進行過優化,優化方法使用資料流分析,將這些區域性變數進行了合併,至此,可以看出,在try finally結構含有return的情況,首先根據java編譯優化處理,得到位元組碼,優化主要是對於一些不必要的程式碼消除合併,如果在try塊中含有return則先將return後的變數存在區域性變量表中,地址存於操作棧中,然後進入finally塊,最後取出區域性變數的值返回。如果在finally裡有return操作,則把操作棧中的彈出,該return地址入棧,並最後返回。
以上不知道我的理解對不對,歡迎討論!
附件是周志明([email protected])等人翻譯的java虛擬機器規範,上文中有引用。