一:异常概述
程序编译或是运行期间发生了意料之外的情况,阻止了程序的正常运行,这种情况就被称之为异常。异常发生时是抛出异常终止程序运行,还是捕获异常做出对应异常逻辑处理,这就涉及到异常的抛出与捕获。Throwable是Java中异常的顶级父类,一个对象只有是该类的子类才能被称之为异常,也就是才能被throw抛出亦或是try-catch捕获
二:继承结构
继承结构为什么会绘制的如此复杂,这是经过深思熟虑后以作者目前水平来讲能够绘制最具代表性的一张结构图。
2.1 异常与错误
错误
:红色标记错误,更吸引人眼球。程序员经常接触到的错误就是第一个子类虚拟机的错误,内存堆栈溢出。错误不应该被代码捕获处理,而是应该抛出后日志记录再处理异常
:异常老生常谈的就是受检异常与运行时异常,除了蓝色标记的运行时异常部分都是受检异常。其中受检异常接触最多的应该就是I/O流操作中对文件、流的操作需要声明的异常
2.2 运行时异常
单独拿出运行时异常来讲是因为这类异常可能是日常工作中处理最多的异常类型。图中也绘制了具备代表性的几种异常划分
数组、集合
:操作引发的下标越界异常反射
:操作引发的系列反射异常如NoSuchMethod、非法访问多线程
:上图中没有绘制多线程出现最多Exception的直接子类InterruptedException其它
:就是数学异常、类型转换异常、空指针异常、非法参数异常
三:阅读异常
整个异常追踪栈信息打印出来是十分庞大的,也容易让初学者一头雾水。这么多信息我该怎么去查看?每一个异常第一行都会打印出异常名字 : 描述,然后第二行给出位置。查看异常也是有步骤的:
- 首先应该关注的是最上层也就是第一个异常,这个异常也就是最顶端被抛出的异常,往往很多时候在项目开发中可以迅速定位到异常点
- 如果在第一个异常点发现有许多异常信息,那么这时候就可以直接跳过这个异常查看,可以直接迅速拉到最后查看最底层的异常信息
- 如果第一个异常与最后一个异常都没有办法分析,这时候可以找异常链中有关自己写的代码抛出的异常。以上三步就是作者看异常时候的方法
四:异常抛出
异常处理分为两种方法,异常抛出与异常捕获。异常抛出指的就是如在某个方法中出现异常,该方法不愿意处理该异常,就可以抛出异常让调用该方法的上层模块进行处理。异常捕获也就是try-catch
4.1 throws声明
例如开发过程中需要编码I/O操作,这时候的受检异常要么声明抛出,要么捕获处理。这时候采取声明抛出就需要使用到关键字throws,在方法声明的最后面进行异常抛出声明。但是注意一点就是子类重写父类方法不能抛出比父类更大的异常,想想里氏替换原则就明白了
4.2 throw抛出
除了上述使用throws进行异常声明之外,比较类似的还有一种异常主动抛出的手段。关键字throw,比如经常在编码时会对参数检验。这时候参数校验异常多数为自定义封装异常,就会采用throw进行异常抛出处理。亦或是在捕获异常后对异常信息进行重组后再抛出也可以采用throw关键字完成
五:异常捕获
5.1 try-catch捕获
try-catch是异常捕获的基本操作,在try代码块中编写会抛出异常代码。当代码抛出异常时catch根据参数判断捕获异常并进行处理。在这里需要做几点声明:
- try-catch代码块中的变量是局部变量,不共享
- catch捕获异常不建议直接捕获Exception等操作,异常捕获异常层层递进
- 多个异常处理方式一致的情况下可以使用 | 进行隔断,写在同一catch代码块
- try-catch代码块会影响JVM代码优化,并且该代码块执行也会有一定额外性能消耗。所以try-catch尽量写必要代码
5.2 finally代码块
try-catch-finally通常是比较熟悉的结构,finally里面做什么?这个代码块有什么特殊的地方?
- finally用来做资源的释放与清理,如流的关闭
- finally在大多数情况下都会执行,即使try语句中有着return等关键字
- finally的执行是在return之前,如果在finally中执行代码逻辑,可能会造成逻辑混淆的情况产生。特别是对返回值进行修改的操作更别写
- finally还是有一定情况不会执行,如守护线程中的被守护线程全部结束、System的exit()方法等
5.3 try-with-resources
JDK1.7提出的语法糖,在try后面声明需要进行资源清理的代码对象。然后在编译的过程中会自动添加finally代码块关闭资源,还有一个前提就是对象的类必须实现接口AutoCloseable亦或是其子类Closeable
六:自定义异常
Java标准库里面内置许多标准异常,但是在开发过程中很多情况下都会自定义异常进行日志输出与逻辑处理。比如说在参数校验场景,入参20个参数。需要对参数进行空值校验、异常值校验、交叉校验等等,难道校验为空直接抛出空指针异常?这显然不是正常处理的逻辑
6.1 异常类型定义
一般来说自定义异常都会重写三个构造函数,分别是带有异常信息、异常链、以及异常链+异常信息。使用父类中的detailMessage属性记录异常信息,自定义code属性记录异常编码
6.2 枚举异常信息
就像上面列举的场景,入参参数有20个。非空校验的时候可能就有20种异常信息与编码,这种异常编码与信息硬编码到代码中?这肯定是不利于代码维护与扩展的操作,这时候一般都采用枚举类枚举异常对象,设置异常信息与编码
6.3 异常处理
优雅大方的自定义异常抛出,方便维护与异常定位。按照这个场景的设计,为了方便后续的扩展与设计,还可以引入策略模式,将不同参数校验的类型封装为不同的策略。上层模块只需要调用策略组成的门面方法,也就是在校验策略变更的时候不用修改上层模块的代码。这样做的好处显而易见,可以将校验策略分门别类的整合,不用在业务代码中随处校验。导致最后因为校验策略的修改而到处寻找校验方法修改,特别是在针对业务校验特别复杂的情况,进行策略封装显得尤为重要
七:思考
- 使用AOP进行异常处理
- SpringMvc中全局异常处理的实现