深入理解Java泛型
7月3日 听雨眠投稿 头条创作挑战赛什么是Java泛型
大家好,我是呼噜噜,Java泛型(generics)是Jdk5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
比如ArrayListlistnewArrayList()这行代码就指明了该ArrayList对象只能存储String类型,如果传入其他类型的对象就会报错。让我们时光回退到Jdk5的版本,那时ArrayList内部其实就是一个Object〔〕数组,配合存储一个当前分配的长度,就可以充当可变数组:publicclassArrayList{privateObject〔〕publicvoidadd(Objecte){。。。}publicvoidremove(intindex){。。。}publicObjectget(intindex){。。。}}
我们来举个简单的例子,ArrayListlistnewArrayList();list。add(test);list。add(666);
我们本意是用ArrayList来装String类型的值,但是突然混进去了Integer类型的值,由于ArrayList底层是Object数组,可以存储任意的对象,所以这个时候是没啥问题的,但我们不能只存不用啊,我们需要把值给拿出来使用,这个时候问题来了:for(Objectitem:list){System。out。println((String)item);}
结果:
Exceptioninthreadmainjava。lang。ClassCastException:java。lang。Integercannotbecasttojava。lang。String
由于我们需要String类型的值,我们需要把ArrayList的Object值强制转型,但是之前混进去了Integer,虽然编译阶段通过了,但程序的运行结果会以崩溃结束,报ClassCastException异常
为了解决这个问题,在Jdk5版本中就引入了泛型的概念,而引入泛型的很大一部分原因就是为了解决我们上述的问题,允许程序员在编译时检测到非法的类型。不是同类型的就不允许在一块存放,这样也避免了ClassCastException异常的出现,而且因为都是同一类型,也就没必要做强制类型转换了。我们可以把ArrayList变量参数化:publicclassArrayListT{privateT〔〕我们假设ArrayListT内部会有个T〔〕publicvoidadd(Te){。。。}publicvoidremove(intindex){。。。}publicTget(intindex){。。。}}
其中T叫类型参数,T可以是任何class类型,现在ArrayList我们可以如下使用:存储String的ArrayListArrayListStringlistnewArrayListString();list。add(666);编译器会在编译阶段发现问题,从而提醒开发者
泛型其本质是参数化类型,也就是说数据类型作为参数,解决不确定具体对象类型的问题。泛型的使用
泛型一般有三种使用方式,分别为:泛型类、泛型接口、泛型方法,我们简单介绍一下泛型的使用泛型类此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型在实例化泛型类时,必须指定T的具体类型publicclassGenericT{privateTpublicGeneric(Tkey){this。}publicTgetKey(){}}
如何实例化泛型类:GenericIntegergenericIntegernewGenericInteger(666);GenericStringgenericStrnewGenericString(hello);泛型接口定义一个泛型接口publicinterfaceGeneratorT{publicTmethod();}实现泛型接口,不指定类型classGeneratorImplTimplementsGeneratorT{OverridepublicTmethod(){}}实现泛型接口,指定类型classGeneratorImplTimplementsGeneratorString{OverridepublicStringmethod(){}}泛型方法publicclassGenericMethods{publicTvoidf(Tx){System。out。println(x。getClass()。getName());}publicstaticvoidmain(String〔〕args){GenericMethodsgmnewGenericMethods();gm。f(啦啦啦);gm。f(666);}}
结果:java。lang。String
java。lang。Integer泛型的底层实现机制ArrayList源码解析
通过上文我们知道,为了让ArrayList存取各种数据类型的值,我们需要把ArrayList模板化,将变量的数据类型给抽象出来,作为类型参数publicclassArrayListT{privateT〔〕我们以为ArrayListT内部会有个T〔〕publicvoidadd(Te){。。。}publicvoidremove(intindex){。。。}publicTget(intindex){。。。}}
但当我们查看Jdk8的ArrayList源码,底层数组还是Object数组:transientObject〔〕elementD那ArrayList为什么还能进行类型约束和自动类型转换呢?什么是泛型擦除
我们再看一个经典的例子:publicclassgenericTest{publicstaticvoidmain(String〔〕args){SIArrayListStringl1newArrayListString();l1。add(aaa);strl1。get(0);ArrayListIntegerl2newArrayListInteger();l2。add(666);paraml2。get(0);System。out。println(l1。getClass()l2。getClass());}}
结果竟然是true,ArrayList。class和ArrayList。class应该是不同的类型。通过getClass()方法获取他们的类的信息,竟然是一样的。我们来查看这个文件的class文件:publicclassgenericTest{publicgenericTest(){}publicstaticvoidmain(String〔〕var0){Stringvar1;Integervar2ArrayListvar3newArrayList();泛型被擦擦了var3。add(aaa);var1(String)var3。get(0);ArrayListvar4newArrayList();泛型被擦擦了var4。add(666);var2(Integer)var4。get(0);System。out。println(var3。getClass()var4。getClass());}}
我们在对其反汇编一下:javapcgenericTest:genericTestcom。zj。demotest。test5。genericTestCompiledfromgenericTest。javapublicclasscom。zj。demotest。test5。genericTest{publiccom。zj。demotest。test5。genericTest();Code:0:aload01:invokespecial1MethodjavalangObject。init:()V4:returnpublicstaticvoidmain(java。lang。String〔〕);Code:0:ldc2String2:astore13:aconstnull4:astore25:new3classjavautilArrayList8:dup9:invokespecial4MethodjavautilArrayList。init:()V12:astore313:aload314:ldc5Stringaaa16:invokevirtual6MethodjavautilArrayList。add:(LjavalangO)Z19:pop20:aload321:iconst022:invokevirtual7MethodjavautilArrayList。get:(I)LjavalangO25:checkcast8classjavalangString28:astore129:new3classjavautilArrayList32:dup33:invokespecial4MethodjavautilArrayList。init:()V36:astore438:aload440:sipush66643:invokestatic9MethodjavalangInteger。valueOf:(I)LjavalangI46:invokevirtual6MethodjavautilArrayList。add:(LjavalangO)Z49:pop50:aload452:iconst053:invokevirtual7MethodjavautilArrayList。get:(I)LjavalangO56:checkcast10classjavalangInteger59:astore260:getstatic11FieldjavalangSystem。out:LjavaioPrintS63:aload364:invokevirtual12MethodjavalangObject。getClass:()LjavalangC67:aload469:invokevirtual12MethodjavalangObject。getClass:()LjavalangC72:ifacmpne7975:iconst176:goto8079:iconst080:invokevirtual13MethodjavaioPrintStream。println:(Z)V83:return}看第16、46处,add进去的是原始类型O看第22、53处,get方法获得也是Object类型,String、Integer类型被擦出,只保留原始类型Object。看25、55处,checkcast指令是类型转换检查,在结合class文件var1(String)var3。get(0);var2(Integer)var4。get(0);我们知晓编译器自动帮我们强制类型转换了,我们无需手动类型转换
经过上面的种种现象,我们可以发现,在类加载的编译阶段,泛型类型String和Integer都被擦除掉了,只剩下原始类型,这样他们类的信息都是Object,这样自然而然就相等了。这种机制就叫泛型擦除。
我们需要了解一下类加载生命周期:
详情见:https:mp。weixin。qq。comsv91bqRiKDWWgeNl1DIdaDQ
泛型是和编译器的约定,在编译期对代码进行检查的,由编译器负责解析,JVM并无识别的能力,一个类继承泛型后,当变量存入这个类的时候,编译器会对其进行类型安全检测,当从中取出数据时,编译器会根据与泛型的约定,会自动进行类型转换,无需我们手动强制类型转换。
泛型类型参数化,并不意味这其对象类型是不确定的,相反它的对象类型对于JVM来说,都是确定的,是Object或Object〔〕数组泛型的边界
来看一个经典的例子,我们想要实现一个ArrayList对象能够储存所有的泛型:ArrayListObjectlistnewArrayListString();
但可以的是编译器提示报错:
明明String是Object类的子类,我们可以发现,泛型不存在继承、多态关系,泛型左右两边要一样别担心,JDK提供了通配符?来应对这种场景,我们可以这样:ArrayL?listnewArrayListString();listnewArrayListInteger();
通配符?表示可以接收任意类型,此处?是类型实参,而不是类型形参。我们可以把它看做是String、Integer等所有类型的父类。是一种真实的类型。通配符还有:上边界限定通配符,如?extendsE;下边界通配符,如?superE;?:无界通配符
?是开放限度最大的,可指向任意类型,但在对于其的存取上也是限制最大的:入参和泛型相关的都不能使用,除了null(禁止存入),比如ArrayL?list不可以添加任何类型,因为并不知道实际是哪种类型返回值和泛型相关的都只能用Object接收extends上边界通配符泛型的上限只能是该类型的类型及其子类,其中Number是Integer、Long、Float的父类ArrayL?extendsNumberlistnewArrayListInteger();ArrayL?extendsNumberlist2newArrayListLong();ArrayL?extendsNumberlist3newArrayListFloat();list。add(1);报错,extends不允许存入ArrayListLonglongListnewArrayList();longList。add(1L);listlongL由于extends不允许存入,list只能重新指向longListNumbernumberlist。get(0);extends取出来的元素(Integer,Long,Float)都可以转Number
extends指向性被砍了一半,只能指向子类型和父类型,但方法使用上又适当放开了:值得注意的是:这里的extends并不表示类的继承含义,只是表示泛型的范围关系extends不允许存入,由于使用extends,比如ArrayL?extendsNumberlist可以接收Integer、Long、Float,但是泛型本质是保证两边类型确定,这样的话在程序运行期间,再存入数据,编译器可无法知晓数据的类型,所以只能禁止了。但为什么ArrayL?extendsNumberlist可以重新指向longList来变向地存储值,那是因为ArrayListlongListnewArrayList();这边的泛型已经约束两边的类型了,编译器知晓longList储存的数据都是Long类型但extends允许取出,取出来的元素可以往边界类型转extends中可以指定多个范围,实行泛型类型检查约束时,会以最左边的为准。super下边界通配符泛型的下限只能是该类型的类型及其父类,其中Number是Integer、Long、Float的父类ArrayL?superIntegerlistnewArrayListInteger();ArrayL?superIntegerlist2newArrayListNumber();ArrayL?superIntegerlist3newArrayListLong();报错ArrayL?superIntegerlist4newArrayListFloat();报错list2。add(123);super可以存入,只能存Integer及其子类型元素Objectaalist2。get(0);super可以取出,类型只能是Object
super允许存入编辑类型及其子类型元素,但取出元素只能为Object类型PECS原则
泛型通配符的出现,是为了获得最大限度的灵活性。如果要用到通配符,需要结合业务考虑,《EffectiveJava》提出了:PECS(ProducerExtendsConsumerSuper)需要频繁往外读取内容(生产者Producer),适合用?extendsT需要频繁写值(消费者Consumer),适合用?superT:super允许存入子类型元素?表示不确定的java类型,一般用于只接收任意类型,而不对其处理的情况泛型是怎么擦除的
Java编译器通过如下方式实现擦除:用Object或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法;在恰当的位置插入强制转换代码来确保类型安全;在继承了泛型类或接口的类中自动产生桥接方法来保留多态性。擦除类定义中的无限制类型参数
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和?的类型参数都被替换为Object
擦除类定义中的有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,形如和?extendsNumber的类型参数被替换为Number,?superNumber被替换为Object
擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,额外补充擦除方法定义中的有限制类型参数的例子
桥接方法和泛型的多态publicclassAT{publicTget(Ta){进行一些操作}}publicclassBextendsAString{overridepublicStringget(Stringa){进行一些操作}}
由于类型擦出机制的存在,按理说编译后的文件在翻译为java应如下所示:publicclassA{publicObjectget(Objecta){进行一些操作}}publicclassBextendsA{overridepublicStringget(Stringa){进行一些操作}}
但是,我们可以发现override意味着B对父类A中的get方法进行了重写,但是依上面的程序来看,只是重载,依然可以执行父类的方法,这和期望是不附的,也不符合java继承、多态的特性。重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。即外壳不变,核心重写!
重载(overloading)是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
为了解决这个问题,java在编译期间加入了桥接方法。编译后再翻译为java原文件其实是:publicclassA{publicObjectget(Objecta){进行一些操作}}publicclassBextendsA{overridepublicStringget(Stringa){进行一些操作}桥接方法!!!publicObjectget(Objecta){returnget((String)a)}}
桥接方法重写了父类相同的方法,并且桥接方法中,最终调用了期望的重写方法,并且桥接方法在调用目的方法时,参数被强制转换为指定的泛型类型。桥接方法搭起了父类和子类的桥梁。
桥接方法是伴随泛型方法而生的,在继承关系中,如果某个子类覆盖了泛型方法,则编译器会在该子类自动生成桥接方法。所以我们实际使用泛型的过程中,无需担心桥接方法。
泛型擦除带来的限制与局限泛型不适用基本数据类型
不能用类型参数代替基本类型(byte、short、int、long、float、double、char、boolean)比如,没有Pair,只有Pair。其原因是泛型擦除,擦除之后只有原始类型Object,而Object无法存储double等基本类型的值。
但Java同时有自动拆装箱特性,可以将基本类型装箱成包装类型,这样就使用泛型了,通过中转,即可在功能上实现用基本类型实例化类型化参数。
数据类型
封装类
byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
char
Character
boolean
Boolean无法创建具体类型的泛型数组ListInteger〔〕l1newArrayListInteger〔10〕;ErrorListString〔〕l2newArrayListString〔10〕;Error
上文我们知晓ArrayList,底层仍旧采用Object〔〕,Integer,String类型信息都被擦除
借助无限定通配符?,可以创建泛型数组,但是涉及的操作都基本上与类型无关L?〔〕l1newArrayL?〔10〕;
如果想对数组进行复制操作的话,可以通过Arrays。copyOfRange()方法publicclassTestArray{publicstaticvoidmain(String〔〕args){Integer〔〕arraynewInteger〔〕{2,3,1};Integer〔〕arrNewcopy(array);}privatestaticEE〔〕copy(E〔〕array){returnArrays。copyOfRange(array,0,array。length);}}反射其实可以绕过泛型的限制
由于我们知晓java是通过泛型擦除来实现泛型的,JVM只能识别原始类型Object,所以我们只需骗过编译器的校验即可,反射是程序运行时发生的,我们可以借助反射来波骚操作ListIntegerl1newArrayList();l1。add(111);l1。add(骚气的我);泛型会报错try{Methodmethodl1。getClass()。getDeclaredMethod(add,Object。class);method。invoke(l1,骚气的我又出现了);}catch(NoSuchMethodExceptione){e。printStackTrace();}catch(IllegalAccessExceptione){e。printStackTrace();}catch(InvocationTargetExceptione){e。printStackTrace();}for(Objecto:l1){System。out。println(o);}
结果:111
骚气的我又出现了
尾语
如果你了解其他语言(例如C)的参数化机制,你会发现,Java泛型并不能满足所有的预期。由于泛型出来前,java已经有了很多项目了,为了兼容老版本,采用了泛型擦除来实现泛型,这会遇到很多意料之外的麻烦,但这并不是说Java泛型毫无用处,它大多数情况能够让代码更加优雅,后面有机会我们会继续深入聊聊泛型擦除带来的麻烦及其历史渊源。
参考资料:
《OnJava8》
《EffectiveJava》
https:www。liaoxuefeng。comwiki12525995483437441265102638843296
https:www。cnblogs。commahuan2p6073493。html
本篇文章到这里就结束啦,如果我的文章对你有所帮助,还请帮忙一键三连:点赞、关注、收藏,你的支持会激励我输出更高质量的文章,感谢!
计算机内功、JAVA源码、职业成长、项目实战、面试相关等更多高质量文章,首发于公众号小牛呼噜噜,我们下期再见。
投诉 评论
收到面试通知后你该做好以下这些事接到面试通知后应该怎么做?1。快速找到企业的招聘广告。审核企业背景(一般在招聘文件中说明),应聘岗位有什么要求等。如果你有几封不同的求职信,你应该知道你发送的是哪一……
梅花造句用梅花造句大全(181)融入梅花,感悟梅花的品格;我赞美贞坚的松柏,我欣赏莲花的出淤泥而不染!可我更爱梅花的傲雪怒放,任你数九隆冬,地冻天寒,梅花依然开的那么艳丽,遥知不是雪,唯有暗香来。……
份子钱给讲究随礼随300有忌讳吗?结婚一定会给份子钱,份子钱给300讲究可不少,来看看别人包300块你怎么回礼?一、份子钱给300讲究1、在中国,一般3这个数字代表着散,寓意是……
小鳄鱼买牙刷小鳄鱼长着一张宽宽的大嘴,大嘴里长满了尖尖的牙齿。有这样锋利的牙齿真好,瞧,小鳄鱼吃起肉来一点也不费力,他天天啊呜啊呜地吃着大块大块的肉,吃得可开心啦,就连有些肉渣塞进了……
生命不是永远差3分的等待叮铃铃叮铃铃清晨正在酣睡的我,突然被一阵吵闹的闹铃声叫醒,我缓缓睁开朦胧惺忪的睡眼,不情愿的从暖烫烫的被窝中伸出一只手,去关闭了闹铃,本想继续懒一会儿床,没想到手机自动连上网络……
风归路上世纪70年代末到80年代初,随着伟人邓小平提出改革开放政策,吕武村也在时代背景之下形成了经商之风以及返家慕亲之风。80年代初到90年代初,计划生育政策的发展变化,吕武村……
揭秘吴三桂处决明永历帝内情有满人欲拥其复位吴三桂还不算绝情,他在政治上对永历帝朱由榔冷血,严密拘禁,在生活上却给予了他们良好的待遇。永乐帝依然是以前的皇家装束,头戴马鬃瓦棱帽,身穿纯绢大袖袍,腰系黄丝带,举止有度,衣着……
西游记第七十七回读书笔记西游记读书笔记第七十七回七十七回群魔欺本性一体拜真如狮驼岭三个妖王,假意殷勤,把司徒四个带进城中。一声炮响,付兵四起。悟空等人手忙脚乱,奋力抵抗,从早到晚,体……
黛玉的柳絮词想表达什么说文解字之《柳絮词》二首红学研究《红楼梦》第七十回,林黛玉作了一首《桃花行》,本来众人打算开一个桃花社的,却因为众多事情耽误了。在暮春时节,湘云作了一首柳絮词拿给众……
英超诺丁汉森林vs埃弗顿昨天的曼城在主场2:0顺利拿下了劲敌纽卡斯尔,咱们昨天的文章方向没有问题,不过晚场的阿森纳在落后两球的情况下还是上演了三球的惊天逆转,曼城仍然落后阿森纳5分,英超冠军的悬念还在……
可以让女生感动的话语你的话已经锁在我的记忆里了那钥匙你就替我保管一辈子吧。这样的话语能不能让女生感动呢?试试去吧!下面是美文网小编给你分享的可以让女生感动的话语,欢迎浏览。可以让女生感动的话……
影响宝宝智力发育,这2个关键期要把握住,别不当回事文余明仁小宝宝的智力发育是很多妈妈的一个盲区,对于很多妈妈而言,可能宝宝出生之后就是关注身高体重的增长,宝宝每一次吃多少为主。但是宝宝的智力发育也是非常关键的,这一点也是……