先来看一下以下的代码,猜猜他们会是什么样的结果:
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的调用。比如在这本书上的一个例子:
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