va泛型

ke_ry發表於2016-11-16

va泛型

什麼是泛型

泛型(Generic type 或者 generics)是對 Java 語言的型別系統的一種擴充套件,以支援建立可以按型別進行引數化的類。可以把型別引數看作是使用引數化型別時指定的型別的一個佔位符,就像方法的形式引數是執行時傳遞的值的佔位符一樣。

可以在集合框架(Collection framework)中看到泛型的動機。例如,Map 類允許您向一個 Map 新增任意類的物件,即使最常見的情況是在給定對映(map)中儲存某個特定型別(比如 String)的物件。

因為 Map.get() 被定義為返回 Object,所以一般必須將 Map.get() 的結果強制型別轉換為期望的型別,如下面的程式碼所示:

Map m = new HashMap();
m.put(“key”, “blarg”);
String s = (String) m.get(“key”);

要讓程式通過編譯,必須將 get() 的結果強制型別轉換為 String,並且希望結果真的是一個 String。但是有可能某人已經在該對映中儲存了不是 String 的東西,這樣的話,上面的程式碼將會丟擲 ClassCastException。

理想情況下,您可能會得出這樣一個觀點,即 m 是一個 Map,它將 String 鍵對映到 String 值。這可以讓您消除程式碼中的強制型別轉換,同時獲得一個附加的型別檢查層,該檢查層可以防止有人將錯誤型別的鍵或值儲存在集合中。這就是泛型所做的工作。

 

泛型的好處

Java 語言中引入泛型是一個較大的功能增強。不僅語言、型別系統和編譯器有了較大的變化,以支援泛型,而且類庫也進行了大翻修,所以許多重要的類,比如集合框架,都已經成為泛型化的了。這帶來了很多好處:

型別安全。 泛型的主要目標是提高 Java 程式的型別安全。通過知道使用泛型定義的變數的型別限制,編譯器可以在一個高得多的程度上驗證型別假設。沒有泛型,這些假設就只存在於程式設計師的頭腦中(或者如果幸運的話,還存在於程式碼註釋中)。

Java 程式中的一種流行技術是定義這樣的集合,即它的元素或鍵是公共型別的,比如“String 列表”或者“String 到 String 的對映”。通過在變數宣告中捕獲這一附加的型別資訊,泛型允許編譯器實施這些附加的型別約束。型別錯誤現在就可以在編譯時被捕獲了,而不是在執行時當作 ClassCastException 展示出來。將型別檢查從執行時挪到編譯時有助於您更容易找到錯誤,並可提高程式的可靠性。

消除強制型別轉換。 泛型的一個附帶好處是,消除原始碼中的許多強制型別轉換。這使得程式碼更加可讀,並且減少了出錯機會。

儘管減少強制型別轉換可以降低使用泛型類的程式碼的羅嗦程度,但是宣告泛型變數會帶來相應的羅嗦。比較下面兩個程式碼例子。

該程式碼不使用泛型

List li = new ArrayList();
li.put(new Integer(3));
Integer i = (Integer) li.get(0);

該程式碼使用泛型

List<Integer> li = new ArrayList<Integer>();
li.put(new Integer(3));
Integer i = li.get(0);

在簡單的程式中使用一次泛型變數不會降低羅嗦程度。但是對於多次使用泛型變數的大型程式來說,則可以累積起來降低羅嗦程度。

潛在的效能收益。 泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制型別轉換(沒有泛型的話,程式設計師會指定這些強制型別轉換)插入生成的位元組碼中。但是更多型別資訊可用於編譯器這一事實,為未來版本的 JVM 的優化帶來可能。

由於泛型的實現方式,支援泛型(幾乎)不需要 JVM 或類檔案更改。所有工作都在編譯器中完成,編譯器生成類似於沒有泛型(和強制型別轉換)時所寫的程式碼,只是更能確保型別安全而已。

泛型用法的例子

泛型的許多最佳例子都來自集合框架,因為泛型讓您在儲存在集合中的元素上指定型別約束。考慮這個使用 Map 類的例子,其中涉及一定程度的優化,即 Map.get() 返回的結果將確實是一個 String:

Map m = new HashMap();
m.put(“key”, “blarg”);
String s = (String) m.get(“key”);

如果有人已經在對映中放置了不是 String 的其他東西,上面的程式碼將會丟擲 ClassCastException。泛型允許您表達這樣的型別約束,即 m 是一個將 String 鍵對映到 String 值的 Map。這可以消除程式碼中的強制型別轉換,同時獲得一個附加的型別檢查層,這個檢查層可以防止有人將錯誤型別的鍵或值儲存在集合中。

下面的程式碼示例展示了 JDK 5.0 中集合框架中的 Map 介面的定義的一部分:

public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}

注意該介面的兩個附加物:

型別引數 K 和 V 在類級別的規格說明,表示在宣告一個 Map 型別的變數時指定的型別的佔位符。

在 get()、put() 和其他方法的方法簽名中使用的 K 和 V。

為了贏得使用泛型的好處,必須在定義或例項化 Map 型別的變數時為 K 和 V 提供具體的值。以一種相對直觀的方式做這件事:

Map<String, String> m = new HashMap<String, String>();
m.put(“key”, “blarg”);
String s = m.get(“key”);

當使用 Map 的泛型化版本時,您不再需要將 Map.get() 的結果強制型別轉換為 String,因為編譯器知道 get() 將返回一個 String。

在使用泛型的版本中並沒有減少鍵盤錄入;實際上,比使用強制型別轉換的版本需要做更多鍵入。使用泛型只是帶來了附加的型別安全。因為編譯器知道關於您將放進 Map 中的鍵和值的型別的更多資訊,所以型別檢查從執行時挪到了編譯時,這會提高可靠性並加快開發速度。

向後相容

在 Java 語言中引入泛型的一個重要目標就是維護向後相容。儘管 JDK 5.0 的標準類庫中的許多類,比如集合框架,都已經泛型化了,但是使用集合類(比如 HashMap 和 ArrayList)的現有程式碼將繼續不加修改地在 JDK 5.0 中工作。當然,沒有利用泛型的現有程式碼將不會贏得泛型的型別安全好處。

 

二 泛型基礎

型別引數

在定義泛型類或宣告泛型類的變數時,使用尖括號來指定形式型別引數。形式型別引數與實際型別引數之間的關係類似於形式方法引數與實際方法引數之間的關係,只是型別參數列示型別,而不是表示值。

泛型類中的型別引數幾乎可以用於任何可以使用類名的地方。例如,下面是 java.util.Map 介面的定義的摘錄:

public interface Map<K, V> {
public void put(K key, V value);
public V get(K key);
}

Map 介面是由兩個型別引數化的,這兩個型別是鍵型別 K 和值型別 V。(不使用泛型)將會接受或返回 Object 的方法現在在它們的方法簽名中使用 K 或 V,指示附加的型別約束位於 Map 的規格說明之下。

當宣告或者例項化一個泛型的物件時,必須指定型別引數的值:

Map<String, String> map = new HashMap<String, String>();

注意,在本例中,必須指定兩次型別引數。一次是在宣告變數 map 的型別時,另一次是在選擇 HashMap 類的引數化以便可以例項化正確型別的一個例項時。

編譯器在遇到一個 Map<String, String> 型別的變數時,知道 K 和 V 現在被繫結為 String,因此它知道在這樣的變數上呼叫 Map.get() 將會得到 String 型別。

除了異常型別、列舉或匿名內部類以外,任何類都可以具有型別引數。

命名型別引數

推薦的命名約定是使用大寫的單個字母名稱作為型別引數。這與 C++ 約定有所不同(參閱 附錄 A:與 C++ 模板的比較),並反映了大多數泛型類將具有少量型別引數的假定。對於常見的泛型模式,推薦的名稱是:

K —— 鍵,比如對映的鍵。 
V —— 值,比如 List 和 Set 的內容,或者 Map 中的值。 
E —— 異常類。 
T —— 泛型

泛型不是協變的

關於泛型的混淆,一個常見的來源就是假設它們像陣列一樣是協變的。其實它們不是協變的。List<Object> 不是 List<String> 的父型別。

如果 A 擴充套件 B,那麼 A 的陣列也是 B 的陣列,並且完全可以在需要 B[] 的地方使用 A[]:

Integer[] intArray = new Integer[10]; 
Number[] numberArray = intArray;

上面的程式碼是有效的,因為一個 Integer 是 一個 Number,因而一個 Integer 陣列是 一個 Number 陣列。但是對於泛型來說則不然。下面的程式碼是無效的:

List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid

最初,大多數 Java 程式設計師覺得這缺少協變很煩人,或者甚至是“壞的(broken)”,但是之所以這樣有一個很好的原因。如果可以將 List<Integer> 賦給 List<Number>,下面的程式碼就會違背泛型應該提供的型別安全:

List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = intList; // invalid
numberList.add(new Float(3.1415));

因為 intList 和 numberList 都是有別名的,如果允許的話,上面的程式碼就會讓您將不是 Integers 的東西放進 intList 中。但是,正如下一屏將會看到的,您有一個更加靈活的方式來定義泛型

型別萬用字元

假設您具有該方法:

void printList(List l) { 
for (Object o : l) 
    System.out.println(o); 
}

上面的程式碼在 JDK 5.0 上編譯通過,但是如果試圖用 List<Integer> 呼叫它,則會得到警告。出現警告是因為,您將泛型(List<Integer>)傳遞給一個只承諾將它當作 List(所謂的原始型別)的方法,這將破壞使用泛型的型別安全。

如果試圖編寫像下面這樣的方法,那麼將會怎麼樣?

void printList(List<Object> l) { 
for (Object o : l) 
    System.out.println(o); 
}

它仍然不會通過編譯,因為一個 List<Integer> 不是 一個 List<Object>(正如前一屏 泛型不是協變的 中所學的)。這才真正煩人 —— 現在您的泛型版本還沒有普通的非泛型版本有用!

解決方案是使用型別萬用字元:

void printList(List<?> l) { 
for (Object o : l) 
    System.out.println(o); 
}

上面程式碼中的問號是一個型別萬用字元。它讀作“問號”。List<?> 是任何泛型 List 的父型別,所以您完全可以將 List<Object>、List<Integer> 或 List<List<List<Flutzpah>>> 傳遞給 printList()。

型別萬用字元的作用

前一屏 型別萬用字元 中引入了型別萬用字元,這讓您可以宣告 List<?> 型別的變數。您可以對這樣的 List 做什麼呢?非常方便,可以從中檢索元素,但是不能新增元素。原因不是編譯器知道哪些方法修改列表哪些方法不修改列表,而是(大多數)變化的方法比不變化的方法需要更多的型別資訊。下面的程式碼則工作得很好:

List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
System.out.println(lu.get(0));

為什麼該程式碼能工作呢?對於 lu,編譯器一點都不知道 List 的型別引數的值。但是編譯器比較聰明,它可以做一些型別推理。在本例中,它推斷未知的型別引數必須擴充套件 Object。(這個特定的推理沒有太大的跳躍,但是編譯器可以作出一些非常令人佩服的型別推理,後面就會看到(在 底層細節 一節中)。所以它讓您呼叫 List.get() 並推斷返回型別為 Object。

另一方面,下面的程式碼不能工作:

List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.add(new Integer(43)); // error

在本例中,對於 lu,編譯器不能對 List 的型別引數作出足夠嚴密的推理,以確定將 Integer 傳遞給 List.add() 是型別安全的。所以編譯器將不允許您這麼做。

以免您仍然認為編譯器知道哪些方法更改列表的內容哪些不更改列表內容,請注意下面的程式碼將能工作,因為它不依賴於編譯器必須知道關於 lu 的型別引數的任何資訊:

List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(42));
List<?> lu = li;
lu.clear();

泛型方法

(在 型別引數 一節中)您已經看到,通過在類的定義中新增一個形式型別引數列表,可以將類泛型化。方法也可以被泛型化,不管它們定義在其中的類是不是泛型化的。

泛型類在多個方法簽名間實施型別約束。在 List<V> 中,型別引數 V 出現在 get()、add()、contains() 等方法的簽名中。當建立一個 Map<K, V> 型別的變數時,您就在方法之間宣稱一個型別約束。您傳遞給 add() 的值將與 get() 返回的值的型別相同。

類似地,之所以宣告泛型方法,一般是因為您想要在該方法的多個引數之間宣稱一個型別約束。例如,下面程式碼中的 ifThenElse() 方法,根據它的第一個引數的布林值,它將返回第二個或第三個引數:

public <T> T ifThenElse(boolean b, T first, T second) {
return b ? first : second;
}

注意,您可以呼叫 ifThenElse(),而不用顯式地告訴編譯器,您想要 T 的什麼值。編譯器不必顯式地被告知 T 將具有什麼值;它只知道這些值都必須相同。編譯器允許您呼叫下面的程式碼,因為編譯器可以使用型別推理來推斷出,替代 T 的 String 滿足所有的型別約束:

String s = ifThenElse(b, “a”, “b”);

類似地,您可以呼叫:

Integer i = ifThenElse(b, new Integer(1), new Integer(2));

但是,編譯器不允許下面的程式碼,因為沒有型別會滿足所需的型別約束:

String s = ifThenElse(b, “pi”, new Float(3.14));

為什麼您選擇使用泛型方法,而不是將型別 T 新增到類定義呢?(至少)有兩種情況應該這樣做:

泛型方法是靜態的時,這種情況下不能使用類型別引數。

當 T 上的型別約束對於方法真正是區域性的時,這意味著沒有在相同類的另一個 方法簽名中使用相同 型別 T 的約束。通過使得泛型方法的型別引數對於方法是區域性的,可以簡化封閉型別的簽名。

有限制型別

在前一屏 泛型方法 的例子中,型別引數 V 是無約束的或無限制的 型別。有時在還沒有完全指定型別引數時,需要對型別引數指定附加的約束。

考慮例子 Matrix 類,它使用型別引數 V,該引數由 Number 類來限制:

public class Matrix<V extends Number> { … }

編譯器允許您建立 Matrix<Integer> 或 Matrix<Float> 型別的變數,但是如果您試圖定義 Matrix<String> 型別的變數,則會出現錯誤。型別引數 V 被判斷為由 Number 限制 。在沒有型別限制時,假設型別引數由 Object 限制。這就是為什麼前一屏 泛型方法 中的例子,允許 List.get() 在 List<?> 上呼叫時返回 Object,即使編譯器不知道型別引數 V 的型別。

 

三 一個簡單的泛型

編寫基本的容器類

此時,您可以開始編寫簡單的泛型類了。到目前為止,泛型類最常見的用例是容器類(比如集合框架)或者值持有者類(比如 WeakReference 或 ThreadLocal)。我們來編寫一個類,它類似於 List,充當一個容器。其中,我們使用泛型來表示這樣一個約束,即 Lhist 的所有元素將具有相同型別。為了實現起來簡單,Lhist 使用一個固定大小的陣列來儲存值,並且不接受 null 值。

Lhist 類將具有一個型別引數 V(該引數是 Lhist 中的值的型別),並將具有以下方法:

public class Lhist<V> { 
public Lhist(int capacity) { … }
public int size() { … }
public void add(V value) { … }
public void remove(V value) { … }
public V get(int index) { … }
}

要例項化 Lhist,只要在宣告時指定型別引數和想要的容量:

Lhist<String> stringList = new Lhist<String>(10);

實現建構函式

在實現 Lhist 類時,您將會遇到的第一個攔路石是實現建構函式。您可能會像下面這樣實現它:

public class Lhist<V> { 
private V[] array;
public Lhist(int capacity) {
    array = new V[capacity]; // illegal
}
}

這似乎是分配後備陣列最自然的一種方式,但是不幸的是,您不能這樣做。具體原因很複雜,當學習到 底層細節 一節中的“擦除”主題時,您就會明白。分配後備陣列的實現方式很古怪且違反直覺。下面是建構函式的一種可能的實現(該實現使用集合類所採用的方法):

public class Lhist<V> { 
private V[] array;
public Lhist(int capacity) {
    array = (V[]) new Object[capacity];
}
}

另外,也可以使用反射來例項化陣列。但是這樣做需要給建構函式傳遞一個附加的引數 —— 一個類常量,比如 Foo.class。後面在 Class<T> 一節中將討論類常量。

實現方法

實現 Lhist 的方法要容易得多。下面是 Lhist 類的完整實現:

public class Lhist<V> {
    private V[] array;
    private int size;

    public Lhist(int capacity) {
        array = (V[]) new Object[capacity];
    }

    public void add(V value) {
        if (size == array.length)
            throw new IndexOutOfBoundsException(Integer.toString(size));
        else if (value == null)
            throw new NullPointerException();
        array[size++] = value;
    }

    public void remove(V value) {
        int removalCount = 0;
        for (int i=0; i<size; i++) {
            if (array[i].equals(value))
                ++removalCount;
            else if (removalCount > 0) {
                array[i-removalCount] = array[i];
                array[i] = null;
            }
        }
        size -= removalCount;
    }

    public int size() { return size; }

    public V get(int i) {
        if (i >= size)
            throw new IndexOutOfBoundsException(Integer.toString(i));
        return array[i];
    }
}

注意,您在將會接受或返回 V 的方法中使用了形式型別引數 V,但是您一點也不知道 V 具有什麼樣的方法或域,因為這些對泛型程式碼是不可知的。

使用 Lhist 類

使用 Lhist 類很容易。要定義一個整數 Lhist,只需要在宣告和建構函式中為型別引數提供一個實際值即可:

Lhist<Integer> li = new Lhist<Integer>(30);

編譯器知道,li.get() 返回的任何值都將是 Integer 型別,並且它還強制傳遞給 li.add() 或 li.remove() 的任何東西都是 Integer。除了實現建構函式的方式很古怪之外,您不需要做任何十分特殊的事情以使 Lhist 是一個泛型類。

 

四 Java類庫中的泛型

集合類

到目前為止,Java 類庫中泛型支援存在最多的地方就是集合框架。就像容器類是 C++ 語言中模板的主要動機一樣(參閱 附錄 A:與 C++ 模板的比較)(儘管它們隨後用於很多別的用途),改善集合類的型別安全是 Java 語言中泛型的主要動機。集合類也充當如何使用泛型的模型,因為它們演示了泛型的幾乎所有的標準技巧和方言。

所有的標準集合介面都是泛型化的 —— Collection<V>、List<V>、Set<V> 和 Map<K,V>。類似地,集合介面的實現都是用相同型別引數泛型化的,所以 HashMap<K,V> 實現 Map<K,V> 等。

集合類也使用泛型的許多“技巧”和方言,比如上限萬用字元和下限萬用字元。例如,在介面 Collection<V> 中,addAll 方法是像下面這樣定義的:

interface Collection<V> {
boolean addAll(Collection<? extends V>);
}

該定義組合了萬用字元型別引數和有限制型別引數,允許您將 Collection<Integer> 的內容新增到 Collection<Number>。

如果類庫將 addAll() 定義為接受 Collection<V>,您就不能將 Collection<Integer> 的內容新增到 Collection<Number>。不是限制 addAll() 的引數是一個與您將要新增到的集合包含相同型別的集合,而有可能建立一個更合理的約束,即傳遞給 addAll() 的集合的元素 適合於新增到您的集合。有限制型別允許您這樣做,並且使用有限制萬用字元使您不需要使用另一個不會用在其他任何地方的佔位符名稱。

應該可以將 addAll() 的型別引數定義為 Collection<V>。但是,這不但沒什麼用,而且還會改變 Collection 介面的語義,因為泛型版本的語義將會不同於非泛型版本的語義。這闡述了泛型化一個現有的類要比定義一個新的泛型類難得多,因為您必須注意不要更改類的語義或者破壞現有的非泛型程式碼。

作為泛型化一個類(如果不小心的話)如何會更改其語義的一個更加微妙的例子,注意 Collection.removeAll() 的引數的型別是 Collection<?>,而不是 Collection<? extends V>。這是因為傳遞混合型別的集合給 removeAll() 是可接受的,並且更加限制地定義 removeAll 將會更改方法的語義和有用性。

 

其他容器類

除了集合類之外,Java 類庫中還有幾個其他的類也充當值的容器。這些類包括 WeakReference、SoftReference 和 ThreadLocal。它們都已經在其包含的值的型別上泛型化了,所以 WeakReference<T> 是對 T 型別的物件的弱引用,ThreadLocal<T> 則是到 T 型別的執行緒區域性變數的控制程式碼。

泛型不止用於容器

泛型最常見最直觀的使用是容器類,比如集合類或引用類(比如 WeakReference<T>)。Collection<V> 中型別引數的含義很明顯 —— “一個所有值都是 V 型別的集合”。類似地,ThreadLocal<T> 也有一個明顯的解釋 —— “一個其型別是 T 的執行緒區域性變數”。但是,泛型規格說明中沒有指定容積。

像 Comparable<T> 或 Class<T> 這樣的類中型別引數的含義更加微妙。有時,就像 Class<T> 中一樣,型別變數主要是幫助編譯器進行型別推理。有時,就像隱含的 Enum<E extends Enum<E>> 中一樣,型別變數只是在類層次結構上加一個約束。

Comparable<T>

Comparable 介面已經泛型化了,所以實現 Comparable 的物件宣告它可以與什麼型別進行比較。(通常,這是物件本身的型別,但是有時也可能是父類。)

public interface Comparable<T> { 
public boolean compareTo(T other);
}

所以 Comparable 介面包含一個型別引數 T,該引數是一個實現 Comparable 的類可以與之比較的物件的型別。這意味著如果定義一個實現 Comparable 的類,比如 String,就必須不僅宣告類支援比較,還要宣告它可與什麼比較(通常是與它本身比較):

public class String implements Comparable<String> { … }

現在來考慮一個二元 max() 方法的實現。您想要接受兩個相同型別的引數,二者都是 Comparable,並且相互之間是 Comparable。幸運的是,如果使用泛型方法和有限制型別引數的話,這相當直觀:

public static <T extends Comparable<T>> T max(T t1, T t2) {
if (t1.compareTo(t2) > 0)
    return t1;
else 
    return t2;
}

在本例中,您定義了一個泛型方法,在型別 T 上泛型化,您約束該型別擴充套件(實現) Comparable<T>。兩個引數都必須是 T 型別,這表示它們是相同型別,支援比較,並且相互可比較。容易!

更好的是,編譯器將使用型別推理來確定當呼叫 max() 時 T 的值表示什麼意思。所以根本不用指定 T,下面的呼叫就能工作:

String s = max(“moo”, “bark”);

編譯器將計算出 T 的預定值是 String,因此它將進行編譯和型別檢查。但是如果您試圖用不實現 Comparable<X> 的 類 X 的引數呼叫 max(),那麼編譯器將不允許這樣做。

 

Class<T>

類 Class 已經泛型化了,但是很多人一開始都感覺其泛型化的方式很混亂。Class<T> 中型別引數 T 的含義是什麼?事實證明它是所引用的類介面。怎麼會是這樣的呢?那是一個迴圈推理?如果不是的話,為什麼這樣定義它?

在以前的 JDK 中,Class.newInstance() 方法的定義返回 Object,您很可能要將該返回型別強制轉換為另一種型別:

class Class { 
Object newInstance();
}

但是使用泛型,您定義 Class.newInstance() 方法具有一個更加特定的返回型別:

class Class<T> { 
T newInstance();
}

如何建立一個 Class<T> 型別的例項?就像使用非泛型程式碼一樣,有兩種方式:呼叫方法 Class.forName() 或者使用類常量 X.class。Class.forName() 被定義為返回 Class<?>。另一方面,類常量 X.class 被定義為具有型別 Class<X>,所以 String.class 是 Class<String> 型別的。

讓 Foo.class 是 Class<Foo> 型別的有什麼好處?大的好處是,通過型別推理的魔力,可以提高使用反射的程式碼的型別安全。另外,還不需要將 Foo.class.newInstance() 強制型別轉換為 Foo。

考慮一個方法,它從資料庫檢索一組物件,並返回 JavaBeans 物件的一個集合。您通過反射來例項化和初始化建立的物件,但是這並不意味著型別安全必須完全被拋至腦後。考慮下面這個方法:

public static<T> List<T> getRecords(Class<T> c, Selector s) {
// Use Selector to select rows
List<T> list = new ArrayList<T>();
for (/* iterate over results */) {
    T row = c.newInstance();
    // use reflection to set fields from result
    list.add(row); 
}
return list;
}

可以像下面這樣簡單地呼叫該方法:

List<FooRecord> l = getRecords(FooRecord.class, fooSelector);

編譯器將會根據 FooRecord.class 是 Class<FooRecord> 型別的這一事實,推斷 getRecords() 的返回型別。您使用類常量來構造新的例項並提供編譯器在型別檢查中要用到的型別資訊。

 

用 Class<T> 替換 T[]

Collection 介面包含一個方法,用於將集合的內容複製到一個呼叫者指定型別的陣列中:

public Object[] toArray(Object[] prototypeArray) { … }

toArray(Object[]) 的語義是,如果傳遞的陣列足夠大,就會使用它來儲存結果,否則,就會使用反射分配一個相同型別的新陣列。一般來說,單獨傳遞一個陣列作為引數來提供想要的返回型別是一個小技巧,但是在引入泛型之前,這是與方法交流型別資訊最方便的方式。

有了泛型,就可以用一種更加直觀的方式來做這件事。不像上面這樣定義 toArray(),泛型 toArray() 可能看起來像下面這樣:

public<T> T[] toArray(Class<T> returnType)

呼叫這樣一個 toArray() 方法很簡單:

FooBar[] fba = something.toArray(FooBar.class);

Collection 介面還沒有改變為使用該技術,因為這會破壞許多現有的集合實現。但是如果使用泛型從新構建 Collection,則當然會使用該方言來指定它想要返回值是哪種型別。

 

Enum<E>

JDK 5.0 中 Java 語言另一個增加的特性是列舉。當您使用 enum 關鍵字宣告一個列舉時,編譯器就會在內部為您生成一個類,用於擴充套件 Enum 併為列舉的每個值宣告靜態例項。所以如果您說:

public enum Suit {HEART, DIAMOND, CLUB, SPADE};

編譯器就會在內部生成一個叫做 Suit 的類,該類擴充套件 java.lang.Enum<Suit> 並具有叫做 HEART、DIAMOND、CLUB 和 SPADE 的常量(public static final)成員,每個成員都是 Suit 類。

與 Class 一樣,Enum 也是一個泛型類。但是與 Class 不同,它的簽名稍微更復雜一些:

class Enum<E extends Enum<E>> { . . . }

這究竟是什麼意思?這難道不會導致無限遞迴?

我們逐步來分析。型別引數 E 用於 Enum 的各種方法中,比如 compareTo() 或 getDeclaringClass()。為了這些方法的型別安全,Enum 類必須在列舉的類上泛型化。

所以 extends Enum<E> 部分如何理解?該部分又具有兩個部分。第一部分指出,作為 Enum 的型別引數的類本身必須是 Enum 的子型別,所以您不能宣告一個類 X 擴充套件 Enum<Integer>。第二部分指出,任何擴充套件 Enum 的類必須傳遞它本身 作為型別引數。您不能宣告 X 擴充套件 Enum<Y>,即使 Y 擴充套件 Enum。

總之,Enum 是一個引數化的型別,只可以為它的子型別例項化,並且這些子型別然後將根據子型別來繼承方法。幸運的是,在 Enum 情況下,編譯器為您做這些工作,一切都很好。

 

與非泛型程式碼相互操作

數百萬行現有程式碼使用已經泛型化的 Java 類庫中的類,比如集合框架、Class 和 ThreadLocal。JDK 5.0 中的改進不要破壞所有這些程式碼是很重要的,所以編譯器允許您在不指定其型別引數的情況下使用泛型類。

當然,以“舊方式”做事沒有新方式安全,因為忽略了編譯器準備提供的型別安全。如果您試圖將 List<String> 傳遞給一個接受 List 的方法,它將能夠工作,但是編譯器將會發出一個可能喪失型別安全的警告,即所謂的“unchecked conversion(不檢查轉換)”警告。

沒有型別引數的泛型,比如宣告為 List 型別而不是 List<Something> 型別的變數,叫做原始型別。原始型別與引數化型別的任何例項化是賦值相容的,但是這樣的賦值會生成 unchecked-conversion 警告。

為了消除一些 unchecked-conversion 警告,假設您不準備泛型化所有的程式碼,您可以使用萬用字元型別引數。使用 List<?> 而不使用 List。List 是原始型別;List<?> 是具有未知型別引數的泛型。編譯器將以不同的方式對待它們,並很可能發出更少的警告。

無論在哪種情況下,編譯器在生成位元組碼時都會生成強制型別轉換,所以生成的位元組碼在每種情況下都不會比沒有泛型時更不安全。如果您設法通過使用原始型別或類檔案來破壞型別安全,就會得到與不使用泛型時得到的相同的 ClassCastException 或 ArrayStoreException。

已檢查集合

作為從原始集合型別遷移到泛型集合型別的幫助,集合框架新增了一些新的集合包裝器,以便為一些型別安全 bug 提供早期警告。就像 Collections.unmodifiableSet() 工廠方法用一個不允許任何修改的 Set 包裝一個現有 Set 一樣,Collections.checkedSet()(以及 checkedList() 和 checkedMap())工廠方法建立一個包裝器(或者檢視)類,以防止您將錯誤型別的變數放在集合中。

checkedXxx() 方法都接受一個類常量作為引數,所以它們可以(在執行時)檢查這些修改是允許的。典型的實現可能像下面這樣:

public class Collections { 
public static <E> Collection<E> checkedCollection(Collection<E> c, Class<E> type ) { 
    return new CheckedCollection<E>(c, type); 
}

private static class CheckedCollection<E> implements Collection<E> { 
    private final Collection<E> c; 
    private final Class<E> type;

    CheckedCollection(Collection<E> c, Class<E> type) { 
      this.c = c; 
      this.type = type; 
    }

    public boolean add(E o) { 
      if (!type.isInstance(o)) 
        throw new ClassCastException(); 
      else
        return c.add(o); 
    } 

}


相關文章