概述
刚入坑java的时候, 经常有类似如下的面试题,并询问执行结果以及为什么?
public class TestTryCatchFinally {
public static void main(String[] args) {
try {
System.out.println("try code");
testMethodThrowException();
} catch (RuntimeException ex) {
System.out.println("catch code");
throw ex;
} finally {
System.out.println("finally code");
}
}
private static void testMethodThrowException() {
throw new RuntimeException("testMethodThrowException...");
}
}
输出如下:
try code
catch code
finally code
Exception in thread "main" java.lang.RuntimeException: testMethodThrowException...
at TestTryCatchFinally.testMethodThrowException(TestTryCatchFinally.java:25)
at TestTryCatchFinally.main(TestTryCatchFinally.java:15)
下面我们将通过以下几点来分析try/catch/finally执行机制:
- try/catch/finally语义
- 在字节码层面是如何处理的
- jdk7之后的语法糖
try/catch/finally语义
try {
...
} catch(XxxException e) {
...
} finally {
...
}
- try{}中的语句块为监控可能发生异常的代码块。
- catch(){}用来捕获异常。当try{}触发异常时,通过匹配catch中的异常类型进行捕获处理。需要注意的是多个catch块从上往下依次匹配,并且前面的Exception范围不能包含下面的Exception,否则会在编译期间报错。
- finally{}主要用于做一些资源清理工作。
try可以随意搭配catch,finally其中一个,或两者都搭配使用。 (但不能单独使用try,这也失去了try的意图)
在字节码层面是如何处理的
将前面概述中的代码通过javap命令翻译成字节码,下面是字节码内容:
下面的字节码简化了一些信息,并标记了一些简单的注释。
public class com.example.leetcode.jvm.day1.TestTryCatchFinally
SourceFile: "TestTryCatchFinally.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
... //常量池
{
public com.example.leetcode.jvm.day1.TestTryCatchFinally();
... // 默认构造方法字节码
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
// 获取System.out标准输出静态属性
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 将常量池字符串"try code"压入操作数栈顶
3: ldc #3 // String try code
// 调用println方法输出
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 调用testMethodThrowException()静态方法
8: invokestatic #5 // Method testMethodThrowException:()V
// 获取System.out标准输出静态属性
11: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 将常量池字符串"finally code"压入操作数栈顶
14: ldc #6 // String finally code
// 调用println方法
16: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 跳转到字节码索引行44 return
19: goto 44
22: astore_1
// 获取System.out标准输出静态属性
23: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 将常量池字符串"catch code"压入操作数栈顶
26: ldc #8 // String catch code
// 调用println方法
28: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: aload_1
// 将栈顶的异常对象抛出
32: athrow
33: astore_2
// 获取System.out标准输出静态属性
34: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// 将常量池字符串"finally code"压入操作数栈顶
37: ldc #6 // String finally code
// 调用println方法
39: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
42: aload_2
// 将栈顶的异常对象抛出
43: athrow
// 正常执行返回
44: return
Exception table:
from to target type
0 11 22 Class java/lang/RuntimeException
0 11 33 any
22 34 33 any
...
}
通过阅读上面的字节码内容可以知道:
finally中的语句块会以“内联”的方式拼接到try后面(其实catch不再次抛出也会拼接,可以注释掉源码中catch语句块中的throw ex再生成字节码观察一下)。
如果try中抛出了异常如何处理呢?
测试源码中try{}里面的testMethodThrowException()方法抛出了一个RuntimeException异常。 那在字节码层面如何执行呢?
先认识一下Exception table
Exception table可以理解成一张表, 表中有如下属性:
{
from 字节码起始索引
to 字节码结束索引(不包含)
target 异常处理字节码索引起始位置
type 异常类型(any表示任何类型)
}
当触发异常时,会在从上到下遍历Exception table每一条,当触发异常的字节码索引在from-to的范围,抛出的异常type也能匹配则通过target进行处理。否则继续往下寻找,没有找到将抛出到上一层方法的stack frame重复执行此过程。
看看Exception table中的内容:
Exception table:
from to target type
0 11 22 Class java/lang/RuntimeException
0 11 33 any
22 34 33 any
- 字节码索引[0-11(不包括11)]类型为RuntimeException的从target字节码索引22行开始处理。
- 字节码索引[0-11(不包括11)]类型为any(任意种类)的从target字节码索引33行开始处理。
- 字节码索引[22-34(不包括34)]类型为any(任意种类)的从target字节码索引33行开始处理。
再来分析测试代码异常抛出的执行流程:
- 首先在try{}中调用的testMethodThrowException()抛出了异常,对应字节码索引[0-11(不包括11)]将从target字节码索引22行开始处理。
- 在字节码索引[22-32]我们先输出了catch code字符串,然后又throw ex(athrow)抛出异常。 此时被Exception table第三行所包含匹配进行处理。
- 而Exception table第三行从字节码索引处理开始处理,输出finally code字符串后再次通过athrow抛出异常。后面没有匹配的异常处理条目,将往上一层stack frame抛出(我们是在man方法中,将直接被虚拟机打印到控制台)。
jdk7之后的语法糖
jdk7之后对try/catch做了一些调整,下面是一段测试代码:
public class TestTryJdk7 {
static class Person implements AutoCloseable{
@Override
public void close() {
System.out.println("Person close");
}
}
public static void main(String[] args) {
try (Person p = new Person()){
System.out.println("try catch");
} catch (IllegalArgumentException | IndexOutOfBoundsException ex) {
System.out.println("catch code");
ex.printStackTrace();
}
}
}
主要有两点:
- try(...){} 只要()中声明的对象实现了AutoCloseable接口,不需要再编写finally语句块手动关闭。
- catch(XxException1 | XxException2 e) 语句块()中可以通过“|”处理多个异常。
有兴趣的同学也可以翻译成字节码查看一下是如何处理的。
总结
通过try/catch/finally语法语义也可以理解执行流程, 但结合字节码分析会有更深层次的理解(也不再担心面试题中各种形形色色的写法)。
思考
不知道大家有没有想过jdk7之后为什么会在try后面增加资源清理支持,难道仅仅是为了编写上面更优雅吗?