Java 泛型的实现原理是类型擦除,想要用好泛型,解决一些泛型的“疑难杂症”问题,就要正确理解和使用类型擦除。
泛型学习资料:《泛型最全知识导图》、《大厂泛型面试真题26道》,到本篇结尾处获得~
1 什么是类型擦除(Type Erasure)
泛型是 java 1.5 版本引进的新特性,Java 1.5 前没有泛型。但是,为什么泛型的代码和之前版本的代码能够很好地兼容呢?
这是因为, 泛型信息只存在于代码编译时,在进入 JVM 之前,与泛型相关的信息就会被擦掉,专业术语叫做类型擦除 。也可以简单理解为:将泛型 Java 代码,转换为普通 Java 代码,只是编译器更直接,将泛型 Java 代码,直接转换成了普通 Java 字节码 。
类型擦除的关键,是从泛型类型中清除类型参数的相关信息,在必要时,再添加类型检查和类型转换的方法。
2 为什么要用类型擦除
在泛型中使用类型擦除,主要是为了“向后兼容”,保证 1.5 版本的程序,在 8.0 版本上也可以运行,让非泛型的 Java 程序,在后续支持泛型的 JVM 上也可以运行。
代码示例:
下面展示的两种代码,在编译成 Java虚拟机 汇编码是一样的。因此,无论函数的返回类型是T,还是我们主动写强转,最后都是插入一条 checkcast 语句而已。
class SimpleHolder{
private Object obj;
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
SimpleHolder holder = new SimpleHolder();
holder.setObj("Item");
String s = (String)holder.getObj();
class GenericHolder<T>{
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
}
GenericHolder<String> holder = new GenericHolder<String>();
holder.setObj("Item");
String s = holder.getObj();
aload_1
invokevirtual // Method get: ()Object
checkcast // class java/lang/String
astore_2
return
我们可以理解为:
- 之前非泛型的写法,编译成的虚拟机汇编码块是 A ;
- 之后的泛型写法,只是在 A 的前面、后面“插入”了其它的汇编码,这并不会破坏 A 这个整体。
这样既把非泛型“扩展为泛型”,又兼容了非泛型。
3 类型擦除的优缺点
Java 的泛型使用类型擦除,只是在编译时做类型检查、在运行时擦除,共享代码好,但是类型精度一般。
4 类型擦除的使用原则
- 消除类型参数声明,即删除 <> 、及其包围的部分;
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符、或没有上下界限定,则替换为Object;如果存在上下界限定,则根据子类替换原则,取类型参数的最左边限定类型(即父类);
- 为了保证类型安全,必要时插入强制类型转换代码;
- 自动产生“桥接方法”,以保证擦除类型后的代码仍然具有泛型的“多态性”。
5 类型擦除的过程
类型擦除的过程:
- 将所有的泛型参数,用其最左边界类型(最顶级的父类型)替换;
- 移除所有的类型参数。
代码示例:
interface Comparable <A> {
public int compareTo( A that);
}
final class NumericValue implements Comparable <NumericValue> {
priva te byte value;
public NumericValue (byte value) { this.value = value; }
public byte getValue() { return value; }
public int compareTo( NumericValue t hat) { return this.value - that.value; }
}
-----------------
class Collections {
public static <A extends Comparable<A>>A max(Collection <A> xs) {
Iterator <A> xi = xs.iterator();
A w = xi.next();
while (xi.hasNext()) {
A x = xi.next();
if (w.compareTo(x) < 0) w = x;
}
return w;
}
}
final class Test {
public static void main (String[ ] args) {
LinkedList <NumericValue> number List = new LinkedList <NumericValue> ();
numberList .add(new NumericValue((byte)0));
numberList .add(new NumericValue((byte)1));
NumericValue y = Collections.max( numberList );
}
}
经过类型擦除后的类型:
interface Comparable {
public int compareTo( Object that);
}
final class NumericValue implements Comparable {
priva te byte value;
public NumericValue (byte value) { this.value = value; }
public byte getValue() { return value; }
public int compareTo( NumericValue t hat) { return this.value - that.value; }
public int compareTo(Object that) { return this.compareTo((NumericValue)that); }
}
-------------
class Collections {
public static Comparable max(Collection xs) {
Iterator xi = xs.iterator();
Comparable w = (Comparable) xi.next();
while (xi.hasNext()) {
Comparable x = (Comparable) xi.next();
if (w.compareTo(x) < 0) w = x;
}
return w;
}
}
final class Test {
public static void main (String[ ] args) {
LinkedList numberList = new LinkedList();
numberList .add(new NumericValue((byte)0)); ,
numberList .add(new NumericValue((byte)1));
NumericValue y = (NumericValue) Collections.max( numberList );
}
}
第一段代码示例中,泛型类 Comparable <A> 擦除后, A 被替换为最左边界 Object 。Comparable<NumericValue> 的类型参数 NumericValue 被擦除掉,这直接导致了 NumericValue 没有实现接口 Comparable 的 compareTo(Object that) 方法。于是,编译器充当好人,添加了一个桥接方法。
第二段代码示例中,限定了类型参数的边界 <A extends Comparable<A>>A , A 必须为Comparable<A> 的子类。按照类型擦除的过程,先将所有的类型参数替换为最左边界Comparable<A>,然后去掉参数类型 A ,得到最终擦除后的结果。
6 类型擦除的使用示例
这是一道经典的测试题:
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
输出结果是 true ,是因为 List<String>和 List<Integer>,在 JVM 中的 Class 都是 List.class ,泛型信息被擦除了。
可能有同学会问,类型 String 和 Integer 怎么办?
答案是 泛型转译 。
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
输出结果:
erasure class is:com.frank.test.Erasure
Class 的类型仍然是 Erasure 形式,而不是 Erasure<T>这种形式。
那么,泛型类中 T 的类型,在 JVM 中是什么类型呢?
Field[] fs = eclz.getDeclaredFields();
for ( Field f:fs) {
System.out.println("Field name "+f.getName()+" type:"+f.getType().getName());
}
输出结果:
Field name object type:java.lang.Object
是不是说,泛型类被类型擦除后,相应的类型就被替换成了 Object 类型呢?
这种说法,不完全正确。
我们更改一下代码。
public class Erasure <T extends String>{
// public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
}
输出结果:
Field name object type:java.lang.String
我们现在可以下结论了,在泛型类被类型擦除时,之前泛型类中的类型参数:
- 如果没有指定上限,如 <T> ,类型参数就会被转译成普通的 Object 类型;
- 如果指定了上限,如 <T extends String>,类型参数就被替换成类型上限。
所以,在反射中:
public class Erasure <T>{
T object;
public Erasure(T object) {
this.object = object;
}
public void add(T object){
}
}
add() 这个方法对应的 Method 的签名,应该是 Object.class。
Erasure<String> erasure = new Erasure<String>("hello");
Class eclz = erasure.getClass();
System.out.println("erasure class is:"+eclz.getName());
Method[] methods = eclz.getDeclaredMethods();
for ( Method m:methods ){
System.out.println(" method:"+m.toString());
}
输出结果:
method:public void com.frank.test.Erasure.add(java.lang.Object)
如果要在反射中找到 add 对应的 Method,我们应该调用 getDeclaredMethod(“add”,Object.class),否则程序会报错,提示没有这么一个方法,原因就是类型擦除时,T 被替换成 Object 类型了。
总结
Java 泛型的实现原理 是 类型擦除 ,理解 类型擦除 ,有利于我们绕过开发当中可能遇到的雷区,也能让我们绕过泛型本身的一些限制。
但是,类型擦除自身也有一些局限性,它会擦除掉很多继承相关的特性,引发了一些新的问题,具体我们在下一篇连载中详解。
以上,是关于 类型擦除 type erasure 的 介绍。实践出真知,利于消化,建议大家多动手练习。
我是大全哥,持续更新成体系的 Java 核心技术。
知识成体系 , 学习才高效 ,如果觉得有帮助,请顺手 点赞 支持下,谢谢。
我们下期见~
附泛型学习资料:
1 《泛型知识全景导图》
快速构建泛型知识体系,高清版本原图,几乎 囊括了所有泛型核心知识点 。
2 《大厂泛型面试真题26道》
精选大厂高频 泛型面试题 ,都是我最新整理的,备面、复习时都可以查看 。
— end —