← 返回文章列表

类型擦除

一 泛型

在JAVA中的泛型,在编译的时候,所有的泛型信息都会被抹去。这个过程成为 类型擦除。这样做的原因是为了兼容老版本。

JAVA中泛型的引入主要是为了解决两个方面的问题:

  • 减少类型转换
  • 解决的时重复代码的编写,能够复用算法,泛化。

以类Dad为例,有一成员是泛型,并且有对应的get,set方法。

public class Dad<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

而在反编译后,代码如下,可以看到类型T全部被Object替换。如果定义的类泛型指定为 <T extend Comparable>,则编译后所有的T替换为Comparable

public class Dad{
    private Object value;

    public Object getValue()
    {
        return value;
    }

    public void setValue(Object value)
    {
        this.value = value;
    }
}

二 类型擦除产生的特性

1 编译检查根据引用确定:

既然编译后类型变为Object,因此赋值的时候传递不同类型的值是否可以呢?答案是不可以,因为编译器在编译前会先检查代码中泛型的类型,不符合条件编译不通过。

//正确方式
List<String> list  = new ArrayList<String>();
list.add("aaa");
list.add(1);//编译报错

而如下这种方式虽然可以通过编译但是会有编译警告,但我们这么用就没意义了,因为存储的类型实际上是Object,如果要强行转换为String是会出现 类型转换异常的。

List list = new ArrayList<String>();
list.add("aaa"); //编译警告,未检查的参数
list.add(1);

for (Object o : list) {
    System.out.println((String) o); //到第二个参数的时候抛出 类型转换异常
}

从上面的例子可以看出,编译器在检查泛型是否合格的时候确实是根据 引用检查的

2 不允许引用传递时改变类型

既然前面我们知道了编译器检查泛型是否合格是根据 引用来决定是否通过的,那么我们引用传递下换成不同的类型是否是可以的呢? 答案是不可以的,泛型的出现就是为了尽量减少类型转换,这样写代码也就失去了意义。

List<Object> list1 = new ArrayList<>();
List<String> list2 = list1; //编译报错

3 集合会自动类型转换

因为在编译的时候都变为了Object,而我们获取后为何还是我们制定的类型呢?以ArrayList为例,在获取元素前已经做了类型转换,我们不再需要进行类型转换了。

    E elementData(int index) {
        return (E) elementData[index]; //转换为指定的类型
    }

4 桥方法解决与多态的冲突

有一个泛型父类如下:

public class Dad<T> {
    private T value;

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}

有子类继承它,并且指定泛型类型,我们会发现这样写 @Override注解是通过的,也就是满足重写。但是父类编译后是Object,而我们子类的两个方法是String类型,因为方法重写的规则是 方法的参数是必须是相同类型的,因此这里的set方法其实并不满足重写规则的,

public class Sub extends Dad<String>{
    public Sub() {
    }

    @Override
    public String getValue() {
        return "hello world";
    }

    @Override
    public void setValue(String value) {
        System.out.println("设置value");
    }
}

将子类反编译后会发现,多出来两个对应的方法。可以看到这两个方法调用了我们重写的方法,实际上这两个方法才满足 @Override,这就是 桥方法,编译后通过这个方法解决了泛型类重写的问题。

public class Sub extends Dad {
    public String getValue() //重写的get方法
    {
        return "hello world";
    }

    public volatile Object getValue() //生成的桥方法
    {
        return getValue(); //调用重写的方法
    }

    public void setValue(String value) //重写的set方法
    {
        System.out.println("设置value");
    }

    public volatile void setValue(Object obj) //生产的桥方法
    {
        setValue((String)obj); //调用重写的set方法
    }
}

5 泛型类型变量不能是基本数据类型

因为类型擦除后所有的泛型关键字都是要替换成Object或者其子类,反正一定要是引用类型。如果要使用基本类型存储数据可以使用相应的包装类。

6 集合的instanceof编译不通过

因为编译后类型被擦除,无论是 List<String>还是 List<Integer>都变成了 List,因此下面的语句在编译时候是不通过的:

ArrayList<String> arrayList = new ArrayList<String>();
if( arrayList instanceof ArrayList<String>) //编译不通过

7 静态成员或者方法无法声明为泛型

因为泛型类型是创建对象的时候才确定是什么类型的,而静态属性或者方法不需要使用对象调用,无法确定泛型是什么类型的。

而下面这种情况例外:因为方法show的返回值类型是由方法参数决定的,返回值类型就是参数类型。

public class Demo<T> {  

    public static <T> T show(T t){ //编译正确 
        return null;  
    }  
}

三 如何保存泛型信息

既然泛型信息擦除了,那么反射应该是获取不到类型信息的吧,但是还是能获取到

有一个泛型父类,在构造方法中通过反射获取类型信息

public class EntityHandler<T> {
    public EntityHandler() {
        //获取class对象
        Class<?> clazz = this.getClass();
        //获取类型
        Type genericSuperclass = clazz.getGenericSuperclass();
        System.out.println(genericSuperclass); //EntityHandler<User>

        //获取泛型信息
        ParameterizedType t = (ParameterizedType)clazz.getGenericSuperclass();
        Type[] ts = t.getActualTypeArguments();
        for (Type type : ts) {
            System.out.println(type); //class User
        }
    }
}

子类赋值为了User类型

public class UserHandler extends EntityHandler<User>{
    public static void main(String[] args) {
        new UserHandler();
    }
}

控制台打印:

test.base.generic.EntityHandler<test.base.generic.User>
class test.base.generic.User

答案就是在编译为字节码文件的时候,泛型信息通过 Signature保存了下来

{
  public test.base.generic.UserHandler();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method EntityHandler."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltest/base/generic/UserHandler;
}
Signature: #12                          // /EntityHandler<Ltest/User;
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();

System.out.println(l1.getClass() == l2.getClass());