在现代的JVM实现上,基于异常的模式比标准模式要慢得多。在我的机器上对于一个有100个元素的数组,基于异常的模式比标准满了2倍。
基于异常的循环模式不仅模糊了代码的意图,降低了他的性能,而且他还不能保证正常工作!如果出现了不相关的Bug,这个模式会悄悄地失效,从而掩盖了这个Bug,极大的增加了调试过程的复杂度。假设循环体中的计算过程调用一个方法,这个方法执行了对某个不相关数组的越界访问。如果使用合理的循环模式,这个Bug会产生未被捕捉的异常,从而导致线程立即结束,产生完整的堆栈轨迹。如果使用这个误导的基于异常的循环模式,与这个Bug相关的异常将会被捕捉到,并且被错误的解释为正常的循环终止条件。
这个例子的教训很简单:顾名思义,异常应该只用于异常的情况下,他们永远不应该用于正常的控制流。更一般的,应该优先使用标准的、容易理解的模式,而不是那些声称可以提供更好性能的、弄巧成拙的方法,即使真的能够改进性能,面对平台实现的不断改进,这种模式的性能优势也不可能一直保持。然而,由这种过渡聪明的模式带来的微妙的Bug,以及维护的痛苦却依然存在。
这条原则对于API设计也有启发。设计良好的API不应该强迫他的客户端为了正常的控制流而使用异常。如果类具有“状态相关”的方法,即只有在特定的不可预知的条件下才可以被调用的方法,这个类往往也应该有一个单独的“状态测试”方法,即指示是否可以调用这个状态的方法。例如Iterator接口有一个“状态相关”的next方法和相应的状态测试方法hasNext方法。
另一种提供单独的状态测试方法的做法是,如果“状态相关的”方法被调用时,该对象处于不适当的状态之中,他就会返回一个可识别的值,比如null。这种方法对于Iterator而言并不合适,因为null是next方法的合法返回值。
对于“状态测试方法”和“可识别的返回值”这两种做法,有些指导原则可以帮助你在两者之中做出选择。如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,使用可被是别的返回值可能是很有必要的,因为在调用“状态测试”方法和调用对应“状态相关”方法的时间间隔之中,对象的状态有可能会发生改变。如果单独的“状态测试”方法必须重复“状态相关”方法的工作,从性能的角度考虑,就应该使用可被识别的返回值。如果所有其他方面都是等同的,那么“状态测试”方法则略优于可被识别的返回值。他提供了更好的可读性,对于使用不当的情形,可能更加易于检测和改正:如果忘了去调用状态测试方法,状态相关的方法就会抛出异常,使这个Bug变得很明显;如果忘了去检查可识别的返回值,这个Bug就很难会被发现。
总而言之,异常是为了在异常情况下使用而设计的。不要将他们用于普通的控制流,也不要编写迫使他们这么做的API。