1. 程式人生 > >Java基礎學習——泛型(generics)學習一

Java基礎學習——泛型(generics)學習一

概述

在JDK 5.0,Java語言引入了好幾個新的功能,其中很重要的一個就是泛型(generics)。

本文就是對泛型的一個概述。你可以很熟悉其他語言中的類似結構,比如C++裡的模板(templates)。如果這樣,你將會看到兩者之間有相似,同時也有很大的不同。如果你之前並不熟悉酷似的東西,那就更好了,你將會沒有包袱,從而重頭開始。

泛型(generics)是一種對型別(types)的抽象。最常見的例子便是容量型別,比如 Java提供的Collections。

下面是Java中典型的排序用法。

List myIntList = new LinkedList(); // 1
myIntList.
add(new Integer(0)); // 2 Integer x = (Integer) myIntList.iterator().next(); // 3

第三行的強制型別轉換是比較煩人的。通常,程式設計師都是知道List裡是放的什麼型別的資料,然後,強制型別轉換卻是必不可少的。編譯器只能僅僅保證Object物件被迭代器返回(iterator)。為了Integer變數賦值操作的型別安全,強制型別轉換是必須的。

當然,強制型別轉換並不僅僅帶來混亂,還有可能因為程式設計師的失誤,帶來執行時的錯誤(run time error)。

能不能有種機制,程式設計師能明確表達意圖,使一個List僅僅只能包含一個指明的型別?那這便是泛型(generics)的核心思想。下面是上面那份程式碼的泛型版本。

List<Integer> myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'

留意下變數myIntList的宣告,它並不只是個List,而是一個被寫作List<Integer>的Integer List。我們叫List是一個泛型介面(generic interface),擁有一個type引數,Integer。當我們新建一個List物件的時候,需要特別指定一個type引數。同時,我們可以看到,第三行的強制型別轉換也消失了。

現在,你也行會認為我們已經將雜亂的麻煩移除了。我們用第一行的型別引數Integer替換了第三行的Integer強制型別轉換。然而,這兩者卻又很大的不同。編譯器現在可以在編譯期進行型別安全檢測了。當我們用List<Integer>型別定義myIntList時,便告訴了編譯器一些約定,同時,編譯器會保證這些。在一些大型程式裡,泛型提供了更好的健壯性。

簡單定義

下面是Jdk中java.util包內,List和Iterator的定義的一些簡單擇錄。

public interface List <E> {
    void add(E x);
    Iterator<E> iterator();
}

public interface Iterator<E> {
    E next();
    boolean hasNext();
}

這些程式碼都是相似的,除了尖括號裡的內容。尖括號裡的內容其實就是List和Iterator的型別引數。

型別引數會用在泛型宣告中所有需要真實型別地方(雖然有很多限制)。

在概述中,我們已經看到了泛型List的呼叫方法;在這個呼叫中,所有出現型別引數(通常也被叫做引數化的型別(parameterized type))的地方(在上面例子中的E)都會被真實型別替換(在上面例子中的Integer)。

你可能會想List<Integer>就是List的一個所有E都會Integer替換的特殊版本,像下面一樣:

public interface IntegerList {
    void add(Integer x);
    Iterator<Integer> iterator();
}

這種類比,雖然有些好處,但它同時也是一種誤導。

說這種類比有好處,是因為引數化型別的List<Integer>的確看起來像是這種擴充套件。
說它誤導,是因為泛型重沒有做這種替換擴充套件。泛型不是原始碼的副本,也不是二進位制的,不是硬碟上副本,也不是記憶體裡的副本。如果你是個C++程式設計師,你就會發現這與C++模板非常不同。

一個泛型型別的原始碼僅僅只會被編譯一次,被轉成一個Class檔案,就像普通類和介面一樣。

型別引數在方法或建構函式裡的使用方式和普通引數一樣。就像函式引數宣告表示值引數(formal value parameters),描述的是執行的值一樣,一個泛型宣告表示一個型別引數(formal type parameters)。當一個方法被呼叫時,實際引數會取代形式引數,然後執行函式體。當一個泛型被呼叫時,這個實際型別會用來替換形式型別引數。

一些關於命名約定的注意事項。我們推薦使用簡短的方式(如果可以使用單字母)。最好能避免使用小寫字母,從而很方便和普通類和介面名稱區分。像上面的例子一樣,很多集合型別都用E代表元素。後面我們還會有更多的例子。

泛型和子型別(Generics and Subtyping)

讓我們測試對泛型的理解。下面的程式碼片段是否合法呢?

List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2 

第一行,毫無疑問是合法的。棘手的問題是第二行。這個也就是說:一個String 的list是否是一個Object的List。大部分的人可能脫口而出說,是的。

那我們繼續看下面的這幾行:

lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!

上面的例子,我們將lo當成ls的別名。通過別名lo訪問了String的list ls,然後隨便插入了一個Object物件進去。結果ls就再也不能只儲存String的物件了,當我們想從ls裡取出一些東西的時候,我們肯定會大吃一驚。

Java的編譯器會阻止上面的事情發生。上面的第二行會引起一個編譯期錯誤。
一般而言,如果Foo是Bar的一個子型別(subtype,(subclass or subinterface)),然後G是一個泛型定義,這不會導致G<Foo>是G<Bar>的一個子型別。這個也是泛型學習裡很困難的事情,因為它違揹我們深信不疑的直覺。

我們不能假設容器都是不變的。我們的直覺總是讓我們感覺這些東西都是一成不變的。舉個栗子,如果交管局(the department of motor vehicles,DMV)擁有一個人口普查局(the census bureau)裡機動車駕駛員戶口資訊的list,這其實挺合理的。我們可能會認為List<Driver> 是一個List<Person>(is-a),假設Driver是Person的子類。但事實,只是把駕駛員資訊做了一份拷貝。否則,如果人口普查局(the census bureau)新增一個非駕駛員的人口資訊,將會汙染DMV裡的記錄。

為了應對這種情況,我們需要更靈活地看待泛型的型別。到目前為止,我們看到的規則都是相當嚴格的。