第57条:只针对异常的情况才使用异常
- 异常机制是用于不正常的情形,很少会有JVM实现试图对它们进行优化。在现代的JVM实现上,基于异常的模式比标准模式要慢得多。
- 把代码放在try-catch块中反而阻止了现代JVM实现本来可能执行的某些特定优化。
- 设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。如果类具有“状态相关”的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的“状态测试”方法,即指示是否可以调用这个状态相关的方法。例如,Iterator接口有一个“状态相关”的next方法,和相应的状态测试方法hasNext。另一种做法是如果对象处于不适当的状态,调用“状态相关”的方法返回一个可识别的值,比如null。
- 选择关于“状态测试方法”和“可识别的返回值”两种做的原则。如果访问者没有做同步,对象会被并发访问的情况下,选择“可识别的返回值”,因为调用“状态测试方法”和“状态相关”的方法之间会有时间间隔。如果其他方面都等同,选择“状态测试方法”,因为有更好的可读性,对于使用不当的情形,“状态相关”的方法会抛出异常,便于检测和修正错误。
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
- Java程序设计语言提供了三种可抛出结构:受检异常、运行时异常和错误。运行时异常和错误都属于未受检异常。
- 使用受检异常或是未受检异常的原则:
- 如果期望调用者能够适当地恢复,使用受检的异常。
- 用运行时异常来表明编程错误。
- 你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接的或间接的)。因为错误被JVM保留用于表示资源不足、约束失败,或者其他使程序无法继续执行的条件。
- 可以定义一个抛出结构,它不是Exception、RuntimeException或Error的子类。从行为意义上讲它们等同于普通的受检异常(即Exception的子类,而不是RuntimeException的子类)。但是不要定义这样的类,没什么好处,只会困扰用户。
第59条:避免不必要地使用受检的异常
- 如果方法抛出一个或者多个受检的异常,调用该方法的代码就必须在一个或者多个catch块中处理异常,或者它必须声明它抛出异常,并让它们传播出去。无论哪种方法,都给程序员增添了不可忽视的负担。
- 如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,则使用受检异常,否则更适合于使用未受检的异常。
- 把受检异常变成未受检异常的一种方法,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明是否应该抛出异常。这个方法同第57条的“状态测试方法”,“状态测试方法”存在的问题(并发访问)在这里也同样存在。
第60条:优先使用标准的异常
- 重用现有异常的好处:
- 使你的API更易于学习和使用。
- 程序可读性好,因为不会出现很多程序员不熟悉的异常。
- 异常类越少,意味着内存印迹就越小,装载这些类的时间开销也越少。
- 常见的可重用异常
异常类型 | 使用场合 |
---|---|
IllegalArgumentException | 非null的参数值不正确 |
IllegalStateExcepition | 对于方法调用而言,对象状态不适合 |
NullPointerException | 在禁止使用null的情况下使用null |
IndexOutOfBoundsException | 下标参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到并发修改 |
UnSupportedOperationException | 对象不支持用户请求的方法 |
第61条:抛出与抽象相对应的异常
- 高层方法调用低层方法,如果低层方法抛出异常而高层方法直接将低异常抛出,这个异常可能和高层方法没有什么联系,会让人不知所措。如果低层方法修改了抛出的异常,导致高层也被修改了,会破坏现有的客户端。
- 更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译。如下:
// 异常转译 try { // 调用低层代码 } catch (LowerLevelException e) { throw new HigherLevelException(); }
- 特殊的异常转译形式:异常链。低层的异常原因被传到高层异常,高层的异常提供访问方法(Throwable.getCause)来获得低层的异常。如下:
// 异常链 try { // 调用低层代码 } catch (LowerLevelException e) { throw new HigherLevelException(e); }
例:
1 /** 2 * 高层异常,有接受异常的构造器 3 * Created by itlivemore on 2017/6/25. 4 */ 5 class HigherLevelException extends Exception { 6 public HigherLevelException() { 7 } 8 9 // 接受异常的构造器 10 public HigherLevelException(Throwable cause) { 11 super(cause); 12 } 13 } 14 15 /** 16 * 高层异常,没有接受异常的构造器 17 */ 18 class HigherLevelException2 extends Exception { 19 } 20 21 /*低层异常*/ 22 class LowerLevelException extends Exception { 23 public LowerLevelException(String message) { 24 super(message); 25 } 26 } 27 28 class Lower { 29 static void f(int param) throws LowerLevelException { 30 throw new LowerLevelException("参数" + param + "调用f()异常"); 31 } 32 } 33 34 /** 35 * 异常链 36 * Created by itlivemore 2017/6/25. 37 */ 38 public class ExceptionChain { 39 public static void main(String[] args) { 40 try { 41 call1(); 42 } catch (HigherLevelException e) { 43 e.printStackTrace(); 44 } 45 try { 46 call2(); 47 } catch (HigherLevelException2 e) { 48 e.printStackTrace(); 49 } 50 } 51 52 private static void call1() throws HigherLevelException { 53 // 有运行异常链的构造器,则将异常放到构造方法中 54 try { 55 Lower.f(1); 56 } catch (LowerLevelException e) { 57 throw new HigherLevelException(e); 58 } 59 } 60 61 private static void call2() throws HigherLevelException2 { 62 // 没有运行异常链的构造器,使用initCause()设置原因 63 try { 64 Lower.f(2); 65 } catch (LowerLevelException e) { 66 HigherLevelException2 exception = new HigherLevelException2(); 67 exception.initCause(e); 68 throw exception; 69 } 70 } 71 }
- 处理低层异常的最好方式是避免抛出异常,如高层在调用低层前检查参数来保证低层执行成功。如果不能避免异常,高层可以绕开异常,仅用日志记录异常,用于排查问题。
第62条:每个方法抛出的异常都要有文档
- 始终要单独地声明受检的异常,并且利用Javadoc的@throws标记,准确记录下抛出每个异常的条件。一个方法可能抛出多个异常,不要声明为"throws Exception"。
- 对于未受检异常,在@throws标签中记录,不要在方法的throws中声明。
- 如果一个类中的许多方法出于同样的原因而抛出同一个异常,可以在该类的文档注释中对这个异常建立文档。如输入参数为空,都抛出NullPointerException。
第63条:在细节消息中包含能捕获失败的消息
- 程序由于未被捕获的异常运行失败时,系统会打印异常栈轨迹,会调用异常的toString方法,所以异常的toString方法要尽可能地返回有关失败原因的信息。
- 为了捕获失败,异常的细节信息应该包含所有“对该异常有贡献”的参数和域的值。
- 为了确保在异常的细节消息中包含足够的能捕获失败的信息,一种办法是在异常的构造器而不是字符串细节消息中引入这些消息。如IndexOutOfBoundsException有 public IndexOutOfBoundsException(int lowerBound,int upperBound,int index) 这样的构造器。
第64条:努力使失败保持原子性
- 失败原子性:失败的方法调用应该使对象保持在被调用之前的状态。
- 实现失败原子性的方法
- 设计一个不可变对象。如果对象是不可变的,失败的原子性就是显然的。
- 执行操作之前检查参数的有效性。
- 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
- 不常用的做法,主要用于永久性的(基于磁盘的)数据结构。做法是编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态。
- 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。
- 不利于实现失败原子性的情况:
- 多线程且没有适当同步机制的情况下,并发地修改同一个对象。
- 对于某些操作,会显著地增加开销或者复杂性。
- 产生任何异常都应该让对象保持在该方法调用之前的状态。如果违反这条规则,API文档应该清楚地指明对象会处于什么样的状态。
第65条:不要忽略异常
- 忽略异常的方法:将方法调通过try块包围起来,并包含一个空的catch块
// 忽略异常 try { // 调用方法 } catch (Exception e) { }
- 如果能够忽略异常,catch块中应该包含一条说明,解释为什么可以忽略这个异常。
- 不要忽略异常,把异常记录下来是明智的做法。
时间: 2024-10-11 16:58:47