thinkinginjava學習筆記07_多態

上一節的學習中,強調繼承通常在須要向上轉型時纔有必要上場,不然都應該謹慎使用;html

向上轉型和綁定

向上轉型是指子類向基類轉型,因爲子類擁有基類中的全部接口,因此向上轉型的過程是安全無損的,全部對基類進行的操做均可以一樣做用於子類;如示例代碼中,Music.tune方法調用時,須要的參數是基類Instrument,而傳入一個子類:Wind類的對象時,該方法同樣能夠被調用,而且play方法執行的是Wind類的對象重載的方法;java

在向上轉型的設計中,只編寫和基類打交道的代碼,這樣全部的特定子類均可以正確使用該方法,而不用針對每個子類都編寫特定的代碼;在一個經典的例子中(示例代碼):一個基類Shape派生出不少子類(各類形狀),每一個子類都對基類的接口進行了重載,工廠類在生成子類時,返回的是基類對象(準確的說是子類對象實體的基類對象引用),而建立的實際對象則是子類對象;由工廠建立的對象調用重載方法時,依然能夠正確調用子類重載的方法;這是因爲Java中,除了static和final(包括private),全部的對象都是後期綁定,也就是在編譯時,執行的方法並不知道執行主體的真正類型,只有當執行的時候纔會肯定該對象的真正類型,雖然工廠建立的是基類對象引用,當調用該對象的方法時,因爲該引用指向的實體子類對象會和該方法完成綁定,並被解釋器解釋;git

缺陷

可是,因爲static和final(包括private)方法是前期綁定的,也就是在編譯時,方法就和類型進行了綁定,只能調用綁定類型的方法;如在示例代碼中,PrivateOverride po = new Derived();雖然子類Derived中有新的方法:f(),可是因爲基類中的f()是private,對子類是不可見的,全部子類並無實現重載,而只是寫了一個同名的方法而已;因爲PrivateOverride類中,f()方法是private,在編譯時,po.f();調用一個基類引用的private方法,編譯器執行了基類對象和f()方法的綁定,因此雖然對象實體是Derived對象,可是執行po.f()時,仍然執行的是基類中的方法;github

這裏就會有一個陷阱,因爲private是對使用者不可見的,因此並不能知道繼承的基類中是否有某種private方法,若是在子類中實現同名方法,而且使用基類引用來引用子類對象實體的話,子類中實現的同名方法就不會獲得正確調用;安全

除了private,類似的問題出如今對靜態方法和域(數據對象)的訪問時,如示例代碼中,基類引用sup和子類引用sub,雖然對象實體都是Sub對象,可是兩個引用訪問獲得的field倒是徹底不一樣的;而且sub引用的對象其實包含了兩個成爲field的域,可是直接調用時的默認field是Sub版本中的field,若是想要調用Super版本中的field,則必須顯式地使用super.field調用;可是域的訪問通常不會出錯,由於一般都會將域設爲private,此時,sup引用的對象實體是Sub,並不能完成對private field的調用,只能經過getField方法進行獲取,而因爲getField方法並非final的,此時就避免了該問題;app

而靜態方法則因爲是隻和類相關聯,因此也並不具有多態性;ide

構造器和多態

經過一個例子來複習繼承中構造器的執行順序以及潛在的問題:示例代碼;輸出結果爲:學習

Glyph() before draw()spa

RoundGlyph.draw(), radius = 0設計

Glyph() after draw()

RoundGlyph.RoundGlyph(), radius = 5

 

執行new RoundGlyph(5);時,初始化過程爲:

1. 在任何事物發生以前,將分配給對象的存儲空間初始化爲二進制0;(較以前新增);

2. 加載基類Glyph,而且Glyph並無其餘基類,執行Glyph的static初始化(並無),執行Glyph構造器;打印

Glyph() before draw()

調用draw()方法,此時因爲RoundGlyph中對draw()進行了重載,因此將調用重載的draw()方法,此時RoundGlyph並無完成初始化,因爲第一步分配,radius=0,故打印:

RoundGlyph.draw(), radius = 0

而後打印:

Glyph() after draw()

3. 加載子類RoundGlyph,執行static初始化,radius=1,執行RoundGlyph構造器,並打印

RoundGlyph.RoundGlyph(), radius = 5

在這個過程當中,在構造器中添加了後期綁定的方法:draw(),致使執行出現邏輯上的問題,由上例能夠總結出構造器的編寫準則:

用盡量簡單的方法使對象進入正常狀態;若是能夠的話,避免使用其餘方法;構造器中惟一能夠調用的方法是基類中的final方法;

協變返回類型

在Java SE5以後,子類中的重載方法能夠返回基類方法的返回類型的某種子類;如:示例代碼中:

Mill m = new Mill();

Grain g = m.process();

println(g);

m = new WheatMill();

g = m.process();

println(g);

因爲m是一個Mill類的引用,m = new WheatMill()表示一個Mill的引用引用了WheatMill對象(向上轉型),而process()方法實現了重載,而在JavaSE5中添加的規則,該方法能夠返回爲Grain的子類Wheat,所以,m.process()返回的是一個Wheat對象實體,用一個Grain類的引用g來引用該實體;

而在這以前,m.process()的返回值將強制返回爲Grain,而不能返回爲Wheat;

繼承設計

這個問題在前一篇隨筆中也有提到,這設計時,應該優先使用組合的方式,而繼承應該在須要被向上轉型時用到;一條通用準則是:用繼承來表達行爲之間的差別,並用字段(即組合)來表達狀態上的變化;如:示例代碼中,Stage類中包含一個基類Actor的引用,並初始化爲HappyActor,可是change方法能夠改變該引用指向的具體對象實體,好比程序中將其改變爲Actor的另一個子類:SadActor中,此時就完成了狀態的變化,Stage類的實例化對象相應的行爲都統一作了改變;

(很cool!)

在繼承設計時,只繼承基類中的已有的方法,這樣作能夠避免一些繼承帶來的問題,而把子類看作是基類的一個替代(is-a關係),兩者具備徹底相同的接口;

可是在實際設計時,擴展接口是難以免的,此時,子類中不只僅有基本接口,還有一些額外方法實現的其餘特性(is-like-a關係);此時,子類中的擴展部分並不能被基類訪問,而且一旦完成向上轉型,則不能繼續調用擴展部分,如示例代碼中,x[1].u()會產生錯誤;從這裏也能夠看到,雖然x[1]的對象實體是MoreUseful,而且執行重載方法時,方法是和MoreUseful對象完成後期綁定,可是x[1]仍然是一個Useful的引用,並不能調用任何Useful類接口以外的方法,不然將會產生類轉型異常;

相關文章
相關標籤/搜索