目录
Item 69: Use exceptions only for exceptional conditions
Item 71: Avoid unnecessary use of checked exception
Item 72: Favor the use of standard exceptions
Item 73: Throw exceptioons appropriate to the abstraction
Item 74: Document all exceptions thrown by each method
Item 75: Include failure-capture information in detail messages
Item 76: Strive for failure atomicity
Item 77: Don't ignore exception
Item 69: Use exceptions only for exceptional conditions
只有针对异常的情况才使用异常
异常模式比正常模式要慢。
用异常来代替正常控制流的弊端:可读性差,降低性能,不能保证正常工作,掩盖其他bug。
Item 70: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
对可恢复的情况使用受检异常,对编程错误使用运行时异常
术语
- precondition violation:前提违例,指API的客户没有遵守API规范建立的约定。
java提供了3种可抛出结构throwables:受检异常checked exception、运行时异常runtime exception、错误error。
1种受检的throwables:checked exception。受检异常的目的是强制用户要从异常中恢复。
2种不受检的throwables:runtime exception、error。它们不需要也不应该被捕获。当抛出这2种异常时,往往就是不可恢复的场景,程序继续执行下去有害无益。
throwable不捕获的影响:当前线程中断,生成适当的错误信息。
runtime exception用来表示编程错误。大多数表示前提违例(precondition violation)
难点:很难判断要处理的情况是可恢复的还是不可恢复的。如资源枯竭,有可能是分配不合理的大数组造成的(可恢复),也可能是资源不足导致的(不可恢复)。
针对难以判断是否可恢复的情况,最好用runtime exception。
error往往被JVM保留下来使用,用于资源不足、约束失败等等,且这几乎是被普遍接受的惯例。所以在编程中应该定义和使用的unchecked throwable只能是RuntimeException的子类,不应该定义和使用Error的子类。
checked exception需提供一些辅助方法,帮助调用者获取一些有助于恢复的信息。例如,用户购买礼品时因资金不足而抛出一个受检异常,这个异常应该提供一个查询所缺金额的方法method,使调用者可以将这个数值传递给用户。
Item 71: Avoid unnecessary use of checked exception
避免不必要地使用受检异常
相比其他异常,受检异常可提高程序的可靠性,但会增加程序员的开发负担,且不能直接用到stream中。
因此可以考虑采用以下方法代替受检异常:
- (优先考虑)用Optional代替。
- 新增一个函数封装抛出异常的函数,用boolean的返回值区分是否发生异常。
代替受检异常方案的弊端:无法提供足够的异常信息。
Item 72: Favor the use of standard exceptions
优先使用标准异常
java类库提供了一组覆盖大多数API场景的标准异常。
不要直接重用Throwable、Exception、RuntimeException和Error,把它们当做抽象类来看待。
常见的可重用标准异常
异常 | 使用场合(彼此不一定排斥) |
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateException | 不适合方法调用的对象状态 |
NullPointerException | 在禁止null的情况下参数为null |
IndexOutOfBoundsException | 索引参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象被并非修改 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
其他标准异常也可以使用,如ArithmeticException、NumberFormatException。
可以继承标准异常定义新的异常。
记住异常时可序列化的。
重用现有的标准异常,一定要符合异常文档中描述的条件,建立在语义而非名字基础上使用。
Item 73: Throw exceptioons appropriate to the abstraction
抛出与抽象对应的异常
异常转译(exception translation):高层实现捕获低层异常,同时抛出在高层实现中可以解释的异常。换句话说,就是不要直接抛出低层异常,应该根据调用层的实际情况,对低层异常进行处理和转换,再抛出。
异常链:大多数标准的异常都有支持链(chaining-aware)的构造器,可通过getCause()访问原因。对于没有支持链的异常,可调用Throable的initCause()实现。
// 含支持链chaining-aware构造器的异常
class HigherLevelExcetpion extends Exception {
HigherLevelExcetpion(Throwable cause) {
super(cause);
}
}
异常处理步骤:
- 首先,尽量避免低层方法抛出异常,如将参数传递给低层方法前校验高层方法参数的有效性。
- 其次,当低层异常发生时,最佳做法是在高层将低层异常处理好,避免低层问题和高层函数混杂在一起。并做好日志记录,这允许调查问题同时,将客户端代码和用户从异常问题中隔离出来。
- 最后,当无法处理异常时,向上抛出:如果低层异常保证其异常适用于所有高层调用,则可以直接抛出,否则使用异常转译。
Item 74: Document all exceptions thrown by each method
每个方法抛出的所有异常都要建立文档
用@throws标签记录每个异常,并记下抛出每个异常的条件。
如果一个类中多个方法由于相同原因抛出同一个异常,可在类(而非方法)的注释中记录异常。
Item 75: Include failure-capture information in detail messages
在消息细节中包含失败-捕获信息
- 异常应该捕获失败信息,供后续分析。
- 失败信息应包含所有相关参数和域的值,但也应尽量精简。
- 失败信息中应注意安全敏感信息的保密性,如密码、秘钥。
应在构造器(而不是字符串细节信息)中引入失败-捕获信息:
// 在构造器中引入失败-捕获信息
public IndexOutOfBoundsExceptions(int lowerBound, int upperBound, int index) {
super(String.format("Lower bound: %d, Upper bound: %d, Index: %d",
lowerBound, upperBound, index));
... ...
}
Item 76: Strive for failure atomicity
努力使失败保持原子性
失败原子性:失败的方法调用应使对象保持调用前的状态。
实现失败原子性的方法:
- 设计并采用immutable不可变对象。
- 在执行操作之前检查参数的有效性,使得在对象的状态被修改前,抛出大多数异常。或者,调整计算处理顺序,将修改对象状态的操作放在可能导致失败原子的计算后面。
- 在执行操作前对对象进行深度拷贝(将数据拷贝到临时数据结构中还可能提高性能,如有些排序算法在排序前,将数据拷贝到一个数组中,以便降低在排序的内循环中访问元素所需要的开销)。
- 增加一段恢复代码(recovery code),拦截失败情况并使对象回滚到初始状态。此方法不常用,主要用于永久性(基于磁盘)的数据结构。
执行操作前未检查导致对象状态被修改的示例:
public Object pop() {
// if(size == 0) throw new EmptyStackException(); // 取消此注释可解决问题
Object result = elements[--size]; // 抛出异常后,size会变为负数
... ...
}
实现失败原子的弊端:可能会显著增加开销或者复杂性。
抛出异常的地方都应尽可能保持失败原子性,如果做不到,API文档中应清楚地指出对象会处于增氧的状态。
Item 77: Don't ignore exception
不要忽略异常
不要使用空的catch块,如果选择忽略异常,catch块中应包含一条注释,说明为啥可以这么做,并且变量名应命名为ignore:
try {
...
} catch (TimeoutException ignore) {
// (不用处理该异常的理由)
}