前言
我們知道,面向對象有三大特徵:封裝、繼承和多態。現在我們已經瞭解了封裝和繼承,接下來在本文中,給大家帶來面向對象的第三大特徵:多態。
在這篇文章中,我們要弄清楚多態的含義、特點、作用,以及如何用代碼進行實現。全文大約【6000】字,不説廢話,只講可以讓你學到技術、明白原理的純乾貨!本文帶有豐富的案例及配圖,讓你更好地理解和運用文中的技術概念,並可以給你帶來具有足夠啓迪的思考
一. 多態簡介
概念多態(polymorphism)本來是生物學裏的概念,表示地球上的生物在形態和狀態方面的多樣性。
而在java的面向對象中,多態則是指同一個行為可以有多個不同表現形式的能力。也就是説,在父類中定義的屬性和方法,在子類繼承後,可以有不同的數據類型或表現出不同的行為。這可以使得同一個屬性或方法,在父類及其各個子類中,可能會有不同的表現或含義。比如針對同一個接口,我們使用不同的實例對象可能會有不同的操作,同一事件發生在不同的實例對象上會產生不同的結果。
當然,如果我們只是看這樣乾巴巴的概念,可能大家還是有點懵,給大家舉個栗子。
我們都聽過“龍生九子”的故事。長子是囚牛,喜歡搞音樂;次子是睚眥,喜歡打架。後面還有喜歡冒險登高的嘲風,愛大喊大叫的蒲牢,喜歡吸煙的狻猊,愛好舉重的霸下,好打官司的狴犴,喜歡斯文的負屓,會滅火的螭吻。他們都是龍的兒子,自然也都是龍,但每個龍都有不同的個性和技能。假如有一天玉帝對龍王説,“讓你的兒子來給我秀個技能”。大家説這個任務的執行結果會怎麼樣?這是不是得看龍王讓哪個兒子來秀了!如果是讓老大來表演,就是演奏音樂;如果是讓老二來表演,就是表演打架
從這個故事中,我們就可以感受到,九個龍子雖然都繼承了共同的父類,但子類在運行某個方法時卻可能會有不同的結果,這就是多態!
作用
根據多態的概念可知,多態機制可以在不修改父類代碼的基礎上,允許多個子類進行功能的擴展。比如父類中定義了一個方法A,有N個子類繼承該父類,這幾個子類都可以重寫這個A方法。並且子類的方法還可以將自己的參數類型改為父類方法的參數類型,或者將自己的返回值類型改為父類方法的返回值類型。這樣就可以動態地調整對象的調用,降低對象之間的依存關係,消除類型之間的耦合,使程序有良好的擴展,並可以對所有類的對象進行通用處理,讓代碼實現更加的靈活和簡潔。
分類
Java中的多態,分為編譯時多態和運行時多態。
● 編譯時多態:主要是通過方法的重載(overload)來實現,Java會根據方法參數列表的不同來區分不同的方法,在編譯時就能確定該執行重載方法中的哪一個。這是靜態的多態,也稱為靜態多態性、靜態綁定、前綁定。但也有一種特殊的方法重寫的情況,屬於編譯時多態。在方法重寫時,當對象的引用指向的是當前對象自己所屬類的對象時,也是編譯時多態,因為在編譯階段就能確定執行的方法到底屬於哪個對象。
● 運行時多態:主要是通過方法的重寫(override)來實現,讓子類繼承父類並重寫父類中已有的或抽象的方法。這是動態的多態,也稱為”後綁定“,這是我們通常所説的多態性。一句話,如果我們在編譯時就能確定要執行的方法屬於哪個對象、執行的是哪個方法,這就是編譯時多態,否則就是運行時多態!
特性
根據多態的要求,Java對象的類型可以分為編譯類型和運行類型,多態有如下特性:
● 一個對象的編譯類型與運行類型可以不一致;
● 編譯類型在定義對象時就確定了,不能改變,而運行類型卻是可以變化的;
● 編譯類型取決於定義對象時 =號的左邊,運行類型取決於 =號的右邊
所以我們在使用多態方式調用方法時,首先會檢查父類中是否有該方法,如果沒有,則會產生編譯錯誤;如果有,再去調用子類中的同名方法。即編譯時取決於父類,運行時取決於子類。
必要條件
我們要想實現多態,需要滿足3個必要條件:
● 繼承:多態發生在繼承關係中,必須存在有繼承關係的父類和子類中,多態建立在封裝和繼承的基礎之上;
● 重寫:必須要有方法的重寫,子類對父類的某些方法重新定義;
● 向上轉型:就是要將父類引用指向子類對象,只有這樣該引用才既能調用父類的方法,又能調用子類的方法。
只有滿足了以上3個條件才能實現多態,開發人員也才能在同一個繼承結構中,使用統一的代碼實現來處理不同的對象,從而執行不同的行為。
二. 多態的實現
實現方式
在Java中,多態的實現有如下幾種方式:
● 方法重載:重載可以根據實際參數的數據類型、個數和次序,在編譯時確定執行重載方法中的哪一個。
● 方法重寫:這種方式是基於方法重寫來實現的多態;
● 接口實現:接口是一種無法被實例化但可以被實現的抽象類型,是對抽象方法的集合。定義一個接口可以有多個實現,這也是多態的一種實現形式,與繼承中方法的重寫類似。
實現過程
2.1 需求分析現在我們有一個需求:有一個客户要求我們給他生產設備器材,他需要的產品類型比較多,可能要圓形的器材,也可能需要三角形、矩形等各種形狀的器材,我們該怎麼生產實現?
如果是按照我們之前的經驗,可以分別創建圓形類、三角形類、矩形類等,裏面各自有對應的生產方法,負責生產出對應的產品。但是如果這樣設計,其實不符合面向對象的要求。以後客户可能還會有很多其他的需求,如果針對每一個需求都設計一個類和方法,最終我們的項目代碼就會很囉嗦。
實際上,在客户的這些需求中,有很多要求是具有共性的!比如,無論客户需要什麼形狀的器材,我們都要進行”繪製生產“,在繪製生產的過程中,可能用到的材料都是一樣的,無非就是形狀不同!就好比生產巧克力,有圓的方的奇形怪狀的,不管怎麼樣,基礎原料都是巧克力。既然如此,我們總不能針對每一種形狀的器材都從頭到尾搞一遍吧?
所以既然它們有很多內容都一樣,我們就可以定義一個共同的父類,在父類中完成共性的功能和特徵,然後由子類繼承父類,每個子類再擴展實現自己個性化的功能。如下圖所示:
這樣就是符合面向對象特徵的代碼設計了!接下來壹哥就通過一些代碼案例,來給大家演示該如何實現這個需求。
2.2 代碼實現接下來會採用實現接口的方式來演示多態的代碼實現過程。方法重載和方法重寫的方式,其實我們在前面的文章中已經有所講解,這裏不再贅述。
2.2.1 定義Shape接口我們首先定義出一個Shape接口,這個接口就是一個父類。在Java中,子類可以繼承父類,也可以實現接口。一個子類只能繼承一個父類,但是卻可以實現多個接口。這些接口,屬於是子類的”間接父類“,你可以理解為是子類的”乾爹“或者爺爺等祖輩。關於接口的內容,會在後面的文章中專門講解,敬請期待哦,此處大家先會使用即可。
2.2.2 定義Circle類定義一個Circle子類,實現Shape接口,注意我們這裏使用了implements關鍵字!
述(最多18字2.2.3 定義Traingle類然後再定義一個Traingle子類,也實現Shape接口。
2.2.4 定義Square類最後定義一個Square子類,同樣實現Shape接口。
述(2.4.5 定義測試類父子關係確定好之後,接下來我們再定義一個額外的測試類。在這個測試類中,我們創建出以上三個圖形對象。注意,在=等號左側,變量的類型都是Shape父類;=等號右側,變量的值是具體的子類!這種變量的定義過程,其實就是符合了多態的第三個必要條件,也就是所謂的”向上轉型,父類引用指向子類對象“。
我們可以看到上述代碼,滿足了多態的3個必要條件:繼承、重新、向上轉型!有子類繼承父類,有方法重寫,有向上轉型。而且根據這個案例,我們可以進一步理解多態的含義和特點。在多態中,針對某個類型的方法調用,其真正執行的方法取決於運行時期實際類型的方法!本案例最終的執行結果如下圖所示:
2.3 結果分析在上述案例中,我們有如下一行代碼:
上述代碼中,我們實際的類型是Circle、Traingle、Square,他們共同的父類,其引用類型是Shape變量。當我們調用shape.draw()時,大家可以想一下,執行的是父類Shape的draw()方法還是具體子類的draw()方法?大多數同學應該能夠想出來,執行的應該是具體子類的draw()方法!
基於以上這個案例,我們可以得出一個結論:
Java實例方法的調用,是基於運行時實際類型的動態調用,而非聲明的變量類型!通俗地説,就是我們調用的到底是哪個對象的方法,不是由=號左側聲明的引用變量來決定的,而是由=號右側的實際對象類型來決定的!
這也是多態的一個重要特徵!所以我們説在多態中,針對某個類型的方法調用,其真正執行的方法取決於運行時期實際類型的方法!即只有在運行期,才能動態決定調用哪個子類的方法。這種不確定性的方法調用,究竟有什麼作用呢?其實主要就是允許我們能夠添加更多類型的子類,實現對父類功能的擴展,而不需要修改父類的代碼。
三. 擴展補充
方法重寫時的編譯時多態當對象的引用指向的是當前對象所屬類的對象,即使是方法重寫,依然屬於編譯時多態。
1.1 定義父類我們先定義一個Father父類,內部定義一個eat()方法。
1.2 定義子類接着定義一個Son子類繼承Father父類,並重寫eat()方法
雖然這裏的Son子類繼承了父類Father,並重寫了父類的方法,但對象的引用指向的是當前對象所屬類的對象,即son引用指向的是new Son()對象,這也是編譯時多態!
實現多態時的若干細節
2.1 定義Father父類我們定義一個Father父類,類中定義了name屬性,成員方法eat(),靜態方法play()。
2.2定義Son子類接着再定義一個Son子類,類中定義了同名的name屬性和特有的age屬性,重寫成員方法eat(),特有的drink()方法,並定義一個同名的靜態方法play()。
2.3 執行結果上述代碼執行結果如下圖所示:
根據上述代碼的執行結果可知,當父類引用指向子類對象時,父類只能調用執行那些在父類中聲明、被子類覆蓋的子類方法,而不能執行子類獨有的成員方法。否則在編譯階段就會出現”The method drink() is undefined for the type Father“異常。
另外當子類和父類有相同屬性時,父類會調用自己的屬性。當父類引用指向子類對象向上轉型時,若父類調用子類特有的屬性,在編譯時期就會報錯”age cannot be resolved or is not a field“。
如果Father父類中定義了一個靜態方法play(),子類也定義了一個同名的靜態方法play(),上述代碼中son.play()執行的是Father類中的play()方法。在進行向上轉型時,父類引用調用同名的靜態方法時,執行的是父類中的方法。這是因為在運行時,虛擬機已經確定了static方法屬於哪個類。“方法重寫”只適用於實例方法,對靜態方法無效。靜態方法,只能被隱藏、重載、繼承,但不會被重寫。子類會將父類的靜態方法隱藏,但不能覆蓋父類的靜態方法,所以子類的靜態方法體現不了多態,這和子類屬性隱藏父類屬性一樣。
四. 結語
至此,我們就把面向對象的三大特徵都學習完畢了,現在你對這三大特徵都熟悉了嗎?最後我們再來看看多態的要點都有哪些吧:
● 多態指的是不同子類型的對象,對同一行為作出的不同響應;
● 實現多態要滿足繼承、重新、向上轉型的條件;
● 多態分為編譯時多態和運行時多態,我們常説的多態是指運行時多態;
● 方法重載是編譯時多態,方法重寫是運行時多態,但重寫有例外情況;
● 父類引用指向子類對象時,調用的實例方法是子類重寫的方法,父類引用不能調用子類新增的方法和子類特有屬性;
● 父類引用指向子類對象時,父類引用只會調用父類自己的屬性和static方法,不會調用子類的;
● 多態使得代碼更加靈活,方便了代碼擴展。