基础说明
虚拟机没有泛型类型对象一所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文件!
由于泛型是在1.5才引入的,为了兼容,在java文件编译后是肯定看不见泛型的。也就是类型擦除,下面就来介绍一下类型擦除
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型(或者,对于无限定的变量则替换为Object)。
无限定
下面先来看下面的代码
public class MyTool<T> {
private T info;
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
}
这就是很简单的一个泛型类。现在,我通过反射来查看T是什么类型。
public static void main(String[] args) throws NoSuchMethodException {
// 得到getInfo方法
Method getInfo = MyTool.class.getDeclaredMethod("getInfo");
System.out.println("getInfo返回值类型为:"+getInfo.getReturnType().getName());
}
上面运行结果如下
可以发现如果没有指定泛型,那么在编译过后T被替换为了Object
即使我们指定了泛型,T还是会被替换为Object
public static void main(String[] args) throws NoSuchMethodException {
MyTool<Comparable> myTool = new MyTool<>();
// 得到getInfo方法
Method getInfo = myTool.getClass().getDeclaredMethod("getInfo");
System.out.println("getInfo返回值类型为:" + getInfo.getReturnType().getName());
}
有限定
上面的泛型类没有限制,下面来看一下有限定的情况
public class Tool<T extends Serializable> {
private T info;
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
}
还是使用反射来查看T类型
// 得到getInfo方法
Method getInfo = Tool.class.getDeclaredMethod("getInfo");
System.out.println("getInfo返回值类型为:"+getInfo.getReturnType().getName());
运行结果如下
可以发现限定符替换了T。
上面是一个限定符的,如果有两个或者多个限定符呢
public class MulTool<T extends Comparator & Comparable & Serializable> {
public T t;
public T getInfo() {
return t;
}
}
还是使用上面的反射代码,输出如下
可以发现返回的是Comparator,下面来交换一下限定的位置,分别让Comparable 和Serializable成为第一个(自己交换即可)。交换后代码运行结果如下
通过上面的运行结果,我们就可以得出结论,使用了类型限定符,那么第一个限定就会替换T
转换泛型表达式
还是上利用MyTool代码举例
public class MyTool<T> {
private T info;
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
}
我们经过上面的学习,知道会进行类型擦除,上面的MyTool的T会被替换为Object。那么getInfo返回值就是Object的类型,但是我们在实际调用getInfo方法时只要传入了类型,那么返回值就是我们传入的类型。看下面代码
public static void main(String[] args) {
MyTool<String> stringMyTool = new MyTool<>();
stringMyTool.setInfo("xxx");
// 得到所有方法
Method[] declaredMethods = stringMyTool.getClass().getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
String methodName = declaredMethod.getName(); // 方法名
String returnType = declaredMethod.getReturnType().getName(); // 返回类型
System.out.println("方法名:" + methodName + "--返回类型:" + returnType);
}
// 返回的类型为String
String info = stringMyTool.getInfo();
}
运行结果如下
可以发现getInfo返回值确实为Object,但是我们 String info = stringMyTool.getInfo(); 这条语句并没有进行强转,这就说明编译器已经帮我们进行了强转。其实在调用stringMyTool.getInfo()编译器将其转换为了2条虚拟机指令
- 对于MyTool.getInfo()的调用
- 将返回值Object强转为String
上面是对方法返回值进行强转,其实对字段的访问也是一样的,如果将info字段修饰符改为public,也可以直接使用String进行接收
String filed = stringMyTool.info;
方法类型擦除(桥方法)
我们不说啥理论,直接看下面代码
public class Animal<T> {
public void setX(T t) {
}
}
这个一个泛型类,有一个set方法
public class Cat extends Animal<String> {
@Override
public void setX(String s) {
}
}
这是Cat类,继承了Animal类,指定了泛型为String,并且重写了setX方法。
下面就是使用Cat
Animal<String> animal = new Cat();
animal.setX("hello world!!!");
大家看看这个代码,有没有发现问题?我们使用Animal来接收了一个Cat对象,这是正确的。但是animal.serX就不怎么对劲了。下面我来分析一下
- 由于Animal会发生类型擦除,所以animal.setX实际会调用 Animal.setX(Object)
- 由于animal引用的是一个Cat,所以会去寻找Cat.setX(Object)
- 问题出现了,Cat根本没有setX(Object),只有setX(String)
可以发现,类型擦除和多态产生了冲突。为了解决这个问题,编译器会在Cat类中生成一个桥方法。在Cat中生成的桥方法如下
public void setX(Object s){
setX((String) s);
}
其实就是生成了一个参数为Object类型的setX方法,这个方法会去调用参数为String类型的方法,就好像桥梁的作用一样,所以我们成为桥方法。
为了验证上面的说法,也就是编译器会给我们的代码生成一个桥方法,下面我就使用反射输出Cat的所有方法。
public static void main(String[] args) {
// 得到所有方法
Method[] declaredMethods = Cat.class.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
String methodName = declaredMethod.getName(); // 方法名
// 参数类型集合
List<String> types = Arrays.stream(declaredMethod.getParameterTypes())
.map(Class::getTypeName).collect(Collectors.toList());
System.out.println("方法名:" + methodName + "--参数类型:" + types);
}
}
上面的代码输出如下
可以发现编译器确实给我们生成了一个setX方法,参数类型就是Object,这个方法就是一个桥方法。有了这个桥方法,多态和类型擦除的问题也就解决了。
关于重载的一些说明
通过上面的例子,大家应该对桥方法有了清晰的认识,有些思想活跃的人可能就会觉得不太对劲了。大家回想一下重载的定义,重载就是参数名相同,参数不同。
这确实没问题,下面我在Animal定义应该getT方法,然后在Cat里面重写这个方法
public class Animal<T> {
private T t;
public void setX(T t) {
}
public T getT() {
return t;
}
}
public class Cat extends Animal<String> {
@Override
public void setX(String s) {
}
@Override
public String getT() {
return "";
}
}
根据上面的桥方法,大家想一下,是不是在Cat里面会生成应该 public Object getT()方法呢?我们还是通过的反射代码查看,代码和运行结果如下
public static void main(String[] args) {
// 得到所有方法
Method[] declaredMethods = Cat.class.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
String methodName = declaredMethod.getName(); // 方法名
// 参数类型集合
List<String> types = Arrays.stream(declaredMethod.getParameterTypes())
.map(Class::getTypeName).collect(Collectors.toList());
// 得到返回类型
String returnType = declaredMethod.getReturnType().getName();
System.out.println("方法名:" + methodName + "\t\t参数类型:" + types + "\t\t返回类型:" + returnType);
}
}
可以发现,在Cat里面存在了2个同名的方法,并且参数相同,这已经违法了重载的定义,按理说程序应该直接报错,但是并没有,原因就是在虚拟机中,会由参数类型和返回类型共同指定一个方法,上面代码中参数为Object的getT方法就是一个桥方法。
总结
在最后,对于java泛型的转换,我们需要记住以下几点
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为它们的限定类型
- 会通过合成桥方法来保持多态
- 为保持类型安全性,必要时会插入强制类型转换
关于泛型的更多知识,参考以下内容
泛型程序设计基础
类型擦除、桥方法、泛型代码和虚拟机
泛型的限制及其继承规则
泛型的通配符(extends,super,?)