覆蓋一個方法時,只能產生已在方法的基礎類版本中定義的違例。這是一個重要的限制,因為它意味著與基礎類協同工作的代碼也會自動應用於從基礎類衍生的任何對象(當然,這屬於基本的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++實際上能夠在編譯期間檢查違例規范。
我們必須認識到這一點:盡管違例規范是由編譯器在繼承期間強行遵守的,但違例規范並不屬於方法類型的一部分,後者僅包括了方法名以及自變量類型。因此,我們不可在違例規范的基礎上覆蓋方法。除此以外,盡管違例規范存在於一個方法的基礎類版本中,但並不表示它必須在方法的衍生類版本中存在。這與方法的“繼承”頗有不同(進行繼承時,基礎類中的方法也必須在衍生類中存在)。換言之,用於一個特定方法的“違例規范接口”可能在繼承和覆蓋時變得更“窄”,但它不會變得更“寬”——這與繼承時的類接口規則是正好相反的。