关于Java泛型擦除的那点事er

    这篇文章先对比了一下C++的泛型代码,能让你更清楚的感受到擦除的效果(请放心,只是简单的c++代码,不了解c++的同学也能看的懂)。

先看下C++的泛型:

#include <iostream>

using namespace std;

template <class  T> class Manipulator{

        T obj;

        public:Manipulator(T x){ obj=x; }

        void manipulate(){ obj.f(); }

};

class HasF{

        public:void f(){ cout<<"HasF::f()"<< endl; }

};

int main(){

        HasF hf;

        Manipulator<HasF> manipulator(hf); 

        manipulator.manipulate();

}

/*Output: HasF::f() */

    Manipulator类存储了一个类型T的对象,有意思的地方是manipulate()方法,这个方法中obj调用了方法f()。在C++中当你实例化这个模板时,C++编译器将进行检查,因此在Manipulator<HasF>被实例化的这一刻,它看到HasF拥有一个方法f()。如果情况并非如此,就会得到一个编译期的错误,这样类型安全就得到保障了。       ——————以上摘自Thinking in Java (事实上这篇文章大部分内容都会摘自Thinking in Java   >.<!   )

接下来看一下Java中的代码:

publicclass HasF {

    public void f() {System.out.println("HasF.f()");}}

}

publicclass Manipulator<T> {

    private T obj;

    public Manipulator(T obj) {

        this.obj = obj;

    }

    public void manipulate() {

        // obj.f();//此处报错

    }

}

public class Manipulation {

    public static void main(String[] args) {

        HasF hasF = new HasF();

        Manipulator<HasF> manipulator = new Manipulation(hasF);

        manipulator.manipulate();

    }

}

可以看出Java和C++泛型代码很明显区别就是C++中的泛型有实际类型信息,而Java中的实际类型信息不管是编译时期还是在运行时期都被擦除了,这就是擦除的效果。由于有了擦除,Java编译器无法将obj调用f()这一需求映射到HasF拥有f()这一事实上。(事实上擦除是将泛型类型信息擦除到了它的第一个边界,默认不设置的边界是Object,你可以调用Object的方法,可以这样设置边界——<T extends HasF>,设置边界后就可以调用f()了。这篇文章不讲边界的概念)

通过上面的对比应该能明显的感受到Java擦除的存在了吧?在Java中,当你使用泛型时,任何具体的类型信息都会被擦除,你唯一知道的就是你在使用一个对象。

那这样问题就来了——实际的泛型类型信息被擦除了,Java是怎么保障类型信息的正确性呢?看下一段Java代码:

public class FilledListMaker<T> {

    List<T> create(T t, int n) {

        List<T> result = new ArrayList<T>();

        for (int i = 0; i < n; i++) {publicresult.add(t);}

    }

    public static void main(String[] args) {

        FilledListMaker<String> stringMaker = new FilledListMaker<String>();

        List<String> list = stringMaker.create("hello", 4);

        System.out.println(list);    

        //System.out.println(Arrays.toString(list.getClass().getTypeParameters()));

    }

}

/*[hello,hello,hello,hello]/*

/* [E] /*

    在这段代码中看起来好像是拥有了String参数类型信息,但实际并非如此。最后一行代码的意思是输出一个TypeVariable对象数组,数组表示泛型所声明的类型信息。但是,正如你看见的,输出的只是用作参数占位符的标识符,并非有用的信息。 

    在这上面这段代码中编译器无法知道有关create()T的任何信息,但是仍旧可以在编译期确保你放置到result中的对象具有T类型,使其适合ArrayList<T>。因此,即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。(记住编译器无法知道类型信息,但它可以保障类型信息安全

   擦除在方法体中移除了类型信息,所以在运行时的问题就是边界,即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。(这段话中的边界和上方设置边界不是一个概念,别搞混了。)再看看下面的两段代码:

public class SimpleHolder {

    private Object obj;

    public Object getObj() { return obj; }

    public void setObj(Object obj) { this.obj = obj; }    

    public static void main(String[] args) {

        SimpleHolder simpleHolder = new SimpleHolder();

        simpleHolder.setObj("Item");

        String s = (String) simpleHolder.getObj();

    }

}

public class GenericHolder<T>{

    private T obj;

    public T getObj() { return obj; }

    public void setObj(T obj) { this.obj = obj; }

    public static void main(String[] args) {

        GenericHolder genericHolder = new GenericHolder();

        genericHolder.setObj("Item");

        String s = genericHolder.getObj();

    }

}

如果用Java -c SimpleHolder反编译这个类,就可以得到下面的内容(直接上图了实在是懒得写了):

SimpleHolder

可以看出set()和get()方法直接存储和产生值,而转型是在调用get()的时候接受检查的。接下来看一下GenericHolder:

GenericHolder

    可以看出GenericHolder产生的字节码操作和SimpleHolder产生的字节码操作是相同的。对于进入set()的类型进行检查是不需要的,因为这将由编译器执行。而对从get()返回的值进行转型仍旧是需要的,但这与你自己必须执行的操作是一样的——此处它将由编译器自动插入。

    那么问题又来了——为什么要有擦除?

    擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。(因为泛型的概念是JavaSE5之后提出的,所以为了兼容以前的客户端和类库提出了擦除的概念,擦除也使得非泛化代码向着泛型的迁移成为了可能。)

    擦除存在的主要理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言中。擦除使得现有的非泛型客户端代码能够在不改变的情况下继续使用,直至客户端准备好用泛型重写这些代码。擦除的代价也是比较明显的,实际类型的信息丢失,无法使用转型、instanceof操作、和new表达式。当你在编写泛型代码时,必须要提醒自己你只是看起来好像拥有了有关参数类型信息而已,实际上这个泛型类型的实际类型信息将被擦除到第一个边界,你唯一知道的就是你只是在操作一个对象。

    那么总结一下,擦除是擦掉了泛型类型的实际类型信息(擦除到了第一个边界),而编译器保障了类型的安全性,擦除的存在是为了实现迁移的兼容性

    有需要改进的地方,望指出,谢谢。

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

推荐阅读更多精彩内容

  • //出自51博客:www.Amanda0928.51.com 第一章 一、选择题 1.B; (typedef ,t...
    Damongggggg阅读 11,130评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,663评论 18 139
  • vhjhhuisnwbbjjsjbbdbjsjj
    仇志轩阅读 146评论 0 5
  • 成长有的时候是一瞬间的,但有的时候真的要伴着伤掺着痛,慢慢愈合。如果说时间是一剂良药,当初所谓的伤心欲绝,到头来也...
    宋明鑫阅读 179评论 0 0
  • 痛苦就是不让我们接受的体验。只有让痛苦流动起来你才会活得不那么累。同事之间因为你的想法或者她的想法不同,会让对方痛...
    潍坊谷德DDM徐芳阅读 193评论 2 0