Java 泛型

Java中的泛型是在编译器这个级别实现的,在生成的Java字节码当中是没有泛型中的类型信息的,使用泛型的时候加上的类型参数会在编译的时候去掉,这个过程称作为类型擦除,如在代码中定义的List 和 List, 在编译之后都变成了List, JVM看到的只有List (所以这一点和C++中的模板是不同的,在C++中为每一个模板类生成一个类, 此外在Java中,泛型中还不支持基本类型) 而由泛型附加的类型信息对JVM来说是不可见的,Java编译器会在编译的时候尽可能发现出错的地方,但是仍然无法避免运行时刻出现的强制类型转换异常,类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。

泛型的很多奇怪特性都与类型擦除有关:

  • 泛型没有自己独有的Class类对象,比如不存在List.class ,只有List.class;
  • 静态变量被泛型类所有的实例共享,例如MyClass 中定义静态变量 var , 访问这个静态变量的方式任然是MyClass.var。
  • 泛型的参数类型不能用在Java的异常处理中,以为异常处理是由JVM的 Runtime 来处理的,但是由于类型擦除,所以JVM是不知道这个类型信息的存在的,对于JVM来说都是MyException.也就无法执行对应的catche语句。(目前在Intelij IDE中,定义的一个泛型的异常直接报错(Generic class may not extends java.lang.Throwable))

这些原因的根源都是泛型的类型信息只是在编译期存在的,而在程序执行的时候无法找到的。

但是这里又有另外一个问题了,我们在程序运行的时候明明是可以通过API 拿到另一个类的泛型信息的,
class 对象由这两个方法

  • getGenericSuperclass()
  • getGenericInterface()

那么这两个类拿到的泛型信息又是从哪里获取的呢?
Java文件在编译之后会生成字节码文件,字节码文件的结构如下图所示

此处输入图片的描述

接下来,先定义两个类型,

public class MyClass  {
    List data;
    public void setData(List data) {
        this.data = data;
    }
}
public class MyClassString {
    List<String> data;
    public void setData(List<String> data) {
        this.data = data;
    }
}

在编译完成之后,我们可以通过javap 来获取两者生成的字节码

运行 javap -c MyClassString 结果如下

public class com.baidu.accweather.MyClassString {
  java.util.List<java.lang.String> data;
  public com.baidu.accweather.MyClassString();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void setData(java.util.List<java.lang.String>);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field data:Ljava/util/List;
       5: return
}

运行javap -c MyClass 结果如下

public class com.baidu.accweather.MyClass {
  java.util.List data;
  public com.baidu.accweather.MyClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void setData(java.util.List);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field data:Ljava/util/List;
       5: return
}

这个时候通过对比发现出列data 的声明不同之外其他的地方完全一样,特别需要注意的是在setData当中,从注释来看 data, 它的类型信息里面已经没有了泛型类型的信息。

接下来我们需要用 -verbose 参数打印堆栈大小、各方法的locals及args参数,以及class文件的编译版本

运行javap -verbose MyClassString , 其常量池的信息输出如下(MyClass 的就不贴出来了)

   Classfile /D:/javaworkspace/AccWeather/out/production/AccWeather/com/baidu/accweather/MyClassString.class
  Last modified 2017-3-27; size 610 bytes
  MD5 checksum caaccea4d689459e3464f2088a3efb34
  Compiled from "MyClassString.java"
public class com.baidu.accweather.MyClassString
  SourceFile: "MyClassString.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#22         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#23         //  com/baidu/accweather/MyClassString.data:Ljava/util/List;
   #3 = Class              #24            //  com/baidu/accweather/MyClassString
   #4 = Class              #25            //  java/lang/Object
   #5 = Utf8               data
   #6 = Utf8               Ljava/util/List;
   #7 = Utf8               Signature
   #8 = Utf8               Ljava/util/List<Ljava/lang/String;>;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/baidu/accweather/MyClassString;
  #16 = Utf8               setData
  #17 = Utf8               (Ljava/util/List;)V
  #18 = Utf8               LocalVariableTypeTable
  #19 = Utf8               (Ljava/util/List<Ljava/lang/String;>;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               MyClassString.java
  #22 = NameAndType        #9:#10         //  "<init>":()V
  #23 = NameAndType        #5:#6          //  data:Ljava/util/List;
  #24 = Utf8               com/baidu/accweather/MyClassString
  #25 = Utf8               java/lang/Object
{
  java.util.List<java.lang.String> data;
    flags:
    Signature: #8                           // Ljava/util/List<Ljava/lang/String;>;

  public com.baidu.accweather.MyClassString();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
        0       5      0      this   Lcom/baidu/accweather/MyClassString;

  public void setData(java.util.List<java.lang.String>);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field data:Ljava/util/List;
         5: return
      LineNumberTable:
        line 12: 0
        line 13: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0       6     0    this   Lcom/baidu/accweather/MyClassString;
          0       6     1    data   Ljava/util/List;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
          0       6     1    data   Ljava/util/List<Ljava/lang/String;>;
    Signature: #19                          // (Ljava/util/List<Ljava/lang/String;>;)V
}

从setData 方法输出的信息来看, 里面有两张表,一个本地变量表,一个是变量类型表,从 LocalVariableTypeTable 中可以看出来 变量 data 的类型信息是 java/util/List
而最后 Signature 的属性在这里具体指明了是常量池中的#19,这里具体说明了这个方法的信息。