当前位置:首页 > 生活 > 正文

Java异常处理和最佳实践含案例分析(java异常处理是怎样实现的)

如何处理Java异常?作者查看了一些异常处理的规范,对 Java 异常处理机制有更深入的了解,并将自己的学习内容记录下来,希望对有同样困惑的同学提供一些帮助。

作者 | 王迪(惜鸟)

来源 | 阿里开发者公众号


一、概述

最近在代码CR的时候发现一些值得注意的问题,特别是在对Java异常处理的时候,比如有的同学对每个方法都进行 try-catch,在进行 IO 操作时忘记在 finally 块中关闭连接资源等等问题。回想自己对 java 的异常处理也不是特别清楚,看了一些异常处理的规范,并没有进行系统的学习,所以为了对 Java 异常处理机制有更深入的了解,我查阅了一些资料将自己的学习内容记录下来,希望对有同样困惑的同学提供一些帮助。

在Java中处理异常并不是一个简单的事情,不仅仅初学者很难理解,即使一些有经验的开发者也需要花费很多时间来思考如何处理异常,包括需要处理哪些异常,怎样处理等等。

在写本文之前,通过查阅相关资料了解如何处理Java异常,首先查看了阿里巴巴Java开发规范,其中有15条关于异常处理的说明,这些说明告诉了我们应该怎么做,但是并没有详细说明为什么这样做,比如为什么推荐使用 try-with-resources 关闭资源 ,为什么 finally 块中不能有 return 语句,这些问题当我们从字节码层面分析时,就可以非常深刻的理解它的本质。

通过本文的的学习,你将有如下收获:

  • 了解Java异常的分类,什么是检查异常,什么是非检查异常
  • 从字节码层面理解Java的异常处理机制,为什么finally块中的代码总是会执行
  • 了解Java异常处理的不规范案例
  • 了解Java异常处理的最佳实践
  • 了解项目中的异常处理,什么时候抛出异常,什么时候捕获异常

二、java 异常处理机制

1、java 异常分类

总结:

  • Thorwable类(表示可抛出)是所有异常和错误的超类,两个直接子类为Error和Exception,分别表示错误和异常。
  • 其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常, 这两种异常有很大的区别,也称之为非检查异常(Unchecked Exception)和检查异常(Checked Exception),其中Error类及其子类也是非检查异常。

检查异常和非检查异常

  • 检查异常也称为“编译时异常”,编译器在编译期间检查的那些异常。由于编译器“检查”这些异常以确保它们得到处理,因此称为“检查异常”。如果抛出检查异常,那么编译器会报错,需要开发人员手动处理该异常,要么捕获,要么重新抛出。除了RuntimeException之外,所有直接继承 Exception 的异常都是检查异常。
  • 非检查异常:也称为“运行时异常”,编译器不会检查运行时异常,在抛出运行时异常时编译器不会报错,当运行程序的时候才可能抛出该异常。Error及其子类和RuntimeException 及其子类都是非检查异常。

说明:检查异常和非检查异常是针对编译器而言的,是编译器来检查该异常是否强制开发人员处理该异常

  • 检查异常导致异常在方法调用链上显式传递,而且一旦底层接口的检查异常声明发生变化,会导致整个调用链代码更改。
  • 使用非检查异常不会影响方法签名,而且调用方可以自由决定何时何地捕获和处理异常

建议使用非检查异常让代码更加简洁,而且更容易保持接口的稳定性

检查异常举例

在代码中使用 throw 关键字手动抛出一个检查异常,编译器提示错误,如下图所示:

通过编译器提示,有两种方式处理检查异常,要么将异常添加到方法签名上,要么捕获异常:

方式一:将异常添加到方法签名上,通过 throws 关键字抛出异常,由调用该方法的方法处理该异常:

方式二:使用 try-catch 捕获异常,在 catch 代码块中处理该异常,下面的代码是将检查异常包装在非检查异常中重新抛出,这样编译器就不会提示错误了,关于如何处理异常后面会详细介绍:

非检查异常举例

所有继承 RuntimeException 的异常都是非检查异常,直接抛出非检查异常编译器不会提示错误:

自定义检查异常

自定义检查异常只需要继承 Exception 即可,如下代码所示:

自定义检查异常的处理方式前面已经介绍,这里不再赘述。

自定义非检查异常

自定义非检查异常只需要继承 RuntimeException 即可,如下代码所示:


2、从字节码层面分析异常处理

前面已经简单介绍了一下Java 的异常体系,以及如何自定义异常,下面我将从字节码层面分析异常处理机制,通过字节码的分析你将对 try-catch-finally 有更加深入的认识。

try-catch-finally的本质

首先查阅 jvm 官方文档,有如下的描述说明:

从官方文档的描述我们可以知道,图片中的字节码是在 JDK 1.6 (class 文件的版本号为50,表示java编译器的版本为jdk 1.6)及之前的编译器生成的,因为有 jsr 和 ret 指令可以使用。然而在 idea 中通过 jclasslib 插件查看 try-catch-finally 的字节码文件并没有 jsr/ret 指令,通过查阅资料,有如下说明:

jsr / ret 机制最初用于实现finally块,但是他们认为节省代码大小并不值得额外的复杂性,因此逐渐被淘汰了。Sun JDK 1.6之后的javac就不生成jsr/ret指令了,那finally块要如何实现?

javac采用的办法是把finally块的内容复制到原本每个jsr指令所在的地方,这样就不需要jsr/ret了,代价则是字节码大小会膨胀,但是降低了字节码的复杂性,因为减少了两个字节码指令(jsr/ret)。

案例一:try-catch 字节码分析

在 JDK 1.8 中 try-catch 的字节码如下所示:

这里需要说明一下 athrow 指令的作用:

异常表

athrow指令:在Java程序中显示抛出异常的操作(throw语句)都是由 athrow指令来实现的,athrow 指令抛出的Objectref 必须是类型引用,并且必须作为 Throwable 类或 Throwable 子类的实例对象。它从操作数堆栈中弹出,然后通过在当前方法的异常表中搜索与 objectref 类匹配的第一个异常处理程序:

  • 如果在异常表中找到与 objectref 匹配的异常处理程序,PC 寄存器被重置到用于处理此异常的代码的位置,然后会清除当前帧的操作数堆栈,objectref 被推回操作数堆栈,执行继续。
  • 如果在当前框架中没有找到匹配的异常处理程序,则弹出该栈帧,该异常会重新抛给上层调用的方法。如果当前帧表示同步方法的调用,那么在调用该方法时输入或重新输入的监视器将退出,就好像执行了监视退出指令(monitorexit)一样。
  • 如果在所有栈帧弹出前仍然没有找到合适的异常处理程序,这个线程将终止。

异常表:异常表中用来记录程序计数器的位置和异常类型。如上图所示,表示的意思是:如果在 8 到 16 (不包括16)之间的指令抛出的异常匹配 MyCheckedException 类型的异常,那么程序跳转到16 的位置继续执行。

分析上图中的字节码:第一个 athrow 指令抛出 MyCheckedException 异常到操作数栈顶,然后去到异常表中查找是否有对应的类型,异常表中有 MyCheckedException ,然后跳转到 16 继续执行代码。第二个 athrow 指令抛出 RuntimeException 异常,然后在异常表中没有找到匹配的类型,当前方法强制结束并弹出当前栈帧,该异常重新抛给调用者,任然没有找到匹配的处理器,该线程被终止。


案例二:try-catch-finally 字节码分析

在刚刚的代码基础之上添加 finally 代码块,然后分析字节码如下:

异常表的信息如下:

添加 finally 代码块后,在异常表中新增了一条记录,捕获类型为 any,这里解释一下这条记录的含义:

在 8 到 27(不包括27) 之间的指令执行过程中,抛出或者返回任何类型的结果都会跳转到 26 继续执行。

从上图的字节码中可以看到,字节码索引为 26 后到结束的指令都是 finally 块中的代码,再解释一下finally块的字节码指令的含义,从 25 开始介绍,finally 块的代码是从 26 开始的:

25 athrow // 匹配到异常表中的异常 any,清空操作数栈,将 RuntimeExcepion 的引用添加到操作数栈顶,然后跳转到 26 继续执行

26 astore_2 // 将栈顶的引用保存到局部变量表索引为 2 的位置

27 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> // 获取类的静态字段引用放在操作数栈顶

30 ldc #9 <执行finally 代码>//将字符串的放在操作数栈顶

32 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>// 调用方法

35 aload_2// 将局部变量表索引为 2 到引用放到操作数栈顶,这里就是前面抛出的RuntimeExcepion 的引用

36 athrow// 在异常表中没有找到对应的异常处理程序,弹出该栈帧,该异常会重新抛给上层调用的方法


案例三:finally 块中的代码为什么总是会执行

简单分析一下上面代码的字节码指令:字节码指令 2 到 8 会抛出 ArithmeticException 异常,该异常是 Exception 的子类,正好匹配异常表中的第一行记录,然后跳转到 13 继续执行,也就是执行 catch 块中的代码,然后执行 finally 块中的代码,最后通过 goto 31 跳转到 finally 块之外执行后续的代码。

如果 try 块中没有抛出异常,则执行完 try 块中的代码然后继续执行 finally 块中的代码,因为编译器在编译的时候将 finally 块中的代码添加到了 try 块代码后面,执行完 finally 的代码后通过 goto 31 跳转到 finally 块之外执行后续的代码 。


剩余60%,完整内容请点击下方链接查看:

https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247532469&idx=1&sn=669f899ec30303717a2384ba8d9acd08&chksm=e92a42bade5dcbac863621d716b9a0f177afee7f92be5edddea8d1ec038b4d0739a1499da081&token=805584059&lang=zh_CN#rd


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。