1. 什么是类型擦除
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
Java的泛型是伪泛型,Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程称为类型擦除。
如在代码中定义List<Object>和List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与C++模板机制实现方式之间的重要区别。
例1:
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("string");
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
System.out.println(stringList.getClass() == integerList.getClass());
}
输出true
这里我们定义两个ArrayList,类型分别是String和Integer,通过.getClass()获取他们的类信息,结果是相等,说明类型被擦除了
例2
public static void main(String[] args) throws Exception{
List<String> stringList = new ArrayList<>();
stringList.add("string");
// stringList.add(1); //这里如果直接添加int类型的参数,会报错
stringList.getClass().getMethod("add", Object.class).invoke(stringList, 1);
System.out.println(stringList.toString());
}
输出[string, 1]
这里我们定义了一个String类型的ArrayList,如果直接调用add()方法添加Integer类型的参数,会报错。我们使用反射调用方法add(),可以添加Integer类型的参数,说明String类型在编译后被擦除了
2. 什么是原始类型
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Pair的原始类型为
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
在Pair<T>中,T是一个无限定的类型变量,所以用Object替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair,如Pair<String>或Pair<Integer>,但是擦除类型后他们的就成为原始的Pair类型了,原始类型都是Object。
如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。
如:
public class Pair<T extends Comparable> {}
那么原始类型就是Comparable
在调用泛型方法时,可以指定泛型,也可以不指定泛型。
在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到Object
在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类
public static void main(String[] args) {
int i = Test.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number f = Test.add(1, 1.2); //这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Number
Object o = Test.add(1, "asd"); //这两个参数一个是Integer,一个是Float,所以取同一父类的最小级,为Object
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能为Integer类型或者其子类
int b = Test.<Integer>add(1, 2.2); //编译错误,指定了Integer,不能为Float
Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}
//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
3. 类型擦除带来的问题和解决方法
3.1 和多态的冲突
有这样一个泛型类
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
一个子类继承它
class DateSon extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在子类中,我们设定泛型类型为Date,那么父类的两个方法参数类型都是Date
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
我们从代码编译来看,@Override重写父类方法,没有任何问题。
但是,类型擦除后,父类的泛型类型都变为了Object,编译后为
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
子类的重写方法
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
参数类型不一样,参数名一样… 根据Java语言特性,这应该是重载,而不是重写啊
写一个方法测试一下
public static void main(String[] args) {
DateSon dateSon = new DateSon();
dateSon.setValue(new Date());
dateSon.setValue(new Object()); // 编译报错
}
如果是重载,第一个和第二个setValue都应该编译通过,但是发现并没有继承父类setValue参数类型是Object的方法,所以说确实是重写
我们本意是通过设置泛型类型为Date,实现重写,但是这和泛型的类型擦除,显然冲突了。
针对这种冲突,JVM采取了一种特殊的方法,桥方法
我们用javap -c className的方式反编译下DateSon子类的字节码,结果如下:
class com.java.generic.DateSon extends com.java.generic.Pair<java.util.Date> {
com.java.generic.DateSon();
Code:
0: aload_0
1: invokespecial #1 // Method com/java/generic/Pair."<init>":()V
4: return
public void setValue(java.util.Date);
Code:
0: aload_0
1: aload_1
2: invokespecial #2 // Method com/java/generic/Pair.setValue:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue();
Code:
0: aload_0
1: invokespecial #3 // Method com/java/generic/Pair.getValue:()Ljava/lang/Object;
4: checkcast #4 // class java/util/Date
7: areturn
public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #4 // class java/util/Date
5: invokevirtual #5 // Method setValue:(Ljava/util/Date;)V
8: return
public java.lang.Object getValue();
Code:
0: aload_0
1: invokevirtual #6 // Method getValue:()Ljava/util/Date;
4: areturn
}
最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。
3.2 不能使用instanceof
List<String> stringList = new ArrayList<>();
System.out.println(stringList instanceof ArrayList<String>);
类型擦除后String类型不存在了,所以不能使用instanceof判断
3.3 在静态类和静态方法中的问题
public class Test2<T> {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
但是注意:
public class Test2<T> {
public static <T> T show(T one){ //这是正确的
return null;
}
}
因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T
到此这篇关于Java泛型类型擦除的文章就介绍到这了,更多相关Java泛型擦除内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!