Java泛型-4(类型擦除后如何获取泛型参数)

泛型学习目录:

Java泛型-1(泛型的定义)
Java泛型-2(通配符)
Java泛型-3(实践篇-protostuff序列化与反序列化)
Java泛型-4(类型擦除后如何获取泛型参数)

编译器会进行泛型擦除。
(1)实际上擦除的只是 参数和自变量 的类型,但会将泛型信息保存到 Signature 中,我们可以通过 匿名类 获取。
(2)类结构相关的信息(属性,类,接口,方法签名)即 元数据 会保存下来,可以通过反射直接获取到的。

1. 泛型和类型擦除

泛型的本质是 参数化类型 Parameterized Type )的应用,也就是说把所操作的数据类型指定为一个参数。这个参数类型可以用在 类、接口、方法 的创建中,分别称为 泛型类、泛型接口、泛型方法

在Java语言还没引进泛型的时候。只能通过 Object 是所有类型的父类 类型强制转换 两个特点的配合来实现类型泛化。由于 Java 语言里面所有类型都继承于 java.lang.Object ,所以 Object 转型成任何对象都是有可能的。但是正是因为有着无限的可能性,所以就只有 程序员 运行期的虚拟机 才知道这个 Object 到底是什么类型的。在编译期间,编译器无法检查这个 Object 强转是否成功,如果仅仅是依赖程序员保障这项操作的正确性,那么许多 ClassCastException 的风险就会出现在运行期。

Java的泛型,只是在程序源码中存在, 在编译后的字节码文件中,就已经替换成原来的原生类型( Raw Type 了,并且在相应的地方插入了强制类型转换的代码。因此对于运行期的Java语言来说, ArrayList<Integer>
ArrayList<String> 就是同一个类,所以泛型技术实际上是java语言的一颗 语法糖 。Java语言中的泛型实现方法称为 类型擦除 ,基于这种方法实现的泛型称为 伪泛型

1.2 源码分析泛型擦除

由于Java泛型的引入,各种场景(虚拟机解析,反射等)下的方法调用都可能对原有的基础产生新的需求,如在泛型类中如何获取传入的参数化类型,因此,引入了诸如 Signature LocalVariableTypeTable 等新的属性用于解决伴随泛型而来的参数识别问题。 Signature 是其中最重要的一项属性,他的作用就是存储一个 方法字节码 层次的特征签名,这个属性保存的参数类型并不是原生类型,而是包括了参数化( Parameterized Type )类型的信息。

另外。从 Signature 属性中,我们也可以得出结论,擦除法所谓的擦除:

  • 方法中 Code 属性中字节码进行擦除,泛型信息保存在 Signature 中。
  • 元数据( 类、属性、方法签名 )还是保存了泛型信息。
  • 1.2.1 方法中Code属性

    什么叫做Code和Signature
    泛型方法 method(List<String> list) 和非泛型方法 method(List list) 对比可以看到:

    code (编译后的方法内部代码)属性完全一样。
  • 泛型方法比非泛型方法多了一个 Signature 的属性。
  •     public static void main(String[] args) {
            Map<String, String> map = new HashMap<>();
            map.put("hello", "world");
            map.put("你好", "世界");
            System.out.println(map.get("hello"));
    

    我们使用反编译工具对源码的Class文件反编译之后,可以看到,泛型都变成了原生类型【即方法内部参数和方法实参被擦除!】

    class文件反编译(Class文件):

      public static void main(String[] args)
        Map map = new HashMap();
        map.put("hello", "world");
        map.put("你好", "世界");
        System.out.println((String)map.get("hello"));
    

    1.2.2 元数据

    元数据(类,属性,方法签名),即类的结构化数据。

    public class Test<T> {
        private T data;
        private Set<String> set = new HashSet<>();
        public <T> boolean isBoolean(Test<T> data) {
            Map<String, String> map = new HashMap<>();
            map.put("hello", "world");
            map.put("你好", "世界");
            System.out.println(map.get("hello"));
            return true;
        //查看反编译文件
        public static void main(String[] args) {
            Test<Integer> test=new Test<>();
    

    源码反编译(Class文件):

    public class Test<T>
      private T data;
      private Set<String> set;
      public Test()
        this.set = new HashSet(); }
      public <T> boolean isBoolean(Test<T> data) {
        Map map = new HashMap();
        map.put("hello", "world");
        map.put("你好", "世界");
        System.out.println((String)map.get("hello"));
        return true;
      public static void main(String[] args)
        Test test = new Test();
    

    类及其字段和方法的类型参数相关的元数据都会被保留下来,可以通过反射获取到。
    这是通过反射取得参数化类型的根本依据。

    2. 如何获取泛型类型

    2.1 获取元数据的泛型参数(反射)

    因为我们知道,泛型擦除的时候,不会将元数据结构(类,属性,方法(结构)返回值及形参)泛型擦除,故可直接通过反射获取泛型类型。

  • 获取属性上的泛型类型:
    field.getGenericType();
  • 获取方法结构——形参的泛型类型:
    method.getGenericParameterTypes()[0];
  • 获取方法结构——返回值的泛型类型:
    method.getGenericReturnType();
  • 我们可以通过元数据获取到泛型类型,源码分析:

    public class Test<T> {
        private T data;
        private Set<String> set = new HashSet<>();
        public <T> Test<T> isBoolean(List<Boolean> data) {
            Map<String, String> map = new HashMap<>();
            map.put("hello", "world");
            map.put("你好", "世界");
            System.out.println(map.get("hello"));
            return new Test<>();
        //查看反编译文件
        public static void main(String[] args) throws NoSuchMethodException {
            //获取Test.class类的class对象
            Class<?> testClass = Test.class;
            //获取类的属性字段
            Field[] declaredField = testClass.getDeclaredFields();
            //暴力解除,可以访问私有变量
            Field.setAccessible(declaredField, true);
            System.out.println("属性名:参数类型:参数泛型类型");
            for (Field field : declaredField) {
                String name = field.getName();
                Class<?> type = field.getType();
                Type genericType = field.getGenericType();
                System.out.println(name + ":" + type + ":" + genericType);
            System.out.println("方法形参的泛型类型");
            Method method = testClass.getMethod("isBoolean", new Class[]{List.class});
            ParameterizedType parameterType = (ParameterizedType) method.getGenericParameterTypes()[0];
            System.out.println(parameterType.getActualTypeArguments()[0]); //获取第一个
            System.out.println("方法返回值的泛型类型");
            ParameterizedType returnType = (ParameterizedType) method.getGenericReturnType();
            System.out.println(returnType.getActualTypeArguments()[0]);
    

    返回结果:

    2.2 获取实参的泛型参数(内部类)

    Java在编译的时候,会对方法实参以及方法内部进行泛型擦除(即用泛型实参上限代替真实的泛型类型)。但是泛型信息会保持在Signature中。故反射 不能获取到泛型对象。

    如下源码,我们获取不到data数据的泛型类型

     public void testGenericType(List<T> data) {
            //如何获取data传入的是泛型类型
            Class<?> aClass = data.getClass();
            //Class实现了Type接口
            Type aType = aClass;
            //判断aType是否有泛型(返回false)
            System.out.println(aType instanceof ParameterizedType);
    
  • 获取传入参数的泛型对象:
    Type type = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
  • 那么实际上传入的就是一个子类对象。
    使用匿名内部类创建该子类对象。
  • 修改使用匿名类获取:

    泛型类型只会在类、字段以及方法形参内保存其签名(Signature),在方法实参不作任何保留而统统擦除。

    我们可以通过匿名类,以子类的方式把主类的Signature保存下来,从而获取到实参的泛型类型。

    3. 源码中的使用

    GoogleGson,阿里的FastJson中,使用了比较多捕获泛型实参的方法,基本都是通过创建一个匿名类来获取的。

    温故知新-内部类

  • 匿名类必须继承一个父类或者实现一个接口,其实创建的是一个子类类型。

  • 可以使用protected构造方法,强制使用子类。

  • FastJsoncom.alibaba.fastjson.TypeReference<T>的源码:

     protected TypeReference() {
            Type superClass = this.getClass().getGenericSuperclass();
            Type type = ((ParameterizedType)superClass).getActualTypeArguments()[0];
            Type cachedType = (Type)classTypeCache.get(type);
            if (cachedType == null) {
                classTypeCache.putIfAbsent(type, type);
                cachedType = (Type)classTypeCache.get(type);