首页 > 代码库 > [读书笔记]《Effective Java》第9章异常

[读书笔记]《Effective Java》第9章异常

第57条:只针对异常的情况才使用异常
  • 异常机制是用于不正常的情形,很少会有JVM实现试图对它们进行优化。在现代的JVM实现上,基于异常的模式比标准模式要慢得多。
  • 把代码放在try-catch块中反而阻止了现代JVM实现本来可能执行的某些特定优化。
  • 设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。如果类具有“状态相关”的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的“状态测试”方法,即指示是否可以调用这个状态相关的方法。例如,Iterator接口有一个“状态相关”的next方法,和相应的状态测试方法hasNext。另一种做法是如果对象处于不适当的状态,调用“状态相关”的方法返回一个可识别的值,比如null。
  • 选择关于“状态测试方法”和“可识别的返回值”两种做的原则。如果访问者没有做同步,对象会被并发访问的情况下,选择“可识别的返回值”,因为调用“状态测试方法”和“状态相关”的方法之间会有时间间隔。如果其他方面都等同,选择“状态测试方法”,因为有更好的可读性,对于使用不当的情形,“状态相关”的方法会抛出异常,便于检测和修正错误。
 
第58条:对可恢复的情况使用受检异常,对编程错误使用运行时异常
  • Java程序设计语言提供了三种可抛出结构:受检异常、运行时异常和错误。运行时异常和错误都属于未受检异常。
  • 使用受检异常或是未受检异常的原则:
  1. 如果期望调用者能够适当地恢复,使用受检的异常。
  2. 用运行时异常来表明编程错误。
  • 你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接的或间接的)。因为错误被JVM保留用于表示资源不足、约束失败,或者其他使程序无法继续执行的条件。
  • 可以定义一个抛出结构,它不是Exception、RuntimeException或Error的子类。从行为意义上讲它们等同于普通的受检异常(即Exception的子类,而不是RuntimeException的子类)。但是不要定义这样的类,没什么好处,只会困扰用户。
 
第59条:避免不必要地使用受检的异常
  • 如果方法抛出一个或者多个受检的异常,调用该方法的代码就必须在一个或者多个catch块中处理异常,或者它必须声明它抛出异常,并让它们传播出去。无论哪种方法,都给程序员增添了不可忽视的负担。
  • 如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,则使用受检异常,否则更适合于使用未受检的异常。
  • 把受检异常变成未受检异常的一种方法,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明是否应该抛出异常。这个方法同第57条的“状态测试方法”,“状态测试方法”存在的问题(并发访问)在这里也同样存在。
 
第60条:优先使用标准的异常
  • 重用现有异常的好处:
  1. 使你的API更易于学习和使用。
  2. 程序可读性好,因为不会出现很多程序员不熟悉的异常。
  3. 异常类越少,意味着内存印迹就越小,装载这些类的时间开销也越少。
  • 常见的可重用异常

异常类型使用场合
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条:努力使失败保持原子性
  • 失败原子性:失败的方法调用应该使对象保持在被调用之前的状态。
  • 实现失败原子性的方法
  1. 设计一个不可变对象。如果对象是不可变的,失败的原子性就是显然的。
  2. 执行操作之前检查参数的有效性。
  3. 调整计算处理过程的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。
  4. 不常用的做法,主要用于永久性的(基于磁盘的)数据结构。做法是编写一段恢复代码,由它来拦截操作过程中发生的失败,以及使对象回滚到操作开始之前的状态。
  5. 在对象的一份临时拷贝上执行操作,当操作完成之后再用临时拷贝中的结果代替对象的内容。
  • 不利于实现失败原子性的情况:
  1. 多线程且没有适当同步机制的情况下,并发地修改同一个对象。
  2. 对于某些操作,会显著地增加开销或者复杂性。
  • 产生任何异常都应该让对象保持在该方法调用之前的状态。如果违反这条规则,API文档应该清楚地指明对象会处于什么样的状态。
 
第65条:不要忽略异常
  • 忽略异常的方法:将方法调通过try块包围起来,并包含一个空的catch块
// 忽略异常
try {
// 调用方法
} catch (Exception e) {
}
  1. 如果能够忽略异常,catch块中应该包含一条说明,解释为什么可以忽略这个异常。
  2. 不要忽略异常,把异常记录下来是明智的做法。

 

[读书笔记]《Effective Java》第9章异常