六、泛型擦除
6-1、什么是泛型擦除
泛型这个概念,只存在于编译器中。而不存在于虚拟机(JVM)中。
意思是说,编译器对带有泛型的java代码进行编译时,会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,供JVM接收并执行。
这个过程就叫做泛型擦除。
下面通过反射,向List<String>类型的容器中添加Integer元素,来证明:只要能想办法绕开编译器检查,泛型的约束?不存在的
public static void main(String... args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<String> strList = new ArrayList<>(3);
strList.add("A1");
strList.add("B2");
// 这样写是一定会报编译错的: strList.add(333); 所以使用反射
strList.getClass().getMethod("add",Object.class).invoke(strList,333);
strList.forEach(System.out::println);
}
在最后一句打上断点,会发现333作为Integer类型已经被成功添加到了strList里面,但是在print的时候,仍然会报类型转换异常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
那么就仍然使用反射的方法绕开泛型的类型检查
Integer c = (Integer)strList.getClass().getMethod("get",int.class).invoke(strList,2);
System.out.println(c);
运行结果:333
小结:对于JVM来说,泛型信息是不可见的。
6-2、擦除的过程
Java 编译器会将泛型代码中的类型完全擦除,使其变成原始类型。
然后在代码中加入类型转换,将原始类型转换成想要的类型。
这些操作都是编译器在后台进行的,以保证类型安全。
所以说泛型就是一个语法糖,对于实际运行不产生任何影响。
看一个例子:
public class ErasureTest<T> {
private T t;
public T getT() {
return t;
}
public void setT(T t) {
this.t = t;
}
public static void main(String... args) {
ErasureTest<String> a = new ErasureTest<>();
a.setT("abc");
System.out.println(a.getT());
}
}
使用 javap -c 命令查看这段代码的字节码
public T getT();
Code:
0: aload_0
1: getfield #2 // Field t:Ljava/lang/Object;
4: areturn
public void setT(T);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field t:Ljava/lang/Object;
5: return
public static void main(java.lang.String...);
Code:
0: new #3 // class com/puhuijia/helloStudy/ErasureTest
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String abc
11: invokevirtual #6 // Method setT:(Ljava/lang/Object;)V
14: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_1
18: invokevirtual #8 // Method getT:()Ljava/lang/Object;
21: checkcast #9 // class java/lang/String
24: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: return
注意看main()方法
标号9的那一行,abc被创建出来时是String,但是马上就开始按照Object进行处理。
标号21以前的处理,使用的类型都是Object。
到了标号21的那一行,进行了“checkcast”,将Object进行了强制类型转换,变成String。
(checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type)
总之,这再度印证了:所有的活都是Object干的;强制类型转换是打死也躲不开的;泛型只是让你舒服一些,把所有脏活累活都藏起来了。
6-3、java擦除的特点
-
C++ 中泛型的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。
-
java 中并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类。
java 不同类型都能使用一套代码,就是因为采用了泛型擦除机制,字节码中根本就没有类型。
实际上,擦除机制的出现,主要目的是为了JDK新老版本在泛型上的兼容性问题。
6-4、擦除带来的一些问题
6-4-1、类型信息丢失
由于泛型擦除机制的存在,在运行期间无法获取关于泛型参数类型的任何信息,自然也就无法对类型信息进行操作;例如:instanceof 、创建对象等
这是“4-1、不能实例化类型变量”的原因
6-4-2、类型擦除对于多态的影响
看下面这个例子,正常来说,这两个方法的参数不同,应该被辨识成重载,但是编译器报错:
void method(List<Integer> a) {
}
void method(List<String> b) {
}
Error:java: name clash: method(java.util.List<java.lang.String>) and method(java.util.List<java.lang.Integer>) have the same erasure
错误信息是说两个方法的参数在擦除之后完全一致(have the same erasure),都是List,所以就不是重载,而是产生了冲突。
6-4-3、泛型在父类子类继承时造成的一个影响
6-4-3-1、问题提出
首先创建一个简单的使用泛型的父类:
public class GsuperClass<T> {
private T t;
public T getT() {return t;}
public void setT(T t) {this.t = t;}
}
然后子类:
public class GchildClass extends GsuperClass<String>{
private String childString;
@Override
public String getT() {
return this.childString;
}
@Override
public void setT(String s) {
this.childString = s;
}
}
现在一个让人疑惑的问题是:子类中的getT和setT真的是重写?(Override)
编译器认为这两个方法是重写,因为不加@Override注解的话会直接报警。
但是,以set方法为例,父类中的setT(T t)经过类型擦除以后是setT(Object t);
子类中的set方法参数是String类型,也就是说方法名相同但是参数不同,这难道不算是重载?(overloading)
6-4-3-2、正常情况下的表现
普通类的话,下面这样显然是合法的:
public class CommonClass {
public void setT (Object t){System.out.println("object");}
public void setT (String s){System.out.println("String");}
public static void main(String[] args) {
CommonClass c = new CommonClass();
c.setT(new Object());
c.setT("123");
}
}
结果:
object
String
Process finished with exit code 0
推及到继承上面,如果在一个普通的父类里面定义
public void setT(Object t) {this.t = t;}
在其子类里面定义
public void setT(String t) {this.childString = t;}
的话,也是完全行得通的,子类就拥有了两个setT方法(重载)。
6-4-3-3、分析
但是,在使用了泛型以后就完全不同了。如果尝试在 GchildClass 里面调用我认为有可能存在的重载方法时,编译直接报错:
public class GchildClass extends GsuperClass<String>{
private String childString;
@Override
public String getT() {
return this.childString;
}
@Override
public void setT(String t) {
this.childString = t;
}
public static void main(String[] args) {
GchildClass child = new GchildClass();
child.setT("123");
child.setT(new Object()); // ERROR:参数与方法类型不匹配
System.out.println(child.getT());
}
}
下面分析子类字节码,看看编译器和jvm到底干了什么见不得人的交易。
public class GchildClass extends GsuperClass<java.lang.String> {
public com.puhuijia.quartz.base.GchildClass();
Code:
0: aload_0
1: invokespecial #1 // Method com/puhuijia/quartz/base/GsuperClass."<init>":()V
4: return
public java.lang.String getT();
Code:
0: aload_0
1: getfield #2 // Field childString:Ljava/lang/String;
4: areturn
public void setT(java.lang.String);
Code:
0: aload_0
1: aload_1
2: putfield #2 // Field childString:Ljava/lang/String;
5: return
public void setT(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #10 // class java/lang/String
5: invokevirtual #6 // Method setT:(Ljava/lang/String;)V
8: return
public java.lang.Object getT();
Code:
0: aload_0
1: invokevirtual #8 // Method getT:()Ljava/lang/String;
4: areturn
}
生成了两套get、set方法。
而且参数是Object的调用了参数是String的方法。
这里实际上使用了桥接模式,相当于jvm自己暗地里完成了对于setT(Object)的重写。
6-4-3-4、彩蛋:关于重载的定义问题
java编译器对于重载的定义不包括返回值。
也就是说两个方法名、参数列表一致的方法,不管返回值是什么,都不可以同时存在,不视为重载;
但是对于jvm来说,上例中存在两个 getT: String getT() 和 Object getT(),
这显然不符合java的语法定义,但是却符合jvm标准。
6-4-4、用泛型擦除来解释 “4-5、泛型类不能继承exception”
如果以下代码可以通过
public class GenericException<T> extends Exception {
}
那么就会出现这样的情况:
try{
}catch(GenericException<String> e1){
}catch(GenericException<Integer> e2){
}
泛型擦除以后,两个catch就都会变成 GenericException<Object>,因此规定泛型类不能继承exception
6-4、擦除导致的泛型不可变性
对于泛型来说,其相同的容器类之间不存在任何的父类子类关系。
也就是说:
-
不管 class A extends B ; 还是 class B extends A
-
List<A>与List<B>之间不存在任何父类子类关系。
这称之为不可变性。
与不可变性相对应的概念是 协变、逆变:
-
协变:如果 A 是 B 的父类,并且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父类,则称之为协变的(父子关系保持一致)
-
逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变
Java 中数组是协变的,泛型是不可变的。
6-4-1、用<?>来解决不可变性造成的问题
class Fruit {}
class Apple extends Fruit {}
class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
像下面这样使用水果盘子放苹果,是会引发编译错误的,因为根据上面的不可变性,容器之间不存在继承关系,无法向上溯型:
Plate<Fruit> p=new Plate<Apple>(new Apple()); // ERROR
解决方法如下:
Plate<? extends Fruit> p=new Plate<Apple>(new Apple());
(完)
网友评论