笔试很多时候都会考try、catch、finally的执行顺序,网上也有很多文章记录他们的执行顺序,但是都是根据代码执行结果推规律,有种死记硬背的感觉。从字节码反编译后的代码可以查看try、catch、finally执行顺序以及返回值,从字节码层面上来看他们的执行顺序。
Java之所以能够 Write Once,Run Anywhere,关键在于Java编译后的生成字节码文件.class
,一个.java
文件对应一个.class
文件。Java程序运行,就是JVM运行这些字节码文件。字节码文件对所有平台版本的JVM都是一样的,所以编译一次后的Java程序可以在各个平台运行,本质就是字节码文件兼容各个平台版本的JVM。
在cmd中
-
javac Main.java
可以将某个.java文件编译成字节码文件,编译成功会生成对应文件名字的.class
文件,里面都是字节码,十六进制。 -
javap -c Main.class
可以查看某个字节码文件反编译后的代码,就是JVM要执行的指令,不同版本的JVM,反编译后的指令可能是有差异的。
下面从几个栗子来探究一下try、catch、finally块到底是怎样一回事。
public class Main{
public void test() {
try {
tryItOut();
} catch(Exception e) {
catchItOut();
} finally {
wrapItUp();
}
}
public void tryItOut() {}
public void catchItOut() {}
public void wrapItUp() {}
}
//Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void test();
Code:
0: aload_0
1: invokevirtual #2 // Method tryItOut:()V
4: aload_0
5: invokevirtual #3 // Method wrapItUp:()V
8: goto 30
11: astore_1
12: aload_0
13: invokevirtual #5 // Method catchItOut:()V
16: aload_0
17: invokevirtual #3 // Method wrapItUp:()V
20: goto 30
23: astore_2
24: aload_0
25: invokevirtual #3 // Method wrapItUp:()V
28: aload_2
29: athrow
30: return
Exception table:
from to target type
0 4 11 Class java/lang/Exception
0 4 23 any
11 16 23 any
public void tryItOut();
Code:
0: return
public void catchItOut();
Code:
0: return
public void wrapItUp();
Code:
0: return
}
0-28指令都是test方法中的指令。查看Exception table,可以得知,0-4指令如果出现异常java.lang.Exception,则跳到指令11,如果出现其他任何异常则跳到指令23,在11-16指令处如果出现任何异常,则跳到指令23。
结合代码来看,
-
指令0-4是try块中的代码,0-4指令正常执行,则执行指令5,然后go to指令30,return也就是该方法结束。如果此处出现异常,两种情况
- 出现
java.lang.Exception
,则跳到指令11,也就是执行catch块中的代码。 - 出现除了
java.lang.Exception
以外其他任何异常,跳到指令23,也就是执行finally块代码,最后返回。
- 出现
指令11-16是catch块中的代码,如果正常执行,则执行指令17,也就是finally块,如果此处出现异常,则跳到指令23,执行finally块。
总结来看,主要有三种情况:
- 无任何异常,则执行try块后代码,再执行finally块代码,返回
- try中出现异常,并且可以被catch捕捉,则是执行try块代码,再执行catch块代码,最后执行finally代码,返回;如果try中出现的异常,不能被catch捕捉,则是执行try块代码,再执行finally代码,返回
- try中出现异常,并且可以被catch捕捉,catch出现异常,则是执行try块代码,再执行catch块代码,最后执行finally代码,返回
也就是说finally代码无论如何都会执行的,主要看会不会执行catch块。另外,可以看出finally中出现异常则不会被处理,如果finally中出现异常应该用catch处理,避免finally中出现异常。
public class Main{
public int test() {
try {
tryItOut();
return 1;
} catch(Exception e) {
catchItOut();
return 2;
}
finally {
wrapItUp();
return 3;
}
}
// auxiliary methods
public void tryItOut() {}
public void catchItOut() {}
public void wrapItUp() {}
public static void main(String[] args) {
System.out.println(new Main().test());
}
}
Compiled from "Main.java"
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int test();
Code:
0: aload_0
1: invokevirtual #2 // Method tryItOut:()V
4: iconst_1
5: istore_1
6: aload_0
7: invokevirtual #3 // Method wrapItUp:()V
10: iconst_3
11: ireturn
12: astore_1
13: aload_0
14: invokevirtual #5 // Method catchItOut:()V
17: iconst_2
18: istore_2
19: aload_0
20: invokevirtual #3 // Method wrapItUp:()V
23: iconst_3
24: ireturn
25: astore_3
26: aload_0
27: invokevirtual #3 // Method wrapItUp:()V
30: iconst_3
31: ireturn
Exception table:
from to target type
0 6 12 Class java/lang/Exception
0 6 25 any
12 19 25 any
public void tryItOut();
Code:
0: return
public void catchItOut();
Code:
0: return
public void wrapItUp();
Code:
0: return
public static void main(java.lang.String[]);
Code:
0: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #7 // class Main
6: dup
7: invokespecial #8 // Method "<init>":()V
10: invokevirtual #9 // Method test:()I
13: invokevirtual #10 // Method java/io/PrintStream.println:(I)V
16: return
}
如上分析可以得到,执行步骤也是一致的。
除此之外,需要注意的是return值。
return的值是就近原则,返回最近存储的值。
譬如try中代码正常执行,优先返回finally值,如果finally没有返回值,则返回try中的返回值。
什么情况返回catch中的返回值呢?根据字节码可以得到当异常被catch捕捉,finally没有返回值,则返回catch中的返回值。
什么时候返回try,catch,finally代码块之外的返回值呢?那么应该是这种情况,没有被指定到try、catch的return代码,并且finally没有返回值。如下代码是一种情况。
这么看来的话,其实try-catch-finally执行顺序跟返回值都跟指令的执行顺序一致,万变不离其宗,这样再来看这些问题的时候,就不是死记硬背,而是比较条理清晰了。