Java 泛型擦除原理

问:请比较深入的谈谈你对 Java 泛型擦除的理解和带来的问题认识?

答:Java 的泛型是伪泛型,因为在编译期间所有的泛型信息都会被擦除掉,譬如 List<Integer> 在运行时仅用一个 List 来表示(所以我们可以通过反射 add 方法来向 Integer 的泛型列表添加字符串,因为编译后都成了 Object),这样做的目的是为了和 Java 1.5 之前版本进行兼容。泛型擦除具体来说就是在编译成字节码时首先进行类型检查,接着进行类型擦除(即所有类型参数都用他们的限定类型替换,包括类、变量和方法,如果类型变量有限定则原始类型就用第一个边界的类型来替换,譬如 class Prd<T extends Comparable & Serializable> {} 的原始类型就是 Comparable),接着如果类型擦除和多态性发生冲突时就在子类中生成桥方法解决,接着如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。

先检查再擦除的类型检查是针对引用的,用引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。可以说这是为了兼容带来的问题,如下:

        ArrayList<String> arrayList1 = new ArrayList<String>();
        arrayList1.add("123"); //编译通过 
        arrayList1.add(123); //编译错误 
        String str1 = arrayList1.get(0); //返回类型是 String 
        ArrayList<String> arrayList2 = new ArrayList();
        arrayList2.add("123"); //编译通过 
        arrayList2.add(123);//编译错误
        String object2 = arrayList2.get(0); //返回类型是 String 
        ArrayList arrayList3 = new ArrayList<String>();
        arrayList3.add("123"); //编译通过 
        arrayList3.add(123); //编译通过 
        Object object3 = arrayList3.get(0); //返回类型是 Object

所以说擦除前的类型检查是针对引用的,用这个引用调用泛型方法就会对这个引用调用的方法进行类型检测而无关它真正引用的对象。

先检查再擦除带来的另一个问题就是泛型中参数化类型无法支持继承关系,因为泛型的设计初衷就是为了解决 Object 类型转换的弊端而存在,如果泛型中参数化类型支持继承操作就违背了设计的初衷而继续回到原始的 Object 类型转换弊端。也同样可以说这是为了兼容带来的问题,如下:

        ArrayList<Object> arrayList1 = new ArrayList<Object>();
        arrayList1.add(new Object());
        arrayList1.add(new Object());
        ArrayList<String> arrayList2 = arrayList1; //编译错误 
        ArrayList<String> arrayList3 = new ArrayList<String>();
        arrayList3.add("abc");
        arrayList3.add(new String());
        ArrayList<Object> arrayList4 = arrayList3; //编译错误 
        ArrayList<String> arrayList5 = new ArrayList<Object>(); //编译错误 
        ArrayList<Object> arrayList6 = new ArrayList<String>(); //编译错误

之所以这样我们可以从反面来论证,假设编译不报错则当通过 arrayList2 调用 get() 方法取值时返回的是 String 类型的对象(因为类型检测是根据引用来决定的),而实际上存放的是 Object 类型的对象,这样 get 出来就会 ClassCastException 了,所以这违背了泛型的初衷。对于 arrayList4 同样假设编译不报错,当调用 arrayList4 的 get() 方法取出来的 String 变成了 Object 虽然不会出现 ClassCastException,但是依然没有意义啊,泛型出现的原因就是为了解决类型转换的问题,其次如果我们通过 arrayList4 的 add() 方法继续添加对象则可以添加任意类型对象实例,这就会导致我们 get() 时更加懵逼不知道加的是什么类型了,所以怎么说都是个死循环。

擦除带来的另一个问题就是泛型与多态的冲突,其通过子类中生成桥方法解决了多态冲突问题,这个问题的验证也很简单,可以通过下面的例子说明:

        class Creater<T> {
            private T value;

            public T getValue() {
                return value;
            }

            public void setValue(T value) {
                this.value = value;
            }
        }
        class StringCreater extends Creater<String> {
            @Override
            public void setValue(String value) {
                super.setValue(value);
            }

            @Override
            public String getValue() {
                return super.getValue();
            }
        }
        StringCreater stringCreater = new StringCreater();
        stringCreater.setValue("abc");
        stringCreater.setValue(new Object()); //编译错误

上面代码段的运行情况很诧异吧,按理来说 Creater 类被编译擦除后 setValue 方法的参数应该是 Object 类型了,子类 StringCreater 的 setValue 方法参数类型为 String,看起来父子类的这组方法应该是重载关系,所以调用子类的 setValue 方法添加字符串和 Object 类型参数应该都是合法才对,然而从编译来看子类根本没有继承自父类参数为 Object 类型的 setValue 方法,所以说子类的 setValue 方法是对父类的重写而不是重载(从子类添加 @Override 注解没报错也能说明是重写关系)。关于出现上面现象的原理其实我们通过 javap 看下两个类编译后的本质即可:


通过编译后的字节码我们可以看到 Creater 泛型类在编译后类型被擦除为 Object,而我们子类的本意是进行重写实现多态,可类型擦除后子类就和多态产生了冲突,所以编译后的字节码里就出现了桥方法来实现多态。可以看到桥方法的参数类型都是 Object,也就是说子类中真正覆盖父类方法的是桥方法,而子类 String 参数 setValue、getValue 方法上的 @Oveerride 注解只是个假象,桥方法的内部实现是直接调用了我们自己重写的那两个方法;不过上面的 setValue 方法是为了解决类型擦除与多态之间的冲突生成的桥方法,而 getValue 是一种协变,之所以子类中 Object getValue() 和 String getValue() 方法可以同时存在是虚拟机内部的一种区分(我们自己写的代码是不允许这样的),因为虚拟机内部是通过参数类型和返回类型来确定一个方法签名的,所以编译器为了实现泛型的多态允许自己做这个看起来不合法的实现,实质还是交给了虚拟机去区别。

先检查再擦除带来的另一个问题就是泛型读取时会进行自动类型转换问题,所以如果调用泛型方法的返回类型被擦除则在调用该方法时插入强制类型转换。
关于这个可以通过 javap 去查看使用 List 的 add、get 方法后的字节码指令,你会发现 checkcast 指令不是在 get 方法里面强转的(虽然 get 方法里面返回值在代码里面是被转换成了 T,实际编译擦除了),而是在调用处强转的。

擦除带来的另一个问题是泛型类型参数不能是基本类型,比如 ArrayList<int> 是不合法的,只有 ArrayList<Integer> 是合法的,因为当类型擦除后 ArrayList 的原始类型是 Object,而 Object 是引用类型而不是基本类型。

擦除带来的另一个问题是无法进行具体泛型参数类型的运行时类型检查,譬如 arrayList instanceof ArrayList<String> 是非法的,Java 对于泛型运行时检查的支持仅限于 arrayList instanceof ArrayList<?> 方式。

擦除带来的另一个问题是我们不能抛出也不能捕获泛型类的对象,因为异常是在运行时捕获和抛出的,而在编译时泛型信息会被擦除掉,擦除后两个 catch 会变成一样的东西。也不能在 catch 子句中使用泛型变量,因为泛型信息在编译时已经替换为原始类型(譬如 catch(T) 在限定符情况下会变为原始类型 Throwable),如果可以在 catch 子句中使用则违背了异常的捕获优先级顺序。

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

推荐阅读更多精彩内容

  • 泛型是Java 1.5引入的新特性。泛型的本质是参数化类型,这种参数类型可以用在类、变量、接口和方法的创建中,分别...
    何时不晚阅读 3,028评论 0 2
  • 前面,由于对泛型擦除的思考,引出了对Java-Type体系的学习。本篇,就让我们继续对“泛型”进行研究: JDK1...
    贾博岩阅读 5,139评论 3 28
  • 摘要: 和C++以模板来实现静多态不同,Java基于运行时支持选择了泛型,两者的实现原理大相庭径。C++可以支持基...
    Java红茶阅读 667评论 0 2
  • 附上思维导图。这篇博客主要讲了如下知识点。 看完了《Thinking in Java》的第十五章泛型,着实被震了一...
    Happioo阅读 722评论 0 1
  • 一、为什么要使用泛型 1.类型参数的好处 类型安全:泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛...
    SeanMa阅读 7,065评论 1 18