Java動態綁定和多態性詳解

首先開始我們以下面的程序來引出今天所講的多態

一、動態綁定

代碼如下

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.多態的前提

  1. 要有繼承關係
  2. 要有方法重寫
  3. 要有父類引用指向子類對象

多態的前提條件就決定了只有成員方法纔有多態,成員變量是沒有的

對於成員變量來說
無論是實例成員變量還是靜態成員變量,都沒有多態這一特性,通過引用變量來訪問它包含的成員變量時,系統總是試圖訪問它編譯時類型所定義的成員變量,而不是它運行時類型所定義的成員變量,成員變量是靜態綁定的(只根據對象的當前表示類型決定使用那個變量)

對於成員方法來說:
調用方法時,成員方法支持動態綁定(根據對象的實際類型確定執行那個方法),這裏注意靜態方法不算

這個口訣可以幫我們理解,但我們一定要弄懂其中原理

  • 成員變量:編譯看左邊(父類),運行看左邊(父類)
  • 成員方法:編譯看左邊(父類),運行看右邊(子類)
  • 靜態方法:編譯看左邊(父類),運行看左邊(父類)
    (靜態和類相關,算不上重寫,所以,訪問還是左邊的)
    只有非靜態的成員方法,編譯看左邊,運行看右邊

我們來個實例解釋一下上面這段話

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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章