問題描述
先來看一下以下的代碼,猜猜他們會是什么樣的結果:
1 public class FinallyIssue {
2 public static void main(String[] args) {
3 System.out.println("finallyReturnTest : ");
4 System.out.println("return value : " + finallyReturnTest(1));
5 System.out.println("return value : " + finallyReturnTest(-1));
6
7 System.out.println("finallyBreakTest : ");
8 System.out.println("return value : " + finallyBreakTest(true));
9 System.out.println("return value : " + finallyBreakTest(false));
10
11 System.out.println("valueChangeInFinallyTest : ");
12 System.out.println("return value : " + valueChangeInFinallyTest());
13
14 System.out.println("valueChangeReturnInFinallyTest : ");
15 System.out.println("return value : " + valueChangeReturnInFinallyTest());
16
17 System.out.println("refValueChangeInFinallyTest : ");
18 System.out.println("return name : " + refValueChangeInFinallyTest().name);
19 }
20
21 private static boolean finallyReturnTest(int value) {
22 try {
23 if(value > 0) {
24 return true;
25 } else {
26 return false;
27 }
28 } finally {
29 return false;
30 }
31 }
32
33 private static boolean finallyBreakTest(boolean value) {
34 while(value) {
35 try {
36 return true;
37 } finally {
38 break;
39 }
40 }
41 return false;
42 }
43
44 private static int valueChangeInFinallyTest() {
45 int i = 10;
46 int j = 1;
47 try {
48 i = 100;
49 j = 2;
50 System.out.println("try : i = " + i);
51 System.out.println("try : j = " + j);
52 return i;
53 } catch(Exception e) {
54 e.printStackTrace();
55 } finally {
56 i = 1000;
57 j = 3;
58 System.out.println("finally : i = " + i);
59 System.out.println("finally : j = " + j);
60 }
61
62 return i;
63 }
64
65 private static int valueChangeReturnInFinallyTest() {
66 int i = 10;
67 int j = 1;
68 try {
69 i = 100;
70 j = 2;
71 System.out.println("try : i = " + i);
72 System.out.println("try : j = " + j);
73 return i;
74 } catch(Exception e) {
75 e.printStackTrace();
76 } finally {
77 i = 1000;
78 j = 3;
79 System.out.println("finally : i = " + i);
80 System.out.println("finally : j = " + j);
81 return i;
82 }
83 }
84
85 private static Person refValueChangeInFinallyTest() {
86 Person p = new Person();
87 try {
88 p.name = "person1";
89 System.out.println("try : Person name is : " + p.name);
90 return p;
91 } catch(Exception e) {
92 e.printStackTrace();
93 } finally {
94 p.name = "person2";
95 System.out.println("finally : Person name is : " + p.name);
96 }
97
98 p.name = "person3";
99 System.out.println("out : Person name is : " + p.name);
100
101 return p;
102 }
103
104 static class Person {
105 public String name;
106 }
107 }
這樣一段代碼的結果會是什么呢?
以下是運行結果:
finallyReturnTest :
return value : false
return value : false
finallyBreakTest :
return value : false
return value : false
valueChangeInFinallyTest :
try : i = 100
try : j = 2
finally : i = 1000
finally : j = 3
return value : 100
valueChangeReturnInFinallyTest :
try : i = 100
try : j = 2
finally : i = 1000
finally : j = 3
return value : 1000
refValueChangeInFinallyTest :
try : Person name is : person1
finally : Person name is : person2
return name : person2
這個結果很出乎我的意料,我們知道finally總是會在try-catch語句塊執行完后執行,不管try語句塊中是否已經返回或者拋出了異常。
但是在上面的代碼測試中,如果finally語句塊中有return、break、continue等語句,那么它們會覆蓋try語句塊中的return、break、continue的語句,如以上的finallyReturnTest()、finallyBreakTest()、valueChangeReturnInFinallyTest()三個函數。
另外,如果在finally語句塊中修改要返回的值類型變量的值,則這些修改不會保存下來,如valueChangeInFinallyTest()函數;如果要返回的值是引用類型,則修改引用類型的內部成員的值會保存下來。
如何解釋這個結果呢?
問題解釋
結合《深入Java虛擬機(第二版)》這本書和代碼編譯后產生的二進制指令代碼,我對以上問題做了部分解釋,鑒于我的才疏學淺,有些觀點是有誤的,希望高手指正(有誤的觀點容易引起誤導,這也是所以我一直非常小心,奈何水平有限,有些時候難免出錯)。
在《深入Java虛擬機(第二版)》的第18章中提到,在早期的Java中,finally的行為是通過JSR指令來實現的,并且為這個指令引入了微型子程序的概念。我的理解,所謂微型子程序就是在函數A中嵌入一個不完整的函數B的調用。比如在這本書上的一個例子:
private static int microSubroutine(boolean bValue) {
try {
if(bValue) {
return 1;
}
return 0;
} finally {
System.out.println("finally");
}
}
會生成以下的二進制代碼:
0 iload_0
1 ifeq 11
4 iconst_1
5 istore_1
6 jsr 24
9 iload_1
10 ireturn
11 iconst_0
12 istore_1
13 jsr 24
16 iload_1
17 ireturn
18 astore_2
19 jsr 24
22 aload_2
23 athrow
24 astore_3
25 getstatic #7 <Field java.io.PrintStream out>
28 ldc #1 <String “finally”>
30 invokevirtual #8 <Method void println(java.lang.String)>
33 ret 3
如上,24前綴的代碼行以后的部分就是微型子程序,在每一個出口之前都會用JSR調用這個微型子例程序,在這個微型子例程序返回(ret)后,返回調用JSR指令的下一條指令,然后返回(ireturn、athrow)。
jsr指令和ret指令的格式如下:
jsr branchbyte1, branchbyte2
把返回地址壓棧,跳轉至((branchbyte1<<8) | branchbyte2)的位置繼續之行。
ret index
返回在index指示的局部變量中存儲的值(位置)。
在上面的二進制代碼中,每次通過jsr 24跳轉到微型子程序,它先將返回地址(jsr 24指令的下一條指令的地址)保存在index為3的局部變量中,執行完微型子程序后,通過ret 3返回到調用jsr 24指令的下一條指令執行,并最終執行返回。
可是后來(有人說是自1.4.2后),JVM中取消了jsr指令了,所有finally內部的代碼都內聯到源代碼中了(二進制的源代碼)。所以以上的代碼在之后的編譯器中會產生如下的二進制代碼:
0 iload_0 [bValue]
1 ifeq 14
4 getstatic java.lang.System.out : java.io.PrintStream [16]
7 ldc <String "finally"> [94]
9 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]
12 iconst_1
13 ireturn
14 getstatic java.lang.System.out : java.io.PrintStream [16]
17 ldc <String "finally"> [94]
19 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]
22 iconst_0
23 ireturn
24 astore_1
25 getstatic java.lang.System.out : java.io.PrintStream [16]
28 ldc <String "finally"> [94]
30 invokevirtual java.io.PrintStream.println(java.lang.String) : void [24]
33 aload_1
34 athrow
額,貌似有點偏題了,以上的描述是為了解釋《深入Java虛擬機(第二版)》中對finally描述過時的描述。下面讓我們來真正的解決這個問題。還是從生成的Java二進制代碼入手。
首先來看一下valueChangeInFinallyTest()函數的二進制代碼(注釋了打印語句,使代碼簡潔):
//int i = 10
0 bipush 10
2 istore_0 [i]
//int j = 1
3 iconst_1
4 istore_1 [j]
//i = 100
5 bipush 100
7 istore_0 [i]
//j = 2
8 iconst_2
9 istore_1 [j]
//保存i的值,因為它是要返回的
10 iload_0 [i]
11 istore 4
//--------------------------------內聯finally語句塊(開始)----------------------
//i = 1000
13 sipush 1000
16 istore_0 [i]
//j = 3
17 iconst_3
18 istore_1 [j]
//--------------------------------內聯finally語句塊(結束)----------------------
//加載保存后的i的值,并返回。這里返回的是finally語句塊執行前的i(由istore 4語句緩存起來)的值,因而在finally語句塊中任何對i的操作并不會保留下來。這是在沒有異常發生的情況下。
19 iload 4
21 ireturn
22 astore_2 [e]
23 aload_2 [e]
24 invokevirtual java.lang.Exception.printStackTrace() : void [104]
//--------------------------------內聯finally語句塊(開始)----------------------
27 sipush 1000
30 istore_0 [i]
31 iconst_3
32 istore_1 [j]
//--------------------------------內聯finally語句塊(結束)----------------------
33 goto 45
36 astore_3
//--------------------------------內聯finally語句塊(開始)----------------------
37 sipush 1000
40 istore_0 [i]
41 iconst_3
42 istore_1 [j]
//--------------------------------內聯finally語句塊(結束)----------------------
//而在異常發生但沒有被正確處理的情況下,返回值已經沒有什么意義了。
43 aload_3
44 athrow
//這里是在有異常發生,并且異常得到了正確處理的情況下返回的,此時在finally語句塊中對i的操作就會保存下來,并返回給調用者。
45 iload_0 [i]
46 ireturn
相信以上的注釋已經能很好的的解決這個問題了(注:這里j的存在是為了證明在內聯finally語句塊的時候,它只緩存返回值i,而無須緩存其他變量的值,如j的值)。需要特別注意的一點是,如果正常返回的話,finally語句塊中修改i的值是保存不下來的,但是如果出現異常,并被正常捕獲后,在finally語句塊中修改的i的值就會保存下來了。
那么對valueChangeReturnInFinallyTest()函數中的現象如何解釋呢?對這個問題解釋,首先要理解ireturn的指令。ireturn指令沒有操作數,它把當前操作棧的棧頂的int值作為默認的操作數。ireturn指令會彈出當前棧頂的int值,將其壓入調用者的操作棧中,同時忽略當前操作棧中的其他值,即函數正常返回。因而如果在不優化的情況下,在finally語句塊中的return語句會返回當前棧頂的int值(修改后的i值),然后函數返回,此時棧上的其他操作數就被忽略了,并且原本應該執行的ireturn語句也不會之行了。這種方式甚至會忽略拋出的異常,即使當前方法有異常拋出,它的調用方法還是認為它正常返回了。
如果查看優化后的valueChangeReturnInFinallyTest()方法的二進制源碼后,會發現當前的代碼更加簡潔了。但是它還是沒有避免在finally語句塊中使用return后,會忽略沒有捕獲到的異常的問題。
//int i = 10
0 bipush 10
2 istore_0 [i]
//int j = 1
3 iconst_1
4 istore_1 [j]
//i = 100
5 bipush 100
7 istore_0 [i]
//j = 2
8 iconst_2
9 istore_1 [j]
10 goto 22
//catch block
13 astore_2 [e]
14 aload_2 [e]
15 invokevirtual java.lang.Exception.printStackTrace() : void [104]
18 goto 22
21 pop
//--------------------------------內聯finally語句塊(開始)----------------------
//i = 100
22 sipush 1000
25 istore_0 [i]
//j = 3
26 iconst_3
27 istore_1 [j]
//--------------------------------內聯finally語句塊(結束)----------------------
//返回finally語句塊中i的值
28 iload_0 [i]
29 ireturn
經過以上的解釋,我想對refValueChangeInFinallyTest()函數中的現象就比較好解釋了,因為當進入finally語句塊的時候,保存的只是Person實例的一個引用,在finally語句塊中依然可以通過引用操作Person內部成員的,因而在finally語句塊中的修改才能保存下來。
而經過編譯器優化后的finallyReturnTest()和finallyBreakTest()函數生成的二進制代碼就成一樣的了:
0 iload_0 [value]
1 ifeq 8
4 goto 8
7 pop
8 iconst_0
9 ireturn
后記
原本以為這是一個小問題的,沒想到花了我一個下午的時間才把問題說清楚了,而在描述問題的過程中,我對問題的本質也看的更加清晰了。這個問題開始是源于我在論壇http://www.javaeye.com/topic/458668中看到,感覺論壇里面的人都沒很好的說清楚這個問題,剛好我看完了《深入Java虛擬機(第二版)》的書,就把這個問題完整的描述出來了。
于2010年9月24日
注:這些文章都是前些時候寫的,之前博客很亂,也都是隨便貼一些自己寫的或轉載的,還有一些則是沒有貼出來過的。現在打算好好整理一下,完整的記錄自己的一些學習歷程,而每次看到過去的時間,則讓我想起以前的日子,因而我對時間一直是很重視的,所以每篇都著名寫的日期,直到最先的文章出現。:)