泛型(二)->擦除&擦除帶來的問題

泛型(二)->擦除&擦除帶來的問題

本篇首先介紹泛型的擦除,然後圍繞泛型擦除所帶來的問題進行精確打擊,話不多說,我們直接開始正文.

文中很多例子都會用到Pair這個對象,這裏統一聲明.

public class Pair<T> {

    private T first;
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

泛型擦除

虛擬機中沒有泛型類型的對象–所有對象都是普通的類,無論我們什麼時候定義的泛型類型,在虛擬機中都自動轉換成了一個相應的原始類型.原始類型就是擦除類型變量,並替換爲限定符類型(沒有限定符用Object替換)後的泛型類型名.文字描述有點繞口,看下例子馬上就能明白.

例如Pair<T>的原始類型如下

public class Pair {

    private Object first;
    private Object second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public void setFirst(Object first) {
        this.first = first;
    }

    public Object getSecond() {
        return second;
    }

    public void setSecond(Object second) {
        this.second = second;
    }
}

因爲T是無限定變量,所以全部替換成Object,看起來跟我們寫的普通類沒有什麼區別.
程序中我們可以寫出不同類別的Pair,比如Pair<String>又或者Pair<Integer>,而在虛擬機中擦除泛型後就變成原始類型的Pair了.

接下來我們具體說說泛型擦除的規則
泛型擦除就是類型變量用第一個限定來替換,如果沒有給定限定就用Object替換,例如類Pair<T>中的類型變量沒有限定所以用Object替換,下面我們看一個聲明瞭類型變量限定的例子.

public class Interval<T extends Comparable & Serializable> {
    private T lower;
    private T upper;

    public Interval(T first, T second) {
        if (first.compareTo(second) > 0){
            lower = second;
            upper = first;
        }else{
            lower = first;
            upper = second;
        }
    }
}

原始類型如下

public class Interval{
  private Comparable lower;
  private Comparable upper;

  public Interval(Comparable first, Comparable second) {

  }
}

看到這裏大家可能有個大膽的想法,把限定改爲Interval<T extends Serializable & Comparable>會發生什麼,這樣做的話原始類型就用Serializable替換T,而編譯器會在需要的時候插入強制類型轉換爲Comparable,爲了提高效率,我們應該把沒有方法的接口放在後面.

翻譯泛型表達式

當調用泛型方法的時候,如果擦除返回值類型,編譯器將強制插入類型轉換.例如下面

Pair<Manager> pair = ...;
Manager manager = pair.getFirst();

擦除後getFirst返回值爲Object,編譯器會自動插入Manager強制類型轉換,所以getFirst()方法會執行如下兩個指令
- 對原始方法調用getFirst()
- 把返回值Object強轉成Manager

擦除所帶來的問題

泛型擦除與多態衝突

我們直接看例子

public class DateInterval extends Pair<Date> {

    public void setSecond(Date date){

    }
}

在擦除後變成

public class DateInterval extends Pair {

    public void setSecond(Date date){

    }
}

我們可以發現DateInterval中我們寫了一個setSecond(Date)方法,並且擦除後我們還從Pair繼承了一個setSecond(Obj)方法.那麼當我們調用setSecond()的時候會發生什麼呢.

是不是發現這跟我們想的不一樣啊,明明有兩個setSecond但是編譯器卻只提示了一個,也就是說這裏泛型擦除後與多態產生了衝突.在這種情況下編譯器會在DateInterval生成一個橋方法public void setSecond(Object second){setSecond((Date)second)},具體流程我們慢慢道來.

Pair聲明爲Pair<Date>,而我們DateInterval中又寫了setSecond(Date)所以我們正常的理解這更應該是重寫對吧,同樣虛擬機也是這樣做的,泛型擦除後將爲原始類型的Pair生成一個橋方法public void setSecond(Object second){setSecond((Date)second)}調用DateInterval的setSecond(Date),這樣就滿足我們需求了.當然這只是理論,接下來我們通過DateInterval.class文件來證明我們的觀點.

通過javap命令我們可以清楚的看到DateInterval中有個兩個setSecond()方法,並且setSecond(Obj)方法先把obj強轉成Date然後調用setSecond(Date)方法,印證了我們前面的理論是正確的.

當然我們還可能出現這種情況

public class DateInterval extends Pair<Date> {
    public Date getSecond() {
        return new Date();
    }
}

泛型擦除後會有兩個方法

public Date getSecond() {}
public Object getSecond(){}

這在java代碼中是肯定不可能出現的,但是虛擬機中,用參數類型和返回類型確定一個方法,所以可能出現兩個返回值類型不同的方法字節碼,並且虛擬機能夠正確處理這個情況.

這裏稍加總結
- 虛擬機中沒有泛型,只有普通的類和方法.
- 所有的參數類型都用它們的類型限定符替換
- 橋方法被合成來保持多態
- 爲了保證類型安全,必要時會插入強制類型轉換.

泛型使用注意事項

不能用基本數據類型實例化類型參數

不能用基本數據類型替代類型變量,因此沒有Pair<int>,只有Pair<Integer>,其原因就是因爲類型擦除後,Pair類型變量會被替換成Object或者限定符,而它們不能存儲int.

運行時類型查詢只適用於原始類型

虛擬機中泛型對象都爲原始類型,所以運行時查詢只能比較原始類型.

像這樣將無法通過編譯.同樣道理,getClass方法總是返回原始類型.

Pair<String> p1 = new Pair();
Pair<Integer> p2 = new Pair();
if(p1.getClass() == p2.getClass())

比較結果爲true,因爲getClass方法返回的都是Pair.class

不允許創建參數化類型數組

不能創建參數化類型的數組,例如
Pair<String>[] pairs = new Pair<String>[2]; //error
因爲泛型擦除後pairs類型爲Pair[],然後我們可以轉換爲Object[]
Object[] o = pairs
然後在賦予新的值
o[0] = new Pair<Integer>();
能夠通過數組存儲檢查,但是當我們使用的時候肯定會產生錯誤,出於這個考慮不允許創建參數化類型數組.

需要注意的是隻是不允許創建,而聲明爲Pair<String>是被允許的,只是不能實例化new Pair<String>[2].

泛型類的靜態上下文中類型變量無效

泛型變量屬於對應實例的,而靜態屬於類的根本沒法使用.下面例子是錯誤的

public class MyInstance<T> {
    private static T t;//error

    private static T getInstance(){//error
        if(t == null){
        }
        return t;
    }

}

擦除後的衝突

這個其實是上面泛型擦除後與多態衝突的另一種情況,我們把上面那個例子在改造下

public class DateInterval extends Pair<Date> {

    public void setSecond(Object obj){

    }
}

看了這個是不是感覺在故意搞事情

沒錯我們就是要搞到底,這樣當泛型擦除後會出現兩個public void setSecond(Object obj),那麼解決辦法只有一個就是重命名我們衝突的方法.

還有一種情況需要注意.如果兩個接口是同一個接口的不同參數化實現,那麼一個類或者類型變量不能同時成爲這兩個接口的子類型,書面表達非常拗口直接看例子.

class Calendar implements Comparable<Calendar>{}
class GregorianCalendar extends Calendar implement Comparable<GregorianCalendar>

GregorianCalendar會實現Comparable<Calendar>Comparable<GregorianCalendar>,這是同一個接口的不同參數化.那麼合成的橋方法就可能產生衝突,例如如下情況

class GregorianCalendar extends Calendar implement Comparable<GregorianCalendar>{
    public int compareTo(Calendar calendar){}
    public int compareTo(GregorianCalendar  gregorianCalendar){}
}

我們不可能合成public int compareTo(obj){compareTo((Calendar)(obj))}public int compareTo(obj){compareTo((GregorianCalendar)(obj))}兩個橋方法這顯然不對.

到此泛型基本說完了,如有疑問歡迎留言.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章