一、什么是泛型擦除

泛型(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最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法。