Java的泛型類(lèi)型擦除及類(lèi)型擦除帶來(lái)的問(wèn)題

1、泛型的類(lèi)型擦除

Java的泛型是偽泛型,不同于C++的模板機(jī)制,這是因?yàn)镴ava的泛型只存在編譯期間,在編譯完成后泛型就會(huì)被擦除。引入泛型是為了將類(lèi)型檢查提前到編譯期間,將類(lèi)型轉(zhuǎn)換交由編譯器處理,那么為什么還要進(jìn)行泛型的擦除呢?泛型擦除的目的是為了向下兼容老的Java版本,老的Java版本是沒(méi)有泛型概念的。
下面通過(guò)一個(gè)例子證明泛型的擦除

public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());
    }

}

在這個(gè)例子中,我們定義了兩個(gè)ArrayList數(shù)組,一個(gè)是ArrayList<String>泛型類(lèi)型的,只能存儲(chǔ)字符串;一個(gè)是ArrayList<Integer>泛型類(lèi)型的,只能存儲(chǔ)整數(shù),最后,我們通過(guò)list1對(duì)象和list2對(duì)象的getClass()方法獲取他們的類(lèi)的信息,最后發(fā)現(xiàn)結(jié)果為true。說(shuō)明泛型類(lèi)型String和Integer都被擦除掉了,只剩下原始類(lèi)型,在運(yùn)行時(shí)ArrayList<String>和ArrayList<Integer>對(duì)應(yīng)的class都是ArrayList.class。
既然泛型的類(lèi)型在編譯完成后就會(huì)被擦除,這樣一來(lái)我們是不是就可以在運(yùn)行時(shí)向ArrayList<Integer>中添加字符串呢?當(dāng)然可以,代碼如下:

public class Test {

    public static void main(String[] args) throws Exception {

        ArrayList<Integer> list = new ArrayList<Integer>();

        list.add(1);  //這樣調(diào)用 add 方法只能存儲(chǔ)整形,因?yàn)榉盒皖?lèi)型的實(shí)例為 Integer

        list.getClass().getMethod("add", Object.class).invoke(list, "asd");

        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }
    }
}

在程序中定義了一個(gè)ArrayList泛型類(lèi)型實(shí)例化為Integer對(duì)象,如果直接調(diào)用add()方法,那么只能存儲(chǔ)整數(shù)數(shù)據(jù),不過(guò)當(dāng)我們利用反射調(diào)用add()方法的時(shí)候,卻可以存儲(chǔ)字符串,這說(shuō)明了Integer泛型實(shí)例在編譯之后被擦除掉了,只保留了原始類(lèi)型。

2、類(lèi)型擦除后保留的原始類(lèi)型

原始類(lèi)型就是擦除去了泛型信息,最后在字節(jié)碼中的類(lèi)型變量的真正類(lèi)型。如果泛型無(wú)限定,則會(huì)用Object替換,如果泛型有限定,則會(huì)用限定類(lèi)型替換。

2.1、無(wú)限定的泛型擦除后替換成Object

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

上面泛型T是無(wú)限定的,所以在編譯完成后就被替換成Object,替換后等同于下面的代碼:

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

2.1、有限定的泛型擦除后替換成第一個(gè)邊界類(lèi)型

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

上面代碼中泛型T的上邊界為Number和Comparable,泛型擦除會(huì)被第一個(gè)邊界類(lèi)型替換,替換后的代碼如下:

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

3、泛型擦除后引起的問(wèn)題

3.1、泛型擦除后如何保證只能使用泛型限定的類(lèi)型

ArrayList<String> list = new ArrayList<String>();  
list.add("123");  
list.add(123);//編譯錯(cuò)誤  

ArrayList<String>泛型擦除會(huì)導(dǎo)致String被替換成Object,為什么只能向list中添加字符串呢?
因?yàn)樵诜盒筒脸?,編譯器會(huì)先進(jìn)行類(lèi)型檢查,然后再擦除,再進(jìn)行編譯。

3.2、泛型的類(lèi)型擦除前,類(lèi)型檢查的原理

先看如下代碼

ArrayList<String> list1 = new ArrayList(); 
list1.add("Hello");//編譯成功
list1.add(1);//編譯失敗
ArrayList list2 = new ArrayList<String>();
list2.add("Hello");//編譯成功
list2.add(1);//編譯成功

從上面代碼可以看出:ArrayList list2 = new ArrayList<String>()的泛型的類(lèi)型檢查是不成功的,我們依然可以向list2中添加任意數(shù)據(jù)。這又是為什么呢?
new ArrayList()只是開(kāi)辟了一個(gè)內(nèi)存空間,可以存儲(chǔ)任何類(lèi)型的對(duì)象,而類(lèi)型檢查是針對(duì)它的引用,引用list2并沒(méi)有使用泛型,所以并不能實(shí)現(xiàn)類(lèi)型檢查的功能。
例子:

public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  
        list1.add("1"); //編譯通過(guò)  
        list1.add(1); //編譯錯(cuò)誤  
        String str1 = list1.get(0); //返回類(lèi)型就是String  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //編譯通過(guò)  
        list2.add(1); //編譯通過(guò)  
        Object object = list2.get(0); //返回類(lèi)型就是Object  

        new ArrayList<String>().add("11"); //編譯通過(guò)  
        new ArrayList<String>().add(22); //編譯錯(cuò)誤  

        String str2 = new ArrayList<String>().get(0); //返回類(lèi)型就是String  
    }  

} 

從上面代碼可以看出類(lèi)型檢查是針對(duì)引用的,誰(shuí)是一個(gè)引用,用這個(gè)引用調(diào)用泛型方法時(shí),就會(huì)對(duì)這個(gè)引用調(diào)用的方法進(jìn)行類(lèi)型檢查,而無(wú)關(guān)它真正引用的對(duì)象。

3.3、泛型的類(lèi)型轉(zhuǎn)換

泛型擦除會(huì)導(dǎo)致泛型類(lèi)型被替換成Object或者上邊界類(lèi)型,那么為什么在獲取值時(shí)并不需要進(jìn)行強(qiáng)轉(zhuǎn)呢?
看下面的例子:

public class Main2<T> {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("aaa");
//      list.get(0);    //語(yǔ)句1    
        String str = list.get(0);
    }
}

泛型擦除會(huì)將泛型String替換成Object,所以在調(diào)用list.get()時(shí)返回的泛型擦除后的Object,那為什么String str = list.get(0);不需要進(jìn)行強(qiáng)轉(zhuǎn),這是因?yàn)榉祷厍皟?nèi)部已經(jīng)進(jìn)行了轉(zhuǎn)換。

3.4、類(lèi)型擦除和多態(tài)的沖突

先看個(gè)例子:定義一個(gè)泛型類(lèi)Parent

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

定義一個(gè)Child類(lèi)實(shí)現(xiàn)Parent,并將泛型的具體類(lèi)型指定為String類(lèi)型。

class Child extends Parent<String> {

     @Override
    public void setValue(String first){
        super.setValue(first);
    } 
     @Override
    public String getValue(){
        return super.getValue();
    }
}

泛型擦除會(huì)導(dǎo)致Parent的泛型被替換成Object,所以子類(lèi)繼承Parent時(shí)重寫(xiě)父類(lèi)的方法應(yīng)該為

class Child extends Parent<String> {

    @Override
    public void setValue(Object first){
        super.setValue(first);
    }
    @Override
    public Object getValue(){
        return super.getValue();
    }
}

可是子類(lèi)中重寫(xiě)父類(lèi)兩個(gè)方法的具體實(shí)現(xiàn)確實(shí)下面這樣的:

@Override
public void setValue(String first){
    super.setValue(first);
} 
 @Override
public String getValue(){
    return super.getValue();
}

可見(jiàn)類(lèi)型擦除和多態(tài)產(chǎn)生了沖突,為了解決這個(gè)沖突Java編譯器使用了橋方法。通過(guò)指令javap -c -s Child.class查看字節(jié)碼文件反編譯的結(jié)果:

橋方法.png

可以看到子類(lèi)中新增了兩個(gè)橋方法:

public void setValue(Object first) {
    setFirst((String)first);
}

public Object getValue() {
    //這里返回的 String 類(lèi)型的 getFirst 方法
    return getFirst();
}

這兩個(gè)橋方法,相當(dāng)于重寫(xiě)了父類(lèi)的兩個(gè)方法,最終還是會(huì)調(diào)用子類(lèi)的方法,相當(dāng)于實(shí)現(xiàn)了子類(lèi)對(duì)父類(lèi)的重寫(xiě)。
橋方法為子類(lèi)和父類(lèi)之間架起了一座連通的橋梁,真正實(shí)現(xiàn)了泛型繼承中的動(dòng)態(tài)綁定,也很好的解決了類(lèi)型擦除與多態(tài)之間的沖突。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容