標籤:
在Java中,父類的變數可以引用父類的執行個體,也可以引用子類的執行個體。
請大家先看一段代碼:
- public class Demo {
- public static void main(String[] args) {
- People obj = new People(); // 引用父類執行個體
- obj.say();
- obj = new Teacher(); // 引用子類執行個體
- obj.say();
- }
- }
- class People{
- void say(){
- System.out.println("大家好,我是良民");
- }
- }
- class Teacher extends People{
- void say(){
- System.out.println("大家好,我是一名老師");
- }
- }
運行結果:
大家好,我是良民
大家好,我是一名老師
上面的代碼,定義了兩個類,分別是 People 和 Teacher,Teacher 類繼承自 People 類。obj 變數的類型為 People,它既可以指向 People 類的執行個體,也可以指向 Teacher 類的執行個體,這是正確的。也就是說,父類的變數可以引用父類的執行個體,也可以引用子類的執行個體。注意反過來是錯誤的,因為所有的教師都是人類,但不是所有的人都是教師。
可以看出,obj 既可以是人類,也可以是教師,它有不同的表現形式,這就被稱為多態。多態是指一個事物有不同的表現形式或形態。
再比如“寵物”,也有很多不同的表達或實現,它可以是小貓、小狗、蜥蜴等,我們到寵物店說“請給我一隻寵物”,服務員給我們小貓、小狗或者蜥蜴都可以,我們就說“寵物”具備了多態性。
多態存在的三個必要條件:要有繼承、要有重寫、父類變數引用子類對象。
當使用多態方式調用方法時:
- 首先檢查父類中是否有該方法,如果沒有,則編譯錯誤;如果有,則檢查子類是否覆蓋了該方法。
- 如果子類覆蓋了該方法,就調用子類的方法,否則調用父類方法。
動態綁定
為了理解多態的本質,下面講一下Java調用方法的詳細流程。
1) 編譯器查看對象的宣告類型和方法名。
假設調用 obj.func(param),obj 為 Teacher 類的對象。需要注意的是,有可能存在多個名字為func但參數簽名不一樣的方法。例如,可能存在方法 func(int) 和 func(String)。編譯器將會一一列舉所有 Teacher 類中名為func的方法和其父類 People 中訪問屬性為 public 且名為func的方法。
這樣,編譯器就獲得了所有可能被調用的候選方法列表。
2) 接下來,編澤器將檢查調用方法時提供的參數簽名。
如果在所有名為func的方法中存在一個與提供的參數簽名完全符合的方法,那麼就選擇這個方法。這個過程被稱為重載解析(overloading resolution)。例如,如果調用 func("hello"),編譯器會選擇 func(String),而不是 func(int)。由於自動類型轉換的存在,例如 int 可以轉換為 double,如果沒有找到與調用方法參數簽名相同的方法,就進行類型轉換後再繼續尋找,如果最終沒有匹配的類型或者有多個方法與之匹配,那麼編譯錯誤。
這樣,編譯器就獲得了需要調用的方法名字和參數簽名。
3) 如果方法的修飾符是private、static、final(static和final將在後續講解),或者是構造方法,那麼編譯器將可以準確地知道應該調用哪個方法,我們將這種調用方式 稱為靜態繫結(static binding)。
與此對應的是,調用的方法依賴於對象的實際類型, 並在運行時實現動態綁。例如調用 func("hello"),編澤器將採用動態綁定的方式產生一條調用 func(String) 的指令。
4)當程式運行,並且釆用動態綁定調用方法時,JVM一定會調用與 obj 所引用對象的實際類型最合適的那個類的方法。我們已經假設 obj 的實際類型是 Teacher,它是 People 的子類,如果 Teacher 中定義了 func(String),就調用它,否則將在 People 類及其父類中尋找。
每次調用方法都要進行搜尋,時間開銷相當大,因此,JVM預先為每個類建立了一個方法表(method lable),其中列出了所有方法的名稱、參數簽名和所屬的類。這樣一來,在真正調用方法的時候,虛擬機器僅尋找這個表就行了。在上面的例子中,JVM 搜尋 Teacher 類的方法表,以便尋找與調用 func("hello") 相匹配的方法。這個方法既有可能是 Teacher.func(String),也有可能是 People.func(String)。注意,如果調用super.func("hello"),編譯器將對父類的方法表迸行搜尋。
假設 People 類包含 say(String)、getName()、getAge() 三個,那麼它的方法表如下:
say(String) -> People.say(String)
getName() -> People.getName()
getAge() -> People.getAge()
實際上,People 也有預設的父類 Object(後續會講解),會繼承 Object 的方法,所以上面列舉的方法並不完整。
假設 Teacher 類覆蓋了 People 類中的 getName() 方法,並且新增了一個方法 raiseSalary(double),那麼它的參數列表為:
say(String) -> People.say(String)
getName() -> Teacher.getName()
getAge() -> People.getAge()
raiseSalary(double) -> Teacher.raiseSalary(double)
在啟動並執行時候,調用 obj.getName() 方法的過程如下:
- JVM 首先訪問 obj 的實際類型的方法表,可能是 People 類的方法表,也可能是 Teacher 類及其子類的方法表。
- JVM 在方法表中搜尋與 getName() 匹配的方法,找到後,就知道它屬於哪個類了。
- JVM 調用該方法。
6.Java多態和動態綁定