你真的了解Java中的finally吗?
你真的了解Java中的finally吗?
序章
首先抛出一个问题:Java中的finally一定会执行吗?
很多人都认为finally是一定会执行的,其中包括一些经验老到的程序员。可惜并不像大多数人所想的那样,这个问题的答案是否定的
接下来带大家一同探讨这个问题。
01 什么情况下finally不会被执行?
例 1:
@Test
void test1() {
try {
System.out.println("try");
System.exit(0);//退出程序
}catch (Exception e){
System.out.println("catch");
}finally {
System.out.println("finally");
}
}
执行结果:
try
finally语句块没有被执行,为什么呢?因为我们在语句块中执行了System.exit(0) ,终止了jvm的运行。有人有疑问了,一般情况下也不会有人在程序中调用System.exit(0) 的,那么我不在程序中调用此方法,finally语句块就一定会执行吗?
答案还是否定的。当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行
02 什么情况下finally会被执行?
例 2:
@Test
void test2() {
try {
System.out.println("try");
return;
}finally {
System.out.println("finally");
}
}
执行结果:
try
finally
此例说明 finally 语句块在 try 语句块中的 return 语句之前执行
例 3:
@Test
void test3() {
System.out.println(test33());
}
public String test33(){
try {
System.out.println("try");
int i=1/0;
return "a";
}catch (Exception e){
System.out.println("catch");
return "b";
}finally {
System.out.println("finally");
}
}
执行结果:
try
catch
finally
b
说明了 finally 语句块在 catch 语句块中的 return 语句之前执行
从例2和例3我们可以看出,其实 finally 语句块是在 try 或者 catch 中的 return 语句之前执行的。更加一般的说法是,finally 语句块应该是在控制转移语句之前执行,控制转移语句除了 return 外,还有 break 和 continue。另外,throw 语句也属于控制转移语句。虽然 return、throw、break 和 continue 都是控制转移语句,但是它们之间是有区别的。其中 return 和 throw 把程序控制权转交给它们的调用者(invoker),而 break 和 continue 的控制权是在当前方法内转移。
接下来
例 4:
@Test
public void test4(){
System.out.println(getValue());
}
public int getValue(){
try {
return 1;
}finally {
return 2;
}
}
执行结果:
2
例5:
@Test
public void test5(){
System.out.println(hello());
}
public String hello(){
String s = "hello";
try {
return s;
}finally {
s = s + " finally";
}
}
执行结果:
hello
利用我们上面分析得出的结论:finally 语句块是在 try 或者 catch 中的 return 语句之前执行的。由此,可以很容易理解例4的结果是2;
那么例5的结果为什么不是hello finally,而是hello呢?
实际上,Java 虚拟机会把 finally 语句块作为 subroutine直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中。待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。请注意,前文中我们曾经提到过 return、throw 和 break、continue 的区别,对于这条规则(保留返回值),只适用于 return 和 throw 语句,不适用于 break 和 continue 语句,因为它们根本就没有返回值。
那么为了切实的看到try-finally执行,我们把例5的字节码文件拿出来分析
public void test5();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; //--PrintStream压入栈顶
3: aload_0 //--将引用类型本地变量推送至栈顶0的位置
4: invokevirtual #3 // Method hello:()Ljava/lang/String; //--调用实例方法hello()
7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V //--调用方法println()
10: return //--从当前方法返回void
public java.lang.String hello();
Code:
0: ldc #5 // String hello //--将常量hello从常量池中推送至栈顶
2: astore_1 //--将引用类型从栈顶拉到本地变量1的位置
3: aload_1 //--将引用类型本地变量推送至栈顶1的位置
4: astore_2 //--将引用类型从栈顶拉到本地变量2的位置
5: new #6 // class java/lang/StringBuilder //--创建一个对象StringBuilder
8: dup //--复制栈顶的值重新压入栈顶
9: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V //--调用超类构造方法StringBuilder()
12: aload_1 //--将引用类型本地变量推送至栈顶1的位置
13: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //--调用方法append()
16: ldc #9 // String finally //--finally从常量池推送至栈顶
18: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //--调用方法append()
21: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; //--调用方法toString()
24: astore_1
25: aload_2
26: areturn //--返回引用类型
27: astore_3
28: new #6 // class java/lang/StringBuilder
31: dup
32: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
35: aload_1
36: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
39: ldc #9 // String finally
41: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
44: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
47: astore_1
48: aload_3
49: athrow //--将栈顶的异常抛出
Exception table:
from to target type
3 5 27 any //--从3到5的这段指令出现异常,则由27开始的指令来处理
可以看到在执行16步之前,先执行了0-4步,将hello存入本地变量表2位置;
没有异常时:
执行finally16-24,拼接成"hello finally",存入本地变量表1的位置;接着执行25-26,取出变量表2位置的"hello"返回给调用者;
有异常时:
执行finally39-47,拼接成"hello finally",存入本地变量表1的位置;如果有异常会把异常存到本地变量表3的位置,然后执行48-49,取出异常抛出。
所以结果不是hello finally,而是hello。
03 练习
例5:
@Test
public void test6(){
System.out.println(test66());
}
public int test66(){
int i = 1;
try {
i = 5;
} finally {
i++;
return i;
}
}
执行结果:
6
例7:
@Test
public void test7() {
System.out.println(test76());
}
public String test76() {
try {
System.out.println("try");
return test77();
} finally {
System.out.println("finally");
}
}
public String test77() {
System.out.println("print test77");
return "return test77";
}
执行结果:
try
print test77
finally
return test77
例8:
@Test
public void test8(){
System.out.println(test88());
}
public int test88(){
try {
int a=1/0;
} catch (Exception e) {
System.out.println("catch");
return 11;
} finally {
System.out.println("finally");
return 22;
}
}
执行结果:
catch
finally
22
例9:
@Test
public void test10(){
try {
int a=1/0;
}catch (Exception e){
throw new RuntimeException("除0异常");
}finally {
System.out.println("finally");
return;
}
}
通过练习之后,小伙伴们是不是掌握finally的用法了。
总结一下:
1、除了前面1中讨论的情况finally 块必然执行,不论发生异常与否,也不论在 finally 之前是否有return。
2、finally 执行总是在return之前。
3、finally 中若有return,则try或者catch中的return就不会执行了。所以要注意不要写出例9代码,防止出现无法察觉的错误。
上一篇: 你真的了解Java反射吗?
下一篇: java面试宝典