[轉]深入理解Java泛型

Java語言的泛型類似於C++中的模板. 但是這僅僅是基於表面的現象。

Java語言的泛型基本上完全在編譯器中實現的,由編譯器執行類型檢查和類型推斷,然後生成普通的非泛型的字節碼。 這種實現稱爲"擦除"
(編譯器使用泛型類型信息保證類型安全,然後在生成字節碼之前將其清除)

泛型不是協變的
協變:
Java 語言中的數組是協變的(covariant),也就是說,
如果 Integer 擴展了 Number(事 實也是如此),那麼不僅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求Number[] 的地方完全可以傳遞或者賦予 Integer[]。
(更 正式地說,如果 Number 是 Integer 的超類型,那麼 Number[] 也是 Integer[] 的超類型)

但是,泛型並不是協變的。 如果,List<Number> 是List<Integer> 的超類型,但是,如果需要List<Integer>的時候, 並不容許傳遞List<Number>,它們並不等價。
不允許的理由很簡單,這樣會破壞要提供的類型安全泛型. 如下代碼:

public static void main(String[] args) {
List<Integer> list=new ArrayList<Integer>();
List<Number> list2=list;//編譯錯誤.
list2.add(new Float(19.0f));
}

其他協變問題
數組能夠協變而泛型不能協變的另外一個問題是:不能實例化泛型類型的數組: (new List<String>[3]是不合法的),除非類型參數是一個未綁定類型的通配符(new List<?>[3]是合法的).

延遲構造
因爲可以擦除功能,所以List<Integer>和List<String>是同一個類,編譯器在編譯List<V>的時候,只生成一個類。
所以,運行時,不能區分List<Integer>和List<String>(實際上,運行時都是List,類型被擦除了),用泛型類型參數標識類型的變量的構造就成了問題。運行時缺乏類型信息,這給泛型容器類和希望創建保護性副本的泛型類提出了難題。
比如泛型類Foo:

class Foo<T>{
public void dosomething(T param){
T copy=new T(param);//語法錯誤.
}
}
因爲編譯的時候,還不知道要構造T的具體類型,所以也無法調用T的構造函數。
那麼,我們是否可以使用克隆來構建呢?
class Foo<T extends Cloneable>{
public void dosomething(T param){
//編譯錯誤,
T copy=(T)param.clone();
}
}
爲什麼會報錯呢? 因爲clone()在Object類中是受保護的。

所以,不能複製在編譯時根本不知道是什麼類的類型引用。

構造通配符引用
那麼使用通配符類型怎麼樣呢? 假設要創建類型爲Set<?>的參數的保護性副本。 我們來看看:

class Foo{
public void doSomething(Set<?> set){
//編譯出錯,你不能用通配符類型的參數調用泛型構造函數
Set<?> copy=new HashSet<?>(set);
}
}

但是下面的方法可以實現:

class Foo{
public void doSomething(Set<?> set){
Set<?> copy=new HashSet<Object>(set);
}
}

構造數組
對於我們常用的ArrayList<V> ,我們需要來探討一下它的內部實現機制:
假設它內部管理着一個V數組,那麼我們希望能在ArrayList<V>的構造函數中來初始化這個數組:

class ArrayList<V>{
V[] content;
private static final int DEFAULT_SIZE=10;
public ArrayList() {
//編譯錯誤。
content=new V[DEFAULT_SIZE];
}
}
但這段代碼不能工作,不能實例化用類型參數表示的類型數組。因爲編譯器不知道V到底是代表什麼類型,所以不能實例化V數組。

Java庫中的 Collections提供了一種思路,用於實現,但是非常彆扭(設置連Collections的作者都這樣說過.) 在Collections類編譯時,會產生警告:
class ArrayList<V> {
private V[] backingArray;
public ArrayList() {
backingArray = (V[]) new Object[DEFAULT_SIZE];
}
}

因爲泛型是通過擦除實現的,backingArray 的類型實際上就是 Object[],因爲 Object 代替了 V。
這意味着:實際上這個類期望 backingArray 是一個 Object 數組,但是編譯器要進行額外的類型檢查,以確保它包含 V 類型的對象。所以這種方法很奏效,但是非常彆扭,因此不值得效仿
另外有一種方法是:
聲明backingArray爲Object數組,並在使用它的各個地方,強轉成V[]

其他方法

最好的方法是: 向構造方法中,傳入類對象,這樣,在運行時,就可以知道T的值了。不採用這種方法的原因是,它無法與之前版本的Collections框架相兼容。
比如:

public class ArrayList<V> implements List<V> {
private V[] backingArray;
private Class<V> elementType;
public ArrayList(Class<V> elementType) {
this.elementType = elementType;
backingArray = (V[]) Array.newInstance(elementType, DEFAULT_LENGTH);
}
}

但是,這裏還是有地方不是很妥當:
調用 Array.newInstance() 時會引起未經檢查的類型轉換。爲什麼呢?同樣是由於向後兼容性。Array.newInstance() 的簽名是:
public static Object newInstance(Class<?> componentType, int length)
而不是類型安全的:
public static<T> T[] newInstance(Class<T> componentType, int length)

爲何 Array 用這種方式進行泛化呢?同樣是爲了保持向後兼容。要創建基本類型的數組,如 int[], 可以使用適當的包裝器類中的 TYPE 字段調用 Array.newInstance()(對於 int,可以傳遞 Integer.TYPE 作爲類文字)。用 Class<T> 參數而不是 Class<?> 泛化 Array.newInstance(),對 於引用類型有更好的類型安全,但是就不能使用 Array.newInstance() 創建基本類型數組的實例了。也許將來會爲引用類型提供新的 newInstance() 版本,這樣就兩者兼顧了。
在這裏可以看到一種模式 —— 與泛型有關的很多問題或者折衷並非來自泛型本身,而是保持和已有代碼兼容的要求帶來的副作用。

擦除的實現
因爲泛型基本上都是在JAVA編譯器中而不是運行庫中實現的,所以在生成字節碼的時候,差不多所有關於泛型類型的類型信息都被“擦除”了, 換句話說,編譯器生成的代碼與手工編寫的不用泛型、檢查程序類型安全後進行強制類型轉換所得到的代碼基本相同。與C++不同,List<Integer>和List<Number>是同一個類(雖然是不同的類型,但是都是List<?>的子類型。)
擦除意味着,一個類不能同時實現 Comparable<String>和Comparable<Number>,因爲事實上,兩者都在同一個接口中,指定同一個compareTo()方法。

擦除也造成了上述問題,即不能創建泛型類型的對象,因爲編譯器不知道要調用什麼構造函數。 如果泛型類需要構造用泛型類型參數來指定類型的對象,那麼構造函數應該傳入類對象,並將它們保存起來,以便通過反射來創建實例。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章