充分发挥异常的有点,可以提高程序的可读性、可靠性和可维护性。
如果使用不当,它们也会带来负面影响。
57. 只针对异常的情况才使用异常
异常应该只用于异常的情况下,它们永远不应该用于正常的控制流。
乱用异常是应该的,不要在有更好的方案下选择使用异常包裹代码
- 异常机制的设计初衷是用于不正常的情形,所以很少会有JVM的实现对它们进行优化,使得与显示的测试一样快速
- 把代码放在try-catch块中反而阻止了现代JVM实现本来可能要执行的某些特定优化
- 把数组进行遍历的标准模式并不会导致冗余的检查,有些现代JVM实现会将它们优化掉
乱用异常不仅模糊了代码的意图,降低了它的性能,而且还不能保证正常的工作。如果出现了不相关的Bug,这种写法会掩盖该Bug,极大的增加了调试的复杂性。
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。
提供状态测试方法、可识别的返回中这两种做法,可以帮助你设计更好的API,使外界不用使用异常。
总之,异常是为了在异常情况下使用而设计的,不要将它们用于控制流,也不要编写迫使它们这么做的API。
58. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
什么时候适合使用哪种异常
如果期望调用者能够适当的恢复,对于这种异常就应该使用受检的异常。
- 通过抛出受检异常,强迫调用者在catch语句中处理该异常,或者throws出去进行处理。
- API的设计者让API用户面对受检的异常,以此强制用户从这个异常条件中恢复。
用运行时异常来表明编程错误
- 如果程序抛出未受检的异常或错误,往往就属于不可恢复的情形,如果程序并未捕获,那么将导致线程终止,并出现适当的错误消息。
- Error往往被JVM保留,所以你实现的所有未受检异常都应该是RuntimeException的子类。
总之,对于可恢复的情况,使用受检的异常;对于程序错误,则使用运行时异常。
异常也是一个对象,可以在它上面定义任何方法以提供所需要的信息。
59. 避免不必要的使用受检的异常
过分的使用受检异常会使API使用起来非常不方便。
60. 优先使用标准的异常
重用现有异常的好处:
- 使你的API更加易于学习和使用
- 因为熟悉,可读性会更好
- 异常类越少,在内存印迹就越小,装载这些类的时间开销也越小
几乎所有的错误方法都可以被归结为非法参数或非法状态,所有这两个异常被经常使用,但也有少数应用于特定条件下的非法参数(NullPointException)和非法状态(IndexOutOfException)。
- IllegalArgumentException
- IllegalStateException
ConcurrentModificationException:
如果一个对象被设计为专用于单线程或者与外部同步机制配合使用,一旦发现它正在或已经被并发的修改,就应该抛出这个异常。UnsupportedOperationException:
如果对象不支持所请求的操作,就应该抛出这个异常。
比如接口的某个实现类不想提高该方法,就可以抛出该异常。
一定要保证抛出的异常的条件与该异常的文档中描述的条件一致,这种重用必须建立在语义的基础上,而不是建立在名称的基础上。
61. 抛出与抽象相对应的异常
如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。
当方法传递由底层抛出的异常时,常常发生这种情况,就需要进行异常转译。
异常转译(exception translation):
更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常。异常链(exception chaining):
如果底层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。底层的异常原因被传导到高层的异常。
对于没有提供支持链(构造方法传入异常原因)的异常类,可以使用Trowable.initCause()
方法设置原因,并使用getCause()来获取该原因。
try {
} catch (LowerLevelException e){
throw new HigherLevelException(e);//传导,new HigherLevelException().initCause(e);
}
- 不要滥用异常转译。
如有可能,在调用底层方法时尽量避免其抛出异常,确保它们会成功。
62. 每个方法抛出的异常都要有文档
始终要单独声明受检异常,并利用Javadoc的@throws标记,准确记录下抛出该异常的条件。
如果一个类中许多方法出于同样的原因抛出同一个异常,就可以在该类的文档注释中对这个异常建立文档,而不是为每个方法单独建立文档。
63. 在细节消息中包含能捕获失败的消息
比如IndexOutOfException,应该尽可能的说明index、length等。
64. 努力使失败保持原子性
失败的方法调用应该使对象保持在被调用之前的状态,具有这种属性的方法被称为具有失败原子性(failure atomic)。
有几种途径可以做到这种效果:
设计一个不可变的对象
失败了永远也不会影响到已有的对象。在执行操作之前检查参数的有效性
这可以使得在对象的状态被修改之前,先抛出适当的异常。编写一段恢复代码
由它来拦截操作过程中发生的失败,使对象回滚到操作开始之前的状态上。在对象的一份临时拷贝上执行操作,当操作完成后再用临时拷贝中的结果代替对象的内容
总之,作为方法规范的一部分,产生的任何异常都应该让对象保持在该方法调用之前的状态;如果违反这条规则,API文档就应该清楚的指明对象将会处于什么样的状态。
65. 不要忽略异常
try {
} catch (Exception e){
//...不作为
}
至少,catch块也应该包含一条说明,解释为什么可以忽略这个异常。
这条建议同样适用于受检异常和未受检异常。
正确的处理异常能够彻底挽回失败,避免可能的失败。