第9章 异常
如果很好地使用异常,异常可改善程序的可读性、可靠性及可维护性,如果使用不当,则会取得相反的效果。本章提供有效使用异常的指南。
Effective Java(2nd Edition) Item 57 仅为例外条件使用异常(译文)
某一天,如果你不幸碰到下面这样一段代码,可能会很迷惑:
// Horrible abuse of exceptions. Don't ever do this!
try {
int i = 0;
while(true)
range[i++].climb();
} catch(ArrayIndexOutOfBoundsException e) {
}
这段代码要干什么?审视代码,其意图绝不一目了然,就凭这点,就有了充分理由不写样的代码(Item 58)。结果这是在数组上循环的极为病态的惯用法。在数组元素第一次越过数组上界时,通过抛出、捕获、忽略ArrayIndexOutofBoundsException来终止一个无穷循环。它被认为与在数组上循环的惯用法相同,这个惯用法只要是Java程序员就能一眼明知:
for (Mountain m : range)
m.climb();
哪为什么还有人认为基于异常的循环优于被证明良好的循环呢?因为这些人被一个错误的理由误导,误以为这样做可以改善性能,能改善性能是因为VM在每一次访问数组时都会检查数组上限,因此正常的循环测试--在for-each循环中,编译器隐藏了这个测试,但仍存在--是多余的,应该避免。这个理由有三处错误:
• 因为设计异常是用于例外情形,所以异常与JVM的设计者使得JVM越来越快没有关系。
• 将代码放在try-catch中反而阻止了现代JVM可能作的优化。
• 数组上for循环的标准用法未必带来多余的检查,现代的JVM实现已作了优化。
事实上,在现代的JVM实现上,基于异常的用法要比标准用法慢得多,在我的机器上,对有一百个元素的数组,基于异常的用法要比标准用法慢两倍。
基于异常的用法不仅混乱了代码,降低了性能,而且还不能保证它正常运行!若出现了一个无关的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方法,客户就不得不这样做:
// 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。
除了提供独立的状态测试方法外,还可让状态依赖方法返回如null这样的特殊值,以表明对象处于非正常状态。该技术不适合于Iterator,因为null是hasNext的一个合法的返回值。
下面可帮助你选择用状态测试方法还是用返回特殊值方法。如果没有使用外步同步并发访问对象,或对象会随外部而改变状态,则必须使用返回特殊值方法,这是因为在调用状态测试方法与状态依赖方法之间对象的状态可能会改变。如果状态测试方法重复了状态依赖方法的工作,哪么从性能方面考虑应使用特殊值返回方法。所有其它都是一样的,状态测试方法稍稍优于返回特殊值方法。因为状态测试方法可读性更好些,易于发现错误使用:如果你忘了调用测度试方法,状态依赖方法会抛出异常,这使bug变为明显。而如果忘了检查返回的特殊值,bug可能是微妙的。
总之,异常设计用于例外条件。请不要将它们用于控制流,也不要写让他人不得不将异常用于控制流的API。