多型
1.多型的概念
- 多型是方法或物件具有多種形態,是物件導向的第三大特徵。
- 多型的前提是兩個物件(類)存在繼承關係,多型是建立在封裝和繼承基礎之上的。
2.為什麼要使用多型
-
程式碼重用和擴充套件性:
多型性使得我們可以編寫通用的程式碼,可以適用於各種不同型別的物件。透過抽象類和介面,我們可以定義通用的方法和屬性,而具體的實現可以由子類來完成。這種靈活性使得程式碼更容易被重用和擴充套件,同時也降低了程式碼的耦合度。 -
簡化程式碼和邏輯:
多型性使得我們可以透過統一的介面來處理不同型別的物件,從而簡化了程式碼和邏輯。我們不需要為每種具體型別都編寫一套獨立的邏輯,而是可以透過一個統一的方法來處理所有型別的物件。這種簡化的程式碼結構更易於理解和維護。 -
更易於測試和除錯:
由於多型性使得程式碼更加模組化和可複用,因此更容易進行單元測試和除錯。我們可以針對介面或抽象類編寫通用的測試用例,而不需要為每個具體的實現編寫獨立的測試程式碼。這樣可以提高測試效率,降低測試成本。 -
擴充套件性和靈活性:
多型性使得程式具有更強的擴充套件性和靈活性。當需要新增新的功能或修改現有功能時,我們可以透過新增新的子類或修改現有子類來實現,而不需要修改現有的程式碼。這種低耦合度的設計使得程式更容易適應變化,並且更容易進行維護和升級。 -
可替換性:
多型性允許我們將父類的引用指向子類的物件,從而實現了物件的替換。這種特性使得程式更具靈活性,可以在執行時動態地替換物件的實現,而不需要修改程式碼。這種可替換性使得程式更容易適應不同的需求和環境。
3.多型的分類
1.1 編譯時多型(靜態繫結)
編譯時多型指的是方法過載(Method Overloading),即同一個類中可以有多個同名的方法,但它們的引數列表不同。編譯器會在編譯時根據方法的引數列表決定呼叫哪個方法。
public class OverloadExample {
public void display(int a) {
System.out.println("Argument: " + a);
}
public void display(String a) {
System.out.println("Argument: " + a);
}
public static void main(String[] args) {
OverloadExample obj = new OverloadExample();
obj.display(10); // 輸出:Argument: 10
obj.display("Hello"); // 輸出:Argument: Hello
}
}
1.2 執行時多型(動態繫結)
執行時多型指的是方法重寫(Method Overriding),即子類可以重寫父類的方法。在執行時,JVM根據物件的實際型別呼叫相應的方法。
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("Cat meows");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDog = new Dog();
Animal myCat = new Cat();
myAnimal.sound(); // 輸出:Animal makes a sound
myDog.sound(); // 輸出:Dog barks
myCat.sound(); // 輸出:Cat meows
}
}
4.多型的機制原理
RTTI
多型實現的技術基礎是 RTTI,即 Run-Time Type Identification(執行時型別判定),它的作用是在我們不知道某個物件的確切的型別資訊時(即某個物件是哪個類的例項),可以透過 RTTI 相關的機制幫助我們在編譯時獲取物件的型別資訊。
而 RTTI 的功能主要是透過 Class 類檔案實現的,更精確一點來說,是透過 Class 類檔案的方法表實現的。
這裡提到的 Class 類可以理解為是 “類的類”(class of classes)。如果說類是物件的抽象的話,那麼 Class 類就是對類的抽象。而每個類都有一個 Class 物件,每當編寫並且編譯成功一個新的類,就會生成一個對應的 Class 物件,被儲存在一個與類同名的 .class 檔案中。
詳細一點來說,就是 Java 原始碼編譯器將 Java 檔案編譯成 .class 檔案,然後透過類裝載器將 .class 檔案裝載到 JVM 中,並在內部建立該類的型別資訊(.class 檔案在 JVM 中儲存的一種資料結構),最後透過執行引擎來執行。
方法表是實現多型的關鍵所在,裡面儲存的是例項方法的引用,且是直接引用。Java 虛擬機器在執行程式時,就是透過方法表來確定執行哪一個多型方法的。
多型方法呼叫
在呼叫方法時,首先需要完成例項方法的符號引用解析,也就是將符號引用解析為方法表的偏移量。
虛擬機器透過物件引用得到方法區中型別資訊的入口,查詢類的方法表,當將子類物件宣告為父類型別時,形式上呼叫的是父類方法;
此時虛擬機器會從實際類的方法表(雖然宣告的是父類,但是實際上這裡的型別資訊中存放的是子類的資訊)中根據偏移量獲取該方法名對應的指標,進而就能指向實際類的方法了。
上面我們討論的僅是利用繼承實現多型的內部機制,多型的另外一種實現方式:介面實現相比而言會更加複雜。原因在於,Java的單繼承保證了類的線性關係,而介面可以同時實現多個,這樣光憑偏移量就很難準確獲得方法的指標。
所以在 JVM 中,多型的例項方法呼叫實際上有兩種指令:
invokevirtual 指令:用於呼叫宣告為類的方法;
invokeinterface 指令:用於呼叫宣告為介面的方法。
當使用 invokeinterface 指令呼叫方法時,就不能採用固定偏移量的辦法了。實際上,Java 虛擬機器對於介面方法的呼叫是採用搜尋方法表的方式來實現的,比如,要在 Father 介面的方法表中找到 dealHouse() 方法,必須搜尋 Father 的整個方法表。所以我們可以得出,在效能上,呼叫介面引用的方法通常總是比呼叫類的引用的方法要慢。這也告訴我們,在類和介面之間優先選擇介面作為設計並不總是正確的。
以上就是多型的原理,總結起來說就是兩點:
方法表起了決定性作用,如果子類改寫了父類的方法,那麼子類和父類的同名方法共享一個方法表項,都被認作是父類的方法,因此可以寫成父類引用指向子類物件的形式。
類和介面的多型實現不一樣,類的方法表可以使用固定偏移,但介面需要進行搜尋,原因是介面的實現不是確定唯一的,所以相對來說效能差一些。