Java 多態(學習 Java 編程語言 043)

1. 多態

一個對象變量能夠指示多種實際類型的現象被稱爲多態(polymorphism)多態是同一個行爲具備多個不一樣表現形式或形態的能力。多態就是同一個接口,使用不一樣的實例而執行不一樣操做。java

有一個簡單規則能夠用來判斷是否應該將數據設計爲繼承關係,就是 「is-a」 規則,它指出子類的每一個對象也是超類的對象。例如,每一個經理都是員工,所以,將 Manager 類設計爲 Employee 類的子類是有道理的;反之亦然,並非每一名員工都是經理。數組

「is-a」 規則的另外一種表述是替換原則(substitution principle),它指出程序中出現超類對象的任何地方均可以使用子類對象替換。ide

Employee 類:this

import java.time.LocalDate;
import java.util.Objects;

public class Employee {
    private String name;
    private double salary;
    private LocalDate hireDay;

    public Employee(String name, double salary, int year, int month, int day) {
        this.name = name;
        this.salary = salary;
        hireDay = LocalDate.of(year, month, day);
    }

    public String getName() {return name;}

    public double getSalary() {return salary;}

    public LocalDate getHireDay() {return hireDay;}

    public void raiseSalary(double byPercent) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

    @Override
    public String toString() {
        String str = "%s[name=%s, salary=%.2f, hireDay=%s]";
        return String.format(str, getClass(), name, this.getSalary(), hireDay);
    }
}

Manager 類:設計

public class Manager extends Employee {

    private double bonus;

    public Manager(String name, double salary, int year, int month, int day) {
        super(name, salary, year, month, day);
        this.bonus = 0;
    }

    public void setBonus(double bonus) {
        this.bonus = bonus;
    }

    @Override
    public double getSalary() {
        double baseSalary = super.getSalary(); 
        return baseSalary + bonus;
    }
}

例如,能夠將子類的對象賦給超類變量。code

Employee e;
e = new Employee(...); // Employee object expected
e = new Manager(...); // OK, Manager can be used as well

在 Java 程序設計語言中,對象變量是多態的(polymorphic)。一個 Employee 類型的變量既能夠引用一個 Employee 類型的對象,也能夠應用 Employee 類的任何一個子類的對象(例如,Manager)。orm

利用這個替換原則:對象

Manager boss = new Manager("Carl Cracker", 75000, 1987, 12, 15);
Employee[] staff = new Employee[3];
staff[0] = boss;

在這個例子中,變量 staff[0] 與 boss 引用同一個對象。但編譯器只將 staff[0] 當作是一個 Employee 對象。
boss 變量能夠這樣調用:
boss.setBonus(50000); // 這是正確的
但不能着這樣調用:
staff[0].setBonus(50000); // 這是錯誤的
這是由於 staff[0] 聲明的類型是 Employee,而 setBonus 不是 Employee 類的方法。
不能將超類的引用賦給子類變量。例如,下面的賦值就是非法的:
Manager m = staff[i]; // 這是錯誤的
緣由很清楚:不是全部的員工都是經理。若是賦值成功,m 有可能引用了一個不是經理的 Employee 對象,然後面有可能會調用 m.setBonus(...),這就會發生運行時錯誤。繼承

在 Java 中,子類引用的數組能夠轉換成超類引用的數組,而不須要採用強制類型轉換。例如,下面是一個經理數組
Manager[] managers = new Manager[10];
將它轉換成 Employee[] 數組徹底是合法的:
Employee[] staff = managers; // OK
這樣作確定不會有問題,請思考一下其中的原因。畢竟,若是 manager[i] 是一個 Manager,它也必定是一個 Employee。不過,實際上將會發生一些使人驚訝的事情。要切記 managers 和 staff 引用的是同一個數組。如今看一下這條語句:
staff[0] = new Employee("Harry Hacker", . . .);
編譯器居然接納了這個賦值操做。但在這裏,staff[0] 與 manager[0] 是相同的引用,彷佛咱們把一個普通員工擅自納入經理行列中了。這是一種很差的情形,當調用 managers[0].setBonus(1000) 的時候,將會試圖調用一個不存在的實例字段,進而攪亂相鄰存儲空間的內容。接口

爲了確保不發生這類破壞,全部數組都要牢記建立時的元素類型,並負責監督僅將類型兼容的引用存儲到數組中。例如,使用 new managers[10] 建立的數組是一個經理數組。若是試圖存儲一個 Employee 類型的引用就會引起 ArrayStoreException 異常。

2. 理解方法調用

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

  1. 編譯器査看對象的聲明類型和方法名。須要注意的是:有可能存在多個名字爲 f 但參數類型不同的方法。例如,可能存在方法 f(int) 和方法 f(String)。編譯器將會一一列舉 C 類中全部名爲 f 的方法和其超類中全部名爲 f 並且可訪問的方法(超類的私有方法不可訪問)。

    至此, 編譯器已知道全部可能被調用的候選方法。

  2. 接下來,編譯器要肯定方法調用中提供的參數類型。若是在全部名爲 f 的方法中存在一個與所提供參數類型徹底匹配的方法,就選擇這個方法。這個過程被稱爲重載解析( overloading resolution)。例如,對於調用 x.f("Hello"),編譯器將會挑選 f(String),而不是 f(int)。因爲容許類型轉換(int 能夠轉換成 double,Manager 能夠轉換成 Employee,等等),因此狀況可能變得很複雜。

    若是編譯器沒有找到與參數類型匹配的方法,或者發現通過類型轉換後有多個方法與之匹配,編譯器就會報告一個錯誤。

    至此,編譯器已經知道須要調用的方法的名字和參數類型。

    方法的名字和參數列表稱爲方法的簽名。例如,f(int) 和 f(String) 是兩個有相同名字、不一樣簽名的方法。若是在子類中定義了一個與超類簽名相同的方法,那麼子類中的這個方法就會覆蓋超類中這個相同簽名的方法。

    返回類型不是簽名的一部分。不過在覆蓋一個方法時,須要保證返回類型的兼容性。容許子類將覆蓋方法的返回類型改成原返回類型的子類型。例如,假設 Employee 類有如下方法:
    public Employee getBuddy() {...}
    經理不會想找這種底層員工做搭檔。爲了反映這一點,在子類 Manager 中,能夠以下覆蓋這個方法:
    public Manager getBuddy() {...} // Ok to change return type
    咱們說,這兩個 getBuddy 方法有可協變的返回類型。

  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 類自己定義的全部方法:

Employee:
        getName() -> Employee.getName()
        getSalary() -> Employee.getSalary()
        getHireDay() -> Employee.getHireDay()
        raiseSalary(double) -> Employee.raiseSalary(double)
        toString() -> Employee.toString()

實際上,上面列出的方法並不完整,Employee 類有一個超類 Object,Employee 類從這個超類中還繼承了大量方法,在此,咱們略去了 Object 方法。

Manager 方法表稍微有些不一樣。其中有三個方法是繼承而來的,一個方法是從新定義的,還有一個方法是新增的。

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

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

  1. 首先,虛擬機獲取 e 的實際類型的方法表。這多是 Employee、Manager 的方法表,也多是 Employee 類的其餘子類的方法表。
  2. 接下來,虛擬機查找了定義了 getSalary() 簽名的類。此時,虛擬機已經知道應該調用哪一個方法。
  3. 最後,虛擬機調用這個方法。

動態綁定有一個很是重要的特性:無需對現有的代碼進行修改就能夠對程序進行擴展。假設增長一個新類 Programmer,Programmer 是 Employee 類派生出的子類,而且變量 e 有可能引用這個類的對象,咱們不須要對包含調用 e.getSalary() 的代碼進行從新編譯。若是 e 剛好引用一個 Programmer 類的對象,就會自動地調用 Programmer.getSalary() 方法。

警告: 在覆蓋一個方法的時候,子類方法不能低於超類方法的可見性。特別是,若是超類方法是 public,子類方法必須也要聲明爲 public。常常會發生這類錯誤:即子類方法不當心遺漏了 public 修飾符。此時,編譯器就會報錯,指出你試圖提供更嚴格的訪問權限。

相關文章
相關標籤/搜索