# 9.5 异常的限制
覆盖一个方法时,只能产生已在方法的基类版本中定义的异常。这是一个重要的限制,因为它意味着与基类协同工作的代码也会自动应用于从基类派生的任何对象(当然,这属于基本的OOP概念),其中包括异常。
下面这个例子演示了强加在异常身上的限制类型(在编译期):
//: StormyInning.java
// Overridden methods may throw only the
// exceptions specified in their base-class
// versions, or exceptions derived from the
// base-class exceptions.
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
abstract class Inning {
Inning() throws BaseballException {}
void event () throws BaseballException {
// Doesn't actually have to throw anything
}
abstract void atBat() throws Strike, Foul;
void walk() {} // Throws nothing
}
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
interface Storm {
void event() throws RainedOut;
void rainHard() throws RainedOut;
}
public class StormyInning extends Inning
implements Storm {
// OK to add new exceptions for constructors,
// but you must deal with the base constructor
// exceptions:
StormyInning() throws RainedOut,
BaseballException {}
StormyInning(String s) throws Foul,
BaseballException {}
// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
// Interface CANNOT add exceptions to existing
// methods from the base class:
//! public void event() throws RainedOut {}
// If the method doesn't already exist in the
// base class, the exception is OK:
public void rainHard() throws RainedOut {}
// You can choose to not throw any exceptions,
// even if base version does:
public void event() {}
// Overridden methods can throw
// inherited exceptions:
void atBat() throws PopFoul {}
public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch(PopFoul e) {
} catch(RainedOut e) {
} catch(BaseballException e) {}
// Strike not thrown in derived version.
try {
// What happens if you upcast?
Inning i = new StormyInning();
i.atBat();
// You must catch the exceptions from the
// base-class version of the method:
} catch(Strike e) {
} catch(Foul e) {
} catch(RainedOut e) {
} catch(BaseballException e) {}
}
} ///:~
在Inning
中,可以看到无论构造器还是event()
方法都指出自己会“抛”出一个异常,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何异常。同样的道理也适用于abstract
方法,就象在atBat()
里展示的那样。
interface Storm
非常有趣,因为它包含了在Incoming
中定义的一个方法——event()
,以及不是在其中定义的一个方法。这两个方法都会“抛”出一个新的异常类型:RainedOut
。当执行到StormyInning extends
和implements Storm
的时候,可以看到Storm
中的event()
方法不能改变Inning
中的event()
的异常接口。同样地,这种设计是十分合理的;否则的话,当我们操作基类时,便根本无法知道自己捕获的是否正确的东西。当然,假如interface
中定义的一个方法不在基类里,比如rainHard()
,它产生异常时就没什么问题。
对异常的限制并不适用于构造器。在StormyInning
中,我们可看到一个构造器能够“抛”出它希望的任何东西,无论基类构造器“抛”出什么。然而,由于必须坚持按某种方式调用基类构造器(在这里,会自动调用默认构造器),所以派生类构造器必须在自己的异常规范中声明所有基类构造器异常。
StormyInning.walk()
不会编译的原因是它“抛”出了一个异常,而Inning.walk()
却不会“抛”出。若允许这种情况发生,就可让自己的代码调用Inning.walk()
,而且它不必控制任何异常。但在以后替换从Inning
派生的一个类的对象时,异常就会“抛”出,造成代码执行的中断。通过强迫派生类方法遵守基类方法的异常规范,对象的替换可保持连贯性。
覆盖过的event()
方法向我们显示出一个方法的派生类版本可以不产生任何异常——即便基类版本要产生异常。同样地,这样做是必要的,因为它不会中断那些已假定基类版本会产生异常的代码。差不多的道理亦适用于atBat()
,它会“抛”出PopFoul
——从Foul
派生出来的一个异常,而Foul
异常是由atBat()
的基类版本产生的。这样一来,假如有人在自己的代码里操作Inning
,同时调用了atBat()
,就必须捕获Foul
异常。由于PopFoul
是从Foul
派生的,所以异常控制器(模块)也会捕获PopFoul
。
最后一个有趣的地方在main()
内部。在这个地方,假如我们明确操作一个StormyInning
对象,编译器就会强迫我们只捕获特定于那个类的异常。但假如我们向上转换到基类型,编译器就会强迫我们捕获针对基类的异常。通过所有这些限制,异常控制代码的“健壮”程度获得了大幅度改善(注释③)。
③:ANSI/ISO C++施加了类似的限制,要求派生方法异常与基类方法抛出的异常相同,或者从后者派生。在这种情况下,C++实际上能够在编译期间检查异常规范。
我们必须认识到这一点:尽管异常规范是由编译器在继承期间强行遵守的,但异常规范并不属于方法类型的一部分,后者仅包括了方法名以及参数类型。因此,我们不可在异常规范的基础上覆盖方法。除此以外,尽管异常规范存在于一个方法的基类版本中,但并不表示它必须在方法的派生类版本中存在。这与方法的“继承”颇有不同(进行继承时,基类中的方法也必须在派生类中存在)。换言之,用于一个特定方法的“异常规范接口”可能在继承和覆盖时变得更“窄”,但它不会变得更“宽”——这与继承时的类接口规则是正好相反的。