Effective Java 第三版——69. 仅在发生异常的条件下使用异常

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,因此JDK 最好下载 JDK 9以上的版本。java

Effective Java, Third Edition

异常

当充分发挥异常的优点时,它能够提升程序的可读性、可靠性和可维护性。若是使用不当,则会产生相反的效果。本章提供了有效使用异常的指南。git

69. 仅在发生异常的条件下使用异常

有一天,若是你运气很差,你可能会偶然发现这样一段代码:程序员

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

这段代码是作什么的?检查结果看来一点也不明显,这就是不使用它的充分理由(条目 67)。事实证实,这是一种用于循环遍历数组元素的很是错误的习惯用法。当试图访问数组边界以外的第一个数组元素时,无限循环经过抛出、捕获和忽略ArrayIndexOutOfBoundsException异常来终止。它应该等同于循环数组的标准习惯用法,任何Java程序员均可以一眼就能识别出来:github

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

那么为何有人会使用基于异常的循环而不是尝试和正确的用法? 根据错误推理提升性能是一种错误的尝试,由于虚拟机检查全部数组访问的边界,由编译器隐藏但仍然存在于for-each循环中的正常循环终止测试是多余的,应该避免。 这个推理有三个问题:数组

  • 由于异常是为特殊状况设计的,因此JVM实现者几乎没有试图让它们像显式测试同样快。
  • 将代码放在try-catch块中会抑制虚拟机实现可能执行的某些优化。
  • 遍历数组的标准习惯用法不必定会致使冗余检查。许多虚拟机实现对它们进行了优化。

事实上,基于异常的习惯用法比标准用法慢得多。在个人机器上,100个元素的数组,基于异常的习惯用法的速度大约是标准习惯用法的两倍。并发

基于异常的循环不只混淆了代码的目的,下降了代码的性能,并且不能保证它能正常工做。若是循环中存在bug,使用异常进行流控制能够掩盖该bug,从而大大增长调试过程的复杂性。假设循环体中的计算调用一个方法,该方法对一些不相关的数组执行越界访问。若是使用合理的循环习惯用法,该bug将生成一个未捕获的异常,致使线程当即终止,并带有完整的堆栈跟踪。若是使用错误的基于异常的循环,则会捕获与bug相关的异常,并将其误解为正常的循环终止。ide

这个示例说明的道理很简单:顾名思义,异常仅用于特殊状况; 它们永远不该该用于正常的控制流程。 一般来讲,使用标准的、易于识别的习惯用法,而不是声称能够提供更好性能的过分聪明的技术。即便性能优点是真实存在的,但在稳步改进平台实现的状况下,这种优点也可能不复存在。然而,来自过分聪明的技术的细微缺陷和维护难题确定会继续存在。性能

这个原则对API设计也有影响。一个设计良好的API不能强迫它的客户端为正常的控制流使用异常。只有在某些不可预知的条件下才能调用具备“状态依赖(state-dependent)”方法的类,一般应该有一个单独的“状态测试(state-testing)”方法,指示是否适合调用状态依赖方法。例如,Iterator接口具备依赖于状态的next方法和对应的状态测试方法hasNext。这支持使用传统for循环(以及for-each循环,其中内部使用了hasNext方法)在集合上迭代的标准习惯用法:测试

for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
    ...
}

若是Iterator缺乏hasNext方法,则客户端将被迫执行此操做:优化

// Do not use this hideous code for iteration over a collection!
try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
    }
} catch (NoSuchElementException e) {
}

这数组迭代的例子很是相似于本条目一开始的那个例子。除了冗长和误导以外,基于异常的循环极可能执行得不好,而且能够掩盖系统中不相关部分中的bug。

提供单独的状态测试方法的另外一种方式是,让依赖于状态的方法返回一个空的Optional值(条目 55),或者在它不能执行所需的计算时返回一个区分值,好比null。

下面是一些指导原则,帮助你在状态测试方法,Optional的或区分的返回值之间进行选择。若是要在没有外部同步的状况下并发地访问对象,或者受制于外部引起的状态转换,则必须使用Optional的或可区分的返回值,由于在调用状态测试方法与其依赖于状态的方法之间的间隔内,对象的状态可能会发生变化。若是一个单独的状态测试方法将重复依赖于状态的方法的工做,那么性能问题可能要求使用一个Optional的或可区分的返回值。在全部其余条件相同的状况下,状态测试方法略优于区分的返回值。它提供了更好的可读性,并且不正确的使用可能更容易检测:若是忘记调用状态测试方法,依赖于状态的方法将抛出异常,使错误变得明显;若是忘记检查一个可区分的返回值,那么这个bug可能很微妙。这不是Optional返回值的问题。

总之,异常是针对特殊状况而设计的。不要将它们用于正常的控制流程,也不要编写强制其余人这样作的API。

相关文章
相关标签/搜索