1. 程式人生 > 實用技巧 >泛型程式設計

泛型程式設計

第八章 泛型程式設計

為什麼要使用泛型程式設計

泛型程式設計(Generic programming) 意味著編寫的程式碼可以被很多不同型別的物件所 重用。例如, 我們並不希望為聚集 String 和 File 物件分別設計不同的類。實際上,也不需要 這樣做,因為一個 ArrayList 類可以聚集任何型別的物件。這是一個泛型程式設計的例項。

型別引數的好處

在 Java 中增加範型類之前, 泛型程式設計是用繼承實現的。ArrayList 類只維護一個 Object 引用的陣列:

public class ArrayList // before generic classes
​
{
    ...
    
private Object[] elementData; public Object get(int i) { . . , } public void add(Object o) { . . . } ​ }

這種方法有兩個問題。當獲取一個值時必須進行強制型別轉換。

ArrayList files = new ArrayList();

String filename = (String) files.get(0);

此外,這裡沒有錯誤檢査。可以向陣列列表中新增任何類的物件。

files,add(new File(". . .");

對於這個呼叫,編譯和執行都不會出錯。然而在其他地方,如果將 get 的結果強制型別 轉換為 String型別, 就會產生一個錯誤。

泛型提供了一個更好的解決方案: 型別引數(type parameters)。ArrayList 類有一個型別 引數用來指示元素的型別:

ArrayList<String> files = new ArrayList<String>();

這使得程式碼具有更好的可讀性。人們一看就知道這個陣列列表中包含的是 String 物件。

誰想成為泛型程式設計師

實現一個泛型類並沒有那麼容易。對於型別引數,使用這段程式碼的程式設計師可能想 要內建(plugin) 所有的類。他們希望在沒有過多的限制以及混亂的錯誤訊息的狀態下, 做 所有的事情。因此,一個泛型程式設計師的任務就是預測出所用類的未來可能有的所有用途。

定義簡單泛型類

一個泛型類(generic class) 就是具有一個或多個型別變數的類。下面 是 Pair 類的程式碼:

public class Pair<T> { 
​
    private T first; 
​
    private T second;
    public Pair() { 
​
        first = null; 
​
        second = null; 
    } 
​
    public PairfT first, T second) { 
        this,first = first; 
        this.second = second; 
    }
    public T getFirst() { 
        return first; 
    } 
    public T getSecondO { 
        return second; 
    }
    public void setFirst(T newValue) { 
        first = newValue; 
    } public void setSecond(T newValue) { 
        second = newValue; 
    }
} 

Pair 類引人了一個型別變數 T,用尖括號 ( < >) 括起來,並放在類名的後面。泛型類可 以有多個型別變數。例如, 可以定義 Pair 類,其中第一個域和第二個域使用不同的型別:

public class Pair<T, U> { . . . }

類定義中的型別變數指定方法的返回型別以及域和區域性變數的型別。例如,

private T first; // uses the type variable

[注] 型別變數使用大寫形式,且比較短, 這是很常見的。在 Java 庫中, 使用變數 E 表示集合的元素型別, K 和 V 分別表示表的關鍵字與值的型別。T ( 需要時還可以用臨近的 字母 U 和 S) 表示“ 任意型別”。

程式清單 8-1 中的程式使用了 Pair 類。靜態的 minmax方法遍歷了陣列並同時計算出最 小值和最大值。它用一個 Pair 物件返回了兩個結果。 回想一下 compareTo 方法比較兩個字元 串, 如果字串相同則返回 0 ; 如果按照字典順序, 第一個字串比第二個字串靠前, 就 返回負值, 否則, 返回正值。

//程式清單 8-1 pair1/PairTest1.java
package pair1;
​
/**
 * @version 1.01 2012-01-26
 * @author Cay Horstmann
 */
public class PairTest1
{
   public static void main(String[] args)
   {
      String[] words = { "Mary", "had", "a", "little", "lamb" };
      Pair<String> mm = ArrayAlg.minmax(words);
      System.out.println("min = " + mm.getFirst());
      System.out.println("max = " + mm.getSecond());
   }
}
​
class ArrayAlg
{
   /**
    * Gets the minimum and maximum of an array of strings.
    * @param a an array of strings
    * @return a pair with the min and max value, or null if a is null or empty
    */
   public static Pair<String> minmax(String[] a)
   {
      if (a == null || a.length == 0) return null;
      String min = a[0];
      String max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair<>(min, max);
   }
}

泛型方法

前面已經介紹瞭如何定義一個泛型類。實際上,還可以定義一個帶有型別引數的簡單方法。

class ArrayAlg {
    public static <T> T getMiddle(T... a) {
        return a[a.length / 2]; 
    } 
} 

這個方法是在普通類中定義的,而不是在泛型類中定義的。然而,這是一個泛型方法, 可以從尖括號和型別變數看出這一點。注意,型別變數放在修飾符(這裡是 public static) 的 後面,返回型別的前面。

泛型方法可以定義在普通類中,也可以定義在泛型類中。

當呼叫一個泛型方法時 在方法名前的尖括號中放人具體的型別:

String middle = ArrayAlg.<String>getMiddle("]ohnM, "Q.n, "Public");

在這種情況(實際也是大多數情況)下,方法呼叫中可以省略 <String> 型別引數。編譯 器有足夠的資訊能夠推斷出所呼叫的方法。

型別變數的限定

有時,類或方法需要對型別變數加以約束。下面是一個典型的例子。我們要計算陣列中 的最小元素:

 class ArrayAIg 
 { 
     public static <T> T min(T[] a) // almost correct 
 { 
     if (a null || a.length = 0) 
         return null; 
     T smallest = a[0]; 
     for (int i = 1 ; i < a.length; i++) 
         if (smallest.compareTo(a[i]) > 0) 
             smallest = a[i]; 
     return smallest; 
 } 
} 

泛型的 min方法只能被實現了 Comparable 介面的類(如 String、 LocalDate 等)的數 組呼叫。由於 Rectangle類沒有實現 Comparable 介面, 所以呼叫 min將會產生一個編譯錯誤。

在程式清單 8-2 的程式中,重新編寫了一個泛型方法 minmax。這個方法計算泛型陣列的 最大值和最小值, 並返回 Pair<T>

//程式清單 8-2 pair2/PairTest2.java 
package pair2;
​
import java.util.*;
​
/**
 * @version 1.01 2012-01-26
 * @author Cay Horstmann
 */
public class PairTest2
{
   public static void main(String[] args)
   {
      GregorianCalendar[] birthdays = 
         { 
            new GregorianCalendar(1906, Calendar.DECEMBER, 9), // G. Hopper
            new GregorianCalendar(1815, Calendar.DECEMBER, 10), // A. Lovelace
            new GregorianCalendar(1903, Calendar.DECEMBER, 3), // J. von Neumann
            new GregorianCalendar(1910, Calendar.JUNE, 22), // K. Zuse
         };
      Pair<GregorianCalendar> mm = ArrayAlg.minmax(birthdays);
      System.out.println("min = " + mm.getFirst().getTime());
      System.out.println("max = " + mm.getSecond().getTime());
   }
}
​
class ArrayAlg
{
   /**
      Gets the minimum and maximum of an array of objects of type T.
      @param a an array of objects of type T
      @return a pair with the min and max value, or null if a is 
      null or empty
   */
   public static <T extends Comparable> Pair<T> minmax(T[] a) 
   {
      if (a == null || a.length == 0) return null;
      T min = a[0];
      T max = a[0];
      for (int i = 1; i < a.length; i++)
      {
         if (min.compareTo(a[i]) > 0) min = a[i];
         if (max.compareTo(a[i]) < 0) max = a[i];
      }
      return new Pair<>(min, max);
   }
}

泛型程式碼和虛擬機器

型別擦除

無論何時定義一個泛型型別, 都自動提供了一個相應的原始型別 ( raw type)。原始型別 的名字就是刪去型別引數後的泛型型別名。擦除(erased) 型別變M, 並替換為限定型別(無 限定的變數用 Object)。

例如, Pair<T> 的原始型別如下所示:

public class Pair { 
    private Object first; 
    private Object second; 
    public Pair(Object first, Object second) { 
        this.first = first; 
        this.second = second;
    }
    public Object getFirst() { 
        return first; 
    } 
    public Object getSecond() { 
        return second; 
    } 
    public void setFirst(Object newValue) { 
        first = newValue; 
    } 
    public void setSecond(Object newValue) { 
        second = newValue; 
    }
} 

因為 T 是一個無限定的變數, 所以直接用 Object 替換。

原始型別用第一個限定的型別變數來替換, 如果沒有給定限定就用 Object 替換。例如, 類 Pair<T> 中的型別變數沒有顯式的限定, 因此, 原始型別用 Object 替換 T。假定聲明瞭一 個不同的型別。

public class Interval<T extends Comparable & Serializable〉implements Serializable { 
    private T lower; private T upper;
    public Interval(T first, T second) { 
        if (first.compareTo(second) <= 0) {
            lower = first; upper = second; 
        } else { 
            lower = second; 
            upper = first; 
        } 
    }
}

原始型別 Interval 如下所示:

 public class Interval implements Serializable { 
     private Comparable lower; 
     private Coiparable upper; 
     public Interval(Coiparable first, Coiparable second) {
         . . . 
     } 
 }

[注] 切換限定: class Interval<T extends Serializable & Comparable> 會發生什麼。如果這樣做, 原始型別用 Serializable 替換 T, 而編譯器在必要時要向 Comparable 插入強制型別轉換。為了提高效率,應該將標籤(tagging) 介面(即沒有方 法的介面)放在邊界列表的末尾。

翻譯泛型表示式

當程式呼叫泛型方法時,如果擦除返回型別, 編譯器插入強制型別轉換。例如,下面這 個語句序列

Pair<Employee> buddies = . . .;

Employee buddy = buddies.getFirst();

擦除 getFirst 的返回型別後將返回 Object 型別。編譯器自動插人 Employee 的強制型別轉換。

也就是說,編譯器把這個方法呼叫翻譯為兩條虛擬機器指令:

•對原始方法 Pair.getFirst 的呼叫。

•將返回的 Object 型別強制轉換為 Employee型別。

當存取一個泛型域時也要插人強制型別轉換。假設 Pair 類的 first 域和 second 域都是公 有的(也許這不是一種好的程式設計風格,但在 Java中是合法的)。表示式:

Employee buddy = buddies.first;

也會在結果位元組碼中插人強制型別轉換。

翻譯泛型方法

型別擦除也會出現在泛型方法中。程式設計師通常認為下述的泛型方法

public static <T extends Comparable〉 T nrin(T[] a)

是一個完整的方法族,而擦除型別之後,只剩下一個方法:

public static Comparable min(Comparable=[] a)

注意,型別引數 T 已經被擦除了, 只留下了限定型別 Comparable。

變數 pair 已經宣告為型別 Pair<LocalDate>, 並且這個型別只有一個簡單的方法叫setSecond, 即 setSecond(Object)。 虛擬機器用 pair 引用的物件呼叫這個方法。這個物件是 Datelnterval 型別的, 因而將會呼叫 Datelnterval.setSecond(Object)方法。這個方法是合成的橋方法。

總之,需要記住有關 Java 泛型轉換的事實:

•虛擬機器中沒有泛型,只有普通的類和方法。

•所有的型別引數都用它們的限定型別替換。

•橋方法被合成來保持多型。

•為保持型別安全性,必要時插人強制型別轉換。

約束與侷限性

不能用基本型別例項化型別引數

沒有 Pair<double>, 只 有 Pair<Double>。 當然, 其原因是型別擦除。擦除之後, Pair 類含有 Object 型別的域, 而 Object 不能儲存 double值。

這的確令人煩惱。但是,這樣做與 Java語言中基本型別的獨立狀態相一致。這並不是一 個致命的缺陷— —只有 8 種基本型別, 當包裝器型別(wrapper type) 不能接受替換時, 可以使用獨立的類和方法處理它們。

執行時型別查詢只適用於原始型別

虛擬機器中的物件總有一個特定的非泛型型別。因此,所有的型別查詢只產生原始型別。

例如:

if (a instanceof Pair<String>) // Error

實際上僅僅測試 a 是否是任意型別的一個 Pair。下面的測試同樣如此:

if (a instanceof Pair<T>) // Error

或強制型別轉換:

Pair<St「ing> p = (Pair<String>) a; // Warning-can only test that a is a Pair

為提醒這一風險, 試圖查詢一個物件是否屬於某個泛型型別時,倘若使用 instanceof 會 得到一個編譯器錯誤, 如果使用強制型別轉換會得到一個警告。

不能建立引數化型別的陣列

不能例項化引數化型別的陣列, 例如:

Pair<String>[] table = new Pair<String>[10]; // Error

陣列會記住它的元素型別, 如果試圖儲存其他型別的元素, 就會丟擲一個 ArrayStoreException 異常:

objarray[0] = "Hello"; // Error component type is Pair

不過對於泛型型別, 擦除會使這種機制無效。以下賦值:

objarray[0] = new Pair<Employee>0;

能夠通過陣列儲存檢査, 不過仍會導致一個型別錯誤。出於這個原因, 不允許建立引數 化型別的陣列。 需要說明的是, 只是不允許建立這些陣列, 而宣告型別為 Pair<String>[] 的變數仍是合法 的。不過不能用

new Pair<String>[10]

初始化這個變數。

Varargs 警告

可以採用兩種方法來抑制這個警告。一種方法是為包含 addAll 呼叫的方法增加註解@ SuppressWamings("unchecked")。 或者在 Java SE 7中, 還 可 以 用@SafeVarargs 直 接 標 注 addAll 方法:

@SafeVarargs

public static <T> void addAll(Collection<T> coll, T... ts)

現在就可以提供泛型型別來呼叫這個方法了。對於只需要讀取引數陣列元素的所有方 法,都可以使用這個註解,這僅限於最常見的用例。

[注] 可以使用 @SafeVarargs 標註來消除建立泛型陣列的有關限制, 方法如下: @SafeVarargs static <E> E()array(E... array) { return array; } 現在可以呼叫: Pair<String>[] table = array(pairl,pai「2); 這看起來彳艮方便,不過隱藏著危險。以下程式碼: Object□ objarray = table; objarray[0] = new Pair<Employee>(); 能順利執行而不會出現 ArrayStoreException 異常(因為陣列儲存只會檢查擦除的類 型),但在處理 table[0] 時你會在別處得到一個異常。

不能例項化型別變置

不能使用像 new T(...),newT[...] 或 T.class 這樣的表示式中的型別變數。例如, 下面的 Pair<T> 構造器就是非法的:

public Pair() { first = new T(); second = new T(); } // Error

型別擦除將 T 改變成 Object, 而且, 本意肯定不希望呼叫 new Object()。在 Java SE 8 之後, 最好的解決辦法是讓呼叫者提供一個構造器表示式。例如:

Pair<String> p = Pair.makePairCString::new);

makePair方法接收一個 Supplier<T>,這是一個函式式介面,表示一個無引數而且返回 型別為 T 的函式:

public static <T> Pair<T> makePair(Supplier<T> constr) { 
​
return new Pair<>(constr.get0.constr.get0);
​
} 

比較傳統的解決方法是通過反射呼叫 Clasmewlnstance 方法來構造泛型物件。

不能構造泛型陣列

型別擦除會讓這個方法永遠構造 Comparable[2] 陣列。 如果陣列僅僅作為一個類的私有例項域, 就可以將這個陣列宣告為 Object[],並且在獲 取元素時進行型別轉換。例如’ ArrayList 類可以這樣實現:

public class ArrayList<E> { 
    private Object口 elements;
    ...
    @SuppressWarnings("unchecked") public E get(int n) { return (E) elements[n]; } 
    public void set(int n, E e) { elements[n] = e; } // no cast needed
} 

實際的實現沒有這麼清晰:

 public class ArrayList<E> { 
     private E[] elements;
     ...
     public ArrayList() { elements = (ED) new Object[10]; 
                        }
 }

這裡, 強制型別轉換 E[ ] 是一個假象, 而型別擦除使其無法察覺。

泛型類的靜態上下文中型別變數無效

不能在靜態域或方法中引用型別變數。例如, 下列高招將無法施展:

public class Singleton<T> 
{ 
    private static T singlelnstance; // Error 
    public static T getSinglelnstanceO // Error
    { 
        if (singleinstance == null) construct new instanceof T 
            return singlelnstance; 
    } 
}

如果這個程式能夠執行, 就可以宣告一個 Singleton<Random> 共享隨機數生成器, 宣告 一個 Singlet0n<JFileCh00Ser> 共享檔案選擇器對話方塊。但是, 這個程式無法工作。型別擦除 之後, 只剩下 Singleton類,它只包含一個 singlelnstance 域。 因此, 禁止使用帶有型別變數 的靜態域和方法。

不能丟擲或捕獲泛型類的例項

既不能丟擲也不能捕獲泛型類物件。實際上, 甚至泛型類擴充套件 Throwable 都是不合法的。

既不能丟擲也不能捕獲泛型類物件。實際上, 甚至泛型類擴充套件 Throwable 都是不合法的。 例如, 以下定義就不能正常編譯:

public class Problem<T> extends Exception { /* ...*/ } // Error can't extend Throwable

catch 子句中不能使用型別變數。

可以消除對受查異常的檢查

Java 異常處理的一個基本原則是, 必須為所有受查異常提供一個處理器。不過可以利用 泛型消除這個限制。關鍵在於以下方法:

@SuppressWamings("unchecked") 
​
public static <T extends Throwable〉void throwAs(Throwable e) throws T { 
​
throw (T) e; 
​
} 

假設這個方法包含在類 Block 中, 如果呼叫

Block.<RuntimeException>throwAs(t);

編譯器就會認為 t 是一個非受查異常。

通過使用泛型類、擦除和@SuppressWamings 註解, 就能消除 Java 型別系統的部分基本 限制。

注意擦除後的衝突

當泛型型別被擦除時, 無法建立引發衝突的條件。

下面是一個示例。假定像下面這樣將 equals方法新增到 Pair 類中:

public class Pair<T> 
{ public boolean equals(T value) { return first,equals(value) && second,equals(value); }
 ...
}

考慮一個 Pair<String>。從概念上講, 它有兩個 equals 方法:

boolean equals(String) // defined in Pair<T>

boolean equals(Object) // inherited from Object

但是,直覺把我們引入歧途。方法擦除

boolean equals(T) 就是

boolean equals(Object)

與 Object.equals方法發生衝突。

當然,補救的辦法是重新命名引發錯誤的方法。

泛型規範說明還提到另外一個原則: “ 要想支援擦除的轉換, 就需要強行限制一個類或類 型變數不能同時成為兩個介面型別的子類,而這兩個介面是同一介面的不同引數化。 ” 例如, 下述程式碼是非法的:

class Employee implements Coinparab1e<Emp1oyee> { . . . } 
class Manager extends Employee implements Comparable<Hanager> 
{... } // Error Manager 

會實現Comparable<Employee> 和 Comparable<Manager>, 這是同一介面的不同 引數化。

泛型型別的繼承規則

永遠可以將引數化型別轉換為一個原始型別。例如,PaiKEmployee> 是原始型別 Pair 的 一個子型別。在與遺留程式碼銜接時,這個轉換非常必要。

必須注意泛型與 Java 陣列之間的重要區別。可以將一個 Manager□ 陣列賦給一個 型別為 Employee[] 的變數:

Manager口 managerBuddies = { ceo, cfo };

Employee[] employeeBuddies = managerBuddies; // OK

然而,陣列帶有特別的保護。如果試圖將一個低級別的僱員儲存到 employeeBuddies[0], 虛擬機器將會丟擲 ArrayStoreException 異常。

永遠可以將引數化型別轉換為一個原始型別。例如,PaiKEmployee> 是原始型別 Pair 的 一個子型別。在與遺留程式碼銜接時,這個轉換非常必要。