深刻理解Java的三大特性之多態

世界上最美麗的東西,看不見也摸不着,要靠心靈去感覺。 ——海倫·凱勒編程

面向對象編程有三大特性:封裝、繼承、多態。數組

封裝隱藏了類的內部實現機制,能夠在不影響類使用的狀況下改變類的內部結構,並保護數據。對於外部世界,其內部細節是隱藏的,而且只有其訪問方法向外部世界公開。函數

繼承就是重用父代碼。若是兩個類之間存在IS-A關係,則可使用繼承。同時,繼承爲多態性鋪平了道路。那麼什麼是多態性?多態性的實現機制是什麼?字體

1  定義

所謂多態性是指在程序中定義的特定類型的引用變量和經過引用變量發出的方法調用,這在編程中不肯定,但在程序運行期間肯定,即,引用變量將指向哪一個類的實例對象和方法調用m。經過引用變量實現ade的類必須由程序運行。只有在這個時期咱們才能做出決定。this

由於只有當程序運行時才肯定特定的類,因此能夠在不修改源代碼的狀況下將引用變量綁定到不一樣的類實現,這致使引用調用的特定方法的改變。也就是說,在不修改程序代碼的狀況下,能夠改變程序運行時綁定的特定代碼,從而使程序能夠選擇多種運行狀態,即多態性。。spa

2  多態的實現

2.1 實現的條件

Java實現多態有三個必要條件:繼承、重寫、向上轉型(父類引用指向子類對象)設計

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

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

向上轉型:在多態中須要將子類的引用賦給父類對象,只有這樣該引用纔可以具有技能調用父類的方法和子類的方法。blog

只有知足了上述三個條件,咱們纔可以在同一個繼承結構中使用統一的邏輯實現代碼處理不一樣的對象,從而達到執行不一樣的行爲。

對於Java,多態的實現機制遵循一個原則:當超類對象引用子類對象時,引用對象的類型而不是引用變量的類型決定要調用的成員方法,可是要調用的方法必須在超類中定義,也就是說,由t重寫的方法。他屬於亞類。

【注】向上轉換存在一些缺點,即不可避免地會致使一些方法和屬性的丟失,並致使沒法訪問它們。所以,對父類型的引用能夠調用父類中定義的全部屬性和方法,對於僅存在於子類中的方法和屬性來講已經太晚了。

2.2 實現的方式

2.2.一、基於繼承實現的多態

基於繼承的實現機制主要表如今父類和繼承該父類的一個或多個子類對某些方法的重寫,多個子類對同一方法的重寫能夠表現出不一樣的行爲。

 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     }

OUTPUT:
Wine : JNC--喝的是 JNC
Wine : JGJ--喝的是 JGJ


在上面的代碼中 JNC、JGJ 繼承 Wine,而且重寫了 drink()、toString() 方法,程序運行結果是調用子類中方法,輸出 JNC、JGJ 的名稱,這就是多態的表現。不一樣的對象能夠執行相同的行爲,可是他們都須要經過本身的實現方式來執行,這就要得益於向上轉型了。

咱們都知道全部的類都繼承自超類 Object,toString() 方法也是Object 中方法,當咱們以下這樣寫時,輸出的結果是 Wine : JGJ。

Object、Wine、JGJ 三者繼承鏈關係是:JGJ—>Wine—>Object。因此咱們能夠這樣說:當子類重寫父類的方法被調用時,只有對象繼承鏈中的最末端的方法纔會被調用。可是注意若是這樣寫:

輸出的結果應該是 Null,由於 JGJ 並不存在於該對象繼承鏈中。

因此基於繼承實現的多態能夠總結以下:對於引用子類的父類類型,在處理該引用時,它適用於繼承該父類的全部子類,子類對象的不一樣,對方法的實現也就不一樣,執行相同動做產生的行爲也就不一樣。

若是父類是抽象類,那麼子類必需要實現父類中全部的抽象方法,這樣該父類全部的子類必定存在統一的對外接口,但其內部的具體實現能夠各異。這樣咱們就可使用頂層類提供的統一接口來處理該層次的方法。

2.2.二、基於接口實現的多態

繼承是經過重寫父類的同一方法的幾個不一樣子類來體現的,那麼也就是經過實現接口並覆蓋接口中同一方法的幾不一樣的類體現的。

在接口的多態中,指向接口的引用必須是指定實現了該接口的一個類的實例程序,在運行時,根據對象引用的實際類型來執行對應的方法。

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

3  經典實戰

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

輸出結果

分析

①②③ 比較好理解,通常不會出錯。④⑤ 就有點糊塗了,爲何輸出的不是「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  總結

因爲向上轉換,對子類的父引用只能訪問父類擁有的方法和屬性。對於存在於子類中但不存在於父類中的方法,儘管方法過載,但不能使用引用。若是子類覆蓋父類中的一些方法,那麼在調用這些方法時必須使用子類中定義的這些方法(動態鏈接、動態調用)。

對於面向對象,多態性能夠分爲編譯時多態性和運行時多態性。編輯的多態性是靜態的,主要是指方法的過載。它根據不一樣的參數列表區分不一樣的函數。編輯以後,它將變成兩個不一樣的函數,在運行時不是多態的。運行時多態性是動態的,而且它是經過動態綁定實現的,這就是咱們所說的多態性。

多態機制的原理歸納以下:當超類對象引用子類對象時,被引用對象的類型而不是被引用變量的類型決定了要調用哪一個成員方法,可是要調用的方法必須在超類中定義,即由子類覆蓋的方法。可是,它仍然取決於繼承鏈中方法調用的優先級。確認方法,優先級以下。顯示(O),超級。顯示(O),這個。show((super)O),super.顯示((超級)O)。

相關文章
相關標籤/搜索