Java的泛型类型擦除及类型擦除带来的问题

1、泛型的类型擦除

Java的泛型是伪泛型,不同于C++的模板机制,这是因为Java的泛型只存在编译期间,在编译完成后泛型就会被擦除。引入泛型是为了将类型检查提前到编译期间,将类型转换交由编译器处理,那么为什么还要进行泛型的擦除呢?泛型擦除的目的是为了向下兼容老的Java版本,老的Java版本是没有泛型概念的。
下面通过一个例子证明泛型的擦除

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());
    }

}

在这个例子中,我们定义了两个ArrayList数组,一个是ArrayList<String>泛型类型的,只能存储字符串;一个是ArrayList<Integer>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型,在运行时ArrayList<String>和ArrayList<Integer>对应的class都是ArrayList.class。
既然泛型的类型在编译完成后就会被擦除,这样一来我们是不是就可以在运行时向ArrayList<Integer>中添加字符串呢?当然可以,代码如下:

public class Test {

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

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

        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

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

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

在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。

2、类型擦除后保留的原始类型

原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。如果泛型无限定,则会用Object替换,如果泛型有限定,则会用限定类型替换。

2.1、无限定的泛型擦除后替换成Object

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

上面泛型T是无限定的,所以在编译完成后就被替换成Object,替换后等同于下面的代码:

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

2.1、有限定的泛型擦除后替换成第一个边界类型

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,泛型擦除会被第一个边界类型替换,替换后的代码如下:

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

3、泛型擦除后引起的问题

3.1、泛型擦除后如何保证只能使用泛型限定的类型

ArrayList<String> list = new ArrayList<String>();  
list.add("123");  
list.add(123);//编译错误  

ArrayList<String>泛型擦除会导致String被替换成Object,为什么只能向list中添加字符串呢?
因为在泛型擦除前,编译器会先进行类型检查,然后再擦除,再进行编译。

3.2、泛型的类型擦除前,类型检查的原理

先看如下代码

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>()的泛型的类型检查是不成功的,我们依然可以向list2中添加任意数据。这又是为什么呢?
new ArrayList()只是开辟了一个内存空间,可以存储任何类型的对象,而类型检查是针对它的引用,引用list2并没有使用泛型,所以并不能实现类型检查的功能。
例子:

public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型就是String  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList<String>().add("11"); //编译通过  
        new ArrayList<String>().add(22); //编译错误  

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

} 

从上面代码可以看出类型检查是针对引用的,谁是一个引用,用这个引用调用泛型方法时,就会对这个引用调用的方法进行类型检查,而无关它真正引用的对象。

3.3、泛型的类型转换

泛型擦除会导致泛型类型被替换成Object或者上边界类型,那么为什么在获取值时并不需要进行强转呢?
看下面的例子:

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

泛型擦除会将泛型String替换成Object,所以在调用list.get()时返回的泛型擦除后的Object,那为什么String str = list.get(0);不需要进行强转,这是因为返回前内部已经进行了转换。

3.4、类型擦除和多态的冲突

先看个例子:定义一个泛型类Parent

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

定义一个Child类实现Parent,并将泛型的具体类型指定为String类型。

class Child extends Parent<String> {

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

泛型擦除会导致Parent的泛型被替换成Object,所以子类继承Parent时重写父类的方法应该为

class Child extends Parent<String> {

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

可是子类中重写父类两个方法的具体实现确实下面这样的:

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

可见类型擦除和多态产生了冲突,为了解决这个冲突Java编译器使用了桥方法。通过指令javap -c -s Child.class查看字节码文件反编译的结果:

桥方法.png

可以看到子类中新增了两个桥方法:

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

public Object getValue() {
    //这里返回的 String 类型的 getFirst 方法
    return getFirst();
}

这两个桥方法,相当于重写了父类的两个方法,最终还是会调用子类的方法,相当于实现了子类对父类的重写。
桥方法为子类和父类之间架起了一座连通的桥梁,真正实现了泛型继承中的动态绑定,也很好的解决了类型擦除与多态之间的冲突。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343