本章讨论一些有关异常的使用。
书中指出,很多人企图使用异常来控制程序流转。 异常不应该被用来控制程序流,比如在死循环中循环一个数组,然后捕获数组越界异常来终止循环。 原因如下: - 使用异常比正常的控制流代码慢的多 - 使用异常的代码丑陋无比,掩盖意图,容易造成误解 - 使用异常的代码块由于会捕获异常,可能会意外地将真正的异常捕获掉并当成控制流的逻辑去处理,造成十分麻烦的bug - try-catch块可能会阻止一些现代JVM本来要进行的优化
本条内容标题已经说得很清楚了。 还有就是,异常也是一个对象,可以为它增加一些方法,尤其是受检异常,增加一些方法可以帮助用户在捕获异常之后,获得一些有助于恢复程序的信息。
受检异常只有以下两种情况满足的时候才使用: 1. 正确地使用API,没有编程错误,仍然无法阻止这个异常的发生 2. 异常发生后,使用API的程序员可以采取一些有用的动作,而不是仅仅能做的只是消极忽略,或者干脆什么也不干
当编写异常的时候,问自己,调用自己方法的程序员将有可能如何处理这个异常,这样可以通过简单的方式测验是否应该使用受检异常。 受检异常强迫程序员处理,因此会造成使用者的负担。(而且说实话,try-catch块的代码挺丑的,很臃肿,还拥有自己的局部作用域,导致局部变量的使用变得很难受)
作者建议有些情况可以将方法拆成两个方法,其中一个方法返回boolean值来替代抛出受检异常。 当然,这种做法并不是万能的,尤其是多线程并发访问时,以及返回boolean值的方法额外的做了很多事影响性能的时候。
优先使用已经存在的标准异常有以下一些好处: - 易于学习和使用,因为绝大多数程序员都已经熟悉那些标准的异常,他们使用起来没有额外学习成本 - 可读性增加,异常类越少越能减小装载类的开销
下面是书中列出的一些常用的标准异常: - IllegalArgumentException:参数不正确(非Null参数) - IllegalStateException:对于方法调用而言,对象状态不合适(比如还没初始化好) - NullPointerException:禁止参数值为Null的情况下传入了Null - IndexOutOfBoundsException:下标参数值越界(不一定是数组的) - ConcurrentModificationException:禁止并发修改的情况下检测到并发修改 - UnsupportOperationException:对象不支持用户请求的方法
有一些底层方法抛出的异常传到高层以后,就变得与所执行的任务联系不是很紧密,这种情形会让API用户感到困惑。 通常的做法是使用异常转译,即将底层异常捕获,转而抛出高层抽象对应的异常。(Spring在封装JDBC的时候,就大量使用到了异常转译,将JDBC难以理解的异常转化为更加清晰的异常) 异常链则是指在使用异常转译的时候,将底层异常传给高层异常的构造函数,从而形成异常链条,使得高层异常在处理的时候,可以通过Throwable.getCause()
方法来获取底层异常,这样在某些场景下,便于调试问题。
Effective Java的很多条目非常的细,比如本条,题目就是内容,内容告诉我们,每个方法抛出的异常都要有文档。实际上这样的内容不足以分出一节,不过书中还是分出了。 注意,非受检的异常,不要用throws关键字标识在方法声明中,这样会令方法的调用者困惑,受检异常才需要检查,非受检异常就是非受检异常,不可混用。 在文档中除了受检异常,非受检异常也应该标出,标出这些异常发生的场景可以帮助调用者了解方法正确执行的前提条件
是什么。
本节标题翻译的不好,可以理解为,在异常的message中,尽可能地包含异常发生时,造成异常的实时数据。 比如数组越界的异常,应该要将异常发生时候数组的上界,下界,越界下标值这些有用的数据都记录在异常的Message内。 通常异常的构造器都有一个类型为string的message入参,但是作者推荐更进一步,将这个异常需要的数据直接传入构造器,由构造器内部来拼接message,这样将拼接message的代码落在一处,更便于程序员抛出异常。
书中建议,即使发生了异常,也应该努力使对象保持在方法调用前的某种可用的良好状态。这样程序还能继续进行,程序的状态也容易恢复。相反地,如果异常发生后,对象的状态变得不可捉摸,或者处于中间状态,程序就会变得难以恢复,有可能还会造成更多异常发生。 实现异常发生时的原子性有以下几个建议: - 不可变对象天生拥有原子性,因为一旦创建就不可改变,不会处于一个不一致的状态中 - 可变对象要想拥有原子性,最好再状态改变前就尽可能地检查参数,将异常抛出在状态改变之前 - 调整计算过程的顺序,是任何可能失败的计算部分在对象状态改变前发生 - 编写一段恢复代码,由它拦截操作过程中的失败,恢复对象之前的状态,这经常用在永久性的数据结构(比如数据库等)上 - 将操作执行在对象的拷贝上,直到操作成功后,再使用对象拷贝来替代原对象