某一天,如果你不走运的话,可能会碰到下面这样的代码:
try{
int i = 0;
while(true){
range[i++].climb();
}
}catch(ArrayIndexOutOfBoundException e){
}
这段代码有什么用?看起来根本不明显,这正是他没有真正被使用的原因。事实证明,作为一个要对数组元素进行访问遍历的实现方式,它的构想是非常拙劣的。当这个循环企图访问数组边界之外的第一个数组元素时,用抛出(throw)、捕获(catch)、忽略ArrayIndexOutOfBoundException的手段来达到终止无限循环的目的。假定它与数组循环的标准模式是等价的,对于任何一个Java程序员来说,下面的标准模式一看就会明白:
for(Mountaion m: range)
m.climb();
那么为什么会有人优先使用基于异常的循环,而不是用行之有效的模式呢?这是被误导了,他们企图利用Java的错误判断机制来提高性能,因为VM对每次数组访问都要检查越界情况,所以它们认为正常的循环终止测试被编译器隐藏了,但在for-each循环中任然可见,这无疑是多余的,应该避免。这种想法有三个错误:
- 因为异常机制的设计初衷适用于不正常的情形,所以几乎没有JVM实现试图对他们进行优化,使他们与显式的测试一样快速。
- 把代码凡在try-catch快中反而阻止了现代JVM实现本来可能要执行的某些特定优化。
- 对数组进行遍历的标准模式不会导致冗余的检测。有些现代的JVM实现会将它们优化掉。
实际上,基于异常的模式比标准的模式要慢得多。在我的机器上,对于一个有100个元素的数组,基于标准的模式比异常的模式快了2倍。
基于异常的循环模式不仅模糊了代码的意图,降低了它的性能,而且它还能保证正常工作!如果处相连不相关的bug,这个模式会悄悄地失效,从而掩盖了这个bug,极大地增加了调试过程的复杂性。假设循环体重的计算过程调用了一个方法,这个方法执行了对某个不相关数组的越界访问。如果使用合理的循环模式,这个bug会产生未被捕捉的异常,从而导致线程立即结束,产生完整的堆栈轨迹。如果使用这个被误导的基于异常的循环模式,与这个bug相关的异常将会捕捉到,并且被错误的解释为正常的循环终止条件。
这个例子的教训很简单:顾名思义,异常应该只用于异常的情况下;它们永远不应该用于正常的控制流。一般的,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法。即使真的能够改进性能,,面对平台实现的不断改进,这种模式的性能又是也不可能一直保持。然而,由这种过度聪明的模式带来的微妙的bug,以及维护的痛苦却依然存在。
这条原则对于API设计也有启发。设计良好的API不应该强迫它的客户端为了正常的控制流而使用异常。如果具有“状态相关”的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有个单独的“状态测试”方法。例如,Iterator接口有一个“状态相关”的next方法,及相应的状态测试方法hasNext。这使得利用传统的for循环(已经for-each循环,在内部使用了hasNext方法)对集合进行迭代的标准模式成为可能:
for(Iterator<Foo> i = collection.iterator(); i.hasNext();){
Foo foo = i.next();
}
如果Iterator缺少hasNext方法,客户端将被迫改用下面的做法:
try{
Iterator<Foo> i = collection.iterator();
while(true)
Foo foo = i.next();
}catch(NoSuchElementExcepion e){
}
这应该非常类似于本条目刚开始对数组进行迭代的例子。除了代码烦琐且令人误解之外,这个基于异常的模式可能执行起来也比标准模式更差,并且还可能掩盖系统中其他不相关部分中的bug。
另一种提供单独的状态测试方法的做法是,如果“状态相关的”方法无法执行想要的计算,就让它返回一个零长度的optional值,或者返回一个可识别的值,比如null。
对于“状态测试方法”和“optional返回值或者可识别的返回值”这两种做法,有些指导原则可以帮助你在这两者之中做出选择。如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,就必须用optional返回值或者可被识别的返回值,因为在调用“状态测试”方法和调用对应的“状态相关”方法的时间间隔之中,对象的状态有可能发生变化。如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就应该使用可被识别的放回值。如果所有其他方面都是等同的,那么“状态测试”方法则略优于可被识别的返回值。它提供了稍微更好的可读性,对于使用不当的情形可能更加易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使这个bug变的很明显;如果忘了去检测可识别的返回值,这个bug就很难被发现。optional返回值不会有这方面的问题。
总而言之,异常是为了在异常情况下使用而设计的。不要将它们用于普通的控制流,也不要编写迫使它们这么做的API。