来源:《阿里Java开发手册》、《杨晓峰Java36讲》

世界上存在永远不会出错的程序吗?也许这只会出现在程序员的梦中。随着编程语言和软件的诞生,异常情况就如影随形地纠缠着我们,只有正确处理好意外情况,才能保证程序的可靠性。

Java语言在设计之初就提供了相对完善的异常处理机制,这也是Java得以大行其道的原因之一,因为这种机制大大降低了编写和维护可靠程序的门槛。如今,异常处理机制已经成为现代编程语言的标配。

Question:Exception 和 Error,运行时异常 和 一般异常有什么区别?

Answer

Exception 和 Error 都是继承了Throwable类,在Java中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它们是异常处理机制的基本组成类型。
Exception 和 Error 提现了Java平台设计者对不同异常情况的分类。Exception 是程序正常运行过程中,可以预料的意外情况,应该被捕获,进行相应的处理。Error是指正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复的状态。既然是非正常情况,所以不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。

Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式的进行处理,这是编译期检查的一部分。
不检查异常就是运行时异常,类似NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,不会在编译期要求处理。

我们在日常编程中,如何处理好异常是比较考验功底的,我觉得需要掌握两个方面。
**
第一,理解Throwable、Exception、Error的设计和分类。比如,掌握那些应用最为广泛的子类,以及如何自定义异常等。简单的类图如下:
image.png

其中有些子类型,最好重点理解一下,比如NoClassDefFoundError 和 ClassNotFoundException有什么区别,这也是个经典的入门题目。

第二,理解Java语言中操作Throwable的元素和实践。掌握最基本的语法是必须的,如try-catch-finally块,throw、throws关键字等。与此同时,也要懂得如何处理典型场景。

异常处理代码比较繁琐,比如我们需要写很多千篇一律的捕获代码,或者在finally里面做一些资源回收工作。随着Java语言的发展,引入了一些更加便利的特性,比如try-with-resources 和 mutiple catch,具体可以参考下面的代码段。在编译时期,会自动生成相应的处理逻辑,比如,自动close那些扩展了AutoCloseable 或者 Closeable的对象。

  1. try (BuferedReader br = new BuferedReader(…);
  2. BuferedWriter writer = new BuferedWriter(…)) {// Try-with-resources
  3. // do something
  4. catch ( IOException | XEception e) {// Multiple catch
  5. // Handle it
  6. }

基本准则

  • 尽量不要捕获类似Exception这样的通用异常,应该捕获特定异常,直观的体现出异常信息。
  • 不要生吞异常。这是异常处理中要特别注意的事情,因为生吞异常可能会导致难以诊断的诡异情况。
  • Throw early,catch late原则。在精确的位置throw异常,在能够获取足够信息的时候捕获异常。
  • try-catch代码段会产生额外的性能开销,所以建议仅捕获有必要的代码段。与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
  • Java每实例化一个Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销就不能被忽略了。当我们的服务出现反应变慢,吞吐量下降的时候,检查发生最频繁的Exception也是一种思路。
  • Java类库中定义的一类RuntimeException可以通过预先检查进行规避,而不应该通过catch来处理,比如:IndexOutOfBoundsException、NullPointerException等等。
  • 对大段代码进行try-catch,这是不负责任的表现。catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能区分异常类型,再做相应的异常处理。
  • 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
  • 有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
  • 不能在finally块中使用return,finally块中的return返回后方法结束执行,不会再执行try块中的return语句。
  • 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
  • 防止NPE,是程序员的基本修养,注意NPE产生的场景:
    • 返回类型是基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生NPE。
    • 数据库的查询结果可能为null。
    • 集合里的元素即使 isNotEmpty,取出的数据元素也可能为null。
    • 远程调用返回对象时,一律要求进行空指针判断。
    • 对于Session中获取的数据,建议NPE检查,避免空指针。
    • 级联调用obj.getA().getB().getC(); ,易产生NPE。
    • 使用JDK8的Optional类来防止NPE问题。
  • 自定义异常时区分unchecked / checked异常,避免直接抛出new RuntimeException(),更不允许抛出Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException 、ServiceException等。