69. 只针对异常的情况才使用异常
异常应该只用于异常的情况下,永远不应该用于正常的控制流。例如基于异常的循环模式会模糊代码的意图,降低性能,还不能保证能正常工作。
设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。
方法 | 原则 |
---|---|
返回optional或可识别的返回值(比如null) | 1.对象在缺少外部同步的情况下被并发访问,或者可被外部改变状态 2. 重复"状态相关"方法的工作 |
提供"状态测试"方法(比如hasNext) | 其它情形(因为可读性更好,易于检测,抛出异常) |
70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常
可抛出结构 | 原则 |
---|---|
受检异常 | 可恢复,提供辅助方法,以便调用者能适当恢复 |
运行时异常 | 编程错误(如前提违例),不确定是否可恢复,抛出未受检异常 |
错误 | 不要实现新的Error子类 |
71. 避免不必要地使用受检异常
在谨慎使用的前提下,受检异常可以提升程序的可读性;但如果过度使用,将会使API使用起来非常痛苦。
消除受检异常的最容易的方法是,返回optional。缺点是无法返回任何额外的信息。
72. 优先使用标准的异常
不要直接重用Exception、RuntimeException、Throwable或者Error。对待这些类要像对待抽象类一样。无法可靠地测试这些异常,因为它们是一个方法可能抛出其它异常的超类。
常见异常 | 使用场合 |
---|---|
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateException | 不适合方法调用的对象状态 |
NullPointerException | 在禁止使用null的情况下参数值为null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportOperationException | 对象不支持用户请求的方法 |
异常是可序列化的,如果没有非常正当的理由,千万不要自己编写异常类。
73. 抛出与抽象对应的异常
更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法称为异常转译。一种特殊的异常转译形式称为异常链。
如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证"它所抛出的所有异常对于更高层也是合适的"情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析。
74. 每个方法抛出的所有异常都要建立文档
始终要单独地声明受检异常,并且利用Javadoc的@throws标签,准确地记录下抛出每个异常的条件。
使用Javadoc的@throws标签记录下一个方法可能抛出的每个受检异常,但是不要使用throws关键字将未受检的异常包含在方法的声明中。
如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,而不是为每个方法单独建立文档。
75. 在细节信息中包含失败-捕获信息
为了捕获失败,异常的细节信息应该包含"对该异常有贡献"的所有参数和域的值。
为了确保在异常的细节信息中包含足够的失败-捕捉信息,一种方法是在异常的构造器而不是字符串细节信息中引入这些信息。
为异常的失败-捕获信息提供一些访问方法是合适的。
千万不要在细节信息中包含密码、密钥以及类似的信息。
76. 努力使失败保持原子性
失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性(failure atomic)。
获得失败原子性的4种方法:
a. 在执行操作之前检查参数的有效性。在对象状态改变前抛出异常。
b. 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
c. 在对象的一份临时拷贝上执行操作,当操作完成后再用临时拷贝中的结果代替对象的内容。
d. 编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作前的状态。
77. 不要忽略异常
空的catch块会使异常达不到应有的目的。
如果选择忽略异常,catch块中应该包含一条注释,说明为什么可以这么做,并且变量应该命名为ignored。
正确地处理异常能够彻底避免失败。只要将异常传播给外界,至少会导致程序迅速失败,从而保留了有助于调试该失败条件的信息。