Java泛型:类型擦除 type erasure

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{
    private T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
}
GenericHolder holder = new GenericHolder();
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  {
  public int compareTo( A 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; }
}
-----------------
class Collections { 
  public static >A max(Collection  xs) {
    Iterator  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  numberList = new LinkedList  ();
    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 被替换为最左边界 Object 。Comparable 的类型参数 NumericValue 被擦除掉,这直接导致了 NumericValue 没有实现接口 Comparable 的 compareTo(Object that) 方法。于是,编译器充当好人,添加了一个桥接方法。

第二段代码示例中,限定了类型参数的边界 >A , A 必须为Comparable 的子类。按照类型擦除的过程,先将所有的类型参数替换为最左边界Comparable,然后去掉参数类型 A ,得到最终擦除后的结果。


6 类型擦除的使用示例

这是一道经典的测试题:

List l1 = new ArrayList();
List l2 = new ArrayList();
        
System.out.println(l1.getClass() == l2.getClass());

输出结果是 true ,是因为 List和 List,在 JVM 中的 Class 都是 List.class ,泛型信息被擦除了。

可能有同学会问,类型 String 和 Integer 怎么办?

答案是泛型转译

public class Erasure {
    T object;
    public Erasure(T object) {
        this.object = object;
    }
}

输出结果:

erasure class is:com.frank.test.Erasure

Class 的类型仍然是 Erasure 形式,而不是 Erasure这种形式。

那么,泛型类中 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 {
//	public class Erasure {
    T object;

    public Erasure(T object) {
        this.object = object;
    }
}

输出结果:

Field name object type:java.lang.String

我们现在可以下结论了,在泛型类被类型擦除时,之前泛型类中的类型参数:

所以,在反射中:

public class Erasure {
    T object;
    public Erasure(T object) {
        this.object = object;
    }
    
    public void add(T object){
    }
}

add() 这个方法对应的 Method 的签名,应该是 Object.class。

Erasure erasure = new Erasure("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道》

精选大厂高频泛型面试题,都是我最新整理的,备面、复习时都可以查看

大厂泛型面试题26道

--- end ---

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章