一.开篇
上文http://zy19982004.iteye.com/blog/1976993中提到“NewCollections.map() return Map<Object, Object>, but not Map<Integer, String>”,为什么呢?对擦除的理解将是对泛型理解的关键。
二.擦除的概念
《Thinking in Java》里说道“在泛型代码内部,无法获得任何有关泛型参数类型的信息”。
《Java核心技术》里说道“虚拟机没有泛型类型对象-所有对象都属于普通类”。
- Java泛型是使用擦除(擦除实际类型参数,替换为限定类型)来实现的。这意味着当你使用泛型时,泛型实际类型参数只有在静态类型检查期间才出现,在此之后,任何具体的类型信息都被擦除,你唯一知道的就是你在使用一个对象。因此List<String>和List<Integer>在运行时是相同的类型,这两种类型都被擦除为它们的原生类型List。
- 一个例子,看看运行时,类型参数是什么样子。你能够发现的则是用作参数占位符的标识符,这些对我们没什么用。
package com.jyz.study.jdk.generic; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 在泛型代码内部,无法获得任何有关泛型参数类型的信息 * @author JoyoungZhang@gmail.com * */ public class ClassTypeParameters { static class Frob<T>{} static class FrobF<T extends Number>{} static class FrobPM<P,M>{} private static List list1 = new ArrayList(); private static List<Integer> list2 = new ArrayList<Integer>(); private static Map<Integer, Integer> map1 = new HashMap<Integer, Integer>(); static Frob f1 = new Frob(); static FrobF<Integer> f2 = new FrobF<Integer>(); static FrobPM<Integer, Double> f3 = new FrobPM<Integer, Double>(); //Calss.getTypeParameters()将返回一个TypeVariable对象数组 //表示有泛型声明所声明的形式类型参数 public static void main(String[] args) { System.out.println(Arrays.toString(list1.getClass().getTypeParameters())); System.out.println(Arrays.toString(list2.getClass().getTypeParameters())); System.out.println(Arrays.toString(map1.getClass().getTypeParameters())); System.out.println(list1.getClass().getSimpleName()); System.out.println(list2.getClass().getSimpleName()); System.out.println(Arrays.toString(f1.getClass().getTypeParameters())); System.out.println(Arrays.toString(f2.getClass().getTypeParameters())); System.out.println(Arrays.toString(f3.getClass().getTypeParameters())); } } 输出结果 [E] [E] [K, V] ArrayList ArrayList [T] [T] [P, M]
三.为什么要使用擦除
核心动机:使得泛型化的客户端代码可以使用非泛型化的类库,非泛型化的客户端代码可以使用泛型化的类库。这个被称为“兼容迁移性”。这也从侧面反应了,前期的设计多么重要,倘若JDK1.0就将泛型纳入其中,必将是Java使用者的一大福音。
三.擦除原则
- 无限定的形式类型参数将被替换为Object。比喻List<T> T是无限定的,被替换为Object。
- 有限定的形式类型参数将被替换为第一个限定类型。比喻List<T extends Comparable & Serializable>,T被替换为Comparable,也称T被擦除到了Comparable。
- 需要注意的是,泛型擦除的对象是实际参数,也就是说是GenericClass<String, Integer>还是GenericClass<StringBuffer, Number>等具体类型参数被擦除到了ErasureClass,并不是GenericClass<K, V extends Number>被擦除到了ErasureClass。但有时候我们也说GenericClass<K, V extends Number>被擦除到了ErasureClass,甚至可以说GenericClass<?,?>被擦除到了ErasureClass,这并不妨碍我们理解,就好像说所有水果都好吃,自然苹果也是好吃的。
package com.jyz.study.jdk.generic; /** * 擦除存在于泛型类,也存在于泛型方法 * 两者擦除原则一样 * 1.声明形式参数的部分“消失” * 2.无限定的形式类型参数将被替换为Object,有限定的形式类型参数将被替换为第一个实际类型参数 * @author JoyoungZhang@gmail.com * */ public class AfterErasure { } class GenericClass<K, V extends Number>{ private K object1; private V object2; private K get1(){ return object1; } private V get2(){ return object2; } private <KK> KK singleMethod1(KK object){ return object; } private <VV extends Number> VV singleMethod2(VV object){ return object; } } class ErasureClass{ private Object object1; private Number object2; private Object get1(){ return object1; } private Number get2(){ return object2; } private Object singleMethod1(Object object){ return object; } private Number singleMethod2(Number object){ return object; } }
四.擦除的问题
- 既然擦除了类型参数的信息,那编译器是怎么确保方法或类中使用的类型的内部一致性呢?下面这个例子1和2产生的字节码是相同的,一方面验证了上面说的擦除原则;另外一方面说明了泛型工作的地方,称之为边界,在边界处,对传递进来的值就行额外的编译器检查,并插入对传递出去值的转型。
package com.jyz.study.jdk.generic; /** * 1 2字节码相同 * 边界(泛型切入点):对传递进来的值就行额外的编译期检查,并插入对传递出去的值的转型 * @author JoyoungZhang@gmail.com * */ public class TestGenericCheckpoint { public static void main(String[] args) { //1 GenericHolder<String> gh = new GenericHolder<String>(); gh.set("sa");//边界 编译器check String sa1 = gh.get();//运行期间仍会checkcast //2 SimpleHolder sh = new SimpleHolder(); sh.set("sa");//编译器不check任何东西 String sa2 = (String) sh.get();//运行期间checkcast } } class GenericHolder<T>{ private T object; public void set(T object){ this.object = object; } public T get(){ return object; } } class SimpleHolder{ private Object object; public void set(Object object){ this.object = object; } public Object get(){ return this.object; } }
- 擦除后显式的引用运行时类型的操作都将无法工作,包括转型,instanceof,new。有什么补救措施?可以采用类型标签。 isInstance代替 instanceof;newInstance代替new,注意newInstance需要class对象具有默认构造函数。
//Determines if the specified <code>Object</code> is assignment-compatible // * with the object represented by this <code>Class</code>. //This method is // * the dynamic equivalent of the Java language //<code>instanceof</code> // * operator. public native boolean isInstance(Object obj); //Creates a new instance of the class represented by this <tt>Class</tt> // * object. public T newInstance() {...}