淺談Java多態(tài)

多態(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)就是指同一個接口在不同的導(dǎo)出類中具有不同的行為表現(xiàn)方式,其意義與自然語言中的多態(tài)十分相似。

繼承與多態(tài)

在OOP中,沒有繼承就沒有多態(tài)(嚴格上這里的多態(tài)是指動態(tài)多態(tài))。
要理解多態(tài),必須結(jié)合面向?qū)ο笾械睦^承來看,它并不是一個可以單獨隔離來看的概念。

繼承在程序設(shè)計中最主要并不是為了復(fù)用父類的代碼,組合也可以完成代碼的復(fù)用,而繼承更多是表現(xiàn)出一種類與類之間的關(guān)系,這種關(guān)系就是子類是父類的一種類型,也就是經(jīng)常提到的"is-a"關(guān)系。而這種關(guān)系正是多態(tài)存在的前提。
由于導(dǎo)出類復(fù)用了父類的接口(具有相同的方法),同一個消息可以發(fā)送給這些不同的導(dǎo)出類,使得相同的接口具有不同的行為表現(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類的導(dǎo)出類,有其獨特的play方法實現(xiàn)。Music.tune()方法中調(diào)用的是Instrument類的play方法。只需要給tune方法傳入Instrument類或其導(dǎo)出類,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ì)在于消除了類型之間的耦合。簡而言之,即一個類的代碼改變盡量少影響另外一個類。如同上文闡述的自然語言中字與字之間的解耦。不希望一個類的改變導(dǎo)致另外一個類的改變,從而使得整個代碼都大幅度的的改動。
使用在上一節(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)造器初始化的時候,導(dǎo)出類的數(shù)據(jù)還沒有構(gòu)造完畢,如果多態(tài)的方法使用了導(dǎo)出類的數(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>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內(nèi)容