【JAVA】对泛型擦除的一点思考

一、什么是泛型擦除

泛型(generics)的真正面目,是参数化对象类型。在使用泛型的时候,我们总是把一个具体的对象类型当作一个参数传入。

泛型的作用就是发生在编译时,它提供了安全检查机制。

可是当处于编译时,所有的泛型都会被去掉,即被还原为原始类型,如java.util.ArrayList,不再有"<T>"。


二、代码验证

创建一个List<String>与List<Integer>

        List<String> stringList = new ArrayList<>();
        stringList.add("123");
        //这句报错,idea提示只能插入String类型
        //如果我们在记事本中这样写,使用javac编译时,就会报错
        //stringList.add(123);
        
        List<Integer> integerList = new ArrayList<>();

        System.out.println(stringList.getClass());
        System.out.println(integerList.getClass());

运行后,输出同样的类型。

        class java.util.ArrayList
        class java.util.ArrayList

这和例子说明:在编译时,编译器会进行安全检查。编译后,泛型的类型全部被擦除,只剩下了原始类型。


三、在字节码指令中观察类型擦除

原始代码:

public class Main<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        Main<String> s = new Main<>();
        s.setT("abc");
        String str = s.getT();
        System.out.println(str);
    }

}

使用javap -c Main.class反编译后得到:

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

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

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

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class com/yang/testGenerics/Main
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String abc
      11: invokevirtual #6                  // Method setT:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getT:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      25: aload_2
      26: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      29: return
}

反编译后,在main方法中,可以发现,set进去的是一个原始类型Object。

第15行,get获取的也是一个Object类型。

重点在于第18行,做了一个checkcast类型转换,将Object强转为了String。

可以看得出,泛型在生成的字节码中,就已经被去掉了,因此在运行时,List<String>与List<Integer>都是一个类。

那么,如果我们在一个类中声明以下的方法:

    private int add(List<Integer> integerList) {
        return 1;
    }

    private double add(List<String> stringList) {
        return 1.0;
    }

这样的代码,无法通过编译。首先方法的返回值是不参与重载选择的,也就是重载不看返回值。此外,泛型的擦除使得方法的特征签名完全一样,因此这里可以看做是重复的方法,因此编译失败。


四、真的无法在运行时获取泛型类型吗?

看以下的代码:

public class Test {

    private List<Integer> list;

    public static void main(String[] args) {
        try {
            Field field = Test.class.getDeclaredField("list");
            System.out.println(field.getGenericType());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

}

运行后,会输出:

java.util.List<java.lang.Integer>

泛型的类型,确实拿到了,这是怎么回事?

由于Java泛型的实现机制,使用了泛型的代码在运行期间相关的泛型参数的类型会被擦除,我们无法在运行期间获知泛型参数的具体类型(所有的泛型类型在运行时都是Object类型)。但是在编译java源代码成 class文件中还是保存了泛型相关的信息,这些信息被保存在class字节码常量池中,使用了泛型的代码处会生成一个signature签名字段,通过签名signature字段指明这个常量池的地址,通过反射获取泛型参数类型,归根结底都是来源于这个signature属性。


五、总结

泛型在编译时,用于安全检查。编译后,将会被编译器擦除成原始类型,但是我们用反射依然可以获取到存于signature中的泛型信息。jvm并不想支持泛型,如果要支持泛型,那么就会在运行时创建很多不必要的类,浪费内存空间。但泛型确实存在诸多好处,因此在编译时支持泛型,在运行直接去除泛型,jvm还向以前的低版本一样,直接处理原始类型。

Java泛型采用的是擦除法实现的伪泛型,泛型信息(类型变量、参数化类型)编译之后通通被除掉了。使用擦除法的好处就是实现简单,运行期也能够节省一些类型所占的内存空间。而擦除法的坏处就是,通过这种机制实现的泛型远不如真泛型灵活和强大。Java选取这种方法是一种折中,因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。

发布了177 篇原创文章 · 获赞 583 · 访问量 115万+

猜你喜欢

转载自blog.csdn.net/qq_33591903/article/details/105226898