【Java學習筆記之二十四】對Java多型性的一點理解

weixin_34162629發表於2017-08-24

物件導向程式設計有三大特性:封裝、繼承、多型。

      封裝隱藏了類的內部實現機制,可以在不影響使用的情況下改變類的內部結構,同時也保護了資料。對外界而已它的內部細節是隱藏的,暴露給外界的只是它的訪問方法。

      繼承是為了重用父類程式碼。兩個類若存在IS-A的關係就可以使用繼承。,同時繼承也為實現多型做了鋪墊。那麼什麼是多型呢?多型的實現機制又是什麼?請看我一一為你揭開:

      所謂多型就是指程式中定義的引用變數所指向的具體型別和通過該引用變數發出的方法呼叫在程式設計時並不確定,而是在程式執行期間才確定,即一個引用變數倒底會指向哪個類的例項物件,該引用變數發出的方法呼叫到底是哪個類中實現的方法,必須在由程式執行期間才能決定。因為在程式執行時才確定具體的類,這樣,不用修改源程式程式碼,就可以讓引用變數繫結到各種不同的類實現上,從而導致該引用呼叫的具體方法隨之改變,即不修改程式程式碼就可以改變程式執行時所繫結的具體程式碼,讓程式可以選擇多個執行狀態,這就是多型性。

      比如你是一個酒神,對酒情有獨鍾。某日回家發現桌上有幾個杯子裡面都裝了白酒,從外面看我們是不可能知道這是些什麼酒,只有喝了之後才能夠猜出來是何種酒。你一喝,這是劍南春、再喝這是五糧液、再喝這是酒鬼酒….在這裡我們可以描述成如下:

      酒 a = 劍南春

      酒 b = 五糧液

      酒 c = 酒鬼酒

      …

      這裡所表現的的就是多型。劍南春、五糧液、酒鬼酒都是酒的子類,我們只是通過酒這一個父類就能夠引用不同的子類,這就是多型——我們只有在執行的時候才會知道引用變數所指向的具體例項物件。

      誠然,要理解多型我們就必須要明白什麼是“向上轉型”。在繼承中我們簡單介紹了向上轉型,這裡就在囉嗦下:在上面的喝酒例子中,酒(Win)是父類,劍南春(JNC)、五糧液(WLY)、酒鬼酒(JGJ)是子類。我們定義如下程式碼:

      JNC a = new  JNC();

      對於這個程式碼我們非常容易理解無非就是例項化了一個劍南春的物件嘛!但是這樣呢?

      Wine a = new JNC();

      在這裡我們這樣理解,這裡定義了一個Wine 型別的a,它指向JNC物件例項。由於JNC是繼承與Wine,所以JNC可以自動向上轉型為Wine,所以a是可以指向JNC例項物件的。這樣做存在一個非常大的好處,在繼承中我們知道子類是父類的擴充套件,它可以提供比父類更加強大的功能,如果我們定義了一個指向子類的父類引用型別,那麼它除了能夠引用父類的共性外,還可以使用子類強大的功能。

      但是向上轉型存在一些缺憾,那就是它必定會導致一些方法和屬性的丟失,而導致我們不能夠獲取它們。所以父類型別的引用可以呼叫父類中定義的所有屬性和方法,對於只存在與子類中的方法和屬性它就望塵莫及了---1。

 

 1 public class Wine {
 2     public void fun1(){
 3         System.out.println("Wine 的Fun.....");
 4         fun2();
 5     }
 6     
 7     public void fun2(){
 8         System.out.println("Wine 的Fun2...");
 9     }
10 }
11 
12 public class JNC extends Wine{
13     /**
14      * @desc 子類過載父類方法
15      *        父類中不存在該方法,向上轉型後,父類是不能引用該方法的
16      * @param a
17      * @return void
18      */
19     public void fun1(String a){
20         System.out.println("JNC 的 Fun1...");
21         fun2();
22     }
23     
24     /**
25      * 子類重寫父類方法
26      * 指向子類的父類引用呼叫fun2時,必定是呼叫該方法
27      */
28     public void fun2(){
29         System.out.println("JNC 的Fun2...");
30     }
31 }
32 
33 public class Test {
34     public static void main(String[] args) {
35         Wine a = new JNC();
36         a.fun1();
37     }
38 }
39 -------------------------------------------------
40 Output:
41 Wine 的Fun.....
42 JNC 的Fun2...

 

從程式的執行結果中我們發現,a.fun1()首先是執行父類Wine中的fun1().然後再執行子類JNC中的fun2()。

      分析:在這個程式中子類JNC過載了父類Wine的方法fun1(),重寫fun2(),而且過載後的fun1(String a)與 fun1()不是同一個方法,由於父類中沒有該方法,向上轉型後會丟失該方法,所以執行JNC的Wine型別引用是不能引用fun1(String a)方法。而子類JNC重寫了fun2() ,那麼指向JNC的Wine引用會呼叫JNC中fun2()方法。

      所以對於多型我們可以總結如下:

      指向子類的父類引用由於向上轉型了,它只能訪問父類中擁有的方法和屬性,而對於子類中存在而父類中不存在的方法,該引用是不能使用的,儘管是過載該方法。若子類重寫了父類中的某些方法,在呼叫該些方法的時候,必定是使用子類中定義的這些方法(動態連線、動態呼叫)。

      對於物件導向而已,多型分為編譯時多型和執行時多型。其中編輯時多型是靜態的,主要是指方法的過載,它是根據引數列表的不同來區分不同的函式,通過編輯之後會變成兩個不同的函式,在執行時談不上多型。而執行時多型是動態的,它是通過動態繫結來實現的,也就是我們所說的多型性。

多型的實現

      2.1實現條件

      在剛剛開始就提到了繼承在為多型的實現做了準備。子類Child繼承父類Father,我們可以編寫一個指向子類的父類型別引用,該引用既可以處理父類Father物件,也可以處理子類Child物件,當相同的訊息傳送給子類或者父類物件時,該物件就會根據自己所屬的引用而執行不同的行為,這就是多型。即多型性就是相同的訊息使得不同的類做出不同的響應。

      Java實現多型有三個必要條件:繼承、重寫、向上轉型。

         繼承:在多型中必須存在有繼承關係的子類和父類。

         重寫:子類對父類中某些方法進行重新定義,在呼叫這些方法時就會呼叫子類的方法。

         向上轉型:在多型中需要將子類的引用賦給父類物件,只有這樣該引用才能夠具備技能呼叫父類的方法和子類的方法。

         只有滿足了上述三個條件,我們才能夠在同一個繼承結構中使用統一的邏輯實現程式碼處理不同的物件,從而達到執行不同的行為。

      對於Java而言,它多型的實現機制遵循一個原則:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。

      2.2實現形式

      在Java中有兩種形式可以實現多型。繼承和介面。

      2.2.1、基於繼承實現的多型

      基於繼承的實現機制主要表現在父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫可以表現出不同的行為。

 

 1 public class Wine {
 2     private String name;
 3     
 4     public String getName() {
 5         return name;
 6     }
 7 
 8     public void setName(String name) {
 9         this.name = name;
10     }
11 
12     public Wine(){
13     }
14     
15     public String drink(){
16         return "喝的是 " + getName();
17     }
18     
19     /**
20      * 重寫toString()
21      */
22     public String toString(){
23         return null;
24     }
25 }
26 
27 public class JNC extends Wine{
28     public JNC(){
29         setName("JNC");
30     }
31     
32     /**
33      * 重寫父類方法,實現多型
34      */
35     public String drink(){
36         return "喝的是 " + getName();
37     }
38     
39     /**
40      * 重寫toString()
41      */
42     public String toString(){
43         return "Wine : " + getName();
44     }
45 }
46 
47 public class JGJ extends Wine{
48     public JGJ(){
49         setName("JGJ");
50     }
51     
52     /**
53      * 重寫父類方法,實現多型
54      */
55     public String drink(){
56         return "喝的是 " + getName();
57     }
58     
59     /**
60      * 重寫toString()
61      */
62     public String toString(){
63         return "Wine : " + getName();
64     }
65 }
66 
67 public class Test {
68     public static void main(String[] args) {
69         //定義父類陣列
70         Wine[] wines = new Wine[2];
71         //定義兩個子類
72         JNC jnc = new JNC();
73         JGJ jgj = new JGJ();
74         
75         //父類引用子類物件
76         wines[0] = jnc;
77         wines[1] = jgj;
78         
79         for(int i = 0 ; i < 2 ; i++){
80             System.out.println(wines[i].toString() + "--" + wines[i].drink());
81         }
82         System.out.println("-------------------------------");
83 
84     }
85 }
86 OUTPUT:
87 Wine : JNC--喝的是 JNC
88 Wine : JGJ--喝的是 JGJ
89 -------------------------------

 

  在上面的程式碼中JNC、JGJ繼承Wine,並且重寫了drink()、toString()方法,程式執行結果是呼叫子類中方法,輸出JNC、JGJ的名稱,這就是多型的表現。不同的物件可以執行相同的行為,但是他們都需要通過自己的實現方式來執行,這就要得益於向上轉型了。

      我們都知道所有的類都繼承自超類Object,toString()方法也是Object中方法,當我們這樣寫時:

 

Object o = new JGJ();

      System.out.println(o.toString());

 

     輸出的結果是Wine : JGJ。

      Object、Wine、JGJ三者繼承鏈關係是:JGJ—>Wine—>Object。所以我們可以這樣說:當子類重寫父類的方法被呼叫時,只有物件繼承鏈中的最末端的方法才會被呼叫。但是注意如果這樣寫:

 

Object o = new Wine();

System.out.println(o.toString());

 

輸出的結果應該是Null,因為JGJ並不存在於該物件繼承鏈中。

      所以基於繼承實現的多型可以總結如下:對於引用子類的父類型別,在處理該引用時,它適用於繼承該父類的所有子類,子類物件的不同,對方法的實現也就不同,執行相同動作產生的行為也就不同。

      如果父類是抽象類,那麼子類必須要實現父類中所有的抽象方法,這樣該父類所有的子類一定存在統一的對外介面,但其內部的具體實現可以各異。這樣我們就可以使用頂層類提供的統一介面來處理該層次的方法。

      2.2.2、基於介面實現的多型

      繼承是通過重寫父類的同一方法的幾個不同子類來體現的,那麼就可就是通過實現介面並覆蓋介面中同一方法的幾不同的類體現的。

      在介面的多型中,指向介面的引用必須是指定這實現了該介面的一個類的例項程式,在執行時,根據物件引用的實際型別來執行對應的方法。

      繼承都是單繼承,只能為一組相關的類提供一致的服務介面。但是介面可以是多繼承多實現,它能夠利用一組相關或者不相關的介面進行組合與擴充,能夠對外提供一致的服務介面。所以它相對於繼承來說有更好的靈活性。

三、經典例項。

      通過上面的講述,可以說是對多型有了一定的瞭解。現在趁熱打鐵,看一個例項。該例項是有關多型的經典例子

 

 1 public class A {
 2     public String show(D obj) {
 3         return ("A and D");
 4     }
 5 
 6     public String show(A obj) {
 7         return ("A and A");
 8     } 
 9 
10 }
11 
12 public class B extends A{
13     public String show(B obj){
14         return ("B and B");
15     }
16     
17     public String show(A obj){
18         return ("B and A");
19     } 
20 }
21 
22 public class C extends B{
23 
24 }
25 
26 public class D extends B{
27 
28 }
29 
30 public class Test {
31     public static void main(String[] args) {
32         A a1 = new A();
33         A a2 = new B();
34         B b = new B();
35         C c = new C();
36         D d = new D();
37         
38         System.out.println("1--" + a1.show(b));
39         System.out.println("2--" + a1.show(c));
40         System.out.println("3--" + a1.show(d));
41         System.out.println("4--" + a2.show(b));
42         System.out.println("5--" + a2.show(c));
43         System.out.println("6--" + a2.show(d));
44         System.out.println("7--" + b.show(b));
45         System.out.println("8--" + b.show(c));
46         System.out.println("9--" + b.show(d));      
47     }
48 }

 

  執行結果:

 

1--A and A
2--A and A
3--A and D
4--B and A
5--B and A
6--A and D
7--B and B
8--B and B
9--A and D

 

      在這裡看結果1、2、3還好理解,從4開始就開始糊塗了,對於4來說為什麼輸出不是“B and B”呢?

      首先我們先看一句話:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。這句話對多型進行了一個概括。其實在繼承鏈中物件方法的呼叫存在一個優先順序:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

      分析:

     從上面的程式中我們可以看出A、B、C、D存在如下關係。

      首先我們分析5,a2.show(c),a2是A型別的引用變數,所以this就代表了A,a2.show(c),它在A類中找發現沒有找到,於是到A的超類中找(super),由於A沒有超類(Object除外),所以跳到第三級,也就是this.show((super)O),C的超類有B、A,所以(super)O為B、A,this同樣是A,這裡在A中找到了show(A obj),同時由於a2是B類的一個引用且B類重寫了show(A obj),因此最終會呼叫子類B類的show(A obj)方法,結果也就是B and A。

      按照同樣的方法我也可以確認其他的答案。

      方法已經找到了但是我們這裡還是存在一點疑問,我們還是來看這句話:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法。這我們用一個例子來說明這句話所代表的含義:a2.show(b);

      這裡a2是引用變數,為A型別,它引用的是B物件,因此按照上面那句話的意思是說有B來決定呼叫誰的方法,所以a2.show(b)應該要呼叫B中的show(B obj),產生的結果應該是“B and B”,但是為什麼會與前面的執行結果產生差異呢?這裡我們忽略了後面那句話“但是這兒被呼叫的方法必須是在超類中定義過的”,那麼show(B obj)在A類中存在嗎?根本就不存在!所以這句話在這裡不適用?那麼難道是這句話錯誤了?非也!其實這句話還隱含這這句話:它仍然要按照繼承鏈中呼叫方法的優先順序來確認。所以它才會在A類中找到show(A obj),同時由於B重寫了該方法所以才會呼叫B類中的方法,否則就會呼叫A類中的方法。

      所以多型機制遵循的原則概括為:當超類物件引用變數引用子類物件時,被引用物件的型別而不是引用變數的型別決定了呼叫誰的成員方法,但是這個被呼叫的方法必須是在超類中定義過的,也就是說被子類覆蓋的方法,但是它仍然要根據繼承鏈中方法呼叫的優先順序來確認方法,該優先順序為:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。

 參考資料:http://blog.csdn.net/thinkGhoster/archive/2008/04/19/2307001.aspx

 百度文庫:http://wenku.baidu.com/view/73f66f92daef5ef7ba0d3c03.html

 

相關文章