問(wèn)題描述
先來(lái)看一下以下的代碼,猜猜他們會(huì)是什么樣的結(jié)果:
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 }
這樣一段代碼的結(jié)果會(huì)是什么呢?
以下是運(yùn)行結(jié)果:
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
這個(gè)結(jié)果很出乎我的意料,我們知道finally總是會(huì)在try-catch語(yǔ)句塊執(zhí)行完后執(zhí)行,不管try語(yǔ)句塊中是否已經(jīng)返回或者拋出了異常。
但是在上面的代碼測(cè)試中,如果finally語(yǔ)句塊中有return、break、continue等語(yǔ)句,那么它們會(huì)覆蓋try語(yǔ)句塊中的return、break、continue的語(yǔ)句,如以上的finallyReturnTest()、finallyBreakTest()、valueChangeReturnInFinallyTest()三個(gè)函數(shù)。
另外,如果在finally語(yǔ)句塊中修改要返回的值類型變量的值,則這些修改不會(huì)保存下來(lái),如valueChangeInFinallyTest()函數(shù);如果要返回的值是引用類型,則修改引用類型的內(nèi)部成員的值會(huì)保存下來(lái)。
如何解釋這個(gè)結(jié)果呢?
問(wèn)題解釋
結(jié)合《深入Java虛擬機(jī)(第二版)》這本書(shū)和代碼編譯后產(chǎn)生的二進(jìn)制指令代碼,我對(duì)以上問(wèn)題做了部分解釋,鑒于我的才疏學(xué)淺,有些觀點(diǎn)是有誤的,希望高手指正(有誤的觀點(diǎn)容易引起誤導(dǎo),這也是所以我一直非常小心,奈何水平有限,有些時(shí)候難免出錯(cuò))。
在《深入Java虛擬機(jī)(第二版)》的第18章中提到,在早期的Java中,finally的行為是通過(guò)JSR指令來(lái)實(shí)現(xiàn)的,并且為這個(gè)指令引入了微型子程序的概念。我的理解,所謂微型子程序就是在函數(shù)A中嵌入一個(gè)不完整的函數(shù)B的調(diào)用。比如在這本書(shū)上的一個(gè)例子:
private static int microSubroutine(boolean bValue) {
try {
if(bValue) {
return 1;
}
return 0;
} finally {
System.out.println("finally");
}
}
會(huì)生成以下的二進(jìn)制代碼:
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前綴的代碼行以后的部分就是微型子程序,在每一個(gè)出口之前都會(huì)用JSR調(diào)用這個(gè)微型子例程序,在這個(gè)微型子例程序返回(ret)后,返回調(diào)用JSR指令的下一條指令,然后返回(ireturn、athrow)。
jsr指令和ret指令的格式如下:
jsr branchbyte1, branchbyte2
把返回地址壓棧,跳轉(zhuǎn)至((branchbyte1<<8) | branchbyte2)的位置繼續(xù)之行。
ret index
返回在index指示的局部變量中存儲(chǔ)的值(位置)。
在上面的二進(jìn)制代碼中,每次通過(guò)jsr 24跳轉(zhuǎn)到微型子程序,它先將返回地址(jsr 24指令的下一條指令的地址)保存在index為3的局部變量中,執(zhí)行完微型子程序后,通過(guò)ret 3返回到調(diào)用jsr 24指令的下一條指令執(zhí)行,并最終執(zhí)行返回。
可是后來(lái)(有人說(shuō)是自1.4.2后),JVM中取消了jsr指令了,所有finally內(nèi)部的代碼都內(nèi)聯(lián)到源代碼中了(二進(jìn)制的源代碼)。所以以上的代碼在之后的編譯器中會(huì)產(chǎn)生如下的二進(jìn)制代碼:
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
額,貌似有點(diǎn)偏題了,以上的描述是為了解釋《深入Java虛擬機(jī)(第二版)》中對(duì)finally描述過(guò)時(shí)的描述。下面讓我們來(lái)真正的解決這個(gè)問(wèn)題。還是從生成的Java二進(jìn)制代碼入手。
首先來(lái)看一下valueChangeInFinallyTest()函數(shù)的二進(jìn)制代碼(注釋了打印語(yǔ)句,使代碼簡(jiǎn)潔):
//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的值,因?yàn)樗且祷氐?/span>
10 iload_0 [i]
11 istore 4
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(開(kāi)始)----------------------
//i = 1000
13 sipush 1000
16 istore_0 [i]
//j = 3
17 iconst_3
18 istore_1 [j]
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(結(jié)束)----------------------
//加載保存后的i的值,并返回。這里返回的是finally語(yǔ)句塊執(zhí)行前的i(由istore 4語(yǔ)句緩存起來(lái))的值,因而在finally語(yǔ)句塊中任何對(duì)i的操作并不會(huì)保留下來(lái)。這是在沒(méi)有異常發(fā)生的情況下。
19 iload 4
21 ireturn
22 astore_2 [e]
23 aload_2 [e]
24 invokevirtual java.lang.Exception.printStackTrace() : void [104]
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(開(kāi)始)----------------------
27 sipush 1000
30 istore_0 [i]
31 iconst_3
32 istore_1 [j]
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(結(jié)束)----------------------
33 goto 45
36 astore_3
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(開(kāi)始)----------------------
37 sipush 1000
40 istore_0 [i]
41 iconst_3
42 istore_1 [j]
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(結(jié)束)----------------------
//而在異常發(fā)生但沒(méi)有被正確處理的情況下,返回值已經(jīng)沒(méi)有什么意義了。
43 aload_3
44 athrow
//這里是在有異常發(fā)生,并且異常得到了正確處理的情況下返回的,此時(shí)在finally語(yǔ)句塊中對(duì)i的操作就會(huì)保存下來(lái),并返回給調(diào)用者。
45 iload_0 [i]
46 ireturn
相信以上的注釋已經(jīng)能很好的的解決這個(gè)問(wèn)題了(注:這里j的存在是為了證明在內(nèi)聯(lián)finally語(yǔ)句塊的時(shí)候,它只緩存返回值i,而無(wú)須緩存其他變量的值,如j的值)。需要特別注意的一點(diǎn)是,如果正常返回的話,finally語(yǔ)句塊中修改i的值是保存不下來(lái)的,但是如果出現(xiàn)異常,并被正常捕獲后,在finally語(yǔ)句塊中修改的i的值就會(huì)保存下來(lái)了。
那么對(duì)valueChangeReturnInFinallyTest()函數(shù)中的現(xiàn)象如何解釋呢?對(duì)這個(gè)問(wèn)題解釋,首先要理解ireturn的指令。ireturn指令沒(méi)有操作數(shù),它把當(dāng)前操作棧的棧頂?shù)?/span>int值作為默認(rèn)的操作數(shù)。ireturn指令會(huì)彈出當(dāng)前棧頂?shù)?/span>int值,將其壓入調(diào)用者的操作棧中,同時(shí)忽略當(dāng)前操作棧中的其他值,即函數(shù)正常返回。因而如果在不優(yōu)化的情況下,在finally語(yǔ)句塊中的return語(yǔ)句會(huì)返回當(dāng)前棧頂?shù)?/span>int值(修改后的i值),然后函數(shù)返回,此時(shí)棧上的其他操作數(shù)就被忽略了,并且原本應(yīng)該執(zhí)行的ireturn語(yǔ)句也不會(huì)之行了。這種方式甚至?xí)雎話伋龅漠惓#词巩?dāng)前方法有異常拋出,它的調(diào)用方法還是認(rèn)為它正常返回了。
如果查看優(yōu)化后的valueChangeReturnInFinallyTest()方法的二進(jìn)制源碼后,會(huì)發(fā)現(xiàn)當(dāng)前的代碼更加簡(jiǎn)潔了。但是它還是沒(méi)有避免在finally語(yǔ)句塊中使用return后,會(huì)忽略沒(méi)有捕獲到的異常的問(wèn)題。
//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
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(開(kāi)始)----------------------
//i = 100
22 sipush 1000
25 istore_0 [i]
//j = 3
26 iconst_3
27 istore_1 [j]
//--------------------------------內(nèi)聯(lián)finally語(yǔ)句塊(結(jié)束)----------------------
//返回finally語(yǔ)句塊中i的值
28 iload_0 [i]
29 ireturn
經(jīng)過(guò)以上的解釋,我想對(duì)refValueChangeInFinallyTest()函數(shù)中的現(xiàn)象就比較好解釋了,因?yàn)楫?dāng)進(jìn)入finally語(yǔ)句塊的時(shí)候,保存的只是Person實(shí)例的一個(gè)引用,在finally語(yǔ)句塊中依然可以通過(guò)引用操作Person內(nèi)部成員的,因而在finally語(yǔ)句塊中的修改才能保存下來(lái)。
而經(jīng)過(guò)編譯器優(yōu)化后的finallyReturnTest()和finallyBreakTest()函數(shù)生成的二進(jìn)制代碼就成一樣的了:
0 iload_0 [value]
1 ifeq 8
4 goto 8
7 pop
8 iconst_0
9 ireturn
后記
原本以為這是一個(gè)小問(wèn)題的,沒(méi)想到花了我一個(gè)下午的時(shí)間才把問(wèn)題說(shuō)清楚了,而在描述問(wèn)題的過(guò)程中,我對(duì)問(wèn)題的本質(zhì)也看的更加清晰了。這個(gè)問(wèn)題開(kāi)始是源于我在論壇http://www.javaeye.com/topic/458668中看到,感覺(jué)論壇里面的人都沒(méi)很好的說(shuō)清楚這個(gè)問(wèn)題,剛好我看完了《深入Java虛擬機(jī)(第二版)》的書(shū),就把這個(gè)問(wèn)題完整的描述出來(lái)了。
于2010年9月24日
注:這些文章都是前些時(shí)候?qū)懙模安┛秃軄y,也都是隨便貼一些自己寫(xiě)的或轉(zhuǎn)載的,還有一些則是沒(méi)有貼出來(lái)過(guò)的。現(xiàn)在打算好好整理一下,完整的記錄自己的一些學(xué)習(xí)歷程,而每次看到過(guò)去的時(shí)間,則讓我想起以前的日子,因而我對(duì)時(shí)間一直是很重視的,所以每篇都著名寫(xiě)的日期,直到最先的文章出現(xiàn)。:)