【java基礎(二十四)】類、超類、子類(二)

繼承層次

繼承並不僅限於一個層次。例如,可以由Manager類派生Executive(高中級管理人員)類。由一個公共超類派生出來的所有類的集合稱爲繼承層次(inheritance hierarchy)。在繼承層次中,從某個特定的類到其祖先的路徑稱爲該類的繼承連(inheritance chain)。如下圖:
在這裏插入圖片描述通常,一個祖先類可以擁有多個子孫繼承連。例如,可以由Employee類派生出子類Programmer(程序員)和Secretary(祕書),它們與Manager類沒有任何關係。必要的話,可以將這個過程一直延續下去。

多態

有一個用來判斷是否應該設計爲繼承關係的簡單規則,這就是“is - a”規則,它表明子類的每個對象也是超類的對象。例如,每個經理都是僱員,因此,將Manager類設計爲Employee類的子類是顯而易見的,反之不然,並不是每一名僱員都是經理。
“is - a”規則的另一種表述法是置換法則。它表明程序中出現超類對象的任何地方都可以用子類對象置換。
例如,可以將一個子類的對象賦給超類變量。

Employee e;
e = new Employee(...); // Employee對象
e = new Manager(...); // e雖然是Employee對象,但可以用Manager對象賦值

在Java程序設計語言中,對象變量是多態的。一個Employee變量既可以引用一個Employee類對象,也可以引用一個Employee類的任何一個子類的對象。
在之前的例子中,我們已經看到置換法則的有點:

Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;

在這個例子中,變量staff[0]與boss引用同一個對象。但編譯器將staff[0]看成Employee對象。
這意味着,可以這樣調用:

boss.setBonus(5000);

但不能這樣調用:

staff[0].setBonus(5000);

這是因爲staff[0]聲明的類型是Employee,而setBonus不是Employee類的方法。
然而,不能將一個超類的引用賦給子類變量。例如,下面的賦值是非法的:

Manager m = staff[i];

原因很清楚:不是所有的僱員都是經理。如果賦值成功,m有可能引用了一個不是經理的Employee對象,當在後面調用m.setBonus()時就有可能發生運行時錯誤。

理解方法調用

弄清楚如何在對象上應用方法調用非常重要。下面假設要調用x.f(args),隱式參數x聲明爲類C的一個對象。下面是調用過程的詳細描述:

  1. 編譯器查看對象的聲明類型和方法名。假設調用x.f(param),且隱式參數x聲明爲C類的對象。需要注意的是:有可能存在多個名字爲f,但參數類型不一樣的方法。例如,可能存在方法f(int)和方法f(String)。編譯器將會一一列舉所有C類中名爲f的方法和其超類中的訪問屬性爲public且名爲f的方法(超類的私有方法不可訪問)。
    至此,編譯器已獲得所有可能被調用的候選方法。
  2. 接下來,編譯器將查看調用方法時提供的參數類型。如果在所有名爲f的方法中存在一個與提供的參數類型完全匹配,就選擇這個方法。這個過程被稱爲重載解析(overloading resolution)。例如,對於調用x.f(“Hello”)來說,編譯器將會挑選f(String),而不是f(int)。
    由於允許類型轉換(int可以轉換成double,Manager可以轉換成Employee,等等),所以這個過程可能很複雜。如果編譯器沒有找到與參數類型匹配的方法,或者發現經過類型轉換後有多個方法與之匹配,就會報告一個錯誤。
    至此,編譯器已獲得需要調用的方法名字和參數類型。
  3. 如果是private方法、static方法、final方法或者構造器,那麼編譯器將可以準確的知道應該調用哪個方法,我們將這種調用方式成爲靜態綁定(static binding)。於此對應的是,調用的方法依賴於隱式參數的實際類型,並且在運行時實現動態綁定。在我們列舉的示例中,編譯器採用動態綁定的方式生成一條調用f(String)的指令。
  4. 當程序運行,並且採用動態綁定調用方法時,虛擬機一定調用與x所引用對象的實際類型最合適的那個類的方法。假設x的實際類型是D,它是C類的子類。如果D類定義了方法f(String),就直接調用他;否則,將在D類的超類中尋找f(String),以此類推。
    每次調用方法都要進行搜索,時間開銷相當大。因此,虛擬機預先爲每個類創建了一個方發表(method table),其中列出了所有方法的簽名和實際調用的方法。這樣一來,在真正調用方法的時候,虛擬機僅查找這個表就行了。在前面的例子中,虛擬機搜索D類的方法表,以便尋找與調用f(String)相匹配的方法。這個方法極有可能是D.f(String),也有可能是X.f(String),這裏X是D的超類。這裏需要提醒一點,如果調用super.f(param),編譯器將對隱式參數超類的方法表進行搜索。
    現在,我們看一下之前程序中調用e.getSalary()的詳細過程。e聲明爲Employee類型。Employee類只有一個名叫getSalary的方法,這個方法沒有參數,因此,在這裏不必擔心重載解析的問題。
    由於getSalary不是private方法、static方法、final方法,所以將採用動態綁定。虛擬機爲Employee和Manager兩個類生成方發表。在Employee的方發表中,列出了這個類定義的所有方法:
Employee:
	getName() -> Employee.getName();
	getSalary() -> Employee.getSalary();
	getHireDay() -> Employee.getHireDay();
	raiseSalary(double) -> Employee.raiseSalary(double);
	... 

實際上,上面列出的方法並不完整,稍後我們會看到Employee類有一個超類Object,Employee類從這個超類中還繼承了許多方法,在此,我們略去了Object方法。
Manage方發表稍微有些不同。其中有三個方法是繼承而來的,一個方法是重新定義的,還有一個方法是新增加的。

Manager:
	getName() -> Employee.getName();
	getSalary() -> Manager.getSalary();
	getHireDay() -> Employee.getHireDay();
	raiseSalary(double) -> Employee.raiseSalary(double);
	setBonus(double) -> Manager.setBonus(double);

在運行時,調用e.getSalary()的解析過程爲:

  1. 首先,虛擬機提取e的實際類型的方發表。既可能是Employee、Manager的方法表,也可能是Employee類的其他子類的方發表。
  2. 接下來,虛擬機搜索定義getSalary簽名的類。此時,虛擬機已經知道應該調用哪個方法。
  3. 最後,虛擬機調用方法。

動態綁定有一個非常重要的特性:無需對現存的代買進行修改,就可以對程序進行擴展。假設增加一個新類Executive,並且變量e可能引用這個類的對象,我們不需要對包含調用e.getSalary()的代碼進行重新編譯。如果e恰好引用一個Executive類的對象,就會自動地調用Executive.getSalary()方法。

捐贈

若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。

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