深入理解 Java 的三大特性之多型
世界上最美麗的東西,看不見也摸不著,要靠心靈去感受。 ——海倫·凱勒
面向物件程式設計有三大特性:封裝、繼承、多型。
封裝隱藏了類的內部實現機制,可以在不影響使用的情況下改變類的內部結構,同時也保護了資料。對外界而言它的內部細節是隱藏的,暴露給外界的只是它的訪問方法。
繼承是為了重用父類程式碼。兩個類若存在IS-A的關係就可以使用繼承。同時繼承也為實現多型做了鋪墊。那麼什麼是多型呢?多型的實現機制又是什麼?
1定義
所謂多型就是指程式中定義的引用變數所指向的具體型別和通過該引用變數發出的方法呼叫在程式設計時並不確定,而是在程式執行期間才確定,即一個引用變數到底會指向哪個類的例項物件,該引用變數發出的方法呼叫到底是哪個類中實現的方法,必須在由程式執行期間才能決定。
因為在程式執行時才確定具體的類,這樣,不用修改源程式程式碼,就可以讓引用變數繫結到各種不同的類實現上,從而導致該引用呼叫的具體方法隨之改變,即不修改程式程式碼就可以改變程式執行時所繫結的具體程式碼,讓程式可以選擇多個執行狀態,這就是多型性。
2 多型的實現
2.1 實現的條件
Java實現多型有三個必要條件:繼承、重寫、向上轉型(父類引用指向子類物件)
繼承:在多型中必須存在有繼承關係的子類和父類。
重寫:子類對父類中某些方法進行重新定義,在呼叫這些方法時就會呼叫子類的方法。
向上轉型:在多型中需要將子類的引用賦給父類物件,只有這樣該引用才能夠具備技能呼叫父類的方法和子類的方法。
只有滿足了上述三個條件,我們才能夠在同一個繼承結構中使用統一的邏輯實現程式碼處理不同的物件,從而達到執行不同的行為。
對於Java而言,多型的實現機制遵循一個原則:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。
【注】向上轉型存在一些缺憾,那就是它必定會導致一些方法和屬性的丟失,而導致我們不能夠獲取它們。所以父類型別的引用可以呼叫父類中定義的所有屬性和方法,對於只存在與子類中的方法和屬性它就望塵莫及了。
2.2 實現的方式
2.2.1、基於繼承實現的多型
基於繼承的實現機制主要表現在父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫可以表現出不同的行為。
public class Wine { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public Wine(){ } public String drink(){ return "喝的是 " + getName(); } /** * 重寫toString() */ public String toString(){ return null; } } public class JNC extends Wine{ public JNC(){ setName("JNC"); } /** * 重寫父類方法,實現多型 */ public String drink(){ return "喝的是 " + getName(); } /** * 重寫toString() */ public String toString(){ return "Wine : " + getName(); } } public class JGJ extends Wine{ public JGJ(){ setName("JGJ"); } /** * 重寫父類方法,實現多型 */ public String drink(){ return "喝的是 " + getName(); } /** * 重寫toString() */ public String toString(){ return "Wine : " + getName(); } } public class Test { public static void main(String[] args) { //定義父類陣列 Wine[] wines = new Wine[2]; //定義兩個子類 JNC jnc = new JNC(); JGJ jgj = new JGJ(); //父類引用子類物件 wines[0] = jnc; wines[1] = jgj; for(int i = 0 ; i < 2 ; i++){ System.out.println(wines[i].toString() + "--" + wines[i].drink()); } System.out.println("-------------------------------"); } }
OUTPUT:
Wine : JNC--喝的是 JNC
Wine : JGJ--喝的是 JGJ
在上面的程式碼中 JNC、JGJ 繼承 Wine,並且重寫了 drink()、toString() 方法,程式執行結果是呼叫子類中方法,輸出 JNC、JGJ 的名稱,這就是多型的表現。不同的物件可以執行相同的行為,但是他們都需要通過自己的實現方式來執行,這就要得益於向上轉型了。
我們都知道所有的類都繼承自超類 Object,toString() 方法也是Object 中方法,當我們如下這樣寫時,輸出的結果是 Wine : JGJ。
Object o = new JGJ(); System.out.println(o.toString());
Object、Wine、JGJ 三者繼承鏈關係是:JGJ—>Wine—>Object。所以我們可以這樣說:當子類重寫父類的方法被呼叫時,只有物件繼承鏈中的最末端的方法才會被呼叫。但是注意如果這樣寫:
Object o = new Wine(); System.out.println(o.toString());
輸出的結果應該是 Null,因為 JGJ 並不存在於該物件繼承鏈中。
所以基於繼承實現的多型可以總結如下:對於引用子類的父類型別,在處理該引用時,它適用於繼承該父類的所有子類,子類物件的不同,對方法的實現也就不同,執行相同動作產生的行為也就不同。
如果父類是抽象類,那麼子類必須要實現父類中所有的抽象方法,這樣該父類所有的子類一定存在統一的對外介面,但其內部的具體實現可以各異。這樣我們就可以使用頂層類提供的統一介面來處理該層次的方法。
2.2.2、基於介面實現的多型
繼承是通過重寫父類的同一方法的幾個不同子類來體現的,那麼也就是通過實現介面並覆蓋介面中同一方法的幾不同的類體現的。
在介面的多型中,指向介面的引用必須是指定實現了該介面的一個類的例項程式,在執行時,根據物件引用的實際型別來執行對應的方法。
繼承都是單繼承,只能為一組相關的類提供一致的服務介面。但是介面可以是多繼承多實現,它能夠利用一組相關或者不相關的介面進行組合與擴充,能夠對外提供一致的服務介面。所以它相對於繼承來說有更好的靈活性。
3 經典實戰
public class A { public String show(D obj) { return ("A and D"); } public String show(A obj) { return ("A and A"); } } public class B extends A { public String show(B obj){ return ("B and B"); } public String show(A obj){ return ("B and A"); } } public class C extends B{} public class D extends B{} public class Test { public static void main(String[] args) { A a1 = new A(); A a2 = new B(); B b = new B(); C c = new C(); D d = new D(); System.out.println(a1.show(b));① System.out.println(a1.show(c));② System.out.println(a1.show(d));③ System.out.println(a2.show(b));④ System.out.println(a2.show(c));⑤ System.out.println(a2.show(d));⑥ System.out.println(b.show(b));⑦ System.out.println(b.show(c));⑧ System.out.println(b.show(d));⑨ } }
輸出結果
①A and A ②A and A ③A and D ④B and A ⑤B and A ⑥A and D ⑦B and B ⑧B and B ⑨A and D
分析
①②③ 比較好理解,一般不會出錯。④⑤ 就有點糊塗了,為什麼輸出的不是「B and B」呢?!!先來回顧一下多型性。
執行時多型性是面向物件程式設計程式碼重用的一個最強大機制,動態性的概念也可以被說成「一個介面,多個方法」。Java 實現執行時多型性的基礎是動態方法排程,它是一種在執行時而不是在編譯期呼叫過載方法的機制。
方法的重寫 Overriding 和過載 Overloading 是 Java 多型性的不同表現。重寫 Overriding 是父類與子類之間多型性的一種表現,過載 Overloading 是一個類中多型性的一種表現。
如果在子類中定義某方法與其父類有相同的名稱和引數,我們說該方法被重寫 (Overriding)。子類的物件使用這個方法時,將呼叫子類中的定義,對它而言,父類中的定義如同被“遮蔽”了。
如果在一個類中定義了多個同名的方法,它們或有不同的引數個數或有不同的引數型別,則稱為方法的過載 (Overloading)。Overloaded 的方法是可以改變返回值的型別。
當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。(但是如果強制把超類轉換成子類的話,就可以呼叫子類中新新增而超類沒有的方法了。)
實際上這裡涉及方法呼叫的優先問題 ,優先順序由高到低依次為:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。讓我們來看看它是怎麼工作的。
比如 ④,a2.show(b),a2 是一個引用變數,型別為 A,則 this為 a2,b 是 B 的一個例項,於是它到類 A 裡面找 show(B obj)方法,沒有找到,於是到 A 的 super(超類) 找,而 A 沒有超類,因此轉到第三優先順序 this.show((super)O),this 仍然是 a2,這裡O為B,(super)O 即 (super)B 即 A,因此它到類 A 裡面找show(A obj) 的方法,類 A 有這個方法,但是由於 a2 引用的是類B的一個物件,B 覆蓋了 A 的 show(A obj) 方法,因此最終鎖定到類 B的show(A obj),輸出為 「B and A」。
再比如 ⑧,b.show(c),b 是一個引用變數,型別為 B,則 this為 b,c 是 C 的一個例項,於是它到類 B 找 show(C obj) 方法,沒有找到,轉而到 B 的超類 A 裡面找,A 裡面也沒有,因此也轉到第三優先順序 this.show((super)O),this 為 b,O 為 C,(super)O 即 (super)C 即 B,因此它到B裡面找 show(B obj) 方法,找到了,由於 b 引用的是類B的一個物件,因此直接鎖定到類B 的 show(B obj),輸出為 「B and B」。
按照上面的方法,可以正確得到其他的結果。
問題還要繼續,現在我們再來看上面的分析過程是怎麼體現出藍色字型那句話的內涵的。它說:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。還是拿 a2.show(b) 來說吧。
a2 是一個引用變數,型別為 A,它引用的是 B 的一個物件,因此這句話的意思是由 B 來決定呼叫的是哪個方法。因此應該呼叫B的show(B obj) 從而輸出「B and B」才對。但是為什麼跟前面的分析得到的結果不相符呢?!問題在於我們不要忽略了藍色字型的後半部分,那裡特別指明:這個被呼叫的方法必須是在超類中定義過的,也就是被子類覆蓋的方法。
B 裡面的 show(B obj) 在超類 A中有定義嗎?沒有!那就更談不上被覆蓋了。實際上這句話隱藏了一條資訊:它仍然是按照方法呼叫的優先順序來確定的。它在類A中找到了 show(A obj),如果子類B沒有覆蓋 show(A obj) 方法,那麼它就呼叫 A 的 show(A obj) (由於B繼承A,雖然沒有覆蓋這個方法,但從超類A那裡繼承了這個方法,從某種意義上說,還是由B確定呼叫的方法,只是方法是在A中實現而已);現在子類 B 覆蓋了 show(A obj),因此它最終鎖定到 B 的 show(A obj)。這就是那句話的意義所在。
4 總結
指向子類的父類引用由於向上轉型了,它只能訪問父類中擁有的方法和屬性,而對於子類中存在而父類中不存在的方法,該引用是不能使用的,儘管是過載該方法。若子類重寫了父類中的某些方法,在呼叫該些方法的時候,必定是使用子類中定義的這些方法(動態連線、動態呼叫)。
對於面向物件而已,多型分為編譯時多型和執行時多型。其中編輯時多型是靜態的,主要是指方法的過載,它是根據引數列表的不同來區分不同的函式,通過編輯之後會變成兩個不同的函式,在執行時談不上多型。而執行時多型是動態的,它是通過動態繫結來實現的,也就是我們所說的多型性。
多型機制遵循的原則概括為:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法,但是它仍然要根據繼承鏈中方法呼叫的優先順序來確認方法,優先順序為 this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。