概述:
Java泛型在使用過程有諸多的問題,如不存在List<String>.class, List<Integer>不能指派給List<Number>(不可協變),奇怪的ClassCastException等。 正确的使用Java泛型需要深入的了解Java的一些概念,如協變,橋接方法,以及這篇筆記記錄的類型擦除。Java泛型的處理幾乎都在編譯器中進行,編譯器生成的bytecode是不包涵泛型資訊的,泛型類型資訊将在編譯處理是被擦除,這個過程即類型擦除。
編譯器如何處理泛型:
通常情況下,一個編譯器處理泛型有兩種方式:
1.Code specialization。在執行個體化一個泛型類或泛型方法時都産生一份新的目标代碼(位元組碼or二進制代碼)。例如,針對一個泛型list,可能需要 針對string,integer,float産生三份目标代碼。
2.Code sharing。對每個泛型類隻生成唯一的一份目标代碼;該泛型類的所有執行個體都映射到這份目标代碼上,在需要的時候執行類型檢查和類型轉換。
C++中的模闆(template)是典型的Code specialization實作。C++編譯器會為每一個泛型類執行個體生成一份執行代碼。執行代碼中integer list和string list是兩種不同的類型。這樣會導緻代碼膨脹(code bloat),不過有經驗的C++程式員可以有技巧的避免代碼膨脹。
Code specialization另外一個弊端是在引用類型系統中,浪費空間,因為引用類型集合中元素本質上都是一個指針。沒必要為每個類型都産生一份執行代碼。而這也是Java編譯器中采用Code sharing方式處理泛型的主要原因。
Java編譯器通過Code sharing方式為每個泛型類型建立唯一的位元組碼表示,并且将該泛型類型的執行個體都映射到這個唯一的位元組碼表示上。将多種泛型類形執行個體映射到唯一的位元組碼表示是通過類型擦除(type erasue)實作的。
類型擦除指的是通過類型參數合并,将泛型類型執行個體關聯到同一份位元組碼上。編譯器隻為泛型類型生成一份位元組碼,并将其執行個體關聯到這份位元組碼上。類型擦除的關鍵在于從泛型類型中清除類型參數的相關資訊,并且再必要的時候添加類型檢查和類型轉換的方法。
類型擦除可以簡單的了解為将泛型java代碼轉換為普通java代碼,隻不過編譯器更直接點,将泛型java代碼直接轉換成普通java位元組碼。
類型擦除的主要過程如下:
1.将所有的泛型參數用其最左邊界(最頂級的父類型)類型替換。
2.移除所有的類型參數。
擦除使我們在泛型代碼内部,無法獲得任何有關參數類型的資訊。很蛋疼...
例如:
C++中我們可以這樣寫:
template<typename T> T imax(T a, T b) {
T copy;
return copy;
}
class A{
};
但是在Java中我們是不能夠生成copy的因為們壓根就不知道T的類型資訊。
那為什麼Java要使用擦除呢?
首先能夠節省空間避免代碼膨脹,主要原因是為了“遷移相容性”,即允許泛型代碼與非泛型代碼共存,因為泛型是Java後期才添加的為了相容以前的代碼是以采取了折中的辦法。
那麼擦除所帶來的問題我們如何解決呢?
1: 通過引入類型标簽來對擦除進行補償:
class Building{
@Override
public String toString() {
return "Building ...";
}
}
class House extends Building{
@Override
public String toString() {
return "House ...";
}
}
class TestItem<T>{
Class<T> type; //通過添加類型标簽,來獲得我要持有的類型的資訊
public TestItem(Class<T> type) {
this.type = type;
}
public T getInstance() throws InstantiationException, IllegalAccessException {
T copy = type.newInstance(); //這樣我就可以利用類型資訊進行必要的處理了
return copy;
}
}
public class Test {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
TestItem<Building> item = new TestItem<>(Building.class);
System.out.println(item.getInstance());
System.out.println("------------------------");
TestItem<House> item2 = new TestItem<>(House.class);
System.out.println(item2.getInstance());
//出錯,因為我們是利用newInstance來建立對象的,就必須保證我們的對象要有預設的構造方法才行,但是Integer沒有
// System.out.println("------------------------");
// TestItem<Integer> item3 = new TestItem<>(Integer.class);
// System.out.println(item3.getInstance());
}
}
上面的Integer的問題,我們可以通過傳入一個工廠來實作
interface Factory<T>{
T create();
}
class IntegerFactory implements Factory<Integer> {
@Override
public Integer create() {
return new Integer(0);
}
}
class TestItem<T>{
Class<T> type; //通過添加類型标簽,來獲得我要持有的類型的資訊
T copy;
Factory<T> factory;
public <F extends Factory<T>>TestItem(F factory) {
this.factory = factory;
}
public TestItem(Class<T> type) {
this.type = type;
}
public T getInstance() throws InstantiationException, IllegalAccessException {
//copy = type.newInstance(); //這樣我就可以利用類型資訊進行必要的處理了
copy = factory.create();
return copy;
}
}
public class Test {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
// TestItem<Building> item = new TestItem<>(Building.class);
// System.out.println(item.getInstance());
// System.out.println("------------------------");
// TestItem<House> item2 = new TestItem<>(House.class);
// System.out.println(item2.getInstance());
//現在把工廠放進去就可以了。
System.out.println("------------------------");
TestItem<Integer> item3 = new TestItem<>(new IntegerFactory());
System.out.println(item3.getInstance());
}
}
2: 同樣我們可以通過使用設定擦互相邊界來補償擦除
就像我們在前一篇的比較的時候我們将擦除邊界設定成了Comparable,保證了我們的類新資訊是可比較的。
注意:
1.虛拟機中沒有泛型,隻有普通類和普通方法
2.所有泛型類的類型參數在編譯時都會被擦除
3.建立泛型對象時請指明類型,讓編譯器盡早的做參數檢查(Effective Java,第23條:請不要在新代碼中使用原生态類型)
4.不要忽略編譯器的警告資訊,那意味着潛在的ClassCastException等着你。