转载

《Effective Java》Second Edition 第9章 异常

第9章 异常

第57条:只针对异常的情况才使用异常

  1. 因为异常机制的设计初衷是用于不正常的情形,所以很少会有JVM实现试图对它们进行优化,使得与显式的测试一样快速。
  2. 把代码放在try-catch块中反而阻止了现代JVM实现本来可能要执行的某些特定优化。
  3. 对数组进行遍历的标准模式并不会导致冗余的检查。有些现代的JVM实现会将它们优化掉。
  4. 设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。如果类具有“状态相关(state-dependent)”的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的“状态测试(state-testing)”方法,即指示是否可以调用这个状态相关的方法。例如,Iterator接口有一个“状态相关”的next方法,和相应的状态测试方法hasNext。
  5. 另一种提供单独的状态测试方法的做法是,如果“状态相关的”方法被调用时,该对象处于不适当的状态之中,它就会返回一个可识别的值,比如null。
  6. 如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,使用可被识别的返回值可能是很有必要的,因为在调用“状态测试”方法和调用对应的“状态相关”方法的时间间隔之中,对象的状态有可能会发生变化。如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就应该使用可被识别的返回值。如果所有其他方面都是等同的,那么“状态测试”方法则略优于可被识别的返回值。它提供了更好的可读性,对于使用不当的情形,可能更加易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使这个Bug变得很明显;如果忘了去检查可识别的返回值,这个Bug就很难会被发现。

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

  1. 在决定使用受检的异常或是未受检的异常时,主要的原则是:如果期望调用者能够适当地恢复,对于这种情况就应该使用受检的异常。通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。因此,方法中声明要抛出的每个受检的异常,都是对API用户的一种潜在指示:与异常相关联的条件是调用这个方法的一种可能的结果。
  2. 有两种未受检的可抛出结构:运行时异常和错误。在行为上两者是等同的:它们都是不需要也不应该被捕获的可抛出结构。如果程序抛出未受检的异常或者错误,往往就属于不可恢复的情形,继续执行下去有害无益。如果程序没有捕捉到这样的可抛出结构,将会导致当前线程停止(halt),并出现适当的错误消息。
  3. 虽然JLS(Java语言规范)并没有要求,但是按照惯例,错误往往被JVM保留用于表示资源不足、约束失败,或者其他使程序无法继续执行的条件。由于这已经是个几乎被普遍接受的惯例,因此最好不要再实现任何新的Error子类。因此,你实现的所有未受检的抛出结构都应该是RuntimeException的子类(直接的或者间接的)。
  4. 因为受检的异常往往指明了可恢复的条件,所以,对于这样的异常,提供一些辅助方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息。例如,假设因为用户没有储存足够数量的钱,他企图在一个收费电话上进行呼叫就会失败,于是抛出受检的异常。这个异常应该提供一个访问方法,以便允许客户查询所缺的费用金额,从而可以将这个数值传递给电话用户。

第59条:避免不必要地使用受检的异常

  1. 如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合于使用未受检的异常。
  2. 如果使用API的程序员无法做得比这更好,那么未受检的异常可能更为合适。这种例子就是CloneNotSupportedException。它是被Object.clone抛出来的,而Object.clone应该只是在实现了Cloneable的对象上才可以被调用。在实践中,catch块几乎总是具有断言(assertion)失败的特征。异常受检的本质并没有为程序员提供任何好处,它反而需要付出努力,还使程序更为复杂。

第60条:优先使用标准的异常

  1. 最经常被重用的异常是IllegalArgumentException。当调用者传递的参数值不合适的时候,往往就会抛出这个异常。例如,假设一个参数代表了“某个动作的重复次数”,如果程序员给这个参数传递了一个负数,就会抛出这个异常。
  2. 另一个经常被重用的异常是IllegalStateException。如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
  3. 可以这么说,所有错误的方法调用都可以被归结为非法参数或者非法状态,但是,其他还有一些标准异常也被用于某些特定情况下的非法参数和非法状态。如果调用者在某个不允许null值的参数中传递了null,习惯的做法就是抛出NullPointerException,而不是IllegalArgumentException。同样地,如果调用者在表示序列下标的参数中传递了越界的值,应该抛出的就是IndexOutOfBoundsException,而不是IllegalArgumentException。
  4. 另一个值得了解的通用异常是ConcurrentModificationException。如果一个对象被设计为专用于单线程或者与外部同步机制配合使用,一旦发现它正在(或已经)被并发地修改,就应该抛出这个异常。
  5. 最后一个值得注意的通用异常是UnsupportedOperationException。如果对象不支持所请求的操作,就会抛出这个异常。与本条目中讨论的其他异常相比,它很少用到,因为绝大多数对象都会支持它们实现的所有方法。如果接口的具体实现没有实现该接口所定义的一个或者多个可选操作,它就可以使用这个异常。
  6. 例如,如果要实现诸如复数或者有理数之类的算术对象,也可以重用ArithmeticException和NumberFormatException。
  7. 这种重用必须建立在语义的基础上,而不是建立在名称的基础之上。而且,如果希望稍微增加更多的失败-捕获(failure-capture)信息,可以放心地把现有的异常进行子类化。

第61条:抛出与抽象相对应的异常

  1. 如果方法抛出的异常与它所执行的任务没有明显的联系,这种情形将会使人不知所措。当方法传递由低层抽象抛出的异常时,往往会发生这种情况。除了使人感到困惑之外,这也让实现细节污染了更高层的API。
  2. 为了避免这个问题,更高层的实现应该捕获低层的异常,同时抛出可以按照高层抽象进行解释的异常。这种做法被称为异常转译(exception translation),如下所示:
// Exception Translation
try {
    // Use lower-level abstraction to do our bidding
    ...
} catch(LowerLevelException e) {
    throw new HigherLevelException(...);
}

一种特殊的异常转译形式称为异常链(exception chaining),如果低层的异常对于调试导致高层异常的问题非常有帮助,使用异常链就很合适。低层的异常(原因)被传到高层的异常,高层的异常提供访问方法(Throwable.getCause)来获得低层的异常:

// Exception Chaining
try {
    ... // Use lower-level abstraction to do our bidding
} catch(LowerLevelException cause) {
    throw new HigherLevelException(cause);
}

如有可能,处理来自低层异常的最好做法是,在调用低层方法之前确保它们会成功执行,从而避免它们抛出异常。有时候,可以在给低层传递参数之前,检查更高层方法的参数的有效性,从而避免低层方法抛出异常。

如果无法避免低层异常,次选方案是,让更高层来悄悄地绕开这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以用某种适当的记录机制(如java.util.logging)将异常记录下来。这样有助于管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。

第62条:每个方法抛出的异常都要有文档

  1. 总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检和受检的异常,以及对于抽象的和具体的方法也都一样。要为每个受检异常提供单独的throws子句,不要为未受检的异常提供throws子句。

第63条:在细节消息中包含能捕获失败的信息

  1. 虽然在异常的细节消息中包含所有相关的“硬数据(hard data)”是非常重要的,但是包含大量的描述信息往往没有什么意义。
  2. 异常的细节消息不应该与“用户层次的错误消息”混为一谈,后者对于最终用户而言必须是可理解的。与用户层次的错误消息不同,异常的字符串表示法主要是让程序员或者域服务人员用来分析失败的原因。因此,信息的内容比可理解性要重要得多。
  3. 为了确保在异常的细节消息中包含足够的能捕获失败的信息,一种办法是在异常的构造器而不是字符串细节消息中引入这些信息。然后,有了这些信息,只要把它们放到消息描述中,就可以自动产生细节消息。例如,IndexOutOfBoundsException并不是有个String构造器,而是有个这样的构造器:
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
    // Generate a detail message that captures the failure
    super("Lower bound: " + lowerBound +
            ", Upper bound: " + upperBound + 
            ", Index: " + index);
    // Save failure information for programmatic access
    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}

第64条:努力使失败保持原子性

  1. 一般而言,失败的方法调用应该使对象保持在被调用之前的状态。具有这种属性的方法被称为具有失败原子性(failure atomic)。
  2. 对于在可变对象上执行操作的方法,获得失败原子性最常见的办法是,在执行操作之前检查参数的有效性。这可以使得在对象的状态被修改之前,先抛出适当的异常。例如,Stack.pop方法:
public Object pop() {
    if (size==0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

虽然一般情况下都希望实现失败原子性,但并非总是可以做到。例如,如果两个线程企图在没有适当的同步机制的情况下,并发地修改同一个对象,这个对象就有可能被留在不一致的状态之中。因此,在捕获了ConcurrentModificationException异常之后再假设对象仍然是可用的,这就是不正确的。错误(相对于异常)通常是不可恢复的,当方法抛出错误时,它们不需要努力保持失败原子性。

第65条:不要忽略异常

原文地址:http://www.jsdblog.com/article/63   

正文到此结束
Loading...