JAVA泛型和类型擦除
什么是类型擦除
Java是使用擦除来实现泛型的。使用泛型后在运行时任何具体的类型信息都被擦除了,关于泛型的处理都是在编译期间发生的。查看以下代码:
public class Holder<T> {
private T data;
public T getData() {
return data;
}
public void setData(T d) {
data = d;
}
}
当新建一个Holder类并获取它的class信息
Holder<String> holder = new Holder<>();
System.out.println(holder.getClass().getName());
输出为 :
com.xx.xx.Holder
可以发现类型信息为Holder而并非Holder<String>。
再查看字段data的类型信息
Class clz = holder.getClass();
System.out.println(clz.getDeclaredField("data").getType().getName());
输出为 :
java.lang.Object
可见字段data的类型信息在运行时已被擦除为Object,(当打印 holder.getData().getName() 时输出为 java.lang.String 因为调用 getData 时 data 已强转为 String 而通过反射才能查看运行时的类型信息,后面会详解 ),另外当使用通配符 <? extend String>
public class Holder<T extends String> {
private T data;
public T getData() {
return data;
}
public void setData(T d) {
data = d;
}
}
再次通过反射打印字段data的类型信息,因为规定了界限泛型在被擦除的时候被替换成了上限String。
类型擦除为什么存在
泛型是Java1.5版本的新特性,以前版本并没有泛型的概念,为了兼容性才使用类型擦除来实现,擦除的核心动机是它使得使用泛化的客户端可以使用Java SE5之前编写的非泛型类库。在后面的字节码分析中可看出如何实现兼容。
类型擦除与编译器
既然泛型特性只存在于编译阶段,java编译是将源码编译成字节码那我们可以通过分析字节码查看泛型是如何实现以及类型擦出的具体表现,观察下列代码:
public class Holder<T> {
private T data;
public T getData() {
return data;
}
public void setData(T d) {
data = d;
}
}
javac 编译以上代码成 Holder.class 后通过 javap -v 查看字节码(省略无关部分)
{
T data;
descriptor: Ljava/lang/Object;
flags:
Signature: #8 // TT;
T getData();
descriptor: ()Ljava/lang/Object;
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field data:Ljava/lang/Object;
4: areturn
LineNumberTable:
line 5: 0
Signature: #15 // ()TT;
void setData(T);
descriptor: (Ljava/lang/Object;)V
flags:
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field data:Ljava/lang/Object;
5: return
LineNumberTable:
line 9: 0
line 10: 5
Signature: #18 // (TT;)V
}
从字节码上方法的 descriptor 描述符上也可以印证类型被擦出成了Object
T getData();
descriptor: ()Ljava/lang/Object;
当我们不是使用泛型时
public class HolderObj {
Object data;
Object getData() {
return data;
}
void setData(Object d) {
data = d;
}
}
同样查看字节码相关部分
{
java.lang.Object data;
descriptor: Ljava/lang/Object;
flags:
java.lang.Object getData();
descriptor: ()Ljava/lang/Object;
flags:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field data:Ljava/lang/Object;
4: areturn
LineNumberTable:
line 5: 0
void setData(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags:
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field data:Ljava/lang/Object;
5: return
LineNumberTable:
line 9: 0
line 10: 5
}
对比2个版本的 getData 和 setData 方法发现2个方法的字节码几乎相同可见编译器对泛型的处理并不在本类中。当我们查看泛型类方法的调用处:
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.setData("test");
String a = holder.getData();
HolderObj holderObj = new HolderObj();
holderObj.setData("test");
Object o = holderObj.getData();
}
查看字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: new #2 // class Holder
3: dup
4: invokespecial #3 // Method Holder."<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String test
11: invokevirtual #5 // Method Holder.setData:(Ljava/lang/Object;)V
14: aload_1
15: invokevirtual #6 // Method Holder.getData:()Ljava/lang/Object;
18: checkcast #7 // class java/lang/String
21: astore_2
//-----------------------HolderObj
22: new #8 // class HolderObj
25: dup
26: invokespecial #9 // Method HolderObj."<init>":()V
29: astore_3
30: aload_3
31: ldc #4 // String test
33: invokevirtual #10 // Method HolderObj.setData:(Ljava/lang/Object;)V
36: aload_3
37: invokevirtual #11 // Method HolderObj.getData:()Ljava/lang/Object;
40: astore 4
42: return
对比2次调用的字节码发现泛型的getData方法后面多了一次类型转换
18: checkcast #7 // class java/lang/String
把原本Object类型转换成了String这样才使得getData可以直接赋值给String类型的变量。
当我们传递不符合的对象给泛型方法:
public static void main(String[] args) {
Holder<String> holder = new Holder<>();
holder.setData(Integer.valueOf(1));
String a = holder.getData();
}
编译时编译会报错(使用IDE时会自动报错)
HolderTest.java:6: 错误: 不兼容的类型: Integer无法转换为String
holder.setData(Integer.valueOf(1));
结合上面2点发现泛型的实现第一是在编译过程中检查赋值给泛型变量类型是否正确,然后在获取泛型变量的地方添加checkcast命令进行类型转换,所以在运行时类型信息是未知的。这样做字节码上不会有新的语法,当然可以解决兼容性带来的问题