泛型擦除的那些事

900 阅读1分钟

泛型擦除的那些事

泛型简介

泛型大家一定再熟悉不过了,java 中的集合类:ArrayList,HashMap ...

或者是,为了避免重复的模板代码,自定义模板方法:

public  class ScheduleTemplate<T> {

    public T doSchedule(T name){
        //some code
        return name;
    }
}

在使用的时候,直接在尖括号内< >传入类型参数,就可以避免很多烦人的强制类型转换。

ScheduleTemplate<String> scheduleTemplate = new ScheduleTemplate<>();

String name = scheduleTemplate.doSchedule("aaa");

本文的重点在于泛型擦除,泛型的使用不再过多介绍。

泛型擦除

证明存在泛型擦除

还是使用上面的 ScheduleTemplate 类,这次我们使用 javap 看一下。

 9: ldc           #4                  // String aaa
11: invokevirtual #5                  // Method git/frank/generics/ScheduleTemplate.doSchedule:(Ljava/lang/Object;)Ljava/lang/Object;
14: checkcast     #6                  // class java/lang/String
17: astore_2

看的出来,在从源文件解释为class文件时,已经把泛型信息去掉了,取而代之的是强制类型转换 checkcast

再或者,我们可以通过运行时来看:

ScheduleTemplate<String> strTemplate = new ScheduleTemplate<>();
ScheduleTemplate<Integer> intTemplate = new ScheduleTemplate<>();

System.out.println(strTemplate.getClass() == intTemplate.getClass());

java 为什么要泛型擦除

我们先来看一下 java 中对象的布局:

image.png

如果没有泛型擦除的话

如果不做泛型擦除的话,使用上面的例子,应该是下面这个样子:

image.png

回想一下,你有多依赖 java 提供的集合类型。

要不是有泛型擦除机制,方法区中将会充斥着各种集合对应的类型数据,例如:ArrayList<String>,ArrayList<Integer>,ArrayList<XXX>,HashMap<String,XXX>,这样的话,方法区会被大量的几乎无用的类型信息填满。

如何"避免"泛型擦除

定义擦除上限

还是开头的例子,这次我这样定义ScheduleTemplate

public  class ScheduleTemplate<T extends Number> {

    public T doSchedule(T name){
        //some code
        return name;
    }
}

泛型定义中限制了必须是Number类型,所以我们的测试代码修改成这样:

ScheduleTemplate<Long> longTemplate = new ScheduleTemplate<>();

Long  a = longTemplate.doSchedule(1L);

使用 javap 再看一下,这次泛型只擦除到了 Number ,而没有一路到底 Object

10: invokestatic  #4                  // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
13: invokevirtual #5                  // Method git/frank/generics/ScheduleTemplate.doSchedule:(Ljava/lang/Number;)Ljava/lang/Number;
16: checkcast     #6                  // class java/lang/Long

当然在运行时也是一样可以获取到 Number 类型的。

image.png

使用继承避免“擦除”

上面我们说过,泛型擦除的出现是为了防止出现过多的类信息而设计的。

如果我们想避开泛型擦除的话,让虚拟机额外给我创建一个类信息就可以了。最简单的办法,就是使用继承。

还是使用上面的例子,这次我继承他创建了类 StrScheduleTemplateImpl

public class StrScheduleTemplateImpl extends ScheduleTemplate<String> {

    @Override
    public String doSchedule(String name) {
        return super.doSchedule(name);
    }
}

现在,我们再使用 javap 看一下。

 9: ldc           #4                  // String aaa
11: invokevirtual #5                  // Method git/frank/generics/impl/StrScheduleTemplateImpl.doSchedule:(Ljava/lang/String;)Ljava/lang/String;
14: astore_2

同样的,在运行时也可以获取到类型信息。

在实际使用中,大多使用匿名内部类的方式:

ScheduleTemplate scheduleTemplate = new ScheduleTemplate<String>(){};

image.png

这种使用内部类的泛型传递方式在框架中应用非常广泛,大家最熟悉的应该就是 fastJson 中的 TypeReference吧~

总结

在 java 中,在源代码编译为 class 文件后,泛型信息就已经被抹除掉了。

虽然泛型擦除机制被很多人诟病,但泛型擦除可以大大减少在运行时 JVM 被迫保存过多的类型信息。

如果,你真的需要在运行时获取到泛型,可以通过匿名内部类的方式构造对象,使其可以将泛型保留至运行时。