Java 泛型的本质——类型擦除

简介

  • 首先必须了解到,java源代码需要经过编译器编译出字节码,在这个过程中,编译器执行编译期的检查,检查通过了就会生成字节码。而字节码存储着能被JVM解释运行的指令,所以说,相对于java源代码,java源代码生成的字节码文件里的指令才是真正被执行到的指令。
  • 而java的泛型由于种种原因,在内部实现方面并不像c++的模板一样,可以在运行时获得类型参数的真正类型。即运行时,在泛型代码内部,无法获得类型参数的真正类型。这是因为编译器在编译过后,泛型代码生成的字节码是不包括类型参数的具体类型的。这也就是泛型的类型擦除

泛型总共干了4件事:

  1. 编译之前编译器进行的静态分析检查(比如你不能把ArrayList<Integer>里添加string,如果做了无法编译)。
  2. 强制转换类型(比如你从ArrayList<Integer>里取一个元素时,编译器隐式地在字节码里添加一句强制转换类型,转换为int)。
  3. 在字节码中,用类型参数的限定(如果没有就是Object)来替换所有类型参数(保留上界)。这就是类型擦除。
  4. 在继承使用了类型参数的方法时(形参或者返回值的类型是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.java

import 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 bounds

public 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_0
       1: getfield      #2                  // Field kind:Ljava/lang/Class;
       4: iload_1
       5: 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: areturn
      
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #5                  // class ArrayMaker
         3: dup
         4: ldc           #6                  // class java/lang/String
         6: invokespecial #7                  // Method "<init>":(Ljava/lang/Class;)V
         9: astore_1
        10: aload_1
        11: bipush        9
        13: 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 ArrayMaker
       3: dup
       4: ldc           #6                  // class java/lang/String
       6: invokespecial #7                  // Method "<init>":(Ljava/lang/Class;)V
       9: astore_1
      10: aload_1
      11: bipush        9
      13: 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_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void set(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public java.lang.Object get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class SimpleHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
}

这是GenericHolder的java汇编代码:

public class GenericHolder<T> {
  public GenericHolder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public T get();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class GenericHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method get:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: 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());
  }
} ///:~
  1. 通过定义interface FactoryI<T>接口,决定了所有工厂类的父类以及它们应该实现的方法。
  2. Foo2方法,首先它是一个构造器,其次构造器没有返回值,它前面的尖括号代表了这是一个泛型方法。
  3. Foo2<T>和它定义中的<F extends FactoryI<T>>是同一个T,这样,你在创建泛型类Foo2的对象,只要显示给定了具体类型,那么就能同时要求到其内部的F必须是FactoryI<具体类型>的子类。
  4. 剩下就是两个工厂类的定义,可以看到工厂类确实是FactoryI<具体类型>的子类。

泛型数组

这里要分为两种情况:

  1. 泛型类对象的数组
  2. 泛型代码里的类型参数的数组,形如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 GenericArray
       3: dup
       4: bipush        10
       6: invokespecial #6                  // Method "<init>":(I)V
       9: astore_1
      10: aload_1
      11: invokevirtual #7                  // Method rep:()[Ljava/lang/Object;
      14: astore_2
      15: aload_1
      16: invokevirtual #7                  // Method rep:()[Ljava/lang/Object;
      19: checkcast     #8                  // class "[Ljava/lang/Integer;"
      22: astore_3
      23: 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); }

    @Override
    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }

    @Override
    public 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");    // 会引发抛出ClassCastException
        Integer 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: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//省略常量池
{
  public MyNode(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #1                  // Method Node."<init>":(Ljava/lang/Object;)V
         5: return
      LineNumberTable:
        line 19: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LMyNode;
            0       6     1  data   Ljava/lang/Integer;

  public void setData(java.lang.Integer);    //此为setData的真正方法
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String MyNode.setData
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: aload_0
         9: aload_1
        10: invokespecial #5                  // Method Node.setData:(Ljava/lang/Object;)V
        13: return
      LineNumberTable:
        line 23: 0
        line 24: 8
        line 25: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LMyNode;
            0      14     1  data   Ljava/lang/Integer;

  public java.lang.Integer getData();    //此为getData的真正方法
    descriptor: ()Ljava/lang/Integer;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String MyNode.getData
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: aload_0
         9: invokespecial #7                  // Method Node.getData:()Ljava/lang/Object;
        12: checkcast     #8                  // class java/lang/Integer
        15: areturn
      LineNumberTable:
        line 29: 0
        line 30: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   LMyNode;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #9                  // class MyNode
         3: dup
         4: iconst_5
         5: invokestatic  #10                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         8: invokespecial #11                 // Method "<init>":(Ljava/lang/Integer;)V
        11: astore_1
        12: aload_1
        13: astore_2
        14: aload_2
        15: ldc           #12                 // String Hello
        17: invokevirtual #5                  // Method Node.setData:(Ljava/lang/Object;)V
        20: aload_1
        21: getfield      #13                 // Field data:Ljava/lang/Object;
        24: checkcast     #8                  // class java/lang/Integer
        27: astore_3
        28: return
      LineNumberTable:
        line 34: 0
        line 35: 12
        line 36: 14
        line 37: 20
        line 38: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      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_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #14                 // Method getData:()Ljava/lang/Integer;   这里调用了真正方法
         4: areturn
      LineNumberTable:
        line 18: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LMyNode;

  public void setData(java.lang.Object);    //此为setData的桥方法
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #8                  // class java/lang/Integer
         5: invokevirtual #15                 // Method setData:(Ljava/lang/Integer;)V   这里调用了真正方法
         8: return
      LineNumberTable:
        line 18: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       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进行的验证。
发布了171 篇原创文章 · 获赞 130 · 访问量 28万+

猜你喜欢

转载自blog.csdn.net/anlian523/article/details/100643209