首先開始我們以下面的程序來引出今天所講的多態
一、動態綁定
代碼如下
public class Main {
public static void main(String[] args) {
Zi b = new Zi();
b.view();
}
}
class Fu {
public int m = 1;
public void common() {
System.out.println("這是Fu的common方法");
}
public void view() {
common(); // 這裏是關鍵,父類和子類都有common方法,那麼調用
//哪個呢,這裏根據下面運行結果就知道是調用的子類的方法
//具體原理是什麼呢,跟動態綁定有關
}
}
class Zi extends Fu {
public int m = 2;
public void common() {
System.out.println("這是Zi的common方法");
}
public void look() {
view(); // 子類沒有該方法,這裏涉及繼承,子類對象會通過繼承鏈
// 找到父類對象中的該方法調用
}
}
輸出結果爲:這是Zi的common方法
接下來我們把子類的common方法註釋起來又會發生什麼
要想搞懂上面兩個程序運行的結果,我們就先要了解下面這些
Java允許程序員不必在編制程序時就確定調用哪一個方法,而是在程序運行的過程中,當方法被調用時,系統根據當時對象本身所屬的類來確定調用哪個方法,這種技術被稱爲後期(動態)綁定。當然這會降低程序的運行效率,所以只在子類對父類方法進行覆蓋時才使用
(這裏我們貼上動態綁定的定義:動態綁定是指在執行期間(非編譯期)判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。程序運行過程中,把函數(或過程)調用與響應調用所需要的代碼相結合的過程稱爲動態綁定。
Java中只有static,final,private和構造方法,以及成員變量是靜態綁定,其他的都屬於動態綁定,而private的方法其實也是final方法(隱式),而構造方法其實是一個static方法(隱式),所以可以看出把方法聲明爲final,第一可以讓他不被重寫,第二也可以關閉它的動態綁定。
所以根據這些,我們就可以解釋上面的例子爲什麼是調用的子類的方法,例子中public方法是動態綁定,因爲實例對象是子類,所以調用的是子類方法。第一個是子類重寫了父類方法,所以運行結果爲"這是子類的common方法";而對於第二個來說也是調用子類方法,只不過子類沒有重寫父類的方法,所以根據繼承鏈找到父類的實現調用,運行結果爲"這是父類的common方法"
這裏就可以引出我們要講的多態,多態的實現關鍵就是靠這種動態綁定(Java中的大多數方法都是屬於動態綁定,也就是實現多態的基礎。)
二、多態詳解
1.什麼是多態
面向對象的三大特性:封裝、繼承、多態。從一定角度來看,封裝和繼承幾乎都是爲多態而準備的。這是我們最後一個概念,也是最重要的知識點。
多態的定義:指允許不同類的對象對同一消息做出響應。即同一消息可以根據發送對象的不同而採用多種不同的行爲方式。(發送消息就是函數調用) ,簡單的說:就是用基類的引用指向子類的對象。
多態的技術稱爲:動態綁定(dynamic binding),是指在執行期間判斷所引用對象的實際類型,根據其實際的類型調用其相應的方法。
2.多態的前提
- 要有繼承關係
- 要有方法重寫
- 要有父類引用指向子類對象
多態的前提條件就決定了只有成員方法纔有多態,成員變量是沒有的
對於成員變量來說
無論是實例成員變量還是靜態成員變量,都沒有多態這一特性,通過引用變量來訪問它包含的成員變量時,系統總是試圖訪問它編譯時類型所定義的成員變量,而不是它運行時類型所定義的成員變量,成員變量是靜態綁定的(只根據對象的當前表示類型決定使用那個變量)
對於成員方法來說:
調用方法時,成員方法支持動態綁定(根據對象的實際類型確定執行那個方法),這裏注意靜態方法不算
這個口訣可以幫我們理解,但我們一定要弄懂其中原理
- 成員變量:編譯看左邊(父類),運行看左邊(父類)
- 成員方法:編譯看左邊(父類),運行看右邊(子類)
- 靜態方法:編譯看左邊(父類),運行看左邊(父類)
(靜態和類相關,算不上重寫,所以,訪問還是左邊的)
只有非靜態的成員方法,編譯看左邊,運行看右邊
我們來個實例解釋一下上面這段話
public class Main {
public static void main(String[] args) {
Fu f = new Zi();
System.out.println(f.m); //與父類一致
f.method1(); //與父類一致
f.method2(); //編譯時與父類一致,運行時與子類一致
System.out.println("-------------------");
Zi z = new Zi();
System.out.println(z.m);
z.method1();
z.method2();
}
}
class Fu {
public int m = 1;
public static void method1() {
System.out.println("這是Fu的靜態method方法");
}
public void method2() {
System.out.println("這是Fu的method方法");
}
}
class Zi extends Fu {
public int m = 2;
public static void method1() {
System.out.println("這是Zi的靜態method方法");
}
public void method2() {
System.out.println("這是Zi的method方法");
}
}
運行結果:
分析:
Fu f = new Zi(); ----------首先了解變量F到底是什麼
把這句子分2段:Fu f;這是聲明一個變量f爲Fu這個類,那麼知道了f肯定是Fu類。然後我們f=newZi();中建立一個子類對象賦值給了f,結果是什麼??
結果是,擁有了被Zi類函數覆蓋後的Fu類對象 ----------f
也就是說:
一、只有子類的函數覆蓋了父類的函數這一個變化,但是f肯定是Fu這個類,也就是說f不可能變成其他比如Zi這個類等等(突然f擁有了Zi類特有函數,成員變量等都是不可能的)。所以f所代表的是函數被複寫後(多態的意義)的一個Fu類,而Fu類原來有的成員變量(不是成員函數不可能被複寫)沒有任何變化。那麼獲得結論:A:成員變量:編譯和運行都看Fu
二、但是f的Fu類函數被複寫了。那麼獲得結論:B:非靜態方法:編譯看Fu,運行看Zi
三、對於靜態方法:編譯和運行都看Fu!!(這個又是怎麼來的)
其實很簡單,首先我們要理解靜態情況下發生了什麼?
當靜態時,Fu類的所有函數跟隨Fu類加載而加載了。也就是Fu類的函數(是先於對象建立之前就存在了,無法被後出現的Zi類對象所複寫的,所以沒發生複寫,那麼獲得結論:C:靜態方法:編譯和運行都看Fu
下面也是一個典型的多態案例
class Demo1_Polymorphic {
public static void main(String[] args) {
Animal a = new Cat(); //父類引用指向子類對象
System.out.println(a.color);
a.eat();
}
}
class Animal {
public String color = "黑色";
public void eat() {
System.out.println("動物喫飯");
}
}
class Cat extends Animal {
public String color = "白色";
public void eat() {
System.out.println("貓喫魚");
}
}
這裏前提是我們知道a是Animal類引用指向的Cat對象,所以它的成員變量編譯是跟Animal類綁定在一起的,方法是跟它運行時的Cat對象綁定在一起的
程序輸出結果爲黑色,貓喫魚。那麼這個是如何實現的呢,首先,對於color這個成員變量來說,它是靜態綁定的,系統總是試圖訪問它編譯時類型所定義的成員變量,而不是它運行時類型所定義的成員變量,所以我們這裏訪問的就是Animal類所定義的成員變量 —color=“黑色”,而對於eat()這個方法來說,是根據對象的實際類型(Cat類)確定執行這個方法,所以我們得到的是—“貓喫魚”。(根據上面的口訣也很快能得出答案)
通過這些,基本上對多態有了詳細的瞭解,成員變量和成員方法多態是不同的表現的,這也就是成員在被子類重寫時,變量稱爲"隱藏",而方法稱爲"覆蓋"的主要原因。
然後我們這裏要特別強調一點,就是如果要實現多態,父類一定要有被重寫的方法,這裏我們以下面爲例:
這裏我們就明顯看到了報錯,說這個方法未定義,所以要想用多態,多態的第二條前提是十分重要的(要有方法重寫,重點是父類要有被重寫的方法,子類不重寫父類方法還可以通過繼承運行,但是父類沒有被重寫的方法是會完全報錯的,編譯都通不過,乾脆就是錯的)。
這個就是子類沒有重寫父類方法,但是程序沒錯,還是可以完整運行,跟剛剛上面那個是不同的。
三、多態中向上轉型和向下轉型
對象的向上轉型:父類 父類對象 = 子類實例
1.父類有的方法,都可以調用,如果被子類重寫了,則會調用子類的方法。
2. 父類沒有的方法,而子類存在,則不能調用。
3.向上轉型只對方法有影響,對屬性沒影響。屬性不存在重寫。
對象的向下轉型:子類 子類對象 = (子類)父類實例
爲什麼要發生向下轉型?當父類需要調用子類的擴充方法時,才需要向下轉型。(這是因爲多態的弊端就是不能使用子類的特有功能)
下面這個實例講述了向上轉型和向下轉型的具體操作
運行結果:
四、多態的好處和弊端
-
A:多態的好處
1.提高了代碼的維護性(繼承保證)
2.提高了代碼的擴展性(由多態保證) -
B:多態的弊端
不能使用子類的特有屬性和行爲。 -
C:常用應用場景
可以當作形式參數,可以接收任意子類對象
五、這裏講一下靜態綁定和動態綁定的區別
靜態綁定(前期綁定):即在程序執行前,即編譯的時候已經實現了該方法與所在類的綁定,像C就是靜態綁定,針對Java簡單的可以理解爲程序編譯期的綁定
具體過程就是執行這個方法,只要到這個類的方法表裏拿出這個方法在內存裏的地址,然後就可以執行了。
Java中只有static,final,private和構造方法,以及成員變量是靜態綁定,其他的都屬於動態綁定,而private的方法其實也是final方法(隱式),而構造方法其實是一個static方法(隱式),所以可以看出把方法聲明爲final,第一可以讓他不被重寫,第二也可以關閉它的動態綁定。
動態綁定(後期綁定):運行時根據對象的類型進行綁定,Java中的大多數方法都是屬於動態綁定,也就是實現多態的基礎。
Java實現了後期綁定,則必須提供一些機制,可在運行期間判斷對象的類型,並分別調用適當的方法。也就是說,編譯的時候該方法不與所在類綁定,編譯器此時依然不知道對象的類型,但方法調用機制能自己去調查,找到正確的方法主體。Java裏實現動態綁定的是JVM.