119 Star 1K Fork 296

lin-mt / effective-java-third-edition

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
第69项:只针对异常的情况才使用异常.md 6.49 KB
一键复制 编辑 原始数据 按行查看 历史
lin-mt 提交于 2019-12-29 15:50 . 修订69、70项

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

  总有一天,如果你运气不好,你可能偶然发现一段看起来像这样的代码:

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

  这段代码有什么作用?看起来【这段代码的作用】并不明显,这就是不使用它的原因(第 67 项)。事实证明,这是一种用于循环遍历数组元素的非常有毛病的构想。当这个无限循环在尝试访问数组边界外的第一个数组元素时,用抛出(throw)、捕获(catch)、忽略 ArrayIndexOutOfBoundsException 异常的手段来达到终止无限循环的目的。假定它与数组循环的标准模式是等价的,对于任何一只 Java 程序猿来说,下面的标准模式一看就会明白:

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

  那么,为什么有人会优先使用基于异常的模式,而不是行之有效的模式呢?这是被误导了,他们企图利用 Java 的错误判断机制来提高性能,因为 VM 对每次数组访问都要检查越界情况,所以他们认为正常的循环终止测试被编译器隐藏了,但在 for-each 循环中仍然可见,这无疑是多余的,应该避免。这种想法有三个错误:

  • 因为异常机制的设计初衷是用于不正常的情形,所以很少会有 JVM 实现试图对它们进行优化,使得与测试时显示的一样快速。

  • 将代码放在 try-catch 块中会阻止 JVM 实现可能要执行的某些优化。

  • 对数组进行遍历的标准模式并不会导致冗余的检查。有些 JVM 实现会将它们优化掉。

  实际上,基于异常的模式比标准模式要慢得多。在我的机器上,对于一个有 100 个元素的数组,基于异常的模式比标准模式慢了 2 倍。

  基于异常的循环模式不仅模糊了代码的意图,降低了它的性能,而且它还不能保证正常工作。如果循环中存在 BUG,那么使用异常控制流会掩盖BUG,从而使调试过程变得非常复杂。假设循环体中的计算【过程】调用一个方法,该方法对一些不相关的数组执行越界访问。如果使用合理的循环模式,这个 BUG 会产生未被捕捉的异常,从而导致线程立即结束,产生完整地堆栈跟踪【信息】。如果使用这个被误导的基于异常的循环模式,与这个 BUG 相关的异常就会被捕捉到,并且被错误地解释为正常的循环终止条件。

  这个例子的教训很简单:顾名思义,异常应该只用于异常的情况下;它们永远不应该用于控制正常的【代码执行】流程 。更一般地说,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。即使真的能够改进性能,面对平台实现的不断改进,这种模式的性能优势也不可能一直保持。然而,由这种过度聪明的模式带来的微妙的 BUG,以及维护的痛苦却依然存在。

  这条原则对于 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();
    ...
}

  如果 Iterable 缺少 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。

  以下是一些指导原则,可以帮助你在“状态测试方法”、option 或可识别的返回值之间进行选择。如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,必须使用 option 或可识别的返回值,因为在调用“状态测试”方法和调用对应的“状态相关”方法的时间间隔中,对象的状态可能会发生变化。如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就应该使用 optional 或者可被识别的返回值。如果其他方面都是等同的,那么“状态测试”则略优于可别识别的返回值。它提供了更好的可读性,对于使用不当的情形,可能更加易于检测和改正:如果忘了去调用状态测试的方法,状态相关的方法就会抛出异常,使这个 BUG 变得很明显;如果忘了去检查可识别的返回值,这个 BUG 就很难会被发现。对于返回 optional,这不是问题【意思就是,如果返回 optional 就没有上面那些问题。OS:optianal 大法好!】。

  总而言之,异常(exception)是为了在异常情况下使用而设计的。不要将它们用于普通的控制流,也不要编写迫使它们这么做的 API。

其他
1
https://gitee.com/lin-mt/effective-java-third-edition.git
git@gitee.com:lin-mt/effective-java-third-edition.git
lin-mt
effective-java-third-edition
effective-java-third-edition
master

搜索帮助