1. 程式人生 > 實用技巧 >Java 繼承

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 + "]";
   }
}

//程式清單 5-10 equals/Manager.java
package equals;
​
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);
      bonus = 0;
   }
​
   public double getSalary()
   {
      double baseSalary = super.getSalary();
      return baseSalary + bonus;
   }
​
   public void setBonus(double bonus)
   {
      this.bonus = bonus;
   }
​
   public boolean equals(Object otherObject)
   {
      if (!super.equals(otherObject)) return false;
      var other = (Manager) otherObject;
      // super.equals checked that this and other belong to the same class
      return bonus == other.bonus;
   }
​
   public int hashCode()
   {
      return java.util.Objects.hash(super.hashCode(), bonus);
   }
​
   public String toString()
   {
      return super.toString() + "[bonus=" + bonus + "]";
   }
}
​

泛型陣列列表

ArrayList 是一個採用型別引數(type parameter) 的泛型類(generic class)。為了指定數 組列表儲存的元素物件型別,需要用一對尖括號將類名括起來加在後面, 例如,ArrayList <Employee>

下面宣告和構造一個儲存 Employee 物件的陣列列表:

ArrayList<Employee> staff = new ArrayList<Eniployee>0;

兩邊都使用型別引數 Employee, 這有些繁瑣。Java SE 7中, 可以省去右邊的型別引數:

ArrayList<Employee> staff = new ArrayListoQ;

這被稱為“ 菱形” 語法,因為空尖括號<>就像是一個菱形。

訪問陣列列表元素

陣列列表自動擴充套件容量的便利增加了訪問元素語法的復 雜程度。 其原因是 ArrayList 類並不是 Java 程式設計語言的一部分;它只是一個由某些人編 寫且被放在標準庫中的一個實用類。

使用 get 和 set 方法實現訪問或改變陣列元素的操作,而不使用人們喜愛的 [ ]語法格式。 例如,要設定第 i 個元素,可以使用:

staff.set(i, harry):

它等價於對陣列 a 的元素賦值(陣列的下標從 0開始):

a[i] = harry;

沒有泛型類時, 原始的 ArrayList 類提供的 get 方法別無選擇只能返回 Object, 因 此, get 方法的呼叫者必須對返回值進行型別轉換:

Employee e = (Eiployee) staff.get(i);

原始的 ArrayList 存在一定的危險性。它的 add 和 set 方法允許接受任意型別的物件。 對於下面這個呼叫

staff.set(i, "Harry Hacker");

編譯不會給出任何警告, 只有在檢索物件並試圖對它進行型別轉換時, 才會發現有 問題。如果使用 ArrayList<Employee>, 編譯器就會檢測到這個錯誤。

程式清單 5-11 是對 EmployeeTest 做出修改後的程式。在這裡, 將 Employee[ ] 陣列替換成了 ArrayList<Employee>。請注意下面的變化:

•不必指出陣列的大小。

•使用 add 將任意多的元素新增到陣列中。

•使用 size() 替代 length 計算元素的數目。

•使用 a.get(i) 替代 a[i] 訪問元素。

//程式清單 5-11 arrayList/ArrayListTestjava
package arrayList;
​
import java.util.*;
​
/**
 * This program demonstrates the ArrayList class.
 * @version 1.11 2012-01-26
 * @author Cay Horstmann
 */
public class ArrayListTest
{
   public static void main(String[] args)
   {
      // fill the staff array list with three Employee objects
      var staff = new ArrayList<Employee>();
​
      staff.add(new Employee("Carl Cracker", 75000, 1987, 12, 15));
      staff.add(new Employee("Harry Hacker", 50000, 1989, 10, 1));
      staff.add(new Employee("Tony Tester", 40000, 1990, 3, 15));
​
      // raise everyone's salary by 5%
      for (Employee e : staff)
         e.raiseSalary(5);
​
      // print out information about all Employee objects
      for (Employee e : staff)
         System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" 
            + e.getHireDay());
   }
}

型別化與原始陣列列表的相容性

假設有下面這個遺留下來的類:

public class EmployeeDB { 
​
public void update(ArrayList list) { .. . } 
​
public ArrayList find(String query) { ... } 
​
} 

可以將一個型別化的陣列列表傳遞給 update方法, 而並不需要進行任何型別轉換。

ArrayList<Employee〉staff = . . .; 

employeeDB.update(staff);

也可以將 staff 物件傳遞給 update方法。

[警告] 儘管編譯器沒有給出任何錯誤資訊或警告, 但是這樣呼叫並不太安全。在 update 方法中, 新增到陣列列表中的元素可能不是 Employee 型別。在對這些元素進行檢索時就 會出現異常。 聽起來似乎很嚇人,但思考一下就會發現,這與在 Java 中增加泛型之前是 一樣的 ,, 虛擬機器的完整性絕對沒有受到威脅。在這種情形下, 既沒有降低安全性,也沒 有受益於編譯時的檢查。

相反地,將一個原始 ArrayList 賦給一個型別化 ArrayList 會得到一個警告。

ArrayList<Employee> result = employeeDB.find(query); // yields warning
[注] 為了能夠看到警告性錯誤的文字資訊,要將編譯選項置為 -Xlint:unchecked。

使用型別轉換並不能避免出現警告。

mployee> result = (ArrayList<Employee>) employeeDB.find(query); // yields another warning

樣,將會得到另外一個警告資訊, 指出型別轉換有誤。