Java基础学习 - Exception 的理解与总结

前言

重新学习 Java 基础知识,对异常 Exception 有了更深入的理解,同时更好地理解有关 Exception 的最佳实践的理由和依据。

异常的本质

异常的本质是一个返回值,由特定指令 throw “返回”给上层代码。可以等同理解为没有异常处理的语言 (比如 C) 中的错误代号。它与业务逻辑返回值 (return xxx) 组成联合体,一同返回给上层代码处理。

异常的实际执行流程 (控制流,control flow) 由 Java 编译器产生,开发者无需过多关心,只需关心其中三个切点:

1.范围:try { }

设定需要关心的异常的发生范围,有异常则自动跳到 catch 块执行。

2.捕捉:catch (XxxException e) { }

相当于 case 不同的异常 XxxException 作相应的处理,没有 case 的异常自动继续向上“返回”,直到被最终捕捉或者程序终止。

3.清场:finally { }

无论有没有异常都将执行的业务逻辑,常用于清场,释放资源。

由此,开发者只需要关注业务逻辑的编写,无需关心“错误代号”在每层代码的控制流如何实现,编译器帮我们实现了。当然,某些边角的情景,还是需要进一步了解其实现,避免误用犯错,比如 try 块抛错同时 finally 块也抛错的情景等。后面最佳实践理解中再述。

异常的分类

1. Checked Excpetion

受检异常,可以理解为业务异常,通常是正常业务操作流程中可能会出现的异常场景。例如,FileNotFoundException。

受检的意思是可以被编译器静态检查,有效地利用受检异常地这个特性,可以很好地在软件工程过程中,做好业务异常的变更管理。后面最佳实践理解中再述。

这类异常的特点是:
JVM 正常运行;
用户代码技术上正常运行;
本层业务流程进入已知的异常场景,可通过稍候重试、或修改输入重试来恢复进入正常场景。

因此,通常处理方式是,捕捉这类异常,修改流程条件 (如,修改输入的值) 继续重试恢复。

2. Unchecked Exception

非受检异常,是 JVM 正常执行用户代码时发生的错误,通常是用户代码技术逻辑有错误导致的,所以可以理解为代码异常。例如,NullPointerException。

这类异常的特点是:
JVM 正常运行;
本层用户代码运行不正常,存在技术错误。
本层业务流程已经被破坏,不可重试恢复。

因此,通常处理方式是,返回上层用户代码,通过日志记录这类异常的详细信息,帮助之后分析错误修改程序。

3. Error

错误,是指 JVM 运行所需的环境和条件已经缺失,由 JVM 底层报出。例如,OutOfMemoryError。

这类异常的特点是:
JVM 运行不正常,已经崩溃。

此时程序运行混乱无法预知,无法捕捉错误,甚至某些 JVM 干脆不实现 Error 的捕捉。因此,通常处理方式是,不做任何处理,让它迅速终止。之后再详细分析程序日志、标准输出、错误输出来定位问题,良好的日志规范此时会起到比较好的作用。

Java的层次结构实现

由上面归纳的三类异常的特点,很容易理解 Java 异常的层次架构:

Java异常层次结构
按 JVM 可否恢复,Throwable 分为:Error、Exception。
按用户代码可否恢复,Exception 分为:RuntimeException、其他 Exception 子类 (即受检异常,Checked Exception)。

关于最佳实践的理解

1.引入异常的目的

是为了避免问题再次发生,因此尽量提供足够的、清晰的上下文信息,帮助定位问题原因。

所以会有如下的一些建议:
(1) 禁止吞掉异常;
(2) 正确包装异常,有原始异常的话需要包装原始异常;
(3) 写日志、或者向上抛,二选一,避免反复出现重复的日志;
(4) 不能处理的异常不要捕捉后继续往上抛;
(5) 不要使用 printStackTrace() 或类似的方法,其他人看到这些信息完全不知道什么情况,也不知道该如何处理;
(6) 早抛出迟捕捉原则。

2.业务异常 (受检异常 Checked Exceptions) 处理原则

尽可能在编码阶段识别和处理。

受检异常通常都是业务流程上的异常场景,属于正常业务流程的一部分,在方法声明中显式定义具体的可抛出的异常,可以更好地做好这类异常的变更管理。一旦某个方法由于业务场景的完善,补充了新的可抛出的异常,由于编译器的静态分析,可以迅速通知到各个调用方,提醒调用方及时了解变更,对新增的业务异常做出相应的异常处理代码调整。

可能有时会认为方法声明中指定过多的具体异常过于繁琐,可以考虑采用自定义异常、包装异常的方式处理,减少数量的同时,保留原始异常的调用链信息 (stacktrace),另外还可以补充有用的上下文信息。包装的粒度、层次深度,应该按工程的具体情况而定。

同时还可以考虑新旧方法同时提供,通过过期标注 (@Deprecated) 旧方法的方式,逐步消灭旧方法的使用,从而最终消灭旧方法。

因此会有如下的建议:
(1) 方法声明中指定具体的会抛出的受检异常;
(2) 不要笼统地 catch (Exception),应该捕捉能处理的、具体的受检异常,然后进行处理。

3.代码异常 (运行时异常 RuntimeException) 处理原则

应通过良好编码风格,使得调用链信息 (stacktrace) 能够准确指示用户代码技术错误的地方。

例如,使用 Object.requireNotNull() 检查非空,可以有效地在调用链信息中清晰指示出 NullPointerException 的确切发生位置。

4.错误处理原则

错误发生时,不做任何处理,尽快终止程序,然后进入事后检查。

由于此时 JVM 已经崩溃,程序运行逻辑已经不可预知,勉强处理,或者会引入混淆信息。

事后检查需要依赖良好规范的程序日志、标准输出、错误输出,帮助定位问题。

所以会有建议:
(1) 禁止 catch (Throwable)。

5.正确理解和使用 try-catch-finally 程序流

抛出异常的本质是返回值 (异常) 给上层代码,返回值的实质是当前代码栈的某个区域,抛出异常就类似将异常的引用保存到当前代码栈的异常返回值区域。

假如 try catch 块发生异常 A,JVM 将该异常 A 的引用保存到异常返回值区域。然后进入 finally 块又再次发生异常 B 的话,JVM 又会将异常 B 的引用保存到异常返回值区域,覆盖了异常 A,结果上层代码只能接收到假的错误原因 – 异常 B,导致错误定位不可能。

类似的情形:
finally 块的 return 会覆盖 try catch 块的 return;
finally 块的 throw 会覆盖 try catch 块的 return;
finally 块的 return 会覆盖 try catch 块的 throw。这个场景甚至清除了应该抛出的错误,没有任何信息了解到发生了错误。

所以建议:
(1) 只使用 finally 块完成清场 (释放资源等);
(2) 禁止在 finally 代码块抛出异常,应该妥善处理掉不抛出,或者只记录日志;
(3) 禁止在 finally 代码块执行 return 语句。

(完)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值