lostars
发布于 2019-02-28 / 3065 阅读
0

EFFECTIVE JAVA 3RD EDITION — 第十章 异常

Item 69 : Use exceptions only for exceptional conditions

只在异常情况下使用异常

// Horrible abuse of exceptions. Don't ever do this! 
try {    
    int i = 0;    
    while(true)        
    range[i++].climb(); 
} catch (ArrayIndexOutOfBoundsException e) {} 

上面错误的使用了异常,在循环中使用ArrayIndexOutOfBoundsException只是为了保证没有越界发生,这种情况应该是使用增强的for循环来避免

for (Mountain m : range)    m.climb(); 

异常只是设计用在一些异常的场景,而不是通过异常来作一些性能提升;同时把代码放在try-catch代码块中会影响jvm的某些优化;通常数组的循环不需要额外的冗余检查,jvm会自动优化。

异常检查的场景通常会影响性能,并且也不一定能够保证代码正常执行,如果在循环中调用了一个方法抛出了数组越界,但是该方法在try-catch块中,这对定位bug增加了不少难度,但是如果没有这样程序则会直接抛出异常并终止运行。

总的来说就是异常只用在需要异常检查的场景,而不是用于普通的流程控制。同时在API中,好的API不会强迫用户在普通流程控制中使用异常。

一个状态依赖的类,如果在不可预测的条件下执行则需要提供一个状态检查的方法来判断是否可执行,比如迭代器的hasNext方法或者也可以返回一个不同的值,比如null来表示无法进行操作。

如果没有外部同步的情况下访问对象或者状态会受到外部影响则应该使用后者,除此之外应使用状态检查方法,状态检测方法拥有更好的可读性,并且能更容易检查到错误使用:如果没有调用状态检测方法则会抛出异常但后者如果你忘了对返回值进行检查的话就可能引起严重的错误。

Item 70 : Use checked exceptions for recoverable conditions and runtime exceptions for programming errors

在需要恢复的场景使用检查异常,程序错误使用运行时异常

Java提供了3种可抛出异常:检查异常、运行时异常、错误
在可以合理预期调用者恢复的情况下使用检查异常,抛出检查异常将强迫调用者去处理该异常或者继续向外抛出。未检查异常包括运行时异常和错误,这两种异常通常都不应该被捕获,抛出未检查异常表明基本不可能被恢复并且如果继续执行将会更糟。

使用运行时异常来表明程序错误,大多数的运行时异常都表示违反了前提条件,比如数组的下标都是非负数。而错误则只是JVM用来表明资源不足,持续性的失败或者其他任何导致程序继续执行的情况。通常情况下都不要继承任何Error类,而你的未检查异常则应该是RuntimeException的子类(直接或间接)。

总的来说检查异常是用于可恢复的场景,而未检查异常则用于程序错误,不知道用什么的时候使用后者。同时不要定义任何既不是检查异常也不是运行时异常的Throwables。

Item 71: Avoid unnecessary use of checked exceptions

避免使用不必要的检查异常

API中过度的使用检查异常会增加使用API的负担,调用者需要反复的编写try-catch代码块来处理异常,这在Java8中更加的明显,因为在流中不能使用抛出检查异常的方法。

这个时候可以返回不同的值以便在流中可以使用,或者把检查异常改为未检查异常。

总的来说适当的使用检查异常可以增加程序的稳定性,但是滥用则会导致使用十分麻烦,如果调用者不能从失败中恢复,则抛出未检查异常更为合适,只有在失败失败情况下信息不足时才抛出检查异常。

Item 72: Favor the use of standard exceptions

使用标准异常

复用标准异常有几个好处:

  • 会使你的API更加的容易上手,因为使用的标准异常都熟悉;
  • 使用API的代码更容易阅读,因为没有不熟悉的异常类;
  • 更少的异常类意味着更少的内存占用和类加载时间;
    可以说每个错误的方法调用都可以归结于非法的参数或状态,其他例外异常都用于某些类型的非法参数和状态。比如传入一个null值,调用则会抛出NullPointerException 异常而不是IllegalArgumentException。

IllegalArgumentException用于调用者的参数不正确;
IllegalStateException表示接收的对象状态不合法,比如调用者想要在对象初始化前使用该对象;
ConcurrentModificationException用于如果一个设计在单线程场景的对象检测到被修改了抛出该异常,但是该异常充其量只是一个提示,并不能可靠的检测并发修改。

不要直接使用Exception RuntimeException Throwable Error,把他们当成抽象类。

Item 73: Throw exceptions appropriate to the abstraction

抛出适合抽象的异常

高级封装的API应该catch掉同层级的低等级异常,同时用高级封装来抛出异常,这通常叫做异常转换,比如AbstractSequentialList

/**
 * Returns the element at the specified position in this list.
 *
 * 
 
This implementation first gets a list iterator pointing to the
 * indexed element (with <tt>listIterator(index)</tt>).  Then, it gets
 * the element using <tt>ListIterator.next</tt> and returns it.
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

异常链:低级异常通过高级异常的封装抛出,同时高级封装包含了低级封装的异常信息有助于debug。

虽然异常转换优于低层异常的抛出,但是也不能过度使用,最好就是处理掉低层的异常。如果无法避免低层异常的抛出,则高层最好静默处理掉这些异常,让上层调用者没有感知,这样有可能让一些日志组件无法记录异常信息。

总的来说避免底层的异常时不太可行的,通过异常转换来处理低层的异常来隔离用户,或者使用异常链来抛出合适的异常。

Item 74: Document all exceptions thrown by each method

用注释说明每个方法抛出的异常

异常描述是一个方法文档的重要部分,所以仔细的说明方法抛出的异常时非常重要的。始终单独声明检查异常,并且用@throws注解来描述清楚每一个异常条件。

不要简单的注解方法抛出了一个异常父类,比如 throws Exception或者throws Throwable,这样的声明极大的妨碍了该方法的使用,同时会让其他异常显得更加模糊。尽管java本身不要求程序员声明未检查异常,但是仔细的用文档描述清楚未检查异常能够帮助用户避免抛出未检查异常。

如果一个类的很多方法抛出同一个异常,那么异常的文档注释最好写在类的注释上,而不是每一个方法上。总的来说就是注释清楚每一个抛出的异常。

Item 75: Include failure-capture information in detail messages

将失败捕获信息包含在异常中

一个程序的异常堆栈信息是分析错误的很重要东西,如果堆栈信息没有输出重要的异常信息同时这个错误又很难浮现那么将很难解决问题。为了捕获一个错误,异常的详细信息应该包含对异常有帮助的所有参数和属性。但为了安全,不要在异常详细信息中包含密码,密钥类似的信息。

总的来说在检查异常中提供一个构造方法来提供错误详细信息。

Item 76: Strive for failure atomicity

保证失败的原子性

一般来说,一个方法失败了应该让对象保持方法执行前的状态,要实现这个可以采用下面的方式:

  • 可以让对象不可变,final;
  • 在方法执行前对参数的有效性进行检查;
  • 对所有的计算进行排序,让所有可能失败的操作都在修改对象状态之前进行;
  • 用一个拷贝的副本进行计算,当计算完成再进行替换操作;
  • 编写恢复代码在失败的时候对状态进行恢复;
    总的来说任何异常都应该在抛出后保证对象的原始状态,但是很多的API都没有保证一这点。

Item 77: Don’t ignore exceptions

不要忽略任何异常

一个空的catch块违背了异常设计的初衷,但是也有场景需要我们忽略一些异常,比如在关闭FileInputStream时,并没有修改文件状态所以也不需要任何恢复操作。同时如果选择忽略异常,那么就要在catch块中注释说明,并且参数应该命名为ignored。

总的来说处理异常能够完全避免失败,如果让异常向外抛出能导致快速失败,保留详细的失败信息来进行调试。


这一章主要介绍了异常的使用规范,同时不要忽略任何异常,也不要尝试去catch掉所有异常而不去处理他们。