多態(tài),英語Polymorphism滤港,由希臘語的兩個單詞polys(意為many, much)和morphē(意為form, shape)組成。從英文單詞也可知道polymorphism的意思是“有著多樣的形態(tài)”筏养。多態(tài)表示的是同一個事物具有的不同形態(tài)谭胚。
引子
在日常使用的語言中,我們隨時使用到多態(tài)颈嚼,也就是一字多義毛秘。舉“洗”(wash)為例,“洗”可以表達多種不同含義的“洗”阻课。洗衣服叫挟、洗澡、洗車中的”洗“實際上都不一樣限煞,都是不盡相同的動作抹恳。但是我們無需專門為了這些情景中的”洗“專門定義一個字或詞。例如不必為”洗車“的”洗“而專門造一個字晰骑。
通過消除文字之間的耦合适秩,極大地減少了語言的文字數(shù)量绊序,提高了語言的簡潔性硕舆、可讀性。消除文字之間的耦合是指自然語言中的文字可以單獨拿出來看待骤公,比如”洗“這個字抚官,單獨拿出來看我們也知道是什么意思,而不是要從”洗車“整個詞理解才能知道”洗“是什么意思阶捆。如果字與字之間的耦合度很高凌节,只要我改變了一整段話的某一個字,就有可能要改掉整段話中的所有字了洒试,會牽一發(fā)而動全身倍奢。比如說”我在室外洗自行車“。如果“洗”和“車”的耦合度很高垒棋,例如為不同的車“洗”都有專門的字卒煞,有為單車的“洗”,摩托車的“洗”叼架,轎車的“洗”畔裕。這樣只要我把”自行車“改為”轎車“衣撬,就要把自行車的“洗”換為轎車的“洗”了。我們希望不管是洗什么車扮饶,都是同一個洗具练,甚至是不管是洗什么物體,都是同一個“洗”甜无。
而在面向?qū)ο蟮某绦蛟O(shè)計中扛点,多態(tài)就是指同一個接口在不同的導出類中具有不同的行為表現(xiàn)方式,其意義與自然語言中的多態(tài)十分相似毫蚓。
繼承與多態(tài)
在OOP中占键,沒有繼承就沒有多態(tài)(嚴格上這里的多態(tài)是指動態(tài)多態(tài))垄琐。
要理解多態(tài)膛腐,必須結(jié)合面向?qū)ο笾械睦^承來看股冗,它并不是一個可以單獨隔離來看的概念按咒。
繼承在程序設(shè)計中最主要并不是為了復用父類的代碼玲销,組合也可以完成代碼的復用卓练,而繼承更多是表現(xiàn)出一種類與類之間的關(guān)系门岔,這種關(guān)系就是子類是父類的一種類型逢净,也就是經(jīng)常提到的"is-a"關(guān)系钥庇。而這種關(guān)系正是多態(tài)存在的前提牍鞠。
由于導出類復用了父類的接口(具有相同的方法),同一個消息可以發(fā)送給這些不同的導出類评姨,使得相同的接口具有不同的行為表現(xiàn)难述。
借用《Java編程思想》的簡單例子
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
class Wind extends Instrument {
public void play(Note n) {
System.out.println("Wind()");
}
}
class Violin extends Instrument {
public void play(Note n) {
System.out.println("Violin()");
}
}
public class Music {
public static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute);
Violin violin = new Violin();
tune(violin);
}
}
在上面的例子中,Wind類和Violin類是Instrument類的導出類吐句,有其獨特的play方法實現(xiàn)胁后。Music.tune()方法中調(diào)用的是Instrument類的play方法。只需要給tune方法傳入Instrument類或其導出類嗦枢,Java就會根據(jù)Instrument類的實際類型使用對應(yīng)的play方法攀芯。同一個play方法,根據(jù)對象的類型具有不同的實現(xiàn)文虏。
綜合來看侣诺,在OOP中,多態(tài)的“同一個東西”就是指有同一個父類的同一個方法氧秘,而“不同的形態(tài)”是說這些子類的方法可以有自己不同的實現(xiàn)年鸳。
繼承是多態(tài)的前提,并且是其實現(xiàn)的條件丸相。
類型解耦
程序設(shè)計語言中多態(tài)的作用與自然語言的非常相似搔确。
多態(tài)的本質(zhì)在于消除了類型之間的耦合。簡而言之,即一個類的代碼改變盡量少影響另外一個類妥箕。如同上文闡述的自然語言中字與字之間的解耦滥酥。不希望一個類的改變導致另外一個類的改變,從而使得整個代碼都大幅度的的改動畦幢。
使用在上一節(jié)中的代碼例子坎吻,就是希望Music類中的tune方法是一個不受具體樂器而改變的方法,不想為了每一種具體的樂器都特地寫一個tune方法宇葱,如tune(Violin)瘦真,tune(Wind)等等,只需要一個tune(Instrument)即可黍瞧。
通過類型的解耦诸尽,使得改變的事物與不變的事物區(qū)別開來,不管新增還是減少樂器印颤,都是使用Music.tune方法您机。
而之所以可以解耦,原因在于將what與how區(qū)別出來年局。Music.tune表示的是what际看,僅僅是一個抽象的概念,正如“洗”本身是一個抽象的“洗”矢否。而具體的how仲闽,則由更細節(jié)的子類來表達,正如“洗車”中的“洗”僵朗。
通過多態(tài)赖欣,程序?qū)⒆兊酶蓴U展,代碼也變得更加的簡練验庙。
后期綁定
在程序設(shè)計
多態(tài)是如何做到區(qū)別不同的子類型顶吮,調(diào)用正確的方法呢?
public static void tune(Instrument i) {
i.play(Note.MIDDLE_C);
}
在tune方法中壶谒,它只接受一個Instrument類的引用云矫。但是實際上編譯器如何知道這個Instrument引用指向的具體對象呢膳沽?是指向Violin對象還是Wind對象呢汗菜?實際上Java編譯器無法得知,只能是在運行時得知挑社。
實際上這個過程稱為綁定陨界,也就是將方法和一個方法主體(對象)關(guān)聯(lián)起來。多態(tài)的實現(xiàn)依賴于后期綁定痛阻,即在運行時根據(jù)對象的類型進行綁定菌瘪。后期綁定的“后期”與“前期”是一個相對的概念,區(qū)別在于是運行前還是運行時。
并非所有的都是多態(tài)
并非所有的東西都能是多態(tài)俏扩。正如在自然語言中糜工,并非所有的字都會有多義。例如“人”录淡,人的本意只能表達人類這種動物捌木,并不會用來表示其他的動物或者事物,除非是后來的引申義嫉戚。而往往謂詞刨裆,可以有多義,如上文提及的“洗”彬檀,是一個動詞帆啃。
在程序設(shè)計語言中,多態(tài)當然也有限制——多態(tài)只能是針對類的非static和final方法窍帝。換句話說努潘,就是類的static和final方法以及類的域不能多態(tài)。private方法實際上是final方法坤学,因此private方法也不能實現(xiàn)多態(tài)慈俯。
類域的多態(tài)并不是“多態(tài)”。域表示的是類的狀態(tài)數(shù)據(jù)拥峦,與自然語言中的體詞類似贴膘,狀態(tài)數(shù)據(jù)不可能有多個,例如boolean類型的成員變量只能是true或者false略号。如果子類的域和父類的域值發(fā)生了改變刑峡,那不是多義,而是值發(fā)生了變化玄柠。
final方法表示的是不可覆寫突梦,自然就無法做到每個子類有不同的實現(xiàn)了。
static方法表示的該方法屬于類羽利,而非對象宫患。多態(tài)的根據(jù)具體子類調(diào)用不同的方法變得毫無意義,因為向上轉(zhuǎn)型后調(diào)用的總會是基類的方法这弧。例如:
class Super {
public static staticMethod() {
System.out.println("Super static method");
}
}
class Sub extends Super {
public static staticMethod() {
System.out.println("Sub static method");
}
}
public class StaticMethodPolymorphismTest {
public static void main(String[] args) {
Super super = new Sub();
super.staticMethod();
}
}
這段代碼的輸出例子是"Super Static method"而不是"Sub static method"娃闲。原因很簡單,static方法是屬于類的匾浪,所以調(diào)用staticMethod方法肯定是調(diào)用Super類皇帮,而非Sub類。順帶一提蛋辈,在實踐中属拾,不建議使用對象實例來調(diào)用static方法,而是直接使用類來調(diào)用靜態(tài)方法,可以減少混淆渐白,如:
Super.staticMethod();
構(gòu)造器中的多態(tài)陷阱
值得一提的是尊浓,如果在多態(tài)中使用多態(tài),很可能會造成一些意想不到的問題纯衍。這是因為在構(gòu)造器初始化的時候眠砾,導出類的數(shù)據(jù)還沒有構(gòu)造完畢,如果多態(tài)的方法使用了導出類的數(shù)據(jù)托酸,會造成意想不到的問題褒颈。
借用《Java編程思想》的簡單例子。
class Glyph {
void draw() {
System.out.println("Glyph.draw");
}
public Glyph() {
System.out.println("Glyph before draw()");
draw();
System.out.println("Glyph after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
void draw() {
System.out.println("RoundGlyph.draw(), radiu = " + radius);
}
public RoundGlyph(int radius) {
this.radius = radius;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
輸出結(jié)果是:
Glyph before draw()
RoundGlyph.draw(), radiu = 0
Glyph after draw()
RoundGlyph.RoundGlyph(), radius = 5
在調(diào)用RoundGlyph構(gòu)造器時励堡,會首先隱式地調(diào)用Glyph構(gòu)造器谷丸。在Glyph方法中會調(diào)用draw方法,而由于后期綁定应结,Java會調(diào)用RoundGlyph的draw方法刨疼。RoundGlyph的draw方法會使用到radius成員變量,而由于此時radius成員變量值只是初始化的零值鹅龄,所以就打印出來0了揩慕。
所以多態(tài)并不建議在構(gòu)造器中使用,我們甚至建議在構(gòu)造器中盡可能簡單地初始化對象扮休,唯一安全使用的就是final方法迎卤。
結(jié)束
從根本上來說,OOP中的多態(tài)消除了類型之間的耦合玷坠,使得“變”與“不變”區(qū)別開來蜗搔,提高了程序的可擴展性,使得代碼更可讀和更可維護八堡,是面向?qū)ο笾械幕咎匦浴?/p>