淺談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)就是指同一個接口在不同的導出類中具有不同的行為表現(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>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末樟凄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子兄渺,更是在濱河造成了極大的恐慌缝龄,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挂谍,死亡現(xiàn)場離奇詭異叔壤,居然都是意外死亡,警方通過查閱死者的電腦和手機凳兵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門百新,熙熙樓的掌柜王于貴愁眉苦臉地迎上來企软,“玉大人庐扫,你說我怎么就攤上這事。” “怎么了形庭?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵铅辞,是天一觀的道長。 經(jīng)常有香客問我萨醒,道長斟珊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任富纸,我火速辦了婚禮囤踩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晓褪。我一直安慰自己堵漱,他們只是感情好,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布涣仿。 她就那樣靜靜地躺著勤庐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪好港。 梳的紋絲不亂的頭發(fā)上愉镰,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機與錄音钧汹,去河邊找鬼丈探。 笑死,一個胖子當著我的面吹牛拔莱,可吹牛的內(nèi)容都是我干的类嗤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼辨宠,長吁一口氣:“原來是場噩夢啊……” “哼遗锣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嗤形,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤精偿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后赋兵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笔咽,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年霹期,在試婚紗的時候發(fā)現(xiàn)自己被綠了叶组。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡历造,死狀恐怖甩十,靈堂內(nèi)的尸體忽然破棺而出船庇,到底是詐尸還是另有隱情,我是刑警寧澤侣监,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布鸭轮,位于F島的核電站,受9級特大地震影響橄霉,放射性物質(zhì)發(fā)生泄漏窃爷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一姓蜂、第九天 我趴在偏房一處隱蔽的房頂上張望按厘。 院中可真熱鬧,春花似錦钱慢、人聲如沸刻剥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽造虏。三九已至,卻和暖如春麦箍,著一層夾襖步出監(jiān)牢的瞬間漓藕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工挟裂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留享钞,地道東北人。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓诀蓉,卻偏偏與公主長得像栗竖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子渠啤,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348

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