第69项:只针对异常的情况才使用异常

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

// 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,那么使用异常控制流【应该就是一直catch异常的代码】会掩盖【某些】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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值