effective java - 异常

2016-10-07 23:43:12

《Effective Java》读书笔记,关于异常系列。

只针对异常的情况才使用异常

异常应该只用于异常情况,不应该用于正常的控制流

设计良好的api不应该强迫客户端为了正常控制流而使用异常。如果类有在特定状态下才可以调用的方法,就应该有对应的“状态检查”方法,如Iterator的next方法和hasNext方法。
或方法返回标识结束的值,如null,-1

对可恢复的情况使用受检异常,对编程错误使用运行时异常

如果期望调用者能够适当地恢复,应该使用受检异常。 通过抛出受检异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。
用户可以根据错误信息进行处理
受检异常往往要指明可恢复的条件

用运行时异常表明编程错误 大多数运行时异常都表示客户没有遵守api规范中的约定,如ArrayIndexOutBoundsException。

避免不必要地使用受检异常

受检异常会增加异常处理的工作量,也会增加代码复杂度。
对于底层接口的方法或经常被调用的方法,如果声明抛出受检异常,可能会造成受检异常的大量蔓延,因为所有实现或调用这些方法的代码都必须处理受检异常。

使用受检异常时,要考虑到异常捕获者能否立即采取有用的措施,如果不能,那考虑是否使用未受检异常更合适。

对于可能失败的方法,可以考虑添加“状态检查”方法并使用未受检异常。
捕获到的受检异常也可以根据情况转译为未受检异常

优先使用标准异常

1.使api易于学习和使用
2.可读性更好,不会出现程序员不熟悉的异常。
3.异常类越少,内存印迹就越小,装载这些类的时间开销就越少。

最常用的可重用异常:

异常 使用场合
IllegalArgumentException 非null的参数值不正确
IllegalStateException 对于方法调用而言,对象状态不合适
NullPointerException 在禁止使用null的情况下参数值为null
IndexOutOfBoundsException 下标参数越界
ConcurrentModificationException 在禁止并发修改的情况下,检测到对象的并发修改。
ConcurrentModificationException 对象不支持用户请求的方法

抛出与抽象层面相对应的异常

如果方法抛出的异常与它所执行的任务没有明显联系(如底层细节方法抛出的异常),往往使人困惑,实现细节也会污染高层api。为了避免这个问题更高层的实现应该捕获底层的异常,同时抛出可以按照高层抽象进行解释的异常这种做法称为异常转译
使用异常链
可以在给底层传递参数前,检查参数的有效性,从而避免底层方法抛出异常。

每个方法抛出的异常都要有文档

始终要单独声明受检异常,并用利用javadoc的@throws标记,准确地记录下抛出每个异常的条件
如果一个方法可能抛出多个异常,不要只声明它会抛出的这些异常的某个超类,即永远不要声明方法throws Exceptionthrows Throwable

使用javadoc的@throws标签记录下一个方法可能抛出的每个未受检异常,但不要使用throws关键字将未受检异常包含在方法声明中。

在异常的细节消息中包含失败的信息

为了捕获失败,异常的细节信息应该包含所有“对该异常有用的”的参数和域的值如IndexOutOfBoundsException异常的细节消息应该包含下界,上界和越界的下标值。
但异常消息中包含大量的描述信息往往没有作用。异常的细节消息与“用户层次的错误信息”不同,后者对于最终用户必须是可理解的,但异常的消息细节是让程序员用于分析错误的,信息的内容比可理解性要重要很多。
可以在异常中使用属性保存关键消息,再生成对应的消息描述,而不是直接使用字符串:

努力使失败保持原子性

对于会修改对象状态的方法,如果调用失败了,应该使对象保持在被调用之前的状态

  • 使用不可变类,如果一个操作失败了,阻止创建新的对象
  • 执行操作前先检查参数的有效性,对无效参数先抛出适合的异常
  • 调整计算处理的顺序,使得任何可能会失败的计算部分都在对象状态被修改之前发生。 这是对上一个方法的扩展
  • 编写恢复代码(使用较少)
  • 在对象的一份临时拷贝上操作,成功后再拷贝内容替换对象内容。

不要忽略异常

这里有三个重要的原则

  • 具体明确
    不要使用Throwable,Error,Exception或RuntimeException,使用具体的异常子类,如FileNotFoundException,EOFException和ObjectStreamException这些子 类,分别描述了一类特定的I/O错误:文件丢失,异常文件结尾和错误的序列化对象流
  • 提早抛出
    如在方法开始时检查参数有效性,而不是将错误参数进行处理,再处理过程再抛出异常。
    通过提早抛出异常(又称"迅速失败"),异常得以清晰又准确。堆栈信息立即反映出什么出了错(非法参数值),为什么出错(文件名不能为空值),以及哪里出的错(readPreferences()的前部分)。

  • 延迟捕获

    public void readPreferences(String filename){
      InputStream in = null;
    
      // DO NOT DO THIS!!!
      try{
          in = new FileInputStream(filename);
      }
      catch (FileNotFoundException e){
          logger.log(e);
      }
    
      in.read(...);
      //...
    }
    

如果出现FileNotFoundException异常,当我们查看日志时,会本能地查看日志最后的异常信息,却看到异常信息为代码 in.read(...); 抛出的NullPointerException异常,这里会造成非常大的误导。
在合适的层面捕获异常,以便你的程序要么可以从异常中有意义地恢复并继续下去,而不导致更 深入的错误;要么能够为用户提供明确的信息,包括引导他们从错误中恢复过来。如果你的方法无法胜任,那么就不要处理异常,把它留到后面捕获和在恰当的层面处理(可能要转译)。
参考
有效处理Java异常三原则