很多Java程序猿不喜欢受检异常,但是使用得当的话,它们可以改善API和程序。与返回代码和未受检异常不同,它们强迫程序猿处理问题,提高了可靠性。也就是说,过分使用受检的异常会使API使用起来非常不方便。如果方法抛出一个或者多个受检的异常,调用该方法的代码就必须在一个或者多个catch块中处理这些异常,或者它必须声明它抛出这些异常,并让它们传播出去。无论哪种方法,它都会给使用API的用户带来负担。Java 8 中增加这种负担,因为抛出受检异常的方法无法直接在流中使用(第45-48项)。
如果正确地使用API并不能阻止这种异常条件的产生,并且一旦产生异常,使用API的程序猿可以立即采取有用的操作,这种负担就被认为是合理的。除非这两个条件都成立,否则更适合使用未受检的异常。作为一个“石蕊试纸”测试【简单而具有决定性的测试】,你可以试着问自己:程序猿将如何处理该异常。下面的做法是最好的吗?
} catch (TheCheckedException e) {
throw new AssertionError(); // Can't happen!
}
或者下面这个?
} catch (TheCheckedException e) {
e.printStackTrace(); // Oh well, we lose.
System.exit(1);
}
如果程序猿无法做得更好,就调用未受检的异常。
如果一个受检异常是由方法抛出的唯一受检异常,会给程序猿带来非常高的额外负担。如果这个方法还有其他的受检异常,则该方法必须出现在try块中,并且该异常最多需要另一个catch块。如果方法抛出单个受检异常,则此异常是该方法必须出现在try块中且不能直接在流中使用的唯一原因。在这种情况下,问问自己是否有办法避免受检异常,这么做是值得的。
消除已检查异常的最简单方法是返回所需结果类型的optional(第55项)。该方法只返回一个空的optional,而不是抛出一个受检异常。该技巧的缺点在于该方法不能返回任何附加信息来详细说明其无法执行所需要的计算。相反,异常类型具有描述性,并且可以为导出的方法提供附加信息(第70项)。
你也可以把受检异常变成未受检异常,这种做法就是,把这个抛出异常的方法分成两个方法,其中一个方法返回一个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在本质上等同于第69项中的“状态测试方法”,并且,同样的告诫依然适用:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在actionPermitted和action这两个调用的时间间隔之中,对象的状态有可能会发生变化。如果单独的actionPermitted方法必须重复action方法的工作,出于性能的考虑,这种API重构就不值得去做。
总之,在谨慎使用时,受检异常可以提高程序的可靠性; 当过度使用时,它们会使API难以使用。如果调用者无法从失败的执行情况恢复,则抛出未受检异常。如果可能恢复【程序正常状态】并且你希望强制调用者处理异常情况,请首先考虑返回optional。只有在失败的情况下提供的信息不足时才应该抛出一个检查过的异常。