Java 繼承
第五章 繼承
人們可以基於已存在的類構造一個新類。繼承已存在的類就 是複用(繼承)這些類的方法和域。在此基礎上,還可以新增一些新的方法和域, 以滿足新 的需求。
類、超類和子類
定義子類
下面是由繼承 Employee 類來定義 Manager 類的格式,關鍵字 extends 表示繼承。
public class Manager extends Employee { 新增方法和域 }[注] :Java 與 C++ 定義繼承類的方式十分相似。Java 用關鍵字 extends 代替了 C++ 中的冒號(:) 。在 Java 中, 所有的繼承都是公有繼承, 而沒有 C++ 中的私有繼承和保 護繼承 。
關鍵字 extends 表明正在構造的新類派生於一個已存在的類。 已存在的類稱為超類 ( superclass)、 基類(base class) 或父類(parent class); 新類稱為子類(subclass)、 派生類 (derivedclass) 或孩子類(child class)。
[提示] : 字首“ 超” 和“ 子” 來源於電腦科學和數學理論中的集合語言的術語。所有僱 員組成的集合包含所有經理組成的集合。可以這樣說, 僱員集合是經理集合的超集, 也 可以說,經理集合是僱員集合的子集。在 Manager類中,增加了一個用於儲存獎金資訊的域,以及一個用於設定這個域的新方法:
public class Manager extends Employee { private double bonus; ... public void setBonos(double bonus){ this.bonus = bonus; } }
這裡定義的方法和域並沒有什麼特別之處。 如果有一個 Manager 物件, 就可以使用 setBonus 方法。
Manager boss = . . .;
boss.setBonus(5000);
當然, 由於 setBonus 方法不是在 Employee 類中定義的,所以屬於 Employee 類的物件不能使 用它。
在通過擴充套件超類定義子類的時候,僅需要指出子類與超類的不同之處。因此在設計類的時候,應該將通用的方法放在超類中, 而將具有特殊用途的方法放在子類中,這種將通用的 功能放到超類的做法,在面向物件程式設計中十分普遍。
覆蓋方法
然而, 超類中的有些方法對子類 Manager 並不一定適用。具體來說, Manager 類中的 getSalary方法應該返回薪水和獎金的總和。為此,需要提供一個新的方法來覆蓋(override) 超類中的這個方法: public class Manager
public class Manager extends Employee { ... public double getSalary(){ ... } ... }
應該如何實現這個方法呢? 只要返回 salary 和 bonus 域的總和就 可以了:
public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; }[注] 有些人認為 super 與 this 引用是類似的概念, 實際上,這樣比較並不太恰當。這是 因為 super 不是一個物件的引用, 不能將 super 賦給另一個物件變數, 它只是一個指示編 譯器呼叫超類方法的特殊關鍵字。
在子類中可以增加域、 增加方法或覆蓋超類的方法,然而絕對 不能刪除繼承的任何域和方法。
子類構造器
我們來提供一個構造器。
public Manager(String name, double salary, int year, int month, int day) { super(name, salary, year, month, day); bonus = 0; }
這裡的關鍵字 super 具有不同的含義。語句 super(n, s, year, month, day); 是“ 呼叫超類 Employee 中含有 n、s、year month 和 day 引數的構造器” 的簡寫形式。
由於 Manager 類的構造器不能訪問 Employee 類的私有域, 所以必須利用 Employee 類 的構造器對這部分私有域進行初始化,我們可以通過 super 實現對超類構造器的呼叫。使用 super 呼叫構造器的語句必須是子類構造器的第一條語句。
如果子類的構造器沒有顯式地呼叫超類的構造器, 則將自動地呼叫超類預設(沒有引數) 的構造器。 如果超類沒有不帶引數的構造器, 並且在子類的構造器中又沒有顯式地呼叫超類 的其他構造器 ’ 則 Java 編譯器將報告錯誤。
[注] 回憶一下, 關鍵字 this 有兩個用途: 一是引用隱式引數,二是呼叫該類其他的構 造器 , 同樣,super 關鍵字也有兩個用途:一是呼叫超類的方法,二是呼叫超類的構造器。 在呼叫構造器的時候, 這兩個關鍵字的使用方式很相似。呼叫構造器的語句只能作為另 一個構造器的第一條語句出現。構造引數既可以傳遞給本類(this) 的其他構造器,也可 以傳遞給超類(super) 的構造器。 [提示] : 在 Java 中, 不需要將方法宣告為虛擬方法。動態繫結是預設的處理方式。如 果不希望讓一個方法具有虛擬特徵, 可以將它標記為 final。程式清單 5-1 的程式展示了 Employee 物件(程式清單 5-2 ) 與 Manager (程式清單 5-3 )物件在薪水計算上的區別。 程式清單 5-1 inheritance/ManagerTest.java
//程式清單 5-1 inheritance/ManagerTest.java package inheritance; /** * This program demonstrates inheritance. * @version 1.21 2004-02-21 * @author Cay Horstmann */ public class ManagerTest { public static void main(String[] args) { // construct a Manager object var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15); boss.setBonus(5000); var staff = new Employee[3]; // fill the staff array with Manager and Employee objects staff[0] = boss; staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1); staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15); // print out information about all Employee objects for (Employee e : staff) System.out.println("name=" + e.getName() + ",salary=" + e.getSalary()); } } //程式清單 5-2 inheritance/Employee.java package inheritance; import java.time.*; 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; } } //程式清單 5-3 inheritance/Manager.java package inheritance; public class Manager extends Employee { private double bonus; /** * @param name the employee's name * @param salary the salary * @param year the hire year * @param month the hire month * @param day the hire day */ public Manager(String name, double salary, int year, int month, int day) { super(name, salary, year, month, day); bonus = 0; } public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; } public void setBonus(double b) { bonus = b; } }
繼承層次
繼承並不僅限於一個層次。 例如, 可以由 Manager 類派生 Executive 類。由一個公共超類派生出來的所有類的集合被稱為繼承層次(inheritance hierarchy), 如圖 5-1 所示。在繼承 層次中, 從某個特定的類到其祖先的路徑被稱為該類的繼承鏈 (inheritance chain)。
多 態
有一個用來判斷是否應該設計為繼承 關係的簡單規則, 這就是“ is-a” 規則, 它 表明子類的每個物件也是超類的物件。
“ is-a” 規則的另一種表述法是置換法則。它表明程式中出現超類物件的任何地方都可以 用子類物件置換。
例如, 可以將一個子類的物件賦給超類變數。
Employee e; e = new Employee(. . .); // Employee object expected e = new Manager(. . .); // OK, Manager can be used as well
在 Java程式設計語言中,物件變數是多型的。 一個 Employee 變數既可以引用一個 Employee 類物件, 也可以引用一個 Employee 類的任何一個子類的物件(例如, Manager、 Executive、Secretary 等)。
從程式清單 5-1 中, 已經看到了置換法則的優點:
Manager boss = new Manager(. . .); Employee[] staff = new Employee[3]; staff[0] = boss;
在這個例子中,變數 staff[0] 與 boss 引用同一個物件。但編譯器將 staff[0]看成 Employee物件。
這意味著, 可以這樣呼叫
boss.setBonus(5000); // OK
但不能這樣呼叫
staff[0].setBonus(5000); // Error
理解方法呼叫
下面是呼叫過程的詳細描述:
1 ) 編譯器査看物件的宣告型別和方法名。假設呼叫 x.f(param),且隱式引數 x宣告為 C 類的物件。需要注意的是:有可能存在多個名字為 f, 但引數型別不一樣的方法。
至此, 編譯器已獲得所有可能被呼叫的候選方法
2 ) 接下來,編譯器將査看呼叫方法時提供的引數型別。如果在所有名為 f 的方法中存在 一個與提供的引數型別完全匹配,就選擇這個方法。這個過程被稱為重栽解析(overloading resolution)。
至此, 編譯器已獲得需要呼叫的方法名字和引數型別
3 ) 如果是 private 方法、 static 方法、final 方法(有關 final 修飾符的含義將在下一節講 述)或者構造器, 那麼編譯器將可以準確地知道應該呼叫哪個方法, 我們將這種呼叫方式稱 為靜態繫結(static binding)。
4 ) 當程式執行,並且採用動態繫結呼叫方法時, 虛擬機器一定呼叫與 x 所引用物件的實 際型別最合適的那個類的方法。
Manager方法表稍微有些不同。其中有三個方法是繼承而來的,一個方法是重新定義的, 還有一個方法是新增加的。
Manager: getName() -> Employee.getName() getSalary() -> Manager.getSalary() getHireDay() -> Employee.getHireDay() raiseSalary(double) -> Employee.raiseSalary(double) setBonus(double) -> Manager.setBonus(double)
在執行時, 呼叫 e.getSalaryO 的解析過程為:
1 ) 首先,虛擬機器提取 e 的實際型別的方法表。
2 ) 接下來, 虛擬機器搜尋定義 getSalary 簽名的類。
3) 最後,虛擬機器呼叫方法。
阻止繼承:final 類和方法
不允許擴充套件的類被稱為 final 類。如果 在定義類的時候使用了 final 修飾符就表明這個類是 final 類。
宣告格式如下所示:
public final class Executive extends Manager { . . . }
類中的特定方法也可以被宣告為 final。如果這樣做,子類就不能覆蓋這個方法(final 類中的所有方法自動地成為 final 方法)。例如
public class Employee { ... public final String getName(){ return name; } . . . }[注] 前面曾經說過, 域也可以被宣告為 final。對於 final 域來說, 構造物件之後就不允 許改變它們的值了。不過, 如果將一個類宣告為 final, 只有其中的方法自動地成為 final, 而不包括域。
強制型別轉換
將一個型別強制轉換成另外一個型別的過程被稱為型別轉換。Java 程 序設計語言提供了一種專門用於進行型別轉換的表示法。例如:
double x = 3.405; int nx = (int) x;
將表示式 x 的值轉換成整數型別, 捨棄了小數部分。
正像有時候需要將浮點型數值轉換成整型數值一樣,有時候也可能需要將某個類的物件 引用轉換成另外一個類的物件引用。物件引用的轉換語法與數值表示式的型別轉換類似, 僅 需要用一對圓括號將目標類名括起來,並放置在需要轉換的物件引用之前就可以了。例如:
Manager boss = (Manager) staff[0];
進行型別轉換的唯一原因是:在暫時忽視物件的實際型別之後,使用物件的全部功能。
這個過程簡單地使用 instanceof操作符就可以實現。 例如:
if (staff[1 ] instanceof Manager) { boss = (Manager) staff[1 ]: }
如果這個型別轉換不可能成功, 編譯器就不會進行這個轉換。
進行強制型別轉換需要注意以下兩點:
•只能在繼承層次內進行型別轉換。
•在將超類轉換成子類之前,應該使用 instanceof進行檢查。
如果 x 為 null, 進行下列測試 x instanceof C 不會產生異常, 只是返回 false。之所以這樣處理是因為 null 沒有引用任何物件, 當 然也不會引用 C 型別的物件。
抽象類
如果自下而上在類的繼承層次結構中上移,位於上層的類更具有通用性,甚至可能更加抽象。從某種角度看, 祖先類更加通用, 人們只將它作為派生其他類的基類,而不作為想使 用的特定的例項類。例如, 考慮一下對 Employee 類層次的擴充套件。一名僱員是一個人, 一名學生也是一個人。下面將類 Person 和類 Student 新增到類的層次結構中。圖 5-2 是這三個類 之間的關係層次圖。
為了提高程式的清晰度, 包含一個或多個抽象方法的類本身必須被宣告為抽象的。
public abstract class Person { public abstract String getDescription(); }
除了抽象方法之外,抽象類還可以包含具體資料和具體方法。例如, Person 類還儲存著 姓名和一個返回姓名的具體方法。
public abstract class Person { private String name; public Person(String name) { this.name = name; } public abstract String getDescription(); public String getName(){ return name; } }[注] 許多程式設計師認為,在抽象類中不能包含具體方法。建議儘量將通用的域和方法(不 管是否是抽象的)放在超類(不管是否是抽象類)中。
抽象方法充當著佔位的角色, 它們的具體實現在子類中。擴充套件抽象類可以有兩種選擇。 一種是在抽象類中定義部分抽象類方法或不定義抽象類方法,這樣就必須將子類也標記為抽 象類;另一種是定義全部的抽象方法,這樣一來,子類就不是抽象的了。
類即使不含抽象方法,也可以將類宣告為抽象類。 抽象類不能被例項化。
需要注意,可以定義一個抽象類的物件變數, 但是它只能引用非抽象子類的物件。例如,
Person p = new Student("Vinee Vu", "Economics");
這裡的 p 是一個抽象類 Person 的變數,Person 引用了一個非抽象子類 Student 的例項。
//程式清單 5-4 abstractClasses/PersonTest.java package abstractClasses; /** * This program demonstrates abstract classes. * @version 1.01 2004-02-21 * @author Cay Horstmann */ public class PersonTest { public static void main(String[] args) { var people = new Person[2]; // fill the people array with Student and Employee objects people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1); people[1] = new Student("Maria Morris", "computer science"); // print out names and descriptions of all Person objects for (Person p : people) System.out.println(p.getName() + ", " + p.getDescription()); } }
//程式清單 5-5 abstractClasses/Person.java package abstractClasses; public abstract class Person { public abstract String getDescription(); private String name; public Person(String name) { this.name = name; } public String getName() { return name; } }
//程式清單 5-6 abstractClasses/Employee.java package abstractClasses; import java.time.*; public class Employee extends Person { private double salary; private LocalDate hireDay; public Employee(String name, double salary, int year, int month, int day) { super(name); this.salary = salary; hireDay = LocalDate.of(year, month, day); } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; } public String getDescription() { return String.format("an employee with a salary of $%.2f", salary); } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } }
//程式清單 5-7 abstractClasses/Student.java package abstractClasses; public class Student extends Person { private String major; /** * @param name the student's name * @param major the student's major */ public Student(String name, String major) { // pass name to superclass constructor super(name); this.major = major; } public String getDescription() { return "a student majoring in " + major; } }
受保護訪問
大家都知道,最好將類中的域標記為 private, 而方法標記為 public。任何宣告為private 的內容對其他類都是不可見的。前面已經看到, 這對於子類來說也完全適用,即子類也不能 訪問超類的私有域。
受保護的方法更具有實際意義。如果需要限制某個方法的使用, 就可以將它宣告為 protected。這表明子類(可能很熟悉祖先類)得到信任,可以正確地使用這個方法,而其他 類則不行。
下面歸納一下 Java 用於控制可見性的 4 個訪問修飾符:
1 ) 僅對本類可見 private。
2 ) 對所有類可見 public。
3 ) 對本包和所有子類可見 protected。
4 ) 對本包可見— —預設(很遺憾), 不需要修飾符。
Object: 所有類的超類
Object 類是 Java 中所有類的始祖, 在 Java 中每個類都是由它擴充套件而來的。但是並不需 要這樣寫:
public class Employee extends Object
如果沒有明確地指出超類,Object 就被認為是這個類的超類。
equals 方法
Object 類中的 equals方法用於檢測一個物件是否等於另外一個物件。在 Object 類中,這 個方法將判斷兩個物件是否具有相同的引用。如果兩個物件具有相同的引用, 它們一定是相 等的。
例如, 如果兩個僱員物件的姓名、 薪水和僱傭日期都一樣, 就認為它們是相等的(在實 際的僱員資料庫中,比較 ID 更有意義。利用下面這個示例演示 equals 方法的實現機制)。
public class Employee{ ... public boolean equals(Object otherObject) { // a quick test to see if the objects are identical if (this == otherObject) return true; // must return false if the explicit parameter is null if (otherObject == null) return false; // if the classes don't match, they can't be equal if (getClassO != otherObject.getClass()) return false; // now we know otherObject is a non-null Employee Employee other = (Employee) otherObject; // test whether the fields have identical values return name.equals(other.name) && salary = other,salary && hi reDay.equals(other,hi reDay): • } }
getClass方法將返回一個物件所屬的類,有關這個方法的詳細內容稍後進行介紹。在檢 測中, 只有在兩個物件屬於同一個類時, 才有可能相等。
[提示] 為了防備 name 或 hireDay 可能為 null 的情況, 需要使用 Objects.equals 方法。如 果兩個引數都為 null, Objects.equals(a,b) 呼叫將返回 true ; 如果其中一個引數為 null, 則返回 false ; 否則, 如果兩個引數都不為 null, 則呼叫 a.equals(b)。 利用這個方法, Employee.equals 方法的最後一條語句要改寫為: return Objects.equals(name, other.name) && salary == other.salary && Object.equals(hireDay, other.hireDay);相等測試與繼承
如果發現類不匹配,equals方法就返冋 false: 但是,許多程式設計師 卻喜歡使用 instanceof進行檢測:
if (KotherObject instanceof Employee)) return false;
這樣做不但沒有解決 otherObject 是子類的情況,並且還有可能會招致一些麻煩。這就是建議 不要使用這種處理方式的原因所在。Java語言規範要求 equals 方法具有下面的特性:
1 ) 自反性:對於任何非空引用 x, x.equals(?0應該返回 truec
2 ) 對稱性: 對於任何引用 x 和 y, 當且僅當 y.equals(x) 返回 true, x.equals(y) 也應該返 回 true。
3 ) 傳遞性: 對於任何引用 x、 y 和 z, 如果 x.equals(y) 返N true, y.equals(z)返回 true, x.equals(z) 也應該返回 true。
4 ) 一致性: 如果 x 和 y引用的物件沒有發生變化,反覆呼叫 x.eqimIS(y) 應該返回同樣 的結果。
5 ) 對於任意非空引用 x, x.equals(null) 應該返回 false。
下面可以從兩個截然不同的情況看一下這個問題:
•如果子類能夠擁有自己的相等概念, 則對稱性需求將強制採用 getClass 進行檢測
•如果由超類決定相等的概念,那麼就可以使用 imtanceof進行檢測, 這樣可以在不同 子類的物件之間進行相等的比較。
hashCode 方法
雜湊碼( hash code) 是由物件匯出的一個整型值。雜湊碼是沒有規律的。如果 x 和 y 是 兩個不同的物件, x.hashCode( ) 與 y.hashCode( ) 基本上不會相同。
String 類使用下列演算法計算雜湊碼:
int hash = 0;
for (int i = 0; i < length0;i++)
hash = 31 * hash + charAt(i);
由於 hashCode方法定義在 Object 類中, 因此每個物件都有一個預設的雜湊碼,其值為 物件的儲存地址。
[提示] :如果存在陣列型別的域, 那麼可以使用靜態的 Arrays.hashCode 方法計算一個雜湊 ?,這個雜湊碼由陣列元素的雜湊碼組成。程式清單 5-8 的程式實現了 Employee 類(程式清單 5-9 ) 和 Manager•類(程式清單 5-10 ) 的 equals、hashCode 和 toString方法。
//程式清單 5-8 equals/EqualsTest.java package equals; /** * This program demonstrates the equals method. * @version 1.12 2012-01-26 * @author Cay Horstmann */ public class EqualsTest { public static void main(String[] args) { var alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15); var alice2 = alice1; var alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15); var bob = new Employee("Bob Brandson", 50000, 1989, 10, 1); System.out.println("alice1 == alice2: " + (alice1 == alice2)); System.out.println("alice1 == alice3: " + (alice1 == alice3)); System.out.println("alice1.equals(alice3): " + alice1.equals(alice3)); System.out.println("alice1.equals(bob): " + alice1.equals(bob)); System.out.println("bob.toString(): " + bob); var carl = new Manager("Carl Cracker", 80000, 1987, 12, 15); var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15); boss.setBonus(5000); System.out.println("boss.toString(): " + boss); System.out.println("carl.equals(boss): " + carl.equals(boss)); System.out.println("alice1.hashCode(): " + alice1.hashCode()); System.out.println("alice3.hashCode(): " + alice3.hashCode()); System.out.println("bob.hashCode(): " + bob.hashCode()); System.out.println("carl.hashCode(): " + carl.hashCode()); } }
//程式清單 5-9 equals/Employee.java package equals; import java.time.*; 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; } public boolean equals(Object otherObject) { // a quick test to see if the objects are identical if (this == otherObject) return true; // must return false if the explicit parameter is null if (otherObject == null) return false; // if the classes don't match, they can't be equal if (getClass() != otherObject.getClass()) return false; // now we know otherObject is a non-null Employee var other = (Employee) otherObject; // test whether the fields have identical values return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay); } public int hashCode() { return Objects.hash(name, salary, hireDay); } public String toString() { return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]"; } }