面试:Java中的泛型会被类型擦除,那为什么在运行期仍然可以使用反射获取到具体的泛型类型
要想理解这个问题,首先应该对java虚拟机class文件有一定的理解,然后对这个问题的理解就会非常清晰。众所周知,java是在Java5的时候引入的泛型,为了支持泛型,JVM的class文件也做了相应的修改,其中最重要的就是新增了属性表,java编译为字节码后,其申明侧泛型信息都存储在Signature中,通过反射获取的泛型信息都来源于这里。而Signature属性表可以被class文件,字段表,
Java中的泛型会被类型擦除,那为什么在运行期仍然可以使用反射获取到具体的泛型类型? - 知乎
面试题:你知道泛型擦除后是如何获取泛型信息的吗?_Zhou Jiang的博客-CSDN博客_泛型擦除后如何获取类型信息 简书
泛型的类型擦除后,fastjson反序列化时如何还原?-有了
要想理解这个问题,首先应该对java虚拟机class文件有一定的理解,然后对这个问题的理解就会非常清晰。
众所周知,java是在Java5的时候引入的泛型,为了支持泛型,JVM的class文件也做了相应的修改,其中最重要的就是新增了Signature属性表,java编译为字节码后,其申明侧泛型信息都存储在Signature中,通过反射获取的泛型信息都来源于这里。
而Signature属性表可以被class文件,字段表,方法表携带,这就使得:类声明,字段声明,方法声明中的泛型信息得以保留。
public class TypeTest<T>{
public List<T> list;
//泛型方法,泛型参数为Y
public <Y> void method1(Y y) {
...
}
public void method2(){
ArrayList<String> arrayString=new ArrayList<String>();
ArrayList<Integer> arrayInteger=new ArrayList<Integer>();
System.out.println(arrayString.getClass()==arrayInteger.getClass());
}
}
例如上面代码中除了method2方法内部的泛型信息均可以保留到字节码中,泛型擦除的仅仅是Code属性表里面的内容,而方法体在字节码中正是存放在Code属性表的。通过上面的回答大家应该很清楚了,所谓的java泛型擦除可以理解为只是擦除了方法体的泛型信息。
结论:
Java泛型信息是否擦除有如下两种情况:
1. 声明侧泛型信息保留 例如声明泛型接口,泛型类,泛型方法时的泛型信息
2. 使用侧泛型信息擦除 例如方法的局部变量等。
为什么要泛型擦除?
原因是为了向后兼容。
你去查查历史就会知道,c#和java在一开始都是不支持泛型的。为了让一个不支持泛型的语言支持泛型,只有两条路可以走,要么以前的非泛型容器保持不变,然后平行的增加一套泛型化的类型。要么直接把已有的非泛型容器扩展为泛型。不添加任何新的泛型版本。
当时c#从1.1升级到了2.0,代码并不是很多,而且都在微软.net的可控范围,所以选择了第一种实现方式
而java的非泛型容器,已经从1.4.2占有到5.0,市面上已经有大量的代码,不得已选择了第二种方法。(之所以是从1.4.2开始,是因为java以前连collection都没有,是一种vector的写法。),而且有一个更重要的原因就是之前提到的向后兼容。所谓的向后兼容,是保证1.5的程序在8.0上还可以运行。(当然指的是二进制兼容,而非源码兼容。)所以本质上是为了让非泛型的java程序在后续支持泛型的jvm上还可以运行。
泛型信息运行时擦除
Java 泛型在运行时擦除这一点应该大部分开发者都有一定了的解,那么什么叫擦除了呢?就是泛型信息在源代码里面有,而在生成的字节码里面就没有了,之所以这么干主要是为了向下兼容老的java版本。那问题就很明了了,字节码中都不存在泛型的信息了,而我们的运行时对象是JVM
从字节码装载进来的,那自然也就没有泛型信息了。看下面代码
ArrayList<String> arrayString=new ArrayList<String>();
ArrayList<Integer> arrayInteger=new ArrayList<Integer>();
System.out.println(arrayString.getClass()==arrayInteger.getClass());
上面代码输入结果为 true,可见通过运行时获取的类信息是完全一致的,泛型类型被擦除了!
至此,我以为我精通了Java 泛型,直到有一天我写了 秒懂Java类型(Type)系统 这篇文章,我变的很困惑。不是说Java的泛型是运行时擦除的吗,为什么我还能通过反射获取到泛型信息,例如我有如下代码
public class TypeTest<T>{
public List<T> list;
}
我通过反射可以获取到这list的声明类型 java.util.List<T>
Field list = TypeTest.class.getField("list");
Type genericType1 = list.getGenericType();
System.out.println("参数类型1:" + genericType1.getTypeName());
输出
参数类型1:java.util.List<T>
是不是很奇怪,不是被擦除了吗?这是为什么?
泛型信息声明时保留
通过仔细思考上面对泛型擦除的原理的理解:泛型擦除是泛型信息在源代码里面有,而在生成的字节码里面就没有了,我们会提出疑问。难道这种情况下,泛型的信息被保留到了字节码里面去了吗?如果你对一个问题有疑问那就去亲自试一下。
第一:定义一个非泛型类,然后查看其生成的字节码文件
public class FruitsContainer{
public Number mNum;
}
我用 Bytecode Viewer 这个工具来查看生成的字节码文件FruitsContainer.class
,如下图所示:第一部分是源代码,第二部分是从class
文件中反编译出的代码,第三部分是字节码文件内容。
我们其实要重点关注的是字节码的那部分,看与其泛型版本的异同
第二:定义一个泛型类,然后查看其生成的字节码文件
public class Fruit {
}
public class FruitsContainer<T extends Fruit>{
public T t;
}
如下图所示:
我们先看最右侧的原始的字节码文件有何异同。可以看到,在第三行多了&<T:Ladvanced/Fruit:>
,下面还有几处带<>
的地方,这些就是被保留了的泛型信息。其实从第二部分反编译后的源码更容易看出,多了很多Signature
.
可见,在这种情况下,泛型的信息被保留到了字节码文件中。
结论
Java泛型信息是否擦除有如下两种情况:
1. 声明侧泛型信息保留 例如声明泛型接口,泛型类,泛型方法时的泛型信息
2. 使用侧泛型信息擦除 例如方法的局部变量等。
获取并使用泛型
下面举一个如何获取并使用泛型信息的例子,接着上面的代码,我们定义如下两个类
public class Apple extends Fruit {
}
public class AppleContainer extends FruitsContainer<Apple> {
public AppleContainer appleContainer;
}
其中AppleContainer
继承至FruitsContainer
泛型类.我们可以通过如下代码获得AppleContainer
带泛型信息的父类FruitsContainer<advanced.Apple>
private static void genericTest(){
Type superType=(AppleContainer.class.getGenericSuperclass());
System.out.println(superType);
if (superType instanceof ParameterizedType){
ParameterizedType paramType=(ParameterizedType)superType;
System.out.println(Arrays.toString(paramType.getActualTypeArguments()));
}
}
输出:
advanced.FruitsContainer<advanced.Apple>
[class advanced.Apple]
可见,我们是有能力在运行时获取声明类型的泛型信息的。
使用场景
那么在运行时获取泛型信息有什么用呢?我现在遇到过两个使用场景。 1. 对泛型类型进行校验 在Retrofit2
这个网络请求库中大量使用,例如其要求定义的方法返回类型必须是泛型的,例如Call<T>
,而不能是Call
。它还会校验T
是否为合法的类型. 2. 序列化和反序列化 例如使用Gson时,我们需要将·json·转换为Java对象,假设这个对象是带泛型信息,就得告诉框架要转换的那个java对象的泛型类型。
List<CommentBean>= fromJson("json...",
new TypeToken<ArrayList<CommentBean>>(){}.getType());
上面那段反序列化的代码就是要告诉Gson框架泛型参数为ArrayList<CommentBean>>
。这里面又有一个很有趣的点,就是 new TypeToken<ArrayList<CommentBean>>(){}
而不是 new TypeToken<ArrayList<CommentBean>>()
加上一对{}
就表示是匿名类,那样通过编译器编译后才能正确获取到其泛型信息。
例如我们有如下代码,那么大家想一想,输出是什么呢?
System.out.println(new FruitsContainer<Apple>().getClass().getGenericSuperclass());
System.out.println(new FruitsContainer<Apple>(){}.getClass().getGenericSuperclass());
输出:
class java.lang.Object
advanced.FruitsContainer<advanced.Apple>
更多推荐
所有评论(0)