多態(tài) 是繼 數(shù)據(jù)抽象 和 繼承 之后的第三種基本特征飞醉。
多態(tài)也稱(chēng)作動(dòng)態(tài)綁定、后期綁定或運(yùn)行時(shí)綁定屯阀。
多態(tài)的一些具象表現(xiàn)缅帘。
- 允許不同類(lèi)的對(duì)象對(duì)同一消息做出響應(yīng)
- 同一個(gè)行為具有多個(gè)不同表現(xiàn)形式或形態(tài)的能力
- 只有在運(yùn)行時(shí)才會(huì)知道引用變量所指向的具體實(shí)例對(duì)象
封裝:通過(guò)合并特征和行為創(chuàng)建新的數(shù)據(jù)類(lèi)型。
實(shí)現(xiàn)隱藏:通過(guò)將細(xì)節(jié)私有化难衰,把接口和實(shí)現(xiàn)分離開(kāi)來(lái)
多態(tài):消除類(lèi)型之間的耦合關(guān)系钦无。
1. 再論向上轉(zhuǎn)型
上一章中我們提到了,對(duì)象及可以作為自己本身的類(lèi)型使用盖袭,也可以作為ita的基類(lèi)型來(lái)使用失暂。而這種把對(duì)某個(gè)對(duì)象的引用視為對(duì)其基類(lèi)型的引用的做法被稱(chēng)作 向上轉(zhuǎn)型彼宠。
我們看下面這個(gè)例子:
class Instrument {
public void play(String song){
System.out.println("Instrument.play() " + song);
}
}
class Piano extends Instrument{
//重寫(xiě)方法
public void play(String song){
System.out.println("Piano.play() " + song);
}
}
public class Music {
public static void tune(Instrument i){
//...
i.play("Fade");
}
public static void main(String[] args){
Piano piano = new Piano;
tune(piano);
}
}
//輸出結(jié)果為 Piano.play() Fade
我們通過(guò)向上轉(zhuǎn)型,將子類(lèi) Paino 類(lèi)型的引用傳入了接收 父類(lèi) Instrument 類(lèi)型引用參數(shù)的 tune() 方法中弟塞,這是沒(méi)有什么問(wèn)題的凭峡,不過(guò)有一個(gè)疑問(wèn):為什么不讓 tune() 方法直接接收一個(gè) Piano 參數(shù)呢?
乍一看下决记,似乎讓 tune() 方法接收一個(gè) Piano 引用作為參數(shù)更符合常理摧冀,但是這樣會(huì)導(dǎo)致一個(gè)嚴(yán)重的問(wèn)題:
- 如果這樣做的話,那么每個(gè) Instrument 的子類(lèi)型都要寫(xiě)一個(gè)新的 tune() 方法
- 這會(huì)造成大量多余的編程
- 同時(shí)霉涨,假如某個(gè)子類(lèi)忘記修改某個(gè)方法按价,編譯器不會(huì)報(bào)任何錯(cuò)誤,此時(shí)會(huì)出現(xiàn)一些隱患
結(jié)論是:這種情況下如果我們只寫(xiě)一個(gè)簡(jiǎn)單方法笙瑟,它僅僅接收基類(lèi)作為參數(shù)楼镐,而不是特殊的導(dǎo)出類(lèi),事情就迎刃而解了往枷。當(dāng)然這正式 多態(tài) 所允許的框产。
但是不能只知其然不知其所以然,下面就仔細(xì)分析一下上述結(jié)論的依據(jù)是什么错洁。
2. 綁定
我們?cè)賮?lái)分析一下 tune() 方法:
public static void tune(Instrument i){
//...
i.play("Fade");
}
前面我們提到 tune() 傳參的時(shí)候秉宿,將 Piano 向上轉(zhuǎn)型作為 Instrument 使用,但與此同時(shí)問(wèn)題出現(xiàn)了:編譯器是如何知道這個(gè) Instrument 引用指向的是 Piano 對(duì)象屯碴?嗯描睦,實(shí)際上,編譯器無(wú)法得知(WTF导而?)忱叭。
為了深入理解這個(gè)問(wèn)題,我們需要研究一下 綁定今艺。
2.1 什么是綁定
綁定 即將一個(gè)方法調(diào)用同一個(gè)方法主題關(guān)聯(lián)起來(lái)
-
若在程序執(zhí)行前進(jìn)行綁定韵丑,叫做 前期綁定
如果有前期綁定的話,是由編譯器和連接程序?qū)崿F(xiàn)
-
如果在運(yùn)行時(shí)根據(jù)對(duì)象的類(lèi)型進(jìn)行綁定虚缎,叫做 后期綁定/動(dòng)態(tài)綁定/運(yùn)行時(shí)綁定
后期綁定使得編譯器一直不知道對(duì)象的類(lèi)型撵彻,但是方法調(diào)用機(jī)制通過(guò)安置的某種“類(lèi)型信息”,能找到正確的方法體实牡,并加以調(diào)用陌僵。
在 Java 中,除了 static 方法和 final 方法以外铲掐,其他所有的方法都是后期綁定拾弃。這點(diǎn)很關(guān)鍵。
關(guān)于把一個(gè)方法聲明為 final 的作用摆霉,在前面一章也提過(guò)了:
- 防止被覆蓋
- 出于性能考慮 -- 有效的關(guān)閉動(dòng)態(tài)綁定
但是應(yīng)該從設(shè)計(jì)方面考慮是否使用 final 方法,而非出于性能的考慮。
2.2 多態(tài)的正確實(shí)踐
從上面分析携栋,我們得知:"Java 中的方法都是通過(guò)動(dòng)態(tài)綁定來(lái)實(shí)現(xiàn)多態(tài)的"搭盾,這樣一來(lái),我們編寫(xiě)代碼時(shí)就只需要和基類(lèi)打交道了婉支,這些代碼自然適用于所有導(dǎo)出類(lèi)鸯隅。換個(gè)說(shuō)法,發(fā)送消息給某個(gè)對(duì)象向挖,讓該對(duì)象去斷定應(yīng)該做什么事蝌以。
我們來(lái)舉個(gè)“幾何形狀”的例子:有一個(gè)基類(lèi) Shape,以及多個(gè)導(dǎo)出類(lèi)何之,如 Circle跟畅、Square、Triangle:
向上轉(zhuǎn)型:Shape s = new Circle()
這里創(chuàng)建了一個(gè) Circle 對(duì)象溶推,并把得到的引用立即賦值給 Shape徊件,能這么做是因?yàn)橥ㄟ^(guò)繼承,Circle 就是一種 Shape蒜危。
繼承表示 is-a 的關(guān)系
此時(shí)如果調(diào)用基類(lèi)方法 s.draw()虱痕,雖然 s 是一個(gè) Shape 引用,但是編譯器實(shí)際上并非調(diào)用 Shape.draw()辐赞,而是由于后期綁定(多態(tài))部翘,會(huì)正確的調(diào)用 Circle.draw() 方法。
在編譯時(shí)响委,編譯器不需要獲得人為添加的任何特殊信息就能進(jìn)行正確的調(diào)用新思。后期綁定 會(huì)替我們進(jìn)行正確調(diào)用。
多態(tài)方法調(diào)用允許一種類(lèi)型表現(xiàn)出與其他相似類(lèi)型之間的區(qū)別晃酒,這種區(qū)別根據(jù)方法行為的不同而表示出來(lái)表牢。
2.3 良好的可擴(kuò)展性
在一個(gè)設(shè)計(jì)良好的 OOP 程序中,大多數(shù)或者所有方法都會(huì)只與基類(lèi)接口通信贝次。這樣的程序是可擴(kuò)展的崔兴,因?yàn)榭梢詮耐ㄓ玫幕?lèi)繼承出新的數(shù)據(jù)類(lèi)型,從而新添一些功能蛔翅,那些操縱基類(lèi)接口的方法不需要任何改動(dòng)就可以應(yīng)用于新類(lèi)敲茄。
回到上面的 “樂(lè)器”(Instrument) 示例。由于多態(tài)機(jī)制山析,我們可根據(jù)自己需求添加任意多的新類(lèi)型堰燎,而無(wú)需改變 tune() 方法。
class Instrument {
void play(Note n) { print("Instrument.play() " + n); }
String what() { return "Instrument"; }
void adjust() { print("Adjusting Instrument"); }
}
class Wind extends Instrument {
void play(Note n) { print("Wind.play() " + n); }
String what() { return "Wind"; }
void adjust() { print("Adjusting Wind"); }
}
class Percussion extends Instrument {
void play(Note n) { print("Percussion.play() " + n); }
String what() { return "Percussion"; }
void adjust() { print("Adjusting Percussion"); }
}
class Stringed extends Instrument {
void play(Note n) { print("Stringed.play() " + n); }
String what() { return "Stringed"; }
void adjust() { print("Adjusting Stringed"); }
}
class Brass extends Wind {
void play(Note n) { print("Brass.play() " + n); }
void adjust() { print("Adjusting Brass"); }
}
class Woodwind extends Wind {
void play(Note n) { print("Woodwind.play() " + n); }
String what() { return "Woodwind"; }
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void tuneAll(Instrument[] e) {
for(Instrument i : e)
tune(i);
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
} /* Output:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
*/
在 main 方法中笋轨,我們將引用至于 orchestra 數(shù)組中秆剪,會(huì)自動(dòng)轉(zhuǎn)型到 Instrument赊淑。通過(guò)多態(tài)機(jī)制,tune() 方法完全忽略了它周?chē)a的全部變化仅讽,依舊運(yùn)行正常陶缺。話句話說(shuō),多態(tài)使得程序員能夠 “將改變的事物和未變的事物分離開(kāi)來(lái)”洁灵。
2.4 多態(tài)的適用范圍
只有普通的方法調(diào)用可以是多態(tài)的饱岸,靜態(tài)方法/域 不具有多態(tài)性。
-
域訪問(wèn)操作會(huì)由編譯器解析徽千,因此不是多態(tài)的
class Super { public int field = 0; public int getField() { return field; } } class Sub extends Super { public int field = 1; public int getField() { return field; } public int getSuperField() { return super.field; } } public class FieldAccess { public static void main(String[] args) { Super sup = new Sub(); // Upcast System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField()); Sub sub = new Sub(); System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField() + ", sub.getSuperField() = " + sub.getSuperField()); } } /* Output: * sup.field = 0, sup.getField() = 1 * sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0 */
上面的例子中苫费,Sub 包含兩個(gè) field(本身的 和 從父類(lèi)Super 繼承來(lái)的),如果需要得到父類(lèi)的的 field双抽,必須顯式指明 Super.field百框。
-
靜態(tài)方法的行為不具有多態(tài)性 -- 靜態(tài)方法是與類(lèi),而非單個(gè)對(duì)象相關(guān)聯(lián)的
class StaticSuper { public static String staticGet() { return "Base staticGet()"; } public String dynamicGet() { return "Base dynamicGet()"; } } class StaticSub extends StaticSuper { public static String staticGet() { return "Derived staticGet()"; } public String dynamicGet() { return "Derived dynamicGet()"; } } public class StaticPolymorphism { public static void main(String[] args) { StaticSuper sup = new StaticSub(); // Upcast System.out.println(sup.staticGet()); System.out.println(sup.dynamicGet()); } } /* Output: Base staticGet() Derived dynamicGet() */
3. 構(gòu)造器和多態(tài)
構(gòu)造器是個(gè)很特殊的方法荠诬,盡管構(gòu)造器不具備多態(tài)性[1]琅翻,但還是有必要理解構(gòu)造器怎樣通過(guò)多態(tài)[2]在復(fù)雜的層次結(jié)構(gòu)中運(yùn)作。
1.構(gòu)造器實(shí)際上是 static 方法柑贞,隱式聲明了 static方椎,因此不支持多態(tài)。
2.指構(gòu)造器內(nèi)部調(diào)用的方法钧嘶,而非構(gòu)造器本身棠众。
3.1 再再論構(gòu)造器的調(diào)用順序
關(guān)于構(gòu)造器的調(diào)用順序在第五章進(jìn)行了簡(jiǎn)要說(shuō)明(最基本),并在第七章再論(加入繼承)有决, 下一節(jié)就再結(jié)合 多態(tài) 來(lái)進(jìn)一步補(bǔ)充闸拿,本節(jié)先來(lái)做一下回顧并討論該順序的意義所在。
Java 之路 (五) -- 初始化和清理(構(gòu)造器與初始化书幕、方法重載新荤、this、垃圾回收器台汇、枚舉類(lèi)型)
Java 之路 (七) -- 復(fù)用類(lèi)(組合苛骨、繼承、代理苟呐、向上轉(zhuǎn)型痒芝、final、再談初始化和類(lèi)的加載)
給出以下例子:
class Meal {
Meal() { print("Meal()"); }
}
class Bread {
Bread() { print("Bread()"); }
}
class Cheese {
Cheese() { print("Cheese()"); }
}
class Lettuce {
Lettuce() { print("Lettuce()"); }
}
class Lunch extends Meal {
Lunch() { print("Lunch()"); }
}
class PortableLunch extends Lunch {
PortableLunch() { print("PortableLunch()");}
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() { print("Sandwich()"); }
public static void main(String[] args) {
new Sandwich();
}
} /* Output:
* Meal()
* Lunch()
* PortableLunch()
* Bread()
* Cheese()
* Lettuce()
* Sandwich()
*/
從結(jié)果中我們可以看出這調(diào)用構(gòu)造器遵循一下的順序:
- 調(diào)用基類(lèi)構(gòu)造器
- 調(diào)用成員的初始化方法
- 調(diào)用導(dǎo)出類(lèi)構(gòu)造器的
這一過(guò)程會(huì)反復(fù)遞歸牵素,首先構(gòu)造根基類(lèi)严衬,然后是下一層導(dǎo)出類(lèi),等等笆呆,知道最底層的導(dǎo)出類(lèi)请琳。
3.1.1 基類(lèi)的構(gòu)造器總是在導(dǎo)出類(lèi)的構(gòu)造器中被調(diào)用
如標(biāo)題所言粱挡,這么做的意義何在?
首先構(gòu)造器擔(dān)負(fù)著檢查對(duì)象是否被正確構(gòu)造的任務(wù)单起,同時(shí)導(dǎo)出類(lèi)只能訪問(wèn)自己的成員抱怔,而不能訪問(wèn)基類(lèi)的成員(基類(lèi)成員通常為 private)劣坊,這就導(dǎo)致了只有基類(lèi)的構(gòu)造器能夠?qū)ψ约旱脑剡M(jìn)行初始化嘀倒。因此,必須令所有構(gòu)造器都得到調(diào)用局冰,否則不可能正確構(gòu)造完整對(duì)象测蘑。
這也是為什么編譯器強(qiáng)制每個(gè)導(dǎo)出類(lèi)構(gòu)造器都必須調(diào)用基類(lèi)構(gòu)造器的原因。
如果導(dǎo)出類(lèi)沒(méi)有明確指定調(diào)用基類(lèi)構(gòu)造器康二,就會(huì)自動(dòng)調(diào)用默認(rèn)構(gòu)造器碳胳;如果不存在默認(rèn)構(gòu)造器,編譯器會(huì)報(bào)錯(cuò)沫勿。
其次挨约,當(dāng)進(jìn)行繼承時(shí),我們獲取了基類(lèi)的一切产雹,并可以訪問(wèn)基類(lèi)中 public 和 protected 的成員诫惭。這就意味著導(dǎo)出類(lèi)中,必須假定基類(lèi)的所有成員都有效蔓挖。通常做法就是在構(gòu)造器內(nèi)部確保所要使用的成員都構(gòu)建完畢夕土,因此,唯一的辦法就是首先調(diào)用基類(lèi)構(gòu)造器瘟判,這樣在進(jìn)入導(dǎo)出類(lèi)構(gòu)造器時(shí)怨绣,積累中可供我們?cè)L問(wèn)的成員就都已被初始化。
3.1.2 清理順序
如果某一子對(duì)象依賴(lài)于其他對(duì)象拷获,銷(xiāo)毀的順序應(yīng)該和初始化順序相反
- 對(duì)于類(lèi)的成員篮撑,意味著與聲明的順序相反,因?yàn)槌蓡T的初始化是按照聲明的順序進(jìn)行的
- 對(duì)于基類(lèi)匆瓜,應(yīng)該首先銷(xiāo)毀其導(dǎo)出類(lèi)赢笨,然后才是基類(lèi)。這是因?yàn)閷?dǎo)出類(lèi)的清理可能會(huì)調(diào)用基類(lèi)的某些方法陕壹,所以需要使基類(lèi)中的構(gòu)建仍起作用质欲。
關(guān)于上述第2條,進(jìn)行補(bǔ)充說(shuō)明:
例子:假定父類(lèi) A 的清理方法為 clean()糠馆,用來(lái)清理 A 中的數(shù)據(jù)嘶伟,然后子類(lèi) B 繼承 A,由于繼承的原因又碌,需要重寫(xiě) clean() 方法九昧,用來(lái)清理 B 中的數(shù)據(jù)绊袋。此時(shí),我們需要 B.clean() 方法中調(diào)用 super.clean()铸鹰。這樣在清理子類(lèi)時(shí)癌别,也會(huì)清理父類(lèi)。反之蹋笼,如果沒(méi)有調(diào)用父類(lèi)的 clean() 方法展姐,父類(lèi)的清理動(dòng)作就不會(huì) 發(fā)生。
將上面的例子一般化:子類(lèi)覆蓋父類(lèi)的某個(gè)方法后(比如為 method())剖毯,如果需要調(diào)用父類(lèi)的 method() 方法圾笨,必須顯式調(diào)用 super.method()。
method() -> 子類(lèi).method()
super.method() -> 父類(lèi).method()
3.2 構(gòu)造器內(nèi)部的多態(tài)方法
雖然前面關(guān)于調(diào)用順序已經(jīng)分析的很清楚了逊谋,但是加入多態(tài)之后擂达,新的問(wèn)題又產(chǎn)生了:如果一個(gè)構(gòu)造器的內(nèi)部 調(diào)用正在構(gòu)建的對(duì)象的某個(gè)動(dòng)態(tài)綁定方法,會(huì)發(fā)生什么情況胶滋?
由于動(dòng)態(tài)綁定的調(diào)用在運(yùn)行時(shí)才決定板鬓,因此對(duì)象無(wú)法知道它是屬于方法所在的那個(gè)類(lèi),還是屬于其導(dǎo)出類(lèi)究恤。如果要調(diào)用構(gòu)造器內(nèi)部的一個(gè)動(dòng)態(tài)綁定方法俭令,那么就要用到那個(gè)方法的被覆蓋之后的定義。但是被覆蓋的方法在對(duì)象被完全構(gòu)造之前就會(huì)被調(diào)用丁溅,這就會(huì)造成一些錯(cuò)誤唤蔗。
對(duì)以上再進(jìn)行補(bǔ)充:任何構(gòu)造器內(nèi)部,整個(gè)對(duì)象可能只是部分形成窟赏,我們只能保證基類(lèi)對(duì)象已經(jīng)進(jìn)行初始化妓柜。如果構(gòu)造器只是在構(gòu)建對(duì)象過(guò)程中的一個(gè)步驟,并且該對(duì)象所屬的類(lèi)是從這個(gè)構(gòu)造器所屬的類(lèi)導(dǎo)出的涯穷,那么導(dǎo)出部分在當(dāng)前構(gòu)造器被調(diào)用的時(shí)刻仍舊是沒(méi)有被初始化的棍掐。然而,一個(gè)動(dòng)態(tài)綁定的方法調(diào)用卻會(huì)向外深入到繼承層次結(jié)構(gòu)內(nèi)部拷况,它可以調(diào)用導(dǎo)出類(lèi)那里的方法作煌。如果我們?cè)跇?gòu)造器內(nèi)部這樣做,那么就可能會(huì)調(diào)用某個(gè)方法赚瘦,而這個(gè)方法所操作的成員可能還未初始化粟誓,這肯定會(huì)招致災(zāi)難。
上面這段話讀起來(lái)可能很拗口起意,舉個(gè)例子:
class Glyph {
void draw() { print("Glyph.draw()"); }
Glyph() {
print("Glyph() before draw()");
draw();
print("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw() {
print("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
} /* Output:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/
可以看到鹰服,RoundGlyph 覆蓋了 draw() 方法,在 Glyph 的構(gòu)造器中調(diào)用 RoundGlyph.draw() 方法時(shí)發(fā)生了錯(cuò)誤:輸出時(shí) radius 的值不是默認(rèn)初始值 1 ,而是 0悲酷。
Glyph 構(gòu)造器調(diào)用 draw() 方法時(shí)套菜,RoundGlyph 的成員還未進(jìn)行初始化。
實(shí)際上一節(jié)的初始化順序并不完整设易。初始化的實(shí)際過(guò)程是:
- 在其他任何事物發(fā)生之前逗柴,將分配給對(duì)象的存儲(chǔ)空間初始化成二進(jìn)制的零。
- 如前所述那樣調(diào)用基類(lèi)構(gòu)造器顿肺。此時(shí)戏溺,調(diào)用被覆蓋后的draw()方法(要在調(diào)用RoundGlyph構(gòu)造器之前調(diào)用),由于步驟 1 的緣故挟冠,我們此時(shí)會(huì)發(fā)現(xiàn) radius 的值為 0于购。
- 按照聲明的順序調(diào)用成員的初始化方法。
- 調(diào)用導(dǎo)出類(lèi)的構(gòu)造器
這樣的好處就是所有東西至少初始化為零知染,而不是僅僅留作垃圾
因此,我們得出以下結(jié)論:構(gòu)造器內(nèi)唯一能夠安全調(diào)用的方法是基類(lèi)中的 final 方法(包含 private 方法斑胜,因?yàn)?private 屬于 final 方法)控淡,這些方法無(wú)法被覆蓋,也就不會(huì)出現(xiàn)上述問(wèn)題止潘。
4. 協(xié)變返回類(lèi)型
協(xié)變返回類(lèi)型指的是:導(dǎo)出類(lèi)中的被覆蓋方法可以返回基類(lèi)方法的返回類(lèi)型的某種導(dǎo)出類(lèi)型掺炭。
換個(gè)說(shuō)法:導(dǎo)出類(lèi) 覆蓋(即重寫(xiě)) 基類(lèi) 方法時(shí),返回的類(lèi)型可以是基類(lèi)方法返回類(lèi)型的子類(lèi)凭戴。
舉個(gè)例子:
class Grain {
public String toString() { return "Grain"; }
}
class Wheat extends Grain {
public String toString() { return "Wheat"; }
}
class Mill {
Grain process() { return new Grain(); }
}
class WheatMill extends Mill {
Wheat process() { return new Wheat(); }
}
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
} /* Output:
Grain
Wheat
*/
5. 用繼承進(jìn)行設(shè)計(jì)
5.1 再論組合與繼承
進(jìn)行設(shè)計(jì)時(shí)涧狮,首選組合。組合可以動(dòng)態(tài)選擇類(lèi)型(因此也就選擇了行為)么夫;相反者冤,繼承再編譯時(shí)就需要知道確切類(lèi)型。
看一個(gè)例子:
class Actor {
public void act() {}
}
class HappyActor extends Actor {
public void act() { print("HappyActor"); }
}
class SadActor extends Actor {
public void act() { print("SadActor"); }
}
class Stage {
private Actor actor = new HappyActor();
public void change() { actor = new SadActor(); }
public void performPlay() { actor.act(); }
}
public class Transmogrify {
public static void main(String[] args) {
Stage stage = new Stage();
stage.performPlay();
stage.change();
stage.performPlay();
}
} /* Output:
HappyActor
SadActor
*/
設(shè)計(jì)的通用準(zhǔn)則:用繼承表達(dá)行為間的差異档痪,用成員表達(dá)狀態(tài)上的變化涉枫。
上例中,通過(guò)繼承得到兩個(gè)不同的類(lèi)腐螟,用于表達(dá) act() 方法的差異愿汰;而 Stage 通過(guò)組合是自己的狀態(tài)發(fā)生變化。這種情況下乐纸,這種狀態(tài)的改變也就產(chǎn)生了行為的改變衬廷。
5.2 純繼承與擴(kuò)展
純粹的"is-a"關(guān)系:基類(lèi)和導(dǎo)出類(lèi)具有相同的接口
- 圖示:
- 也可以認(rèn)為這是一種純替代,即導(dǎo)出類(lèi)可以完全代替基類(lèi)汽绢,基類(lèi)可以接收發(fā)送給導(dǎo)出類(lèi)的任何消息吗跋。
- 我們只需從導(dǎo)出類(lèi)向上轉(zhuǎn)型,永遠(yuǎn)不要知道正在處理的對(duì)象的確切類(lèi)型庶喜。
擴(kuò)展的"is-like-a"關(guān)系:導(dǎo)出類(lèi)有著和基類(lèi)相同的基本接口小腊,同時(shí)還具有由額外方法實(shí)現(xiàn)的其他特性
- 是更為有用且明智的方法
- 問(wèn)題在于救鲤,當(dāng)進(jìn)行向上轉(zhuǎn)型時(shí),不能使用擴(kuò)展部分(基類(lèi)無(wú)法訪問(wèn)導(dǎo)出類(lèi)的擴(kuò)展部分)
5.3 向下轉(zhuǎn)型與運(yùn)行時(shí)類(lèi)型識(shí)別
5.3.1 運(yùn)行時(shí)類(lèi)型識(shí)別
運(yùn)行時(shí)類(lèi)型識(shí)別(RTTI)指的是在運(yùn)行期間對(duì)類(lèi)型進(jìn)行檢查的行為秩冈。
Java 語(yǔ)言中本缠,所有轉(zhuǎn)型都會(huì)在運(yùn)行期時(shí)對(duì)其進(jìn)行檢查,以便保證它的確是我們希望的那種類(lèi)型入问。如果不是丹锹,就會(huì)返回一個(gè) ClassCastException。
RTTI 不僅僅包括轉(zhuǎn)型處理芬失。比如它提供一種方法楣黍,使我們?cè)谠噲D向下轉(zhuǎn)型之前,查看索要處理的類(lèi)型棱烂。
5.3.2 向下轉(zhuǎn)型
由于向上轉(zhuǎn)型會(huì)丟失具體的類(lèi)型信息租漂,所以希望通過(guò)向下轉(zhuǎn)型(在繼承層次中向下移動(dòng))重新獲取類(lèi)型信息。
通過(guò)例子來(lái)講解向下轉(zhuǎn)型的要點(diǎn):
class Fruit {
void name(){
System.out.println("Fruit");
}
}
class Apple extends Fruit{
@Override
void name(){
System.out.println("Apple");
}
void color(String c){
System.out.println("This apple's color is " + c);
}
}
-
正確的向下轉(zhuǎn)型:
先進(jìn)行向上轉(zhuǎn)型颊糜,然后再進(jìn)行向下轉(zhuǎn)型哩治。此時(shí)會(huì)轉(zhuǎn)型成功,可以調(diào)用子類(lèi)的特殊方法衬鱼。Fruit a = new Apple();//先向上轉(zhuǎn)型 a.name(); Apple apple = (Apple)a;//再向下轉(zhuǎn)型业筏,不會(huì)出錯(cuò)(正確的) apple.name(); apple.color("red"); //輸出: //Apple //Apple //This apple's color is red
-
不安全的向下轉(zhuǎn)型:
不經(jīng)過(guò)向上轉(zhuǎn)型,直接向下轉(zhuǎn)型鸟赫。此時(shí)編譯不會(huì)報(bào)錯(cuò)蒜胖,但運(yùn)行時(shí)會(huì)拋出ClassCastException 異常Fruit f = new Fruit(); Apple apple = (Apple)f;//此處異常
總結(jié)
這一張內(nèi)容看起來(lái)比較多,實(shí)際上都是圍繞多態(tài)展開(kāi)抛蚤。
多態(tài)是一種不能單獨(dú)來(lái)看待的特性台谢,相反它只能作為類(lèi)關(guān)系”全景“的一部分,與其他特性協(xié)同工作霉颠。
另外对碌,面向?qū)ο蟮木幊趟枷脒€需打磨。
就這樣吧蒿偎,共勉朽们。