【Java】 泛型擦除

2024-03-20 5310阅读

文章目录

  • 1. 泛型擦除的介绍
    • 1.1 泛型擦除的原因
    • 1.2 泛型擦除规则
    • 1.3 泛型擦除规则的验证
    • 2. 使用反射获取被擦除泛型信息的技巧
    • 3. 泛型擦除导致的两大经典问题的解决方案
      • 3.1 在泛型类中获取实际泛型类型
      • 3.2 在泛型接口进行接口回调后获取实际泛型类型

        1. 泛型擦除的介绍

        1.1 泛型擦除的原因

        1. 原因一:JDK1.5及1.5之前都是没有泛型的概念的,JDK1.5之后引入了泛型的概念并为了与之前的JDK版本兼容,所以引入了泛型擦除的概念。
        2. 原因二:若对每个泛型类型都生成不同的目标代码,现有10个不同泛型的List,就要生成10份字节码,这样会造成不仅造成代码膨胀,而且一份字节码对应一个Class对象,占据大量的内存。

        1.2 泛型擦除规则

        1. 情况一:首先将 所有声明泛型的地方 都擦除,然后若 定义该泛型的地方 没有指定泛型上界,则 所有该泛型类型的变量的数据类型 在编译之后都替换为Object

          【Java】 泛型擦除 第1张

        2. 情况二:首先将 所有声明泛型的地方 都擦除,然后若 定义该泛型的地方 指定了泛型上界,则 所有该泛型类型的变量的数据类型 在编译之后都替换为泛型上界

          【Java】 泛型擦除 第2张

        • 例题1:

          【Java】 泛型擦除 第3张

        • 例题2:

          【Java】 泛型擦除 第4张

          1.3 泛型擦除规则的验证

          1. 方式一:通过Class对象验证:想通过Class对象获取泛型信息,但是仅仅获取的泛型信息是占位符,并不是实际的泛型类型
            List list = new ArrayList();  
            Map map = new HashMap();  
            System.out.println(Arrays.toString(list.getClass().getTypeParameters())); // 输出:[E] 
            System.out.println(Arrays.toString(map.getClass().getTypeParameters()));  // 输出:[K, V]
            
          2. 方式二:通过反射机制验证:我们知道泛型只是用来对变量类型进行约束,这个约束只在编译阶段有效,在编译之后泛型就被擦除了。比如:

            【Java】 泛型擦除 第5张

            因此,如果可以绕过编译阶段对泛型的约束检测,那么就可以传入任何类型的变量(因为都可以向上转型为Object类型):

            ArrayList list = new ArrayList();
            list.add("张三");
            list.add("李四");
            // 通过反射绕过编译
            Class clazz = list.getClass();
            Method method = clazz.getMethod("add", Object.class); // 这里必须是Object,因为泛型擦除规则:ArrayList中E没有泛型上界,所以泛型擦除后占位符E用Object代替
            method.invoke(list, 21);
            // 打印该集合
            System.out.println(list);
            System.out.println(list.get(2));
            
            运行结果为:

            【Java】 泛型擦除 第6张

          2. 使用反射获取被擦除泛型信息的技巧

          • 问:进行泛型擦除后的程序就能够在JDK1.5上正确执行了,那么还有必要保存原本的泛型信息吗?

          • 答:会保存。所有类会先将自己类中涉及的所有实际泛型备份放在自己类中,然后自己类再将进行泛型擦除 【超级重要!!!!!!!!!!!!!!!!!!!!!!!!!】

          • 原理及获取方式:使用了泛型的类编译为class文件时会生成一个signature字段,而原本的泛型信息就被保存在class文件中signature指向的常量池中。

            【Java】 泛型擦除 第7张

            但是signature是一个private修饰的属性,不能直接访问,只能通过反射访问。因为class文件中会生成一些public修饰的方法能将访问signature属性并获取相关信息。所以,在实际中一般采用这些方法来获取泛型,而不是直接使用signature字段。具体方法为:

            【Java】 泛型擦除 第8张

          • 例子:

            class A {  
            }  
              
            class B extends A {  
            }  
              
            public class Generic {  
                public static void main(String[] args) {  
                    ParameterizedType parameterizedType = (ParameterizedType) B.class.getGenericSuperclass();  
                    
            		Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();  
            		for(Type actualTypeArgument: actualTypeArguments) {  
            		    System.out.println(actualTypeArgument);  
            		}  
                }  
            }  
            // 输出:
            class java.lang.String
            class java.lang.Integer
            

            3. 泛型擦除导致的两大经典问题的解决方案

            泛型有3类:泛型类、泛型接口、泛型方法。但无论是哪种都会造成泛型擦除,而这也造成了问题:

            1. 问题1:由于泛型擦除,导致无法在 泛型类 中获取实际泛型类型

              解决方法:使用匿名内部类

            2. 问题2:由于泛型擦除,导致无法在 泛型接口 进行 接口回调 之后获取实际泛型类型

              解决方法:使用匿名内部类

            3. 泛型方法的泛型擦除导致的问题,目前还没有遇到,遇到了再补充!!!

            3.1 在泛型类中获取实际泛型类型

            根据泛型擦除规则知道类在编译之后所有泛型都会被Object或者泛型上界代替,因此导致 泛型类 中无法获取原本的泛型信息。

            那么问题来了,我就是要获取原本的泛型信息该怎么办????

            • 比如:要实现以下需求

              【Java】 泛型擦除 第9张

            • 有两种方式实现该需求,这两种方式都是借助反射实现该需求的:
              1. 方式一:将MyTest的class对象当做参数传入即可。但是这样需要修改Stream类的代码,也就是说要修改源码

                ① 分析

                【Java】 泛型擦除 第10张

                ② 解决方法:

                【Java】 泛型擦除 第11张

              2. 方式二:使用匿名内部类。这样不用修改Stream类的代码,也就是说不需要修改源码,只要在使用该类的地方做修改即可
                1. 分析:找一个类来继承泛型类,那么这个类就能保留泛型类的实际泛型类型:

                  【Java】 泛型擦除 第12张

                  此时,以上代码从逻辑上就等价于以下代码:

                  【Java】 泛型擦除 第13张

                  如果还不能理解,请运行以下代码并思考:

                  class A {  
                  }  
                    
                  class B extends A {  
                  }  
                    
                  public class Generic {  
                      public static void main(String[] args) {  
                          ParameterizedType parameterizedType = (ParameterizedType) B.class.getGenericSuperclass();  
                          
                  		Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();  
                  		for(Type actualTypeArgument: actualTypeArguments) {  
                  		    System.out.println(actualTypeArgument);  
                  		}  
                      }  
                  }  
                  // 输出:
                  class java.lang.String
                  class java.lang.Integer
                  
                2. 最终解决方法:

                  class Stream {
                      public Stream(List list) {
                          ParameterizedType genericSuperclass = (ParameterizedType) this.getClass().getGenericSuperclass();
                          for (Type actualTypeArgument : genericSuperclass.getActualTypeArguments()) {
                              System.out.println(actualTypeArgument);
                          }
                      }
                  }
                  public class MyTest {
                      public static void main(String[] args) throws Exception {
                          ArrayList list = new ArrayList();
                          Stream stream = new Stream(list) {
                          };
                      }
                  }
                  

              3.2 在泛型接口进行接口回调后获取实际泛型类型

              问题:对于函数式接口,如果使用匿名内部类创建该接口对象的话,一般都会使用lambda表达式来代替匿名内部类。但是,在某些情况下匿名内部类不会报错,而使用lambda表达式会报错。

              • 比如:要实现以下需求

                【Java】 泛型擦除 第14张

                • 最重要的一个点就是Lambda表达式是将对应的接口改造为对应的类,并没有构造新的类来实现泛型接口。所以泛型接口的实际泛型类型无法保存。也就是说等价于以下代码:

                  【Java】 泛型擦除 第15张

                • 有两种方式实现该需求,这两种方式都是借助反射实现该需求的:
                  1. 方式一:将MyTest的class对象当做参数传入即可。但是这样需要修改FlatMapFunc类的代码,也就是说要修改源码

                    【Java】 泛型擦除 第16张

                  2. 方式二:使用匿名内部类。这样不用修改Stream类的代码,也就是说不需要修改源码,只要在使用该类的地方做修改即可

                    【Java】 泛型擦除 第17张


    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]