深入Java泛型(Java泛型擦除机制,使用泛型强转时机,擦除对复写影响,协变返回类型)
1.Java中泛型的擦除机制
Java中的泛型是伪泛型,这个”伪”体现在你在xxx.java源文件中使用的参数化类型并不会保留到编译后的xxx.class文件中,而是会被擦除掉, 保留原始类型.(raw type)
例如:自定义类上的泛型
/*GenericTest.java*/ class GenericTest<T>{ private T variable; public void setVariable(T variable){ this.variable=variable; } public T getVariable(){ return variable; } }javap –c –p GenericTest.class:
Compiled from "GenericTest.java" class generic.GenericTest<T> { generic.GenericTest(); Code: 0: aload_0 1: invokespecial #12 // Method java/lang/Object."<init>":()V 4: return public void setVariable(T); Code: 0: aload_0 1: aload_1 2: putfield #23 // Field variable:Ljava/lang/Object; 5: return public T getVariable(); Code: 0: aload_0 1: getfield #23 // Field variable:Ljava/lang/Object; 4: areturn }对以上字节码解释:
class generic.GenericTest<T> { generic.GenericTest(); Code: 0: aload_0 //将位置为0的对象引用局部变量压入栈 //在这里将隐含的this引用压入栈 ,原因在下面阐述 1: invokespecial #12 // Method java/lang/Object."<init>":()V //其实就是隐含的super();调用Object的无参构造函数 4: return //从方法中返回,返回值为void,使用这条指令返回的类型必须为void //由此可见构造函数被编译器认为返回值为void,上面的Object中的构造函数返回值为V(void) public void setVariable(T); Code: 0: aload_0 1: aload_1 //将位置为1的对象引用局部变量压入栈,相当于将局部变量variable压入栈 2: putfield #23 // Field variable:Ljava/lang/Object; //该位置编译器给出注释:字段variable其类型为Object //由此可看出编译后类中使用的泛型被擦除为原始类型(Object) //putfield指令会把局部变量variable的值赋给this指向的对象 //中字段variable,也就是this.variable=variable; 5: return public T getVariable(); Code: 0: aload_0 1: getfield #23 // Field variable:Ljava/lang/Object; //会将引用变量的值以一个字长的形式压入操作数栈 //其实就是成员变量variable中的值,注意到这里variable的类型依然为Object 4: areturn //从方法中返回引用类型的值,及variable的值 }在最开始的aload_0指令把隐含的this引用压入栈,因为可能调用实例方法或实例变量会用到this.
那么由此我们可以推断GenericTest.java在经过编译后类文件中的GenericTest.class相当于下面的源程序:
class GenericTest{ private Object variable; /* //默认构造函数 public GenericTest(){ super(); } */ public void setVariable(Object variable){ this.variable=variable; } public Object getVariable(){ return variable; } }原始类型用第一个限定的类型变量来替换,如果没有给定限定类型就用Object替换,以上的例子没有给出限定类型,如果有上限:
例如:
class GenericTest2<T extends Animal>{ private T name; void method_1(T t){ this.name=t; } void method_2(){ method_1(name); } } class Animal{ }反编译:
经过编译后相当于下面的源程序:Compiled from "GenericTest.java" class generic.GenericTest2<T extends generic.Animal> { generic.GenericTest2(); Code: 0: aload_0 1: invokespecial #12 // Method java/lang/Object."<init>":()V 4: return void method_1(T); Code: 0: aload_0 1: aload_1 2: putfield #23 // Field name:Lgeneric/Animal; 5: return void method_2(); Code: 0: aload_0 1: aload_0 2: getfield #23 // Field name:Lgeneric/Animal; 5: invokevirtual #27 // Method method_1:(Lgeneric/Animal;)V 8: return }
class GenericTest2{ private Animal name; void method_1(Animal t){ this.name=t; } void method_2(){ method_1(name); } }
2.Java使用泛型后的强转时机:
既然你编译器把泛型擦除为原始类型,那么我们为什么又不需要强转为需要的类型,反而可以直接使用???
一步一步来说明:
例如:
class GenericMainClass{ public static void main(String[] args){ GenericTest<Integer> gt=new GenericTest<Integer>(); Integer in=gt.getVariable(); } }反编译:
Compiled from "GenericTest.java" class generic.GenericMainClass { generic.GenericMainClass(); Code: 0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: new #16 // class generic/GenericTest //new GenericTest();new指令会把该对象的引用压入栈 3: dup //复制栈顶内容(该对象的引用),把复制内容压入栈,此时保存了该对象两份引用 //为什么要保留一份该对象的引用?因为下面会用该引用指明构造函数 4: invokespecial #18 // Method generic/GenericTest."<init>":()V //需要弹出一个引用调用GenericTest的构造函数进行初始化 7: astore_1 //从栈中弹出一个引用赋值给1号局部变量1就是gt,第二次用到该对象引用 8: aload_1 9: invokevirtual #19 // Method generic/GenericTest.getVariable:()Ljava/lang/Object; //调用了getVariable方法,返回值类型为Object 12: checkcast #23 // class java/lang/Integer //此指令由Integer in=gt.getVariable();引发,在这里为了检测能否完成从Object->//Ineteger转换, //相当于(Integer)gt.getVariable(),如果不能强转 //抛出ClassCastException 15: astore_2 16: return }由此可见:
Integer in=gt.getVariable();
编译器把这个方法的调用翻译为两条虚拟机指令:
1. 对原始方法getVariable()调用 (invokevirtual)
2. 将返回值的Object类型强制转换为Integer类型(checkcast)
如果纯粹的只用gt.getVariable()是不会涉及到强制转换的,
也即是说在需要的时候,编译器会自动插入强制类型转化.
3.Java中泛型擦除对方法复写的影响
我们通过举例来引出问题:
class CopyTest<T>{ public T get(T t){ return t; } } class Son extends CopyTest<String>{ public String get(String t){ return t; } }由于java中的泛型有擦除机制,那么CopyTest.class与Son.class相当于以下源程序:
class CopyTest{ public Object get(Object t){ return t; } } class Son extends CopyTest{ public String get(String t){ return t; } }可以看到Son中的get方法并没有复写CopyTest中的get方法,因为参数列表都不相同,何谈复写?
但是的确完成了复写,如何完成的呢?
反编译Son.class:
Compiled from "GenericTest.java" class generic.Son extends generic.CopyTest<java.lang.String> { generic.Son(); Code: 0: aload_0 1: invokespecial #8 // Method generic/CopyTest."<init>":()V 4: return public java.lang.String get(java.lang.String); Code: 0: aload_1 1: areturn public java.lang.Object get(java.lang.Object); Code: 0: aload_0 1: aload_1 2: checkcast #19 // class java/lang/String 5: invokevirtual #21 // Method get:(Ljava/lang/String;)Ljava/lang/String; 8: areturn }对于以上指令,如果用Java代码体现就是:
class Son extends CopyTest{ public Object get(Object obj){ this.get((String)obj); } public String get(String t){ return t; } }Object get(Object)方法就是为了完成复写动作编译器自动生成的桥方法(bridge method),可以看到桥方法间接的调用了String get(String),达到我们主观上的”复写”.
4.Java中的协变返回类型:
在Jdk 1.4及以前,子类方法如果要覆盖超类的某个方法,必须具有完全相同的方法签名,包括返回值也必须完全一样。
(也就是说子类复写父类中的方法必须与其方法完全一致)
Jdk 1.5及以上版本放宽了这一限制,只要子类方法与超类方法具有相同的方法签名,或者子类方法的返回值是超类方法的子类型,就可以覆盖
(也就是说如果子类复写父类中的方法,子类的方法返回值类型为父类方法返回值类型的子类型也可以完成复写)
详细参照这篇博文:http://blog.csdn.net/dichyzhu/article/details/4052031
5.小结:
Java中使用的泛型为伪泛型,并不会在.class文件中保留泛型
1.在java源文件中的泛型在编译后的.class文件中都将被擦除
并且类型参数都将会被原始类型替代,如果有泛型限定,以第一个限定类型替代
2.当需要强制转换时,由编译器来自动插入强制类型转换,而我们不用在显式强转
3.在使用泛型的方法被复写时,编译器会生成一个桥方法完成
复写并且间接调用要复写父类中的那个方法,由此实现多态.