当前位置: 首页 > 科技观察

面试官:告诉我什么是泛型类型擦除?

时间:2023-03-23 10:46:21 科技观察

先看一道常见的面试题。下面代码执行的结果是什么?publicstaticvoidmain(String[]args){Listlist1=newArrayList();Listlist2=newArrayList();System.out.println(list1.getClass()==list2。getClass());}首先我们知道getClas方法在运行时获取的是对象的类(Class),所以这个问题也可以转化为ArrayListt和ArrayList对应的对象运行时同一个类?我们直接揭晓答案,运行上面的代码,程序会打印true,说明代码中虽然声明了具体的泛型类型,但是两个List对象对应的Classes是相同的,打印它们的结果类型有:classjava.util.ArrayList也就是说,虽然ArrayList和ArrayList在编译时是不同的类型,但是编译完成后,它们都被编译器简化成了ArrayList。这种现象称为泛型类型擦除(TypeErasure)。泛型的本质是参数化类型,类型擦除使得类型参数只存在于编译期。在运行时,jvm不知道泛型的存在。那么为什么我们需要泛型类型擦除呢?在一些参考资料中,解释了类型擦除的主要目的是为了避免过度创建类而导致过多的运行时消耗。试想一下,如果用List表示一种类型,然后用List表示另一种类型,如此类推,无疑会造成类型数量的爆炸式增长。对类型擦除有了一个大概的了解之后,我们来看下面的问题。类型擦除有什么作用?上面我们说了,类型擦除会在编译完成后对泛型进行类型擦除。如果你想眼见为实,那么如果你真的看着它,你应该怎么做?然后需要编译字节码文件已经反编译了,这里我们使用一个轻量级的工具Jad来反编译(这个地址可以下载:https://varaneckas.com/jad/)Jad的使用也很简单的。下载解压后,将需要反编译的字节码文件放在该目录下,然后在命令行中执行以下命令,在同一目录下生成反编译后的.java文件:jad-sjavaTest.class就准备好了,工具准备好了现在,让我们来看看不同情况下的类型擦除。1.无限制类型擦除当类定义中的类型参数没有限制时,类型擦除后会直接替换为Object。下面的例子中,将中的类型参数T全部替换为Object(左边是编译前的代码,右边是字节码文件反编译得到的代码):2.限制类型擦除Exceptwhen类定义中对类型参数有限制,在类型擦除中用类型参数的上限或下限代替。下面代码中,擦除后T被Integer替换:3.擦除方法中的类型参数对比下面两边的代码,可以看到当擦除方法中的类型参数与擦除类定义进行对比时类型参数相同。如果没有限制,直接擦除为Object,如果有限制,则擦除为上界或下界:反射可以获取泛型吗?估计熟悉Java反射的都会有疑惑。反射中的getTypeParameters方法可以获取类、数组、接口等实体的类型参数,如果擦除类型,可以获取到什么?让我们尝试使用反射来获取类型参数:System.out.println(Arrays.asList(list1.getClass().getTypeParameters()));执行结果如下:[E]同理,如果打印出Map对象的参数类型:Mapmap=newHashMap<>();System.out。println(Arrays.asList(map.getClass().getTypeParameters()));最后只能得到:[K,V]可以看到通过getTypeParameters方法只能得到泛型参数占位符,并不能在你的代码中得到真正的泛型类型。指定类型的List可以放其他类型的对象吗?使用泛型的好处之一是可以在编译时检查类型安全,但是通过上面的例子我们知道在运行时是没有泛型约束的。那么是不是意味着一种类型的对象可以在运行时放入另一种类型的List中呢?我们先看看正常情况下直接调用add方法会报什么错:当我们尝试添加一个User类型的对象时,将一个对象放入String类型的数组中,泛型约束在编译时会报错,说明提供的User类型对象不适合String类型数组。所以既然编译期不行,那我们就在运行期写。借助实际运行的类没有泛型约束的事实,我们使用反射在运行时编写它:publicclassReflectTest{staticListlist=newArrayList<>();publicstaticvoidmain(String[]args){list.add("1");ReflectTestreflectTest=newReflectTest();try{Fieldfield=ReflectTest.class.getDeclaredField("list");field.setAccessible(true);Listlist=(List)field.get(reflectTest);列表.add(newUser());}catch(Exceptione){e.printStackTrace();}}}执行上面的代码,不仅可以通过编译时的语法检查,还可以正常使用debug看看数组内容:我们可以看到,虽然数组中声明的泛型类型是String,但是User类型的对象还是成功放置了。那么,如果我们尝试把代码中的User对象取出来,程序还能正常执行吗?我们在上面代码的最后加一句:System.out.println(list.get(1));再次执行代码,程序运行到最后一条print语句时,报错如下:异常提示User类型的对象不能转换成String类型。这是否意味着在取出对象时进行了强制类型转换?我们看一下ArrayList中的get方法源码:publicEget(intindex){rangeCheck(index);returnelementData(index);}EelementData(intindex){return(E)elementData[index];}可以看出,当元素被取出来,元素会被强制转换成泛型中的类型,也就是说,在上面的代码中,最终会尝试强制将User对象转换成String类型,程序会报错这个阶段的错误。通过这个过程,再次证明泛型可以检测类型安全。类型擦除会导致什么问题?我们来看一个稍微复杂一点的例子,首先声明一个接口,然后创建一个实现该接口的类:按照我们之前的理解,类型擦除之后,应该是这样的:,因为Apple类虽然也有get方法,但是和接口中的方法参数不一致,也就是说没有覆盖接口中的方法。在这种情况下,编译器会添加一个桥接方法来满足语法要求,同时保证基于泛型的多态性能够有效。我们对上面代码生成的字节码文件进行反编译:可以看到,编译后的代码中生成了两个get方法。参数为Object的get方法负责实现Fruit接口中的同名方法,然后在实现类中额外增加一个参数为Integer的get方法。该方法是理论上应该生成的带有参数类型的方法。最后,接口方法用于调用附加方法。这样就构建了接口和实现类的关系,类似于桥接的作用,所以也叫桥接方法。最后通过Java多态下的这种机制来保证泛型的情况。总结本文从面试中的一道常见面试题入手,介绍java中泛型类型擦除的相关知识。通过这个过程,大家也很容易理解为什么总是说java中的泛型是伪泛型。同时也帮助大家认识到java中泛型的一些缺陷。了解类型擦除的原因和原理,将有助于你在日常工作中更好地使用泛型。【小编推荐】19岁“神童”自制CPU!1200个晶体管,手工打造马斯克发布机器人,“钢铁侠”!特斯拉推出全球超快AI电脑Windows11来了,它们还活着!盘点老牌长青小软件Windows11预览更新!带你看Windows11内置新功能,千万别用5G,非5G手机为何不买