天天看點

泛型(一)——泛型和類型擦除詳解一、泛型闡述二、類型擦除——泛型隻存在于編譯期三、類型擦除帶來的問題

目錄

一、泛型闡述

1、泛型——重用程式的設計手段

2、為什麼需要使用泛型?

二、類型擦除——泛型隻存在于編譯期

1、類型擦除概述

2、擦除後隻保留原始類型

(1)什麼是原始類型?

(2)無限定類型擦除——原始類用Object替換

(3)限定類型擦除——原始類型用邊界的類型替換

(4)指定泛型和不指定泛型

三、類型擦除帶來的問題

1、引用傳遞問題(傳入)——確定使用正确的限定類型

(1)檢查針對引用,而非引用對象

(2)泛型中參數化類型不考慮繼承關系

2、自動類型轉換問題(取出)

3、類型擦除與多态沖突

4、泛型類型變量不能是基本資料類型

5、運作時類型檢查異常——instanceof

6、泛型和異常捕獲

(1)不能抛出也不能捕獲泛型類的對象

(2)不能在catch句子中使用泛型變量

7、不能建立泛型類型數組

8、不能執行個體化泛型類型

9、類型擦除後的沖突

10、不能使用靜态域

一、泛型闡述

1、泛型——重用程式的設計手段

泛型,即參數化類型,目的是将具體類型參數化,在使用時需要傳入具體類型進行替換。

參數分為實參和形參,泛型屬于類型形參(好比抽象函數,是一種泛指,類似于數學函數中用x,y,z代表具體的值)。

2、為什麼需要使用泛型?

(1)保證類型安全,進行編譯期錯誤檢查,使代碼具有更好的安全性和可讀性。

(2)不需要進行類型強制轉換

示範案例:——不使用泛型前,強轉結果很容易出錯

public class GenericDemo {
    public static void main(String[] args) {
        // 不使用泛型,集合中傳入的值類型不會受到限制
        // 存值沒有限制,取值就容易出錯,容器安全性得不到保障
        List list = new ArrayList();
        list.add("String");
        list.add(10);
        for(Object obj : list){
            // 資料取出,需要進行強轉,代碼可讀性和使用性降低
            String str = (String) obj;
        }
    }
}
           

測試結果:抛出類型轉換異常

泛型(一)——泛型和類型擦除詳解一、泛型闡述二、類型擦除——泛型隻存在于編譯期三、類型擦除帶來的問題

示範案例:——使用泛型,編譯器會進行檢查

泛型(一)——泛型和類型擦除詳解一、泛型闡述二、類型擦除——泛型隻存在于編譯期三、類型擦除帶來的問題

二、類型擦除——泛型隻存在于編譯期

1、類型擦除概述

java泛型的實作是在編譯層,編譯後生成的位元組碼中是不包含泛型中的類型資訊的。使用泛型時,加上的類型參數,會在編譯器編譯的時候去掉,這個過程稱為類型擦除。

示範案例:存放不同類型的集合,編譯後隻剩下相同的原始類型。

public class GenericDemo {
    public static void main(String[] args) {
        List<String> list1 = new ArrayList();
        list1.add("String");
        List<Integer> list2 = new ArrayList<>();
        list2.add(10);
        System.out.println("兩者相同嗎?:"+(list1.getClass()==list2.getClass()));
        System.out.println("list1集合:"+list1.getClass());
        System.out.println("list2集合:"+list2.getClass());
    }
}
           

示範結果:存放String 和 Integer 類型集合的集合,原始類型均為java.util.ArrayList

泛型(一)——泛型和類型擦除詳解一、泛型闡述二、類型擦除——泛型隻存在于編譯期三、類型擦除帶來的問題

注:也可以使用反射調用add()方法,往隻能添加字元串類型的集合中添加整型資料(同樣也可以證明泛型類型資訊趨勢在編譯後的檔案中是不存在的)。

2、擦除後隻保留原始類型

(1)什麼是原始類型?

原始類型(raw type)就是擦除去了泛型資訊,最後在位元組碼中的類型變量的真正類型。

無論何時定義一個泛型類型,相應的原始類型都會被自動地提供。類型變量被擦除(crased),并使用其限定類型(無限定變量用Object)替換。

(2)無限定類型擦除——原始類用Object替換

下邊定義的是一個泛型類:

public class Pair<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
}
           

編譯後,它是下面這樣的,類型被擦除,并用原始類型替換

public class Pair{
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object value) {
        this.value = value;
    }
}
           

這是因為在Pair<T>中,T是一個無限定的類型變量,是以用Object替換。其結果就是一個普通的類,如同泛型加入java變成語言之前已經實作的那樣。在程式中可以包含不同類型的Pair,如Pair<Stirng>,Pair<Integer>,但是,類型擦除後它們就變成原始的Pair類型了,原始類型都是Object。

(3)限定類型擦除——原始類型用邊界的類型替換

如果類型變量有限定,那麼原始類型就用第一個邊界的類型變量來替換。

比如Pair<T>這樣申明:

public class Pair<T extends Comparable & Serializable>{}
           

那麼原始類型就是Comparable

注意:如果Pair這樣申明

public class Pair<T extends Serializable & Comparable>{}
           

那麼原始類型就用Serializable替換,而編譯器在必要時要向Comparable插入強制類型轉換。為了提高效率,應該将标簽(tagging)接口(即沒有方法的接口)放在邊界限定清單的末尾。

(4)指定泛型和不指定泛型

調用方法時,可以指定泛型類型,也可以不指定泛型類型。

不指定泛型類型:泛型變量的類型為該方法中的幾種類型的同一父類的最小級,直到Object。

指定泛型:該方法中的幾種類型必須是該泛型執行個體類型或者其子類。

public class Demo<T>{

    public static<T> T add(T x,T y){
       return y; 
    }

    public static void main(String[] args) {
        /**
         * 不指定泛型的時候
         */
        Integer add = Demo.add(1, 2);// 這兩個參數都是Integer,是以T為Integer類型
        Number add1 = Demo.add(1, 1.2); // 這兩個參數一個是Integer,一個是Float,是以取同一父類的最小級,為Number
        Object add2 = Demo.add(1, false); // 這兩個參數一個是Integer,一個是boolean,是以取同一父類的最小級,為Object

        /**
         * 指定泛型的時候
         */
        Demo.<Integer>add(1,2);// 指定類型為Integer,是以隻能為Integer類型或其子類
//        Demo.<Integer>add(1,1.2);// 編譯錯誤,指定了Integer類型,不能為Float
        Demo.<Number>add(1,1.2);// 編譯通過,指定為Number,是以可以為Integer和Float
    }
}
           

三、類型擦除帶來的問題

1、引用傳遞問題(傳入)——確定使用正确的限定類型

問題描述:

類型資訊被擦除,怎麼能保證隻使用泛型變量的限定類型?編譯後String是Object,Integer也是Object,怎麼能确定使用哪一個呢?

(1)檢查針對引用,而非引用對象

什麼是引用?

比如 A a = new A();

此時變量a指向了一個A對象,a被稱為引用變量,也可以說a是A對象的一個引用。我們通過操縱引用變量a來操作A對象。變量a的值為它所引用對象的位址。

為確定正确的使用類型,java的實作順序是這樣的:

先檢查泛型類型(針對引用)——類型擦除——編譯

代碼示例:list1使用泛型,list2沒有使用泛型

泛型(一)——泛型和類型擦除詳解一、泛型闡述二、類型擦除——泛型隻存在于編譯期三、類型擦除帶來的問題

類型檢查就是編譯時完成的。new ArrayList()隻是在記憶體中開辟了一個存儲空間,可以存儲任何的類型對象。而真正涉及類型檢查的是它的引用,因為我們是使用它的引用來調用它的方法,比如說list1調用add()方法,它做了泛型限定,是以list1引用能完成泛型類型的檢查。而引用list2沒有使用泛型,是以沒有進行類型檢查。

通過上邊的例子,我們可以明白,類型檢查就是針對引用的。誰是一個引用,用這個引用調用泛型方法,就會針對這個引用調用的方法進行類型檢測,而無關它真正引用的對象。

(2)泛型中參數化類型不考慮繼承關系

下邊情況的引用傳遞是不被允許的——集合不存在類型之間的繼承關系

泛型(一)——泛型和類型擦除詳解一、泛型闡述二、類型擦除——泛型隻存在于編譯期三、類型擦除帶來的問題

第一種情況:

父類型集合強轉為子類型集合,java不允許這樣的引用傳遞。因為在父類型元素強轉為子類型元素時,存在很大的類型轉換安全隐患,因為你不知道ArrayList<Object>集合裡邊存的到底是String還是Integer或者是其他的值,這也是泛型出現的原因——類型轉換安全。

public static void main(String[] args) {
        ArrayList<Object> objecList = new ArrayList<Object>(); // 編譯通過
        objecList.add(new Object());
        // Object類型轉String類型,會抛出類型轉換錯誤
        ArrayList<String> list1 = objecList; // 編譯錯誤
    }
           

第二種情況:

子類型集合強轉為父類型集合,違背泛型設計初衷,編譯報錯。這個原因是,list2也可以存放資料,它不一定就是String類型的,是以取值時無法确定具體的類型。另外,使用了泛型以後,StringList取值的時候還是要進行強轉,這樣泛型的存在便沒有任何意義了。

public static void main(String[] args) {
        // 集合之間不存在繼承關系
        ArrayList<String> StringList = new ArrayList<String>(); // 編譯通過
        StringList.add(new String());
        ArrayList<Object> list2 = StringList;// 編譯錯誤,無繼承關系 
    }
           

2、自動類型轉換問題(取出)

當類型替換為原始類型,我們在擷取的時候,為什麼不需要進行類型的強制轉換呢?

使用ArrayList和get方法作示例:源碼中,資料在return之前會根據泛型變量進行強轉。

/**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }

    E elementData(int index) {
        return (E) elementData[index];
    }
           

3、類型擦除與多态沖突

現在給定一個泛型類:

class Pair<T> {
	private T value;
	public T getValue() {
		return value;
	}
	public void setValue(T value) {
		this.value = value;
	}
}
           

然後通過一個子類去繼承它:

class DateInter extends Pair<Date> {
	@Override
	public void setValue(Date value) {
		super.setValue(value);
	}
	@Override
	public Date getValue() {
		return super.getValue();
	}
}
           

那麼問題就來了,泛型在經過編譯,類型資訊被擦除,實際上泛型類Pair經過編譯後是這種樣子的:

class Pair {
	private Object value;
	public Object getValue() {
		return value;
	}
	public void setValue(Object  value) {
		this.value = value;
	}
}
           

但是我們在子類繼承父類的時候,為泛型父類指定了具體的類型,我們重寫的的方法是這樣的:

@Override
public void setValue(Date value) {
	super.setValue(value);
}
@Override
public Date getValue() {
	return super.getValue();
}
           

很快我們就可以發現子類重寫的方法,在父類中根本就沒有,這樣分析起來,子類倒像是寫了兩個重載的方法。

補充重載和重寫知識點:

重寫:傳回值和形參都不能改變,外殼不變,核心重寫。

重載:方法名字相同,而參數不同,傳回類型可以相同也可以不同。

問題浮現:

原本是子類對父類方法進行重寫,實作多态。但類型擦除以後,就變成了重載(形參、反參不一緻),這樣類型擦除就和多态有了沖突。

詭異的橋方法——之是以詭異是因為你看不到,它是由編譯器生成的,它的作用就是用來解決泛型和多态的沖突。

橋方法的做法是這樣的:

子類A重寫了泛型父類的方法:setValue(Date date) —— 指定了具體類型Date

泛型父類B的橋方法:setValue(Object obj) ——>在方法中調用子類重寫的方法setValue(Date date)

這時候這個setValue(Object obj)就是橋方法了,它唯一的功能就是去調用子類生成的setValue(Date date)方法,滿足子類重寫的需要。這是因為,泛型在沒有傳入具體類型前,它也不知道自己是什麼樣的(抽象存在),現在子類傳入了具體類型,那泛型就按照子類要求的具體類型來實作,父随子變。

這種做法其實了解起來像是在複制,子類有什麼,泛型父類就生成一個橋方法調用什麼(複制,當然也是有原則的複制),使得子類重寫的方法,在父類中是存在的,不破壞代碼的多态特性。

4、泛型類型變量不能是基本資料類型

不能用類型參數替換基本類型。就比如,沒有ArraryList<double>,隻有ArraryList<Double>。因為當類型擦除後,ArraryList的原始類型變為Object,但是Object類型不能存儲double值,隻能引用Double的值。

5、運作時類型檢查異常——instanceof

例如:

ArrayList<String> arrayList = new ArrayList<String>();
           

因為類型擦除後,ArrayList<String>隻剩下原始類型,泛型資訊String不存在了。

是以你不能這樣去判斷: 

if(arrayList instanceof ArrayList<String>){}
           

正确的做法是通過通配符的方式:

if(arrayList instanceof ArrayList<?>){}
           

6、泛型和異常捕獲

(1)不能抛出也不能捕獲泛型類的對象

原因:異常都是在運作時捕獲和抛出的,編譯後,泛型類型資訊被擦除,會導緻catch兩個一模一樣的普通異常,這個是不允許的,是以編譯會報錯。

try{
}catch(Problem<Integer> e1){
// 代碼省略
}catch(Problem<Number> e2){
...
} 
           

編譯後是這樣的:catch了兩個一模一樣的異常

try{
}catch(Problem<Object> e1){
// 代碼省略
}catch(Problem<Object> e2){
...
           

(2)不能在catch句子中使用泛型變量

示例如下:

public static <T extends Throwable> void doWork(Class<T> t){
        try{
            ...
        }catch(T e){ //編譯錯誤
            ...
        }catch(IndexOutOfBounds e){
        }                         
 }
           

上述示例違背了異常捕獲的原則,異常捕獲一定是子類在前,父類在後,而使用泛型T,編譯後類型擦除會變成Throwable,與異常捕獲原則沖突。

7、不能建立泛型類型數組

不允許使用參數化類型數組,下邊的代碼是錯誤的:

List<String>[] stringLists=new List<String>[1]; // 編譯報錯
           
泛型(一)——泛型和類型擦除詳解一、泛型闡述二、類型擦除——泛型隻存在于編譯期三、類型擦除帶來的問題

原因:編譯器使用類型擦除,參數類型被替換為Object,使用者可以向數組中添加任何類型對象,是以下邊紅色部分是不能限定死類型的。

List<Object>[] stringLists=new List<String>[1];  這樣是不對等的

List<Object>[] stringLists=new List[1];  // 這樣是對等的

數組是協變的(關于協變,在通配符中有介紹),不過,有時候錯誤的使用數組協變特性,還是會帶來安全隐患,示例如下:

List<String>[] stringLists = new List[1]; // List<String>類型數組
List<Integer> intList = Arrays.asList(40); // 建立一個整型List元素
Object[] objects = stringLists; // 數組協變這是可以的,萬物皆Object,出錯的關鍵在這裡
objects[0]= intList; // 将intList放入objects數組中
String s= stringLists[0].get(0); // 強轉錯誤
           

8、不能執行個體化泛型類型

下邊的代碼是不能通過編譯的:new 無法為不确定的類型配置設定記憶體空間

public static <T> void add(Box<T> box) {
        T item = new T(); // 編譯不能通過,T沒有具體類型
        box.add(item);
    }
           

想執行個體化參數化類型,可以借助反射:

public static <T> void add(Box<T> box, Class<T> clazz)
            throws InstantiationException, IllegalAccessException{
        // 因為T是在運作時通過反射才能知道是什麼類型
        T item = clazz.newInstance();   // 通過反射使用位元組碼
        box.add(item);
    }
           

9、類型擦除後的沖突

1、不能使用泛型建立與父類方法名重名的方法

public class Pair<T> {
    // 泛型擦除後,與父類方法産生沖突,兩個同名,同參數的方法
    public boolean equals(T value) {// 編譯報錯
        return null; 
    }
    public static void main(String[] args) {
        Object obj = new Object();
        obj.equals("object已經有equals方法了");
    }
}
           

2、要支援擦除的轉化,需要強制一個類或者類型變量不能同時成為兩個接口的子類,而這兩個子類是同一接口的不同參數化。

如下邊這種情況是不被允許的:

父類實作了Comparable接口,并限定了參數類型為Pair

public class Pair implements Comparable<Pair> {
    ...
}
           

子類繼承了父類Pair,同時自己又去實作了Comparable接口,也限定了類型

public class PairChild extends Pair implements Comparable<PairChild> {
    ... // 編譯報錯
}
           

這種情況使得PairChild類同時實作了Comparable<Pair>和Comparable<PairChild>接口,這是同一接口的不同參數化實作。

但是,去除泛型後,這樣是可以的:

public class Pair implements Comparable {
    @Override
    public int compareTo(Object o) {
        // 父類的具體實作邏輯
        return 0;
    }
}
           
public class PairChild extends Pair implements Comparable {}// 編譯不會報錯
           

這種情況下Comparable的具體實作都在父類Pair裡邊,不過這種實作是隻能是Object類型的,因為Pair和PairChild歸根結底都是Object.

10、不能使用靜态域

泛型類中的靜态方法和靜态變量不可以使用泛型類所申明的泛型類型參數。

原因:因為泛型類中的泛型參數的執行個體化是在定義對象的時候指定的,而靜态變量和靜态方法不需要使用對象來調用,對象都沒有建立,是以不能确定這個泛型參數是何種類型,是以編譯的時候會報錯。

舉例說明:

public class Pair<T> {
    public static T one;   //編譯錯誤  
    public static  T show(T one){ //編譯錯誤  
        return null;
    }
}
           

但是下邊這種情況是正确的

public class Pair<T> {
    // 這是一個泛型方法,對象調用的時候需要傳入具體的類型參數
    public static <T>T show(T one){  
        return null;
    }
}
           

上述是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。