思维导图
使用
异常处理中,try、catch、finally的执行顺序,大家都知道是按顺序执行的。即,如果try中没有异常,则顺序为try→finally,如果try中有异常,则顺序为try→catch→finally。
但是当try、catch、finally中加入return之后,就会有几种不同的情况出现,下面分别来说明一下
各种 return 情况
一、try中带有return
private int testReturn1() {
int i = 1;
try {
i++;
System.out.println("try:" + i);
return i;
} catch (Exception e) {
i++;
System.out.println("catch:" + i);
} finally {
i++;
System.out.println("finally:" + i);
}
return i;
}
输出:
try:2
finally:3
2
因为当try中带有return时,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try中计算后的2,而非finally中计算后的3。但有一点需要注意,当返回类型是引用类型时,finally 修改了对象,最后返回的同一个对象也会跟着变化
catch中带有return
private int testReturn3() {
int i = 1;
try {
i++;
System.out.println("try:" + i);
int x = i / 0 ;
} catch (Exception e) {
i++;
System.out.println("catch:" + i);
return i;
} finally {
i++;
System.out.println("finally:" + i);
}
return i;
}
输出:
try:2
catch:3
finally:4
3
catch中return与try中一样,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,最后再通过return返回之前保存的信息。所以,这里方法返回的值是try、catch中累积计算后的3,而非finally中计算后的4。
finally中带有return
private int testReturn4() {
int i = 1;
try {
i++;
System.out.println("try:" + i);
return i;
} catch (Exception e) {
i++;
System.out.println("catch:" + i);
return i;
} finally {
i++;
System.out.println("finally:" + i);
return i;
}
}
输出:
try:2
finally:3
3
当finally中有return的时候,try中的return会失效,在执行完finally的return之后,就不会再执行try中的return。这种写法,编译是可以编译通过的,但是编译器会给予警告,所以不推荐在finally中写return,这会破坏程序的完整性,而且一旦finally里出现异常,会导致catch中的异常被覆盖。
小总结:
1、finally中的代码总会被执行。
2、当try、catch中有return时,也会执行finally。return的时候,要注意返回值的类型,是否受到finally中代码的影响。
3、finally中有return时,会直接在finally中退出,导致try、catch中的return失效。
原理(图解)
这里直接来解读字节码,studio 或 idea 安装这个插件 ASM Bytecode Viewer
, 找到编译好的 class 文件,右击 ASM Bytecode Viewer
即可查看,这里有一段代码
public static void main(String[] args) {
try {
System.out.println("enter try block");
} catch (Exception var2) {
System.out.println("enter catch block");
}
}
查看是这样子的:
可以看到 l0 l1是 try 代码块中的输出语句,l2 是 catch 代码块中的输出语句。然后重点来了。
字节码是l1 _goto l3
,这是什么意思呢?没错,盲猜就能猜到,这个字节码指令就是跳转到 l3 的意思。这一行是说,如果 try 代码块中没有出现异常,那么就跳转到第l3,也就是整个方法行完成后 return 了。
这是正常的代码执行流程,那么如果出现异常了,虚拟机是如何知道应该“监控” try 代码块?它又是怎么知道该捕获何种异常呢?
答案就是——异常表。
异常表
在一个类被编译成字节码之后,它的每个方法中都会有一张异常表。异常表中包含了“监控”的范围,“监控”何种异常以及抛出异常后去哪里处理。安装 jclasslib bytecode viewer
,找到编译好的 class 文件,选择 view -> show bytecode with jclasslib
,比如上述的示例代码,在 jclasslib 中它的异常表如下图。
异常表中每一行就代表一个异常处理器。
- Nr. 代表异常处理器的序号
- Start PC (from),代表异常处理器所监控范围的起始点
- End PC (to),代表异常处理器所监控范围的结束点(该行不被包括在监控范围内,一般是 goto 指令)
- Handler PC (target),指向异常处理器的起始位置,在这里就是 catch 代码块的起始位置
- Catch Type (type),代表异常处理器所捕获的异常类型
如果程序触发了异常,Java 虚拟机会按照序号遍历异常表,当触发的异常在这条异常处理器的监控范围内(from 和 to),且异常类型(type)与该异常处理器一致时,Java 虚拟机就会跳转到该异常处理器的起始位置(target)开始执行字节码。
如果程序没有触发异常,那么虚拟机会使用 goto 指令跳过 catch 代码块,执行 finally 语句或者方法返回。
Finally
代码修改如下
public static void main(String[] args) {
try {
System.out.println("enter try block");
} catch (Exception var5) {
System.out.println("enter catch block");
} finally {
System.out.println("enter finally block");
}
}
字节码
出现三块重复字节码指令的原因是在 JVM 中,所有异常路径(如try、catch)以及所有正常执行路径的出口都会被附加一份 finally 代码块。也就是说,在上述的示例代码中,try 代码块后面会跟着一份 finally 的代码,catch 代码块后面也是如此,再加上原本正常流程会执行的 finally 代码块,在字节码中一共有三份 finally 代码块代码块。
而针对每一条可能出现的异常的路径,JVM 都会在异常表中多生成一条异常处理器,用来监控整个 try-catch 代码块,同时它会捕获所有种类的异常,并且在执行完 finally 代码块之后会重新抛出刚刚捕获的异常。
上述示例代码的异常表如下:
可以看到与原来相比异常表增加了两条,第2条异常处理器异常监控 try 代码块,第3条异常处理器监控 catch 代码块,如果出现异常则会跳转到第39行的 finally 代码块执行。
这就是 finally 一定会在 try-catch 代码块之后执行的原因了(某些能中断程序运行的操作除外)。
如果 finally 也抛出异常
上文说到虚拟机会对整个 try-catch 代码块生成一个或多个异常处理器,如果在 catch 代码块中抛出了异常,这个异常会被捕获,并且在执行完 finally 代码块之后被重新抛出。
那么在这里有一个额外的问题需要提及,假设在 catch 代码块中抛出了异常 A,当执行 finally 代码块时又抛出了异常 B,那么最后抛出的是什么异常呢?
如果有同学自己尝试过这个操作,就会知道最后抛出的异常 B。也就是说,在捕获了 catch 代码块中的异常后,如果 finally 代码块中也抛出了异常,那么最终将会抛出 finally 中抛出的异常,而原来 catch 代码块中的异常将会被忽略。
如果代码块中有 return
如果 try 或者 catch 中有 return,finally 还会执行吗?如果 finally 中也有 return,那么最终返回的值是什么?为了说明这个问题,编写了一段测试代码,然后找到它的字节码指令
public static int get() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3;
}
}
可以看到每个异常分支情况都附加上了 finally 的代码块,无论怎么走都会执行 finally 区块,这个方法最终的返回结果是3。