Java程序设计语言提供了三种可抛出结构:受检异常、运行时异常和错误。关于什么时候适合使用哪些可抛出结构,程序员中间存在一些困惑。虽然这项决定并不总是那么清晰,但还是有些一般性的原则提出了强有力的指导。
在决定使用受检异常或是未受检异常时,主要的原则是:如果期望调用者能够适当的恢复,对于这种情况就应该使用受检异常
。通过抛出受检的异常,强迫调用者在一个catch子句中处理该异常,或者将它传播出去。因此,方法中声明要抛出的每个受检异常,都是对API用户的一种潜在指示:与异常相关联的条件是调用这个方法的一种可能的结果。
API的设计者让API用户面对受检异常,以此强制用户从这个异常条件中恢复。用户可以忽视这样的强制要求,只需要捕获异常并忽略即可,但这往往不是好办法(详见第77条)。
有两种未受检的可抛出结构:运行时异常和错误。在行为上两者是等同的:它们都是不需要也不应该被捕获的可抛出结构。如果程序抛出未受检的异常或错误,往往就是属于不可恢复的情形,继续执行下去有害无益。如果程序没有捕捉到这样的可抛出结构,将会导致当前线程中端,并出现适当的错误消息。
用运行时异常来表明编程错误
。大多数的运行时异常都表示前提违例。所谓前提违例是指API的客户没有遵守API规范建立的约定。例如,数组访问的约定指明了数组的下标值必须在零和数组长度减1之间。ArrayIndexOutOfBoundsException表明违反了这个前提。
这条建议有一个问题:对于要处理可恢复的条件,还是处理编程错误,情况并非总是那么黑白分明。例如,考虑资源枯竭的情形,这可能是由于程序错误引起的,比如分配了一块不合理的过大的数组,也可能确实是由于资源不足而引起的。如果资源枯竭是由于临时的短缺,或是临时需求太大所造成的,这种情况可能就是可恢复的。API设计者需要判断这样的资源枯竭是否允许恢复。如果你相信一种情况可能允许恢复,就使用受检的异常;如果不是,则使用运行时异常。如果不清楚是否可能恢复,最好使用未受检的异常,原因请参考第71条的讨论。
虽然JLS(Java语言规范)并没有要求,但是按照惯例,错误往往被JVM保留下来使用,以表明资源不足、约束失败,或者其他使用程序无法继续执行的条件。由于这已经是个几乎被普遍接受的惯例,因此最好不要再实现任何新的Error子类。因此,你实现的所有未受检的抛出结构都应该是RuntimeException的子类
(直接的或者间接的)。不仅不应该定义Error子类,甚至也不应该抛出AssertionError异常。
要想定义一个抛出结构,使它不是Exception、RuntimeException或Error的子类,这也是可能的。JLS并没有直接规定这样的抛出结构,而是隐式的指定了:从行为意义上讲,它们等同于普通的受检异常(即Exception的子类,但不是RuntimeException的子类)。那么,什么时候应该使用这样的抛出结构呢?一句话:永远也不会用到。它与普通的受检异常相比没有任何益处,只会困扰API的用户。
API的设计者往往会忘记,异常也是个完全意义上的对象,可以在它上面定义任意的方法。这些方法的主要用途是为了捕获异常的代码而提供额外的信息,特别是关于引发这个异常条件的信息。如果没有这样的方法,程序员必须要懂得如果解析“该异常的字符串表示法”,以便获得这些额外信息。这是极为不好的做法(详见第12条)。类很少会指定它们的字符串表示法中的细节,因此,对于不同的实现及不同的版本,字符串表示法大相径庭。由此可见,“解析异常的字符串表示法”的代码可能是不可移植的,也是非常脆弱的。
因为受检异常往往指明了可恢复的条件,所以,对于这样的异常,提供一些辅组方法尤其重要,通过这些方法,调用者可以获得一些有助于恢复的信息。例如,假设因为用户资金不足,当他企图购买一张礼品卡时导致失败,于是抛出一个受检的异常。这个异常应该提供一个访问方法,以便允许客户查询所缺的费用金额,使得调用者可以将这个数值传递给用户。关于这个主题的更多详情,请参阅第75条。
总而言之,对于可恢复的情况,要抛出受检异常;对于程序错误,要抛出运行时异常。不确定是否可恢复,则抛出未受检异常。不要定义任何既不是受检异常也不是运行时异常的抛出类型。要在受检异常上提供方法,以便协助恢复。