Java 泛型的本质——类型擦除
文章目录
- 简介
- Java泛型的类型擦除的证明例子
- 类型擦除到边界
- 擦除的代价与使命
- 使用泛型不是强制的
- 泛型代码边界的动作
- 非泛型类库和泛型类库:字节码一模一样
- 擦除的补偿
- 泛型与工厂模式
- 泛型数组
- 泛型类对象的数组
- 类型参数的数组
- 继承和桥方法
- 其他
简介
- 首先必须了解到,java源代码需要经过编译器编译出字节码,在这个过程中,编译器执行编译期的检查,检查通过了就会生成字节码。而字节码存储着能被JVM解释运行的指令,所以说,相对于java源代码,java源代码生成的字节码文件里的指令才是真正被执行到的指令。
- 而java的泛型由于种种原因,在内部实现方面并不像c++的模板一样,可以在运行时获得类型参数的真正类型。即运行时,在泛型代码内部,无法获得类型参数的真正类型。这是因为编译器在编译过后,泛型代码生成的字节码是不包括类型参数的具体类型的。这也就是泛型的类型擦除。
泛型总共干了4件事:
- 编译之前编译器进行的静态分析检查(比如你不能把
ArrayList<Integer>
里添加string,如果做了无法编译)。 - 强制转换类型(比如你从
ArrayList<Integer>
里取一个元素时,编译器隐式地在字节码里添加一句强制转换类型,转换为int)。 - 在字节码中,用类型参数的限定(如果没有就是
Object
)来替换所有类型参数(保留上界)。这就是类型擦除。 - 在继承使用了类型参数的方法时(形参或者返回值的类型是
T
),用桥方法来保持多态。
Java泛型的类型擦除的证明例子
import java.util.*;public class ErasedTypeEquivalence {public static void main(String[] args) {Class c1 = new ArrayList<String>().getClass();Class c2 = new ArrayList<Integer>().getClass();System.out.println(c1 == c2);}
} /* Output:
true
*///:~
ArrayList<String>
和ArrayList<Integer>()
可能会被你认为是不同的类型,但是这二者的Class对象竟然判断是相等的。大家都知道,java里类加载器通过加载.class文件(字节码),加载后可以生成Class对象,然后你才可以生成对象啥的。那么这里几乎可以肯定,new ArrayList<String>()
和new ArrayList<Integer>()
肯定用的是同一个.class文件,既然是同一个.class文件,那么唯一的解释就是ArrayList的.class文件不带有< >
里面的具体信息。
import java.util.*;class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION,MOMENTUM> {}public class LostInformation {public static void main(String[] args) {List<Frob> list = new ArrayList<Frob>();Map<Frob,Fnorkle> map = new HashMap<Frob,Fnorkle>();Quark<Fnorkle> quark = new Quark<Fnorkle>();Particle<Long,Double> p = new Particle<Long,Double>();System.out.println(Arrays.toString(list.getClass().getTypeParameters()));System.out.println(Arrays.toString(map.getClass().getTypeParameters()));System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));System.out.println(Arrays.toString(p.getClass().getTypeParameters()));}
} /* Output:
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
*///:~
getTypeParameters
方法可以获得Class对象的类型参数,也就是泛型类的类型参数(类名后面的< >
),但获得的居然是那些占位符信息(T、V、K什么的)。这也说明了泛型类的字节码中根本没有真正的类型,只有虚假的占位符。
类型擦除到边界
class HasF {public void f() {System.out.println("HasF.f()");}
}class Manipulator<T> {private T obj;public Manipulator(T x) { obj = x; }// Error: cannot find symbol: method f():public void manipulate() { obj.f(); }
}public class Manipulation {public static void main(String[] args) {HasF hf = new HasF();Manipulator<HasF> manipulator = new Manipulator<HasF>(hf);manipulator.manipulate();}
}
你的疑问在于为什么new Manipulator<HasF>(hf)
给了泛型类以类型参数HasF
,为什么还不可以调用obj.f()
,这就是类型擦除在搞鬼。这里其实是T在运行时被擦除成了基类Object了,既然编译器认为T是一个Object,是不可能让你调用一个不存在的方法f()
的。
class Manipulator2<T extends HasF> {private T obj;public Manipulator2(T x) { obj = x; }public void manipulate() { obj.f(); }
}
如果将泛型类改写为class Manipulator2<T extends HasF>
,那么这里便可以执行f方法了,这里使用了extends
关键字,T在运行时被擦除成了HasF了,编译器认为T如果不是HasF本身,那就是HasF的子类,本身或子类自然可以调用f方法了。
从上面两个例子可以看出,类型擦除的边界已经在泛型类定义完成时就决定好了,之后无论你创建泛型类对象时给了什么具体类型给类型参数,编译器都会无视。
class testO<T> {public T t;testO(T a) {t = a;}public void f() {t.equals(new Object());//调用了Object的方法System.out.println(t.hashCode());//调用了Object的方法}
}
既然类型擦除的边界已经决定好了,而且上例的边界是Object(testO<T>
相当于testO<T extends Object>
),所以类型擦除会擦除到Object,那么我肯定可以调用Object的方法。
擦除的代价与使命
- 如果泛型在java一开始就有,那么其内部实现将不会使用擦除的方式。但由于泛型是在jdk1.5才有的特性,为了保持兼容性,即将一些类库升级为泛型类库后,使用了老版本代码的客户端也不会出现问题。具体地说就是,就算使用了泛型类库没有给出类型参数,原代码一样能运行成功。(比如你既可以使用原生的
ArrayList
,也可以使用ArrayList<Integer>
) - 泛型的擦除有一个伟大的使命——完全兼容以前的非泛型类库。具体的说:以前的非泛型类库存取对象时,都是把对象认为是Object。现在的泛型类库生成的字节码,却还是和非泛型类库生成的字节码一模一样,因为类型擦除把存取的对象都认为是Object。唯一的区别是,在泛型类对象明确给出了类型参数的具体类型后,在调用一些返回值类型为类型参数的泛型代码时(比如
T getXXX()
方法),会在调用方法返回的时候在调用处隐式地加一句强制类型转换。 - 如果你完全理解了上面这段话,你会知道:Java的泛型根本就是个假的泛型,它的运行时类型居然只是类型参数的上界,而不是我们给定的具体类型。
- 擦除的代价就是:不可以使用
arg instanceof T
表达式和new T()
表达式。因为这些操作都需要显示地引用运行时类型。换句话说,这些操作如果仅仅靠擦除的边界,是无法得到正确的运行结果的,所以需要运行时类型。 - 注意泛型方法也会发生擦除。
使用泛型不是强制的
擦除和这种兼容性意味着使用泛型不是强制的,即使它是个泛型类。
//: generics/ErasureAndInheritance.javaimport java.util.ArrayList;class GenericBase<T> {private T element;public void set(T arg) { arg = element; }public T get() { return element; }
}class Derived1<T> extends GenericBase<T> {}class Derived2 extends GenericBase {} // No warning// class Derived3 extends GenericBase<?> {}
// Strange error:
// unexpected type found : ?
// required: class or interface without boundspublic class ErasureAndInheritance {@SuppressWarnings("unchecked")public static void main(String[] args) {Derived2 d2 = new Derived2();Object obj = d2.get();d2.set(obj); // Warning here!}
} ///:~
class Derived2 extends GenericBase
这里继承了泛型类却没有给类型参数,也是可以的。此时,继承过来的函数的类型参数都将被Object替换。
class Derived3 extends GenericBase<?>
这里会报错,因为这里需要的是“不带限制范围的类或者接口”, 即使你写成? extends Object
,也是错的。
泛型代码边界的动作
边界一般指的是T get()
或void set(T arg)
这种函数。void set(T arg)
会执行编译器的静态检查,T get()
则是该方法调用处隐式添加一句强制类型转换。
import java.lang.reflect.*;
import java.util.*;public class ArrayMaker<T> {private Class<T> kind;public ArrayMaker(Class<T> kind) { this.kind = kind; }@SuppressWarnings("unchecked")T[] create(int size) {return (T[])Array.newInstance(kind, size);}public static void main(String[] args) {ArrayMaker<String> stringMaker = new ArrayMaker<String>(String.class);String[] stringArray = stringMaker.create(9);System.out.println(Arrays.toString(stringArray));}
} /* Output:
[null, null, null, null, null, null, null, null, null]
*///:~
Array
是属于package java.lang.reflect
的,它是反射包里的类。这个newInstance
明显是静态方法,通过传入的Class对象和size大小,可以创建出该Class的类型的真正数组,不过该方法返回是Object,需要你去自己类型转换。注意,在泛型代码中创建数组,推荐使用Array.newInstance
。- 虽然
new ArrayMaker<String>(String.class)
通过构造器给kind成员变量传递了Class<String>
对象,但是类型擦除,这个成员变量从头到尾都只是个Class
,而不是个Class<String>
。由于Class
对象进行反射工作时是依靠自身属性来做的,所以这里就算Class
的类型参数被擦除,也可以正确的进行反射的工作,比如Array.newInstance
。 - 不管
new ArrayMaker
时有没有给定具体类型,由于类型擦除(当给定时发生),成员变量kind只能被认为是一个Class
对象。 new ArrayMaker<String>(String.class);
在创建泛型类对象,注意这个是显式给出了泛型的类型参数了的,这里是String
。如果这里没有显式给出,那么编译器就轻松了:1.不用进行静态检查,是个Object进来就行 2.调用某些方法返回时也不用隐式加类型转换了。因为类型擦除,实际上T[]
就是Object[]
,并且create方法返回时会隐式加一句类型转换(String[])
。
这里有必要给出两种情况的java汇编代码:
1.ArrayMaker<String> stringMaker = new ArrayMaker<String>(String.class)
显式给出具体类型。
T[] create(int);Code:0: aload_01: getfield #2 // Field kind:Ljava/lang/Class;4: iload_15: invokestatic #3 // Method java/lang/reflect/Array.newInstance:(Ljava/lang/Class;I)Ljava/lang/Object;8: checkcast #4 // class "[Ljava/lang/Object;"11: checkcast #4 // class "[Ljava/lang/Object;"14: areturnpublic static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=3, args_size=10: new #5 // class ArrayMaker3: dup4: ldc #6 // class java/lang/String6: invokespecial #7 // Method "<init>":(Ljava/lang/Class;)V9: astore_110: aload_111: bipush 913: invokevirtual #8 // Method create:(I)[Ljava/lang/Object;16: checkcast #9 // class "[Ljava/lang/String;" checkcast这里隐式加了强制类型转换为String[ ]19: astore_2
所以create方法的执行过程类似于如下:
Object a = new Integer[5];//Array.newInstance返回的永远是个Object,即使对象的实际类型是Integer[]
Object[] b = (Object[])a;//这里是由于create方法的返回类型就是T[],但类型擦除,所以返回Object[]
Integer[] iArray = (Integer[])b;//这里是由于类型参数已经被Integer确定,所以赋值给Integer[]时,编译器会隐式加类型转换
2.ArrayMaker<String> stringMaker = new ArrayMaker(String.class)
没有给出具体类型。注意这里改完就会报警告:
public static void main(java.lang.String[]);Code:0: new #5 // class ArrayMaker3: dup4: ldc #6 // class java/lang/String6: invokespecial #7 // Method "<init>":(Ljava/lang/Class;)V9: astore_110: aload_111: bipush 913: invokevirtual #8 // Method create:(I)[Ljava/lang/Object;16: checkcast #9 // class "[Ljava/lang/String;" 隐式加了类型转换19: astore_2
虽然new的时候没有指定具体类型,但由于引用类型为ArrayMaker<String>
,所以泛型的相关工作还是照常进行的,所以这里编译器还是隐式加了类型转换。
非泛型类库和泛型类库:字节码一模一样
这里用两个自己写的类来示例,非泛型类库用一个持有Object成员变量的对象来代替。
public class SimpleHolder {private Object obj;public void set(Object obj) { this.obj = obj; }public Object get() { return obj; }public static void main(String[] args) {SimpleHolder holder = new SimpleHolder();holder.set("Item");String s = (String)holder.get();}
} ///:~
public class GenericHolder<T> {private T obj;public void set(T obj) { this.obj = obj; }public T get() { return obj; }public static void main(String[] args) {GenericHolder<String> holder = new GenericHolder<String>();holder.set("Item");String s = holder.get();}
} ///:~
这是SimpleHolder的java汇编代码:
public class SimpleHolder {public SimpleHolder();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic void set(java.lang.Object);Code:0: aload_01: aload_12: putfield #2 // Field obj:Ljava/lang/Object;5: returnpublic java.lang.Object get();Code:0: aload_01: getfield #2 // Field obj:Ljava/lang/Object;4: areturnpublic static void main(java.lang.String[]);Code:0: new #3 // class SimpleHolder3: dup4: invokespecial #4 // Method "<init>":()V7: astore_18: aload_19: ldc #5 // String Item11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V14: aload_115: invokevirtual #7 // Method get:()Ljava/lang/Object;18: checkcast #8 // class java/lang/String21: astore_222: return
}
这是GenericHolder的java汇编代码:
public class GenericHolder<T> {public GenericHolder();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic void set(T);Code:0: aload_01: aload_12: putfield #2 // Field obj:Ljava/lang/Object;5: returnpublic T get();Code:0: aload_01: getfield #2 // Field obj:Ljava/lang/Object;4: areturnpublic static void main(java.lang.String[]);Code:0: new #3 // class GenericHolder3: dup4: invokespecial #4 // Method "<init>":()V7: astore_18: aload_19: ldc #5 // String Item11: invokevirtual #6 // Method set:(Ljava/lang/Object;)V14: aload_115: invokevirtual #7 // Method get:()Ljava/lang/Object;18: checkcast #8 // class java/lang/String21: astore_222: return
}
可以看到二者的汇编是一样的。而之所以GenericHolder源码里面,String s = holder.get()
不用显式地加强制类型转换,是因为编译器帮你做了。
所以说,唯一区别就是在客户端程序有所不同,泛型代码产生的字节码根本一样,这就做到了兼容。如果用的泛型类库,那么就不用加类型转换了。
擦除的补偿
在泛型代码中,想要得到运行时类型只能依靠Class对象帮忙了,因为Class对象获得运行时类型跟泛型根本没有关系。考虑到之前arg instanceof T
表达式不可以用,下例就是解决方案:
class Building {}
class House extends Building {}public class ClassTypeCapture<T> {Class<T> kind;public ClassTypeCapture(Class<T> kind) {this.kind = kind;}public boolean f(Object arg) {return kind.isInstance(arg);//调用Class对象的方法}public static void main(String[] args) {ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class);System.out.println(ctt1.f(new Building()));System.out.println(ctt1.f(new House()));ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class);System.out.println(ctt2.f(new Building()));System.out.println(ctt2.f(new House()));}
} /* Output:
true
true
false
true
*///:~
Building.class
通过构造器传递给kind成员变量后,由于擦除,被看做了一个Class对象,没有< >
了。这里被擦除类型参数也无所谓,因为Class对象调用isInstance
是靠自身,跟泛型没有关系。
同理,在泛型代码调用Class对象的newInstance()
可以返回真实对象,不过由于类型擦除,返回的多是Object引用的真实对象,这种还得自己再类型转换成真实类型。因为其方法签名为public T newInstance()
,一旦擦除那么就返回Object引用。
泛型与工厂模式
Class对象就是最天然方便的工厂对象,因为它可以直接调用newInstance()
来创建产品实例,但它有一点缺陷就是,当类没有无参构造器,会抛出运行时异常。通过正规地设计泛型接口的方式,可以很好解决这个问题。注意下面这个例子不是完美的,但它很好地体现了泛型的编译期检查。
interface FactoryI<T> {T create();
}class Foo2<T> {private T x;public <F extends FactoryI<T>> Foo2(F factory) {x = factory.create();}// ...
}class IntegerFactory implements FactoryI<Integer> {public Integer create() {return new Integer(0);}
} class Widget {public static class Factory implements FactoryI<Widget> {public Widget create() {return new Widget();}}
}public class FactoryConstraint {public static void main(String[] args) {new Foo2<Integer>(new IntegerFactory());new Foo2<Widget>(new Widget.Factory());}
} ///:~
- 通过定义
interface FactoryI<T>
接口,决定了所有工厂类的父类以及它们应该实现的方法。 Foo2
方法,首先它是一个构造器,其次构造器没有返回值,它前面的尖括号代表了这是一个泛型方法。Foo2<T>
和它定义中的<F extends FactoryI<T>>
是同一个T,这样,你在创建泛型类Foo2的对象,只要显示给定了具体类型,那么就能同时要求到其内部的F必须是FactoryI<具体类型>
的子类。- 剩下就是两个工厂类的定义,可以看到工厂类确实是
FactoryI<具体类型>
的子类。
泛型数组
这里要分为两种情况:
- 泛型类对象的数组
- 泛型代码里的类型参数的数组,形如
T[] array
。注意,还是不能使用new T[size]
。
泛型类对象的数组
因为之前讲过的类型擦除,所以数组元素的真正类型只能是List
而不是List<Integer>
。并且数组作为java中一种很重要的数据结构,必须明确知道内部元素的类型。这两点原因使得你只能执行new ArrayList[10]
,而不能使用new ArrayList<Integer>[10]
,毕竟就算写成了后者,数组也记不住数组元素的类型参数,反而还会给程序员一种“我的泛型数组能够记住类型参数”的错觉。
虽然真实的数组不能记住泛型的类型参数,只能将其保存为原生类型raw type,但我们可以通过有类型参数的引用来以约束:
List<String>[] list = new ArrayList[5];
//list[0] = new ArrayList<Integer>();//无法用此句替换下一句,因为引用的类型List<String>[]进行了检查
list[0] = new ArrayList<String>();//引用类型的数组初始值是null,这里初始化元素
list[0].add("only string can pass");
String str = list[0].get(0);
List<String>[] list = new ArrayList[5]
,虽然new的时候数组元素是raw type,但通过引用的约束可以获得泛型带来的类型检查和隐式加的类型转换。但注意此句编译器报了一句警告unchecked assignment,编译器意思就是,虽然你这里的引用是个泛型数组还给定了类型,但要是出了什么问题都不关我的事哈,当初new的时候我只记得数组元素是个raw type呢,责任全部推卸给你了(编译器否认三连:我不是,我没有,别瞎说)。list[0].add("only string can pass")
这里编译器进行了类型检查,不是String就不能add呢。毕竟这个引用叫List<String>[] list
。list[0].get(0)
这里编译器加了隐式的强制类型转换(证据就是String sss = new Object();
编译器报错的,所以这里是编译器悄悄给你加的),你都不用自己加一句类型转换了。毕竟这个引用叫List<String>[] list
。
这时你可能想知道,编译器到底把什么责任推卸给你了,因为我们用泛型数组来做下面的“坏事”。
class Node<T> {public T data;public Node(T data) { this.data = data; }public void setData(T data) {System.out.println("Node.setData");this.data = data;}public T getData() {return data;}
}public class test{public static void main(String[] args) {Node<String>[] gia = new Node[5];//Node<Integer> gia1 = (Node<Integer>) gia;//编译报错Node<Integer>[] gia2 = (Node<Integer>[]) (Node[]) gia;//此句能通过Node[] gia3 = gia;gia3[0] = new Node<Integer>(1);String s = gia[0].getData();
// Object[] o = gia;//使用这三句也能报同样的错
// o[0] = new Node<Integer>(1);
// String s = gia[0].getData();}
}
相信聪明的你一定能马上想到String s = gia[0].getData()
这句会报一个运行时异常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。这里且听我慢慢分析:
Node<Integer> gia1 = (Node<Integer>) gia;
这句会报错,毕竟原来引用类型是Node<String>[]
,现在你想强转成Node<Integer>[]
。如果你真的想这么做,你可以Node<Integer>[] gia2 = (Node<Integer>[]) (Node[]) gia
,道理很简单,先转成raw type数组,再转成带泛型数组,不过这一切都是你自己和引用在玩,编译器可只知道数组只是个raw type数组。Node[] gia3 = gia;
这里引用类型变成了raw type数组,但是gia3和gia都是引用啊,操作的是同一个数组啊,这就可以做坏事了。gia3[0] = new Node<Integer>(1)
,因为gia3是个raw type数组,那便可以随便赋值带泛型的对象进去了,但gia3和gia引用了同一个数组。String s = gia[0].getData()
通过gia引用取回数据时,编译器发现需要把一个Integer的数据转成String,所以报错。
总结一下就是:成功创建泛型类数组的唯一方式就是创建一个被擦除类型的数组,然后对其转型。——来自java编程思想。
类型参数的数组
在讲解之前,有必要讲一个关于数组的预备知识:数组协变,Object[]
可以理解为Integer[]
的父类,当然这二者都是Object
的子类。
Object[] a = new Integer[5];//a[0] = 1.2;//运行报错ArrayStoreException//Integer[] c = new Object[5];//编译报错Integer[] b = (Integer[]) new Object[5] ;//运行报错ClassCastException
Object[] a = new Integer[5]
这句能赋值是因为数组协变(不展开讲解,如有需要自行百度)。这样赋值是有意义的,一个Integer必定是一个Object。a[0] = 1.2
这句运行会报错java.lang.ArrayStoreException: java.lang.Double,是因为数组在new的时候已经记住了其元素的类型,所以往Integer数组存一个Double时会报错。而此句能通过编译,是因为a引用是个Object[],一个Double必定是一个Object。Integer[] c = new Object[5]
之所以会编译报错,因为左右两边类型不一样,而且左边也不是右边的父类。Integer[] b = (Integer[]) new Object[5]
这里加了类型转换自然能通过编译了,但编译器也会提示你这里有一个警告:可能产生ClassCastException。果然运行时报错了:java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;。因为对象的真实类型就是Object[]
,不可能强转为Integer[]
,所以运行时jvm发现了对象的真实类型,从而发现这里不能强转,然后报错。
以下示例展示了泛型类的一个返回类型参数的数组的方法:
public class GenericArray<T> {private T[] array;public GenericArray(int sz) {array = (T[])new Object[sz];}public void put(int index, T item) {array[index] = item;}public T get(int index) { return array[index]; }// Method that exposes the underlying representation:public T[] rep() { return array; }public static void main(String[] args) {GenericArray<Integer> gai = new GenericArray<Integer>(10);// This is OK:Object[] oa = gai.rep();//此句编译能通过// This causes a ClassCastException:Integer[] ia = gai.rep();}
} ///:~
这里再贴上main函数的汇编代码:
public static void main(java.lang.String[]);Code:0: new #5 // class GenericArray3: dup4: bipush 106: invokespecial #6 // Method "<init>":(I)V9: astore_110: aload_111: invokevirtual #7 // Method rep:()[Ljava/lang/Object;14: astore_215: aload_116: invokevirtual #7 // Method rep:()[Ljava/lang/Object;19: checkcast #8 // class "[Ljava/lang/Integer;"22: astore_323: return
- 和以前一样,由于类型擦除,运行时泛型代码的
T[]
实际都为Object[]
,所以rep方法实际返回也是一个Object[]
咯。 Integer[] ia = gai.rep()
,由于req方法返回了一个T[]
,所以这里隐式加了类型转换。根据之前讲的预备知识,运行时这里会报错。从checkcast #8 // class "[Ljava/lang/Integer;"
可以看到这句类型转换。当然,如果写成String[] sa = gai.rep()
也是不行的,因为类型检查这里编译就会报错了。Integer[] ia = gai.rep()
报错的根本原因是:当初new的时候就是new Object[sz]
,而这就是对象的真实类型,根据这个真实类型来进行类型转换为Integer[]
必然会报错。Object[] oa = gai.rep()
这句对应的汇编只有一句:invokevirtual #7 // Method rep:()[Ljava/lang/Object;
,说明编译器还是比较智能的,发现你赋值给的引用就是Object[]
时,那就不画蛇添足再加一句类型转换了。
import java.lang.reflect.*;public class GenericArrayWithTypeToken<T> {private T[] array;public GenericArrayWithTypeToken(Class<T> type, int sz) {array = (T[])Array.newInstance(type, sz);}public void put(int index, T item) {array[index] = item;}public T get(int index) { return array[index]; }// Expose the underlying representation:public T[] rep() { return array; }public static void main(String[] args) {GenericArrayWithTypeToken<Integer> gai =new GenericArrayWithTypeToken<Integer>(Integer.class, 10);// This now works:Integer[] ia = gai.rep();}
} ///:~
将例子改造成GenericArrayWithTypeToken这样就能正常运行了,这是因为Array.newInstance
通过反射,返回对象的真实类型就是Integer[]
,自然类型转换也可以成功。
继承和桥方法
class Node<T> {public T data;public Node(T data) { this.data = data; }public void setData(T data) {System.out.println("Node.setData");this.data = data;}public T getData() {return data;}
}public class MyNode extends Node<Integer> {public MyNode(Integer data) { super(data); }@Overridepublic void setData(Integer data) {System.out.println("MyNode.setData");super.setData(data);}@Overridepublic Integer getData() {System.out.println("MyNode.getData");return super.getData();}public static void main(String[] args) {MyNode mn = new MyNode(5);Node n = mn; // 多态行为,且赋值为了原生类型n.setData("Hello"); // 会引发抛出ClassCastExceptionInteger x = mn.data;}
}
此例中一个普通类继承了一个被确定了具体类型的泛型类。从代码层面讲:
Node n = mn
此句为多态行为,子类对象赋值父类引用,但注意父类引用是泛型类的原生类型raw type。如果这里是Node<Integer> n = mn
,那么由于类型检查,下一句将编译报错。如果这里是Node<String> n = mn
,那么此句编译报错,因为MyNode的父类是Node<Integer>
。- 由于n是原生类型,
n.setData
的实参要求就变成了只要是个Object
就行,所以编译能通过。由于多态行为,实际调用的setData的是setData的子类版本,而子类的setData的形参类型要求是Integer
,所以实参赋值给形参会运行时报错。
从字节码层面讲(javap -v):
public class MyNode extends Node<java.lang.Integer>minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//省略常量池
{public MyNode(java.lang.Integer);descriptor: (Ljava/lang/Integer;)Vflags: ACC_PUBLICCode:stack=2, locals=2, args_size=20: aload_01: aload_12: invokespecial #1 // Method Node."<init>":(Ljava/lang/Object;)V5: returnLineNumberTable:line 19: 0LocalVariableTable:Start Length Slot Name Signature0 6 0 this LMyNode;0 6 1 data Ljava/lang/Integer;public void setData(java.lang.Integer); //此为setData的真正方法descriptor: (Ljava/lang/Integer;)Vflags: ACC_PUBLICCode:stack=2, locals=2, args_size=20: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String MyNode.setData5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: aload_09: aload_110: invokespecial #5 // Method Node.setData:(Ljava/lang/Object;)V13: returnLineNumberTable:line 23: 0line 24: 8line 25: 13LocalVariableTable:Start Length Slot Name Signature0 14 0 this LMyNode;0 14 1 data Ljava/lang/Integer;public java.lang.Integer getData(); //此为getData的真正方法descriptor: ()Ljava/lang/Integer;flags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #6 // String MyNode.getData5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: aload_09: invokespecial #7 // Method Node.getData:()Ljava/lang/Object;12: checkcast #8 // class java/lang/Integer15: areturnLineNumberTable:line 29: 0line 30: 8LocalVariableTable:Start Length Slot Name Signature0 16 0 this LMyNode;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=4, args_size=10: new #9 // class MyNode3: dup4: iconst_55: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;8: invokespecial #11 // Method "<init>":(Ljava/lang/Integer;)V11: astore_112: aload_113: astore_214: aload_215: ldc #12 // String Hello17: invokevirtual #5 // Method Node.setData:(Ljava/lang/Object;)V20: aload_121: getfield #13 // Field data:Ljava/lang/Object;24: checkcast #8 // class java/lang/Integer27: astore_328: returnLineNumberTable:line 34: 0line 35: 12line 36: 14line 37: 20line 38: 28LocalVariableTable:Start Length Slot Name Signature0 29 0 args [Ljava/lang/String;12 17 1 mn LMyNode;14 15 2 n LNode;28 1 3 x Ljava/lang/Integer;public java.lang.Object getData(); //此为getData的桥方法descriptor: ()Ljava/lang/Object;flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETICCode:stack=1, locals=1, args_size=10: aload_01: invokevirtual #14 // Method getData:()Ljava/lang/Integer; 这里调用了真正方法4: areturnLineNumberTable:line 18: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this LMyNode;public void setData(java.lang.Object); //此为setData的桥方法descriptor: (Ljava/lang/Object;)Vflags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETICCode:stack=2, locals=2, args_size=20: aload_01: aload_12: checkcast #8 // class java/lang/Integer5: invokevirtual #15 // Method setData:(Ljava/lang/Integer;)V 这里调用了真正方法8: returnLineNumberTable:line 18: 0LocalVariableTable:Start Length Slot Name Signature0 9 0 this LMyNode;
}
Signature: #40 // LNode<Ljava/lang/Integer;>;
SourceFile: "MyNode.java"
之前讲过类型擦除的伟大使命,而这里也是。为了兼容以前非泛型源码产生的字节码,但是又要体现出多态,所以要用桥方法。具体地讲,setData
函数和getData
函数由于类型擦除,它们的函数签名实际是void setData(Object)
和Object getData()
,但是又为了表示出多态行为,所以这里用桥方法,再去调用了子类真正的setData
函数和getData
,它们的函数签名就是void setData(Integer)
和Integer getData()
。可以看到桥方法的作用就是起到一个连接的作用,所以叫做桥方法。
- 桥方法
void setData(Object)
和真正方法void setData(Integer)
共存,这里解释为函数重载。 - 桥方法
Object getData()
和真正方法Integer getData()
共存,这里也应该解释为函数重载。但这种只有返回值类型不同的重载方式不允许存在与源码之中,这个共存形式只能在编译器存在,桥方法能够存在也是多亏了这一点。 - 字节码中函数的标志有
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
。ACC_PUBLIC代表访问权限public,ACC_BRIDGE代表此方法为桥方法,ACC_SYNTHETIC代表此方法是编译器自动生成的。
其他
- 本文所有示例均使用的jdk1.8进行的验证。
Java 泛型的本质——类型擦除相关推荐
- 一句话,讲清楚java泛型的本质(非类型擦除)
?欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. 背景 昨天,在逛论坛时遇到个这么个问题,上代码: public class GenericT ...
- 泛型中的类型擦除和桥方法
在Java中,泛型的引入是为了在编译时提供强类型检查和支持泛型编程.为了实现泛型,Java编译器应用类型擦除实现: 1. 用类型参数(type parameters)的限定(如果没有就用Object ...
- java泛型之有界类型
为什么80%的码农都做不了架构师?>>> 在前面的例子中,可以使用任意类替换类型参数.对于大多数情况这很好,但是限制能够传递给类型参数的类型有时是有用的.例如,假设希望创建一个 ...
- java泛型程序设计——通配符类型+通配符的超类型限定
[0]README 0.1) 本文描述+源代码均 转自 core java volume 1, 旨在理解 java泛型程序设计 的 通配符类型+通配符的超类型限定 的知识: [1]通配符类型相关 1. ...
- java 泛型参数具体类型获取、泛型返回具体类型获取
自从java支持泛型后,现在,spring生态中的框架及目前的应用都使用了泛型.有的框架如mybatis和spring都需要反射获取类型,以使得序列化和反序列化得以实现,但有时候我们根据项目的需要获取 ...
- java 泛型参数的类型_Java获得泛型参数类型
在Android开发中,使用Gson将json字符串转换为Java对象尤为常见.在这个转换过程中,通常会结合泛型参数.接口或者抽象类来封装处理. T t = new Gson().fromJson(r ...
- Java 泛型获取实体类型
学习了动态初始化类,如果参数是各种类型的,要如何处理呢? 这时候,需要用到泛型,而传的数据是实体类型,如果从泛型中获取实体类型呢? 需要使用反射,获得指定类的父类的泛型参数的实际类型,直接上代码 ge ...
- Java泛型总结---基本用法,类型限定,通配符,类型擦除
一.基本概念和用法 在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化.例如在哈希表的存取中,JDK1.5之前使用HashMap的 ...
- 用了这么多年的 Java 泛型,你对它到底有多了解?|原创
作为一个 Java 程序员,日常编程早就离不开泛型.泛型自从 JDK1.5 引进之后,真的非常提高生产力.一个简单的泛型 T,寥寥几行代码, 就可以让我们在使用过程中动态替换成任何想要的类型,再也不用 ...
最新文章
- 【Design pattern】简单工厂过渡策略模式
- SSM高级整合项目实战
- SDUT-2449_数据结构实验之栈与队列十:走迷宫
- php限制ip访问次数 并发_PHP实现redis限制单ip、单用户的访问次数功能示例
- System V与Posix
- synchronized猎奇
- LYNC功能之呼叫合并
- Xcode8自带注释不管用解决办法
- android倒计时像音乐,Android实现倒计时的几种方式
- Centos7搭建coreseek
- 怎么将几张pdf合并成一张_如何将多个PDF合并成一个PDF?PDF文档合并成单个的方法...
- Ping命令出现 Packet filtered
- Python 获取 网易云音乐热门评论
- html用div排版类型table,DIV排版和Table排版的区别
- python-matplotlib库绘制饼形图专题(从一般饼状图到内嵌环形图)
- 移动硬盘数据恢复需多少钱?关于这个不伤钱的方法
- 「备战春招/秋招系列」程序员的简历就该这样写...
- 关闭电脑时提示有人远程使用计算机,关机时提示有人正在远程使用
- 文秀才文档管理系统文档管理,CAD, 图纸管理, 百度文库, 文档在线预览
- 工作邦智慧水务公众号系统
热门文章
- BPA - 一揽子采购协议 Blanket Purchase Agreement
- Oracle数据表创建规则
- ATTCK框架以及使用场景
- 逛画展(二分+队列)
- Qt开发北斗定位系统融合百度地图API及Qt程序打包发布
- Kaggle: Tweet Sentiment Extraction 方法总结 Part 1/2: 常用方法总结
- C#练习之打印出所有水仙花数
- python使用大漠插件教程_python调用大漠插件教程05字库
- 如何视频裁剪?建议收藏这几种裁剪视频的方法
- 微信小程序:组件Component接收不到参数id