大家好,欢迎来到IT知识分享网。
对于开发者来说,应该没有哪位老铁敢自诩自己开发的程序不会出错吧,越是复杂的应用,在开发的过程中,可能出现问题或者考虑不到的地方就会越多。这种可能在我们预料之外的情况,我们通常将其称为异常(Exception),只有正确处理好意外情况,才能保证程序的可靠性。Java 语言在设计之初就提供了相对完善的异常处理机制,这也是 Java 得以大行其道的原因之一,因为这种机制也大大降低了编写和维护可靠程序的门槛。
对异常的一些基础概念不了解老铁们,可以看下异常到底该怎么处理,接下来,我们聊一些关于异常处理流程相关的内容。看后,相信你会有一定的收获。
正常情况下,代码的执行流程从上到下,先执行try,在执行catch,最后执行finally。然而,很多时候,try中抛出的异常,catch可能无法处理,即使catch可以处理,但是catch在处理的过程中也有可能会产生新的异常。对于后面两种特殊情况的处理,是由finally来完成的,finally会捕获到这些异常,但是由于finally中没有执行这些异常的引用,所以finally对捕获异常的处理,只能将他们抛出去。
上图是目前java中对异常处理的实现方式。java编译器会将finally代码块的内容,复制到try-catch代码块所有正常执行路径和以及异常执行路径的出口处。对于正常执行路径:try代码正常执行的情况和try触发的异常被catch捕获情况,如图中蓝色和黄色的finally代码块。对于异常执行路径:try中抛出了catch无法捕获异常,或者catch中抛出了异常,如图中红色的finally代码块。
下面我们从字节码层洞察一下,编译器对try-catch-finally的编译结果。为了更好的进行验证,异常处理的java语言层面的代码如下:
public class ExceptionTest {
int tryBlock ; int catchBlock; int finallyBlock; int otherBlock; public void tryCatchFinally() {
try{
tryBlock = 1; }catch (Exception e) {
catchBlock = 2; }finally {
finallyBlock = 3; } otherBlock = 4; } }
通过使用javap命令可以查看编译后的字节码:
public void tryCatchFinally(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: iconst_1 2: putfield #5 // Field tryBlock:I 5: aload_0 6: iconst_3 7: putfield #6 // Field finallyBlock:I 10: goto 35 13: astore_1 14: aload_0 15: iconst_2 16: putfield #8 // Field catchBlock:I 19: aload_0 20: iconst_3 21: putfield #6 // Field finallyBlock:I 24: goto 35 27: astore_2 28: aload_0 29: iconst_3 30: putfield #6 // Field finallyBlock:I 33: aload_2 34: athrow 35: aload_0 36: iconst_4 37: putfield #9 // Field otherBlock:I 40: return Exception table: from to target type 0 5 13 Class java/lang/Exception 0 5 27 any 13 19 27 any
从字节码中可以看出,带有try-catch-finally异常处理的方法都会附带一个异常表 Exception table,表中每一个条目代表一个异常处理器,每个条目由from指针,to指针,target指针以及可处理异常类型构成。其中from和to指针表示,这个异常处理器,监控可能出现异常的代码范围(字节码层面),也就是这个异常处理器的作用范围。
type表示这个异常处理器,可以处理的异常类型。只有异常类型相互匹配的异常,才可以被这个异常处理器处理。
target:表示这个异常处理器,处理逻辑开始的指针位置。
总结来说,就是一个异常处理器,会监控从from指针到to指针(不包括to指针位置)的代码的异常情况,如果该范围内代码抛出了异常,且异常类型和该异常处理器的异常类型可匹配的话,那么程序执行流程会跳转到target指针指向的位置。
通常情况下一个方法的异常处理器会有多个,在本例中有3个,且三个异常处理器的优先级从上到下依次递减,当出现异常后,会从上到下遍历异常表,寻找符合条件的异常处理器,如果遍历完所有异常处理器后,jvm仍未找到匹配的异常处理器的话,那么它会弹出当前方法对应的栈帧,在调用者中异常表中查找合适的异常处理器。
在本例的异常处理表中,我们可以看到第一个异常处理器的from和to指针分别执行0和5,刚好就是try代码块的范围,如果该范围内代码抛出的异常被该异常处理器处理话,那么程序就会跳转到taget指向的8,并从这个位置开始向后执行。也就是执行 catch代码块,finally代码块和otherBlock。
第二个异常处理器的from和to指针指向的位置和第一个异常处理器相同,只不过异常类型为any,any表示任何异常,该异常处理器,刚好是第一个异常处理的一个补充,用来处理try代码块抛出的异常不能被catch捕获的情况,对于这种情况,程序执行流程会跳转到target指针指向的27行,也就是finally代码块,然后再将异常throw出去,也就是34行指向的字节码。
第三个异常处理器的from和to指针指向13和19,也就是catch代码块,该异常处理器可以处理任何类型的异常,对catch代码块抛出异常的处理方式是将程序执行流程跳转到27行,和第二个异常处理器的处理逻辑相同。
看到这里,对于Java中异常处理流程,你是不是有了一个更深层的认识了呢?
优雅的处理异常
其实对于上面第三个异常处理器,还有一个小问题,catch处理try中抛出的异常的过程中,又抛出了异常,那么被finally捕获后并重新抛出的异常,是try代码块中抛出的异常,还是catch代码块中抛出的异常呢? 答案是后者。也就是说最开始触发的异常被忽略掉了,这对于问题的排查是不利的,因为异常打印的堆栈中,并没有最开始出现问题的代码信息。
为了解决这个问题,在java7中引入了Suppressed异常来解决这个问题,这个新特性,可以将一个异常附于另一个异常之上,这就可以在一个异常上附加多个异常的信息。
除此之外,为了方便对资源类型对象的释放,在java7中专门构造了一个名为try-with-resource的语法糖,该语法糖可以精简资源的打开关闭。
在java7前,为了保证打开的资源在异常情况下也可以正常关闭,每一个资源都要对应一个独立的try-finally代码块,将关闭资源的操作放在finally中,这样以来,代码将会变得十分繁琐,具体如下:
FileInputStream in0 = null; FileInputStream in1 = null; FileInputStream in2 = null; ... try {
in0 = new FileInputStream(new File("in0.txt")); ... try {
in1 = new FileInputStream(new File("in1.txt")); ... try {
in2 = new FileInputStream(new File("in2.txt")); ... } finally {
if (in2 != null) in2.close(); } } finally {
if (in1 != null) in1.close(); } } finally {
if (in0 != null) in0.close(); }
而使用try-with-resouce语法糖,则可以极大的简化上述代码,该语法糖的使用,需要对应的资源类型实现AutoCloseable接口类,使用如下语法糖,可以实现和上述代码相同的作用,而且还会使用到Suppressed异常的功能:
public class Foo implements AutoCloseable {
private final String name; public Foo(String name) {
this.name = name; } @Override public void close() {
throw new RuntimeException(name); } public static void main(String[] args) {
try (Foo foo0 = new Foo("Foo0"); // try-with-resources Foo foo1 = new Foo("Foo1"); Foo foo2 = new Foo("Foo2")) {
throw new RuntimeException("Initial"); } } } // 运行结果: Exception in thread "main" java.lang.RuntimeException: Initial at Foo.main(Foo.java:18) Suppressed: java.lang.RuntimeException: Foo2 at Foo.close(Foo.java:13) at Foo.main(Foo.java:19) Suppressed: java.lang.RuntimeException: Foo1 at Foo.close(Foo.java:13) at Foo.main(Foo.java:19) Suppressed: java.lang.RuntimeException: Foo0 at Foo.close(Foo.java:13) at Foo.main(Foo.java:19)
看到这里是不是觉得很神奇,好奇这个语法糖是如何实现的呢?其实很多语法糖都是编译器提供的一种障眼法,通过查看编译后的字节码,也就没什么秘密了。使用了语法糖可以在java语言层面简化代码,但是字节码层面,代码并没有简化。有兴趣的老铁,可以使用javap查看相应的字节码,进行验证。
异常被finally吞掉了?
最后和大家分享一个异常处理的踩坑经历,如果finally中有return语句的话,那么finally捕获到的异常,就不会抛出去了,给人一种异常被吞掉的感觉。
实验代码如下:
public void tryCatchFinallyWithReturn() {
try{
throw new RuntimeException("try exception"); }catch (NullPointerException e) {
// ignore }finally {
return; } }
而如果finally中没有return的话,异常堆栈会被错误输出流打印出来,为什么使用错误输出流打印呢,可以参考 如何在多线程环境中优雅的处理异常。
为什么有return的情况下,异常不会抛出去呢,老规矩,看一下字节码就清楚了。
public void tryCatchFinallyWithReturn(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=1 0: new #10 // class java/lang/RuntimeException 3: dup 4: ldc #15 // String try exception 6: invokespecial #12 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V 9: athrow 10: astore_1 11: return 12: astore_2 13: return Exception table: from to target type 0 10 10 Class java/lang/NullPointerException 0 11 12 any
try代码块中抛出的异常,会被第二个异常处理器处理,该异常处理器的处理逻辑从12行开始,接着13行就return了,并没有athrow指令。为了方便对比,我们再查看一下,把finally代码块中return去掉后的字节码:
public void tryCatchFinallyWithReturn(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=3, args_size=1 0: new #10 // class java/lang/RuntimeException 3: dup 4: ldc #15 // String try exception 6: invokespecial #12 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V 9: athrow 10: astore_1 11: goto 17 14: astore_2 15: aload_2 16: athrow 17: return Exception table: from to target type 0 10 10 Class java/lang/NullPointerException 0 11 14 any
同样是第二个异常处理器处理处理异常情况,处理逻辑从14行开始,获取捕获的异常实例,然后通过athrow完成抛出操作。
到这里,终于明白了为啥finally中有return的情况下,异常不会抛出去了。关于语法糖,它只是编译器在语言层面的一种特殊语法的支持,可以很大程度上减少语言层面的代码量,但是在字节码层面,代码量并没有减少,有可能还会更加复杂,到这里突然想到一句很有道理的话:哪有什么岁月静好,只不过有人替你负重前行。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/136949.html