泛型的內部原理就是:類型擦除。
java泛型被稱爲僞泛型,主要是因爲在編譯期間,所有的泛型信息都會被擦除掉。整個java泛型都是在編譯器層次實現的。泛型基礎知識裏面測試過那個例子(colleage1和colleage2的類型相同),也是因爲類型擦除的原因。
一、類型擦除
1.類型擦除:
使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱爲類型擦除。(生成的Java字節碼中是不包含泛型中的類型信息的)
栗子:
public static void main(String[] args){
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add("caka");
ArrayList<Integer> arrayList2=new ArrayList<Integer>();
arrayList2.add(100);
System.out.println("arrayList1類型爲:"+arrayList1.getClass());
System.out.println("arrayList2類型爲:"+arrayList2.getClass());
System.out.println(arrayList1.getClass()==arrayList2.getClass());
}
上面這段代碼的輸出結果是true,說明arrayList1和arrayList2的類型在運行的時候是相同的。泛型類型String和Integer都在編譯的時候被擦除掉了,只剩下了原始類型。比如ArrayList<Integer>和ArrayList<String>等類型,在編譯後都會變成ArrayList。JVM看到的只是ArrayList,泛型附加的類型信息對JVM來說是不可見的。
2.原始類型:
原始類型顧名思義就是沒有泛型的最初類型,即擦除去了泛型信息,最後在字節碼中的類型變量的真正類型。無論何時定義一個泛型類型,相應的原始類型都會被自動地提供。
沒有限定類型時,類型變量被擦除,原始類型爲Object。
class Colleage<T>{
private T student;
public Colleage(T student){
this.student=student;
}
public T getStudent() {
return student;
}
public void setStudent(T student) {
this.student = student;
}
}
上例中Colleage類在經過編譯之後,就成爲原始的Colleage類了,因爲泛型形參T是一個無限定的類型變量,所以它的原始類型爲Object。有限定類型時,類型變量被擦除,原始類型是其限定類型。對於有多個限定的類型變量,那麼原始類型就用第一個邊界的類型變量來替換。
class Colleage <T extends Comparable & Serializable>{
此時Colleage的原始類型就是Comparable。
class Colleage <T extends Serializable & Comparable>{
此時Colleage的原始類型就是Serializable。編譯器在必要的時要向Comparable插入強制類型轉換。爲了提高效率,應該將標籤接口(即沒有方法的接口)放在邊界限定列表的末尾。
3.原始類型和泛型變量的類型的區分
調用泛型方法的時候,可以指定泛型,也可以不指定泛型。原始類型和泛型變量的類型的區分也分爲兩種情況:
不指定泛型的情況下,泛型變量的類型爲 該方法中的幾種類型的同一個父類的最小級。
指定泛型的時候,該方法中的幾種類型必須是該泛型實例類型或者其子類。
public class Test{
public static void main(String[] args){
/**不指定泛型的時候*/
int i=Test.add(1,2); //這兩個參數都是Integer,所以T爲Integer類型
Number f=Test.add(1, 1.2);//這兩個參數一個是Integer,一個是Float,所以取同一父類的最小級,爲Number
Object o=Test.add(1,"asd");//這兩個參數一個是Integer,一個是String,所以取同一父類的最小級,爲Object
/**指定泛型的時候*/
int a=Test.<Integer>add(1,2);//指定了Integer,所以只能爲Integer類型或者其子類
Number c=Test.<Number>add(1,2.2); //指定爲Number,所以可以爲Integer和Float
}
public static <T> T add(T x,T y){
return y;
}
}
4.泛型擦除疑問
先檢查,後編譯。
比如上例中加入這句代碼:
int b=Test2.<Integer>add(1, 2.2)
就會出現編譯錯誤,因爲泛型是在編譯之前就檢查,檢查到類型爲Integer,所以2.2這個Double類型的就沒法執行add。
泛型檢查針對引用與該引用的對象無關
ArrayList<String> arrayList1=new ArrayList();
arrayList1.add("1");//編譯通過
arrayList1.add(1);//編譯錯誤
String str1=arrayList1.get(0);//返回類型爲String
ArrayList arrayList2=new ArrayList<String>();
arrayList2.add("1");//編譯通過
arrayList2.add(1);//編譯通過
Object object=arrayList2.get(0);//返回類型爲Object
上例中可以看出,類型檢查是針對引用的,誰是一個引用,用這個引用調用泛型方法,就會對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。arrayList1是String類型的ArraryList的對象的引用。會對arrayList1的add方法進行類型檢測,integer類型不能通過編譯。arrayList2是一個ArraryList的對象,add方法可以添加String和Integer,此時對象類型爲公共父類Object。
泛型中參數化類型不能繼承,泛型中不允許引用傳遞
ArrayList<String> arrayList1=new ArrayList<Object>();//編譯錯誤
相當於用已經規定必須存放String類型的數組,可實際上已經被存放了Object類型的對象,這樣,就會有ClassCastException了。所以爲了避免這種極易出現的錯誤,Java不允許進行這樣的引用傳遞。
問題:編譯的時候進行類型擦除,那所有的泛型類型變量最後都會被替換爲原始類型。並且獲取的時候不需要手動的類型轉換,那麼類型轉換是在什麼時候進行的?
學習了一個厲害的大牛泛型講解,以ArrayList的get方法的字節碼文件爲例,強制類型轉換是在調用get()方法的時候進行的。
5.多態下的泛型(橋方法實現父類方法重寫)
類型擦除後,父類的的泛型類型全部變爲了原始類型Object。當在子類中給定類型時,重寫了父類的方法,事實上子類中會存在方法的類型並不是父類的Object而是我們指定的類型。所以,JVM使用橋方法來完成多態。
橋方法的參數類型都是Object,也就是說,子類中真正覆蓋父類方法的就是我們看不到的橋方法。橋方法的內部實現,就只是去調用我們自己重寫的方法。
6.不能拋出也不能捕獲泛型類的對象。因爲異常都是在運行時捕獲和拋出的,而在編譯的時候,泛型信息全都會被擦除掉。
public class Problem<T> extends Exception{......}
在異常聲明中可以使用類型變量。
public static<T extends Throwable> void doWork(T t) throws T{
不能聲明參數化類型的數組 Colleage<String>[] t = new Colleage <String>(10); //ERROR,擦除之後,t的類型爲Colleage[]
泛型類中的靜態方法和靜態變量不可以使用泛型類所聲明的泛型類型參數 public class Test <T> {
public static T testNum; //編譯錯誤
public static T getValue(T testNum){ //編譯錯誤
return null;
}
}
因爲泛型類中的泛型參數的實例化是在定義對象的時候指定的,而靜態變量和靜態方法不需要使用對象來調用。無法確定這個泛型參數是何種類型,所以會出現編譯錯誤。(靜態的泛型方法是沒有問題的。)