深入理解系列之JAVA泛型机制

时间:2022-08-12 19:26:02

泛型是指在声明(类,方法,属性)的时候采用一个“标志符”来代替,而只有在调用的时候才传入真正的类型,我们最常见的泛型实例就是前面讲述的集合类,集合类在声明的时候都是通过泛型方式来声明的,只有在调用(实例化)时我们才确定传入的是Integer亦或是String等等!

注:本文着重叙述泛型实现的原理,而忽略一些泛型应用时的注意事项,详细应用时的注意事项请参看其他博文

问题一、为什么要采用泛型?

泛型机制是JDK1.5出现的。拿ArrayList举例,在JDK1.5出现之前,为了解决存储不同参数类型数据的问题,ArrayList声明的时候传入参数定义为Object,因为Object是所有类型的父类,这样在取出的时候再通过手动的强制转换为实际的类型。大概的实现是这样的(原理性描述):

class ArrayList_Before_JDK5{
  private Object[] elements=new Object[10];
  public Object get(int i){
    return elements[i];
  }

 public void add(Object o){
    if(elements.length > 1)
      elements[1] = o;
    elements[0] = o;
  }
}

这样我们在使用ArrayList的时候,将会这样用:

public static void main(String[] args) {

    ArrayList_Before_JDK5 arrayList_before_jdk5 = new ArrayList_Before_JDK5();
    arrayList_before_jdk5.add("123");
    arrayList_before_jdk5.add(123);
    String string = (String)arrayList_before_jdk5.get(0);
    Integer integer = (Integer) arrayList_before_jdk5.get(1);
}

我们这里发现两个问题:
1、当我们获取某一个值的时候必须手动强转;
2、假设我们想把这个实例全部存入String类型的,由于底层的参数是Object,所以程序并不会阻止我们传入Integer类型的参数,这时如果我们仍然使用:

String string = (String) arrayList_before_jdk5.get(1);

获取位置1的数据(注意此时我们认为存入的是string,所以我们都使用String强转符,但是实际上位置1被我们传入了Interger类型)时,程序在编译期间不会出现任何错误,但是运行的时候却会出现异常:
Exception in thread “main” java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at Fxing.main(Fxing.java:10)
能不能通过一种机制在编译的时候就(或者说在IDE语法提示的时候)就能提前检测出错误,避免不必要的运行异常呢?答案是肯定的,就是泛型!采用泛型设计的ArrayList将会是这样的(JDK8源码,但是剔除了不必要的源代码):

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 transient Object[] elementData; 

public boolean add(E e) {
        elementData[size++] = e;
        return true;
    }

    public E get(int index) {
        return elementData(index);
    }

这里的E就是在ArrayList中的“泛型标志符”,我们在创建实例的时候就会依照自己的需求传入String、Integer等等,那么在底层这个E就变成了String、Integer等等,所以传入参数的时候就必须按照相应的类型来写入了!

问题二、泛型的底层原理是什么?

我们上面说过,当依照自己的需求传入实际的类型参数的时候,E将会变成实际的类型参数——泛型追求的效果就是这样的,但是在JVM编译后其实并不是这样的,我们称之为“泛型擦除”!也就是说,编译过后的字节码将会还原回原始类型——和JDK5之前的一样。所以,我们也称JAVA的泛型为伪泛型!为了证明这一点,我们通过两种反编译方法来验证,以下面代码为例:

public class Fxing {
  public static void main(String[] args) {
    ArrayList<String> arrayList_string = new ArrayList<>();
    ArrayList<Integer> arrayList_integer = new ArrayList<>();
    arrayList_integer.add(1);
    arrayList_string.add("1");
    System.out.println(arrayList_integer.get(0));
    System.out.println(arrayList_string.get(0));
  }
}

第一:IDE反编译字节码
如果你使用IDE反编译字节码,你会发现下面的情形:

public class Fxing {
  public Fxing() {
  }

  public static void main(String[] args) {
    ArrayList<String> arrayList_string = new ArrayList();
    ArrayList<Integer> arrayList_integer = new ArrayList();
    arrayList_integer.add(Integer.valueOf(1));
    arrayList_string.add("1");
    System.out.println(arrayList_integer.get(0));
    System.out.println((String)arrayList_string.get(0));
  }
}

不是说编译字节码的时候会发生“泛型擦除”回归到原始类型吗?为什么我们可以看到String、Integer类型的呢?这个问题在博客 关于java泛型擦除反编译后泛型会出现问题也同样出现了,同时在《深入理解JVM虚拟机》中也找到了佐证:

JCP组织对虚拟机规范做出了相应的修改,引入了诸如Signature、Loca-lVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型 ,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的Class文件的虚拟机都要能正确地识别Signature参数。

通俗点将就是,JCP组织要求class文件保存实际类型信息,所以IDE可以由特殊字段获取实际类型,从而智能的反编译!所以事实上,反编译后的代码是这样的:

public class Fxing {
  public Fxing() {
  }

  public static void main(String[] args) {
    ArrayList arrayList_string = new ArrayList();
    ArrayList arrayList_integer = new ArrayList();
    arrayList_integer.add(Integer.valueOf(1));
    arrayList_string.add("1");
    System.out.println(arrayList_integer.get(0));
    System.out.println((String)arrayList_string.get(0));
  }
}

方法二:javap反编译
如果上述还是不能使你信服,我们可以通过javap(javap -c XXX.class)编译出字节码指令来看看到底发生了什么:

public static void main(java.lang.String[]);
    Code:
       0: new           #2 // class java/util/ArrayList
       3: dup
       4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: new           #2 // class java/util/ArrayList
      11: dup
      12: invokespecial #3 // Method java/util/ArrayList."<init>":()V
      15: astore_2
      16: aload_2
      17: iconst_1
      18: invokestatic  #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      21: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      24: pop
      25: aload_1
      26: ldc           #6 // String 1
      28: invokevirtual #5 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
      31: pop
      32: aload_2
      33: iconst_0
      34: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      37: pop
      38: aload_1
      39: iconst_0
      40: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      43: pop
      44: getstatic     #8 // Field java/lang/System.out:Ljava/io/PrintStream;
      47: aload_2
      48: iconst_0
      49: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      52: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      55: getstatic     #8 // Field java/lang/System.out:Ljava/io/PrintStream;
      58: aload_1
      59: iconst_0
      60: invokevirtual #7 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
      63: checkcast     #10 // class java/lang/String
      66: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      69: return
}

我们可以看到(4、12)(21、28、34、40),初始化的时候并没有携带实际类型信息,add、get的时候使用的都是object!同时当需要获取最终数据的时候,JVM虚拟机将自动强制转型(63、66),所以我们需要注意的是:

泛型是伪泛型,即使泛型对象是传入不同的类型参数泛型对象,因为在编译阶段会被擦除,所以实际上该泛型对象属于同一个类,进而在函数重载的时候不会因为泛型参数不同而“重载成功”!

类似还用其他类似引用传递问题、通配符问题请移步博客Java泛型深入理解查看!