抖音二面栽在泛型擦除?答应我,别和我倒在同一地方!
这是我在抖音二面的时候自我感觉没有答好的一题。因为我的中心只是围绕在了T
被Object
替换的问题上了,并没有去讲解他会带来的问题。
什么是泛型擦除?
其实我们很常见这个问题,你甚至经常用,只是没有去注意罢了,但是很不碰巧这样的问题就容易被面试官抓住。下面先来看一段代码吧。
Listlist=newArrayList();ListlistString=newArrayList<String>();ListlistInteger=newArrayList<Integer>();
这几段代码简单、粗暴、又带有很浓厚的熟悉感是吧。那我接下来要把一个数字1
插入到这三段不一样的代码中了。
作为读者的你可能现在已经黑人问号了????你肯定有很多疑问,这明显不一样啊,怎么可能。
publicclassMain{publicstaticvoidmain(String[]args){Listlist=newArrayList();ListlistString=newArrayList<String>();ListlistInteger=newArrayList<Integer>();try{list.getClass().getMethod("add",Object.class).invoke(list,1);listString.getClass().getMethod("add",Object.class).invoke(listString,1);//给不服气的读者们的测试之处,你可以改成字符串来尝试。listInteger.getClass().getMethod("add",Object.class).invoke(listInteger,1);}catch(Exceptione){e.printStackTrace();}System.out.println("listsize:"+list.size());System.out.println("listStringsize:"+listString.size());System.out.println("listIntegersize:"+listInteger.size());}}
不好意思,有图有真相,我就是插进去了,要是你还不信,我还真没办法了。
探索真相上述的就是泛型擦除的一种表现了,但是为了更好的理解,当然要更深入了是吧。虽然List
很大,但却也不是不能看看。
两个关键点,来验证一下:
数据存储类型数据获取//先来看看画了一个大饼的List//能够过很清楚的看到泛型EpublicclassArrayList<E>extendsAbstractList<E>implementsList<E>,RandomAccess,Cloneable,java.io.Serializable{//第一个关键点//还没开始就出问题的存储类型//难道不应该也是一个泛型E?transientObject[]elementData;publicEget(intindex){rangeCheck(index);returnelementData(index);//1---->}//由1直接调用的函数//第二个关键点,强制转化得来的数据EelementData(intindex){return(E)elementData[index];}}
我想,其实你也能够懂了,这个所谓的泛型T
最后会被转化为一个Object
,最后又通过强制转化来进行一个转变。从这里我们也就能够知道为什么我们的数据从前面过来的时候,String
类型数据能够直接被Integer
进行接收了。
(1) 强制类型转化
这个问题的结果我们已经在上述文章中提及到了,通过反射的方式去进行插入的时候,我们的数据就会发生错误。
如果我们在一个List<Integer>
中在不知情的情况下插入了一个String
类型的数值,那这种重大错误,我们该找谁去说呢。
(2)引用传递问题
上面的问题中,我们已经说过了T
将在后期被转义成Object
,那我们对引用也进行一个转化,是否行得通呢?
List<String>listObject=newArrayList<Object>();List<Object>listObject=newArrayList<String>();
如果你这样写,在我们的检查阶段,会报错。但是从逻辑意义上来说,其实你真的有错吗?
假设说我们的第一种方案是正确的,那么其实就是将一堆Object
数据存入,然后再由上面所说的强制转化一般,转化成String
类型,听起来完全ok,因为在List
中本来存储数据的方式就是Object
。但其实是会出现ClassCastException
的问题,因为Object
是万物的基类,但是强转是为子类向父类准备的措施。
再来假设说我们的第二种方案是正确的,这个时候,根据上方的数据String
存入,但是有什么意义存在呢?最后都还是要成Object
的,你还不如就直接是Object
。
其实很简单,如果看过一些公开课想来就见过这样的用法。
publicclassPart<TextendsParent>{privateTval;publicTgetVal(){returnval;}publicvoidsetVal(Tval){this.val=val;}}
相比较于之前的Part
而言,他多了<T extends Parent>
的语句,其实这就是将基类重新规划的操作,就算被编译,虚拟机也会知道将数据转化为Parent
而不是直接用Object
来直接进行替代。
这里需要感谢给我提出问题的大佬读者:挖掘机技术
该部分的思路来自于Java泛型中extends和super的区别?
上面我们说过了解决方案,使用<T extends Parent>
。其实这只是一种方案,在不同的场景下,我们需要加入不同的使用方法。另外官方也是提倡使用这样的方法的,但是我们为了避免我们上述的错误,自然需要给出一些使用场景了。
基于的其实是两种场景,一个是扩展型super
,一个是继承型extends
。下面都用一个列表来举例子。
//承载者classPlate<T>{privateTitem;publicPlate(Tt){item=t;}publicvoidset(Tt){item=t;}publicTget(){returnitem;}}//Lev1classFood{}//Lev2classFruitextendsFood{}classMeatextendsFood{}//Lev3classAppleextendsFruit{}classBananaextendsFruit{}classPorkextendsMeat{}classBeefextendsMeat{}//Lev4classRedAppleextendsApple{}classGreenAppleextendsApple{}<T extends Parent>
继承型的用处是什么呢?
其实他期待的就是这整个列表的数据的基础都是来自我们的Parent
,这样获取的数据全部人的父类其实都是来自于我们的Parent
了,你可以叫这个列表为Parent
家族。所以也可以说这是一个适合频繁读取的方案。
Plate<?extendsFruit>p1=newPlate<Apple>(newApple());Plate<?extendsFruit>p2=newPlate<Apple>(newBeef());//检查不通过//修改数据不通过p1.set(newBanana());//数据获取一切正常//但是他只能精确到由我们定义的FruitFruitresult=p1.get();<T super Parent>
扩展型的作用是什么呢?
你可以把它当成一种兼容工具,由super
修饰,说明兼容这个类,通过这样的方式比较适用于去存放上面所说的Parent
列表中的数据。这是一个适合频繁插入的方案。
//填写Food的位置,级别一定要大于或等于FruitPlate<?superFruit>p1=newPlate<Food>(newApple());//和extends不同可以进行存储p1.set(newBanana());//get方法Bananaresult1=p1.get();//会报错,一定要经过强制转化,因为返回的只是一个ObjectObjectresult2=p1.get();//返回一个Object数据我们已经属于快要丢失掉全部数据了,所以不适合读取
最后有话说以上就是我的学习成果,如果有什么我没有思考到的地方或是文章内存在错误,欢迎与我分享。
附上我的Android核心技术学习大纲,获取相关内容来我的GitHub一起玩耍:https://github.com/Meng997998/AndroidJX
vx:xx1341452
对于进阶这条路而言,学习是会有回报的!
你把你的时间投资在学习上,就意味着你可以收获技能,更有机会增加收入。
在这里分享我的Android学习PDF大全来学习,这份Android学习PDF大全真的包含了方方面面了,内含Java基础知识点、Android基础、Android进阶延伸、算法合集等等
我的这份学习合集,可以有效的帮助大家掌握知识点。
总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习
获取方式:关注我看个人介绍,或直接点击我免费领取
声明:本站所有文章资源内容,如无特殊说明或标注,均为采集网络资源。如若本站内容侵犯了原著者的合法权益,可联系本站删除。