第59条:避免不必要地使用受检的异常

受检的异常是Java程序设计语言的一项很好的特性。与返回代码不同,它们强迫程序员处理异常的条件,大大增强了可靠性。也就是说,过分使用受检的异常会使API使用起来非常不方便。如果方法抛出一个或者多个受检的异常,调用该方法的代码就必须在一个或者多个catch块中处理这些异常,或者它必须声明它抛出这些异常,并让它们传播出去。无论哪种方法,都给程序员增添了不可忽视的负担。

如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序员可以立即采取有用的动作,这种负担就被认为是正当的。除非这两个条件都成立,否则更适合于使用未受检的异常。作为一个“石蕊”测试(指简单而具有决定性的测试),你可以试着问自己:程序员将如何处理该异常。下面的做法是最好的吗?

} catch(TheCheckedExceptione){

          throw new AssertionError();        //Can’t happen!

}

下面这种做法如何?'

} catch(TheCheckedException e) {

          e.printStackTrace();          // Oh well, we lose.

          System.exit(1);

}

如果使用API的程序员无法做得比这更好,那么未受检的异常可能更为合适。这种例子就是CloneNotSupportedException。它是被Object.clone抛出来的,而Object.clone应该只是在实现了Cloneable的对象上才可以被调用(见第11条:谨慎地覆盖clone)。在实践中,catch块几乎总是具有断言(assertion)失败的特征。异常受检的本质并没有为程序员提供任何好处,它反而需要付出努力,还使程序更为复杂。

被一个方法单独抛出的受检异常,会给程序员带来非常高的额外负担。如果这个方法还有其他的受检异常,它被调用的时候一定已经出现在一个try块中,所以这个异常只需要另外一个catch块。如果方法只抛出单个受检的异常,仅仅一个异常就会导致该方法不得不外于try块中。在这些情况下,应该问自己,是否有别的途径来避免使用受检的异常。

“把受检的异常变成未受检的异常”的一种方法是,把这个抛出异常的方法分成两个方法,其中第一个方法返回一个boolean,表明是否应该抛出异常。这种API重构,把下面的调用序列:

//Invocation with checked exception

try {

     obj.action(args);

} catch(TheCheckedException e) {

      // Handle exceptional condition

}

重构为:

// Invocation with state-testing method and unchecked exception

if (obj.actionPermitted(args))  { 

      obj.action(args);.

} else {

       // Handle exceptional condition

}

这种重构并不总是恰当的,但是,凡是在恰当的地方,它都会使API用起来更加舒服。虽然后者的调用序列没有前者的漂亮,但是这样得到的API更加灵活。如果程序员知道调用将会成功,或者不介意由于调用失败而导致的线程终止,这种重构还允许以下这个更为简单的调用形式:

        obj.action(args);

如果你怀疑这个简单的调用序列是否合乎要求,这个API重构就是恰当的。这种重构之后的API在本质上等同于第57条中的“状态测试方法”,并且,同样的告诫依然适用:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在actionPermitted和action这两个调用的时间间隔之中,对象的状态有可能会发生变化。如果单独的actionPermitted方法必须重复action方法的工作,出于性能的考虑,这种API重构就不值得去做。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容