1. 程式人生 > 實用技巧 >Java面向物件基礎——多型

Java面向物件基礎——多型

在繼承關係中,子類如果定義了一個與父類方法簽名完全相同的方法,被稱為覆寫(Override)。

例如,在Person類中,我們定義了run()方法:

class Person {
    public void run() {
        System.out.println("Person.run");
    }
}

在子類Student中,覆寫這個run()方法:

class Student extends Person {
    @Override
    public void run() {
        System.out.println("Student.run");
    }
}

Override和Overload不同的是,如果方法簽名如果不同,就是Overload,Overload方法是一個新方法;如果方法簽名相同,並且返回值也相同,就是Override

注意:方法名相同,方法引數相同,但方法返回值不同,也是不同的方法。在Java程式中,出現這種情況,編譯器會報錯。

class Person {
    public void run() { … }
}

class Student extends Person {
    // 不是Override,因為引數不同:
    public void run(String s) { … }
    
// 不是Override,因為返回值不同: public int run() { … } }

但是@Override不是必需的。

在上一節中,我們已經知道,引用變數的宣告型別可能與其實際型別不符,例如:

Person p = new Student();

現在,我們考慮一種情況,如果子類覆寫了父類的方法:

public class Main {
    public static void main(String[] args) {
        Person p = new Student();
        p.run(); // 應該列印Person.run還是Student.run?
} } class Person { public void run() { System.out.println("Person.run"); } } class Student extends Person { @Override public void run() { System.out.println("Student.run"); } }

那麼,一個實際型別為Student,引用型別為Person的變數,呼叫其run()方法,呼叫的是Person還是Studentrun()方法?

執行一下上面的程式碼就可以知道,實際上呼叫的方法是Studentrun()方法。因此可得出結論:

Java的例項方法呼叫是基於執行時的實際型別的動態呼叫,而非變數的宣告型別。

這個非常重要的特性在面向物件程式設計中稱之為多型。它的英文拼寫非常複雜:Polymorphic。

多型

多型是指,針對某個型別的方法呼叫,其真正執行的方法取決於執行時期實際型別的方法。例如:

Person p = new Student();
p.run(); // 無法確定執行時究竟呼叫哪個run()方法

有童鞋會問,從上面的程式碼一看就明白,肯定呼叫的是Studentrun()方法啊。

但是,假設我們編寫這樣一個方法:

public void runTwice(Person p) {
    p.run();
    p.run();
}

它傳入的引數型別是Person,我們是無法知道傳入的引數實際型別究竟是Person,還是Student,還是Person的其他子類,因此,也無法確定呼叫的是不是Person類定義的run()方法。

所以,多型的特性就是,執行期才能動態決定呼叫的子類方法。對某個型別呼叫某個方法,執行的實際方法可能是某個子類的覆寫方法。這種不確定性的方法呼叫,究竟有什麼作用?

我們還是來舉栗子。

假設我們定義一種收入,需要給它報稅,那麼先定義一個Income類:

class Income {
    protected double income;
    public double getTax() {
        return income * 0.1; // 稅率10%
    }
}

對於工資收入,可以減去一個基數,那麼我們可以從Income派生出SalaryIncome,並覆寫getTax()

class Salary extends Income {
    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

如果你享受國務院特殊津貼,那麼按照規定,可以全部免稅:

class StateCouncilSpecialAllowance extends Income {
    @Override
    public double getTax() {
        return 0;
    }
public class Main {
    public static void main(String[] args) {
        // 給一個有普通收入、工資收入和享受國務院特殊津貼的小夥伴算稅:
        Income[] incomes = new Income[] {
            new Income(3000),
            new Salary(7500),
            new StateCouncilSpecialAllowance(15000)
        };
        System.out.println(totalTax(incomes));
    }

    public static double totalTax(Income... incomes) {
        double total = 0;
        for (Income income: incomes) {
            total = total + income.getTax();
        }
        return total;
    }
}

class Income {
    protected double income;

    public Income(double income) {
        this.income = income;
    }

    public double getTax() {
        return income * 0.1; // 稅率10%
    }
}

class Salary extends Income {
    public Salary(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        if (income <= 5000) {
            return 0;
        }
        return (income - 5000) * 0.2;
    }
}

class StateCouncilSpecialAllowance extends Income {
    public StateCouncilSpecialAllowance(double income) {
        super(income);
    }

    @Override
    public double getTax() {
        return 0;
    }
}

觀察totalTax()方法:利用多型totalTax()方法只需要和Income打交道,它完全不需要知道SalaryStateCouncilSpecialAllowance的存在,就可以正確計算出總的稅。如果我們要新增一種稿費收入,只需要從Income派生,然後正確覆寫getTax()方法就可以。把新的型別傳入totalTax(),不需要修改任何程式碼。

可見,多型具有一個非常強大的功能,就是允許新增更多型別的子類實現功能擴充套件,卻不需要修改基於父類的程式碼。

覆寫Object方法

因為所有的class最終都繼承自Object,而Object定義了幾個重要的方法:

  • toString():把instance輸出為String
  • equals():判斷兩個instance是否邏輯相等;
  • hashCode():計算一個instance的雜湊值

在必要的情況下,我們可以覆寫Object的這幾個方法。例如:

class Person {
    ...
    // 顯示更有意義的字串:
    @Override
    public String toString() {
        return "Person:name=" + name;
    }

    // 比較是否相等:
    @Override
    public boolean equals(Object o) {
        // 當且僅當o為Person型別:
        if (o instanceof Person) {
            Person p = (Person) o;
            // 並且name欄位相同時,返回true:
            return this.name.equals(p.name);
        }
        return false;
    }

    // 計算hash:
    @Override
    public int hashCode() {
        return this.name.hashCode();
    }
}

呼叫super

在子類的覆寫方法中,如果要呼叫父類的被覆寫的方法,可以通過super來呼叫。例如:

class Person {
    protected String name;
    public String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    @Override
    public String hello() {
        // 呼叫父類的hello()方法:
        return super.hello() + "!";
    }
}

final

繼承可以允許子類覆寫父類的方法。如果一個父類不允許子類對它的某個方法進行覆寫,可以把該方法標記為final。用final修飾的方法不能被Override

class Person {
    protected String name;
    public final String hello() {
        return "Hello, " + name;
    }
}

Student extends Person {
    // compile error: 不允許覆寫
    @Override
    public String hello() {
    }
}

如果一個類不希望任何其他類繼承自它,那麼可以把這個類本身標記為final。用final修飾的類不能被繼承:

final class Person {
    protected String name;
}

// compile error: 不允許繼承自Person
Student extends Person {
}

對於一個類的例項欄位,同樣可以用final修飾。用final修飾的欄位在初始化後不能被修改。例如:

class Person {
    public final String name = "Unamed";
}

final欄位重新賦值會報錯:

Person p = new Person();
p.name = "New Name"; // compile error!

這種方法更為常用,因為可以保證例項一旦建立,其final欄位就不可修改

小結

  • 子類可以覆寫父類的方法(Override),覆寫在子類中改變了父類方法的行為;

  • Java的方法呼叫總是作用於執行期物件的實際型別,這種行為稱為多型;

  • final修飾符有多種作用:

    • final修飾的方法可以阻止被覆寫;

    • final修飾的class可以阻止被繼承;

    • final修飾的field必須在建立物件時初始化,隨後不可修改。