Java防止范型擦除的方法

作者: 一只写代码的猫 | 来源:发表于2019-06-11 20:03 被阅读2次

    防止范型擦除的方法

    前言

    java的范型我最喜欢的东西, 他可以把代码变得更精简, 但是也可能会是代码中的一个小陷阱.

    如果让我去比喻一下范型在java是一个什么角色.

    如果java项目工程是一个小区(爪哇小区), 那各种class 可能就是小区的住户了. 范型就是这个小区的门卫大爷. 他可以拦住那些看起来就是不法分子的入侵者. 但是还是有办法偷偷进入小区, 或者像外卖小哥进入小区.

    作为小区的“物业”的我, 应该对范型的行为有所掌握, 至少出了问题, 我有方向可寻.

    范型举例

    // 这可能是我最常用的范型应用场景了
    List<String> list = new HashList<>(15);
    

    范型擦除场景

    故名思义, 范型被干掉了.
    范型擦出发生在什么阶段呢??

    背景设置

    这里有两个class

    class A {
        @Override
        public String toString() {
            return "A";
        }
    }
    
    class B {
        @Override
        public String toString() {
            return "B";
        }
    }
    class C {
        @Override
        public String toString() {
            return "C";
        }
    }
    class D {
        @Override
        public String toString() {
            return "D";
        }
    }
    

    以及2个范型使用的class

    public class Test<T> {
        T obj;
        Test(T obj) {
            this.obj = obj;
        }
        Test() {}
        public void setObj(Object obj) {
            // 注意该方法的区别
            this.obj = (T)obj;
        }
        @Override
        public String toString() {
            return obj.toString();
        }
    }
    public class Test2<T> {
        T obj;
        Test(T obj) {
            this.obj = obj;
        }
        Test2() {}
        
        public void setObj(T obj) {
            // 注意该方法的区别
            this.obj = obj;
        }
        @Override
        public String toString() {
            return obj.toString();
        }
    }
    

    以及一个测试输出方法

    public static void show(String mark, Object o) {
        System.out.println("[ " + mark + " ] " + o);
    }
    

    场景1-被忽略的范型

    注意方法

    B b = new B();
    A a = new A();
    
    Test<A> test1 = new Test<>();
    test1.setObj(b);
    show("1", test1);
    Test<A> test2 = new Test<>(a);
    test2.setObj(b);
    show("2", test2);
    

    结果

    [ 1 ] B
    [ 2 ] B
    

    编译通过了也没有出现了报错, Test<A> 范型我也写了, 但是B还是被set进去.
    与预想中 可能会报错的地方(T)obj;
    可以看出 这个过程中并未发生 B->A 的强制转换.
    (原因: 发现在编译后的文件被擦除, 该处(T)为 (Object), 验证在后面)

    场景2-常规操作

    B b = new B();
    A a = new A();
    
    Test2<A> test3 = new Test2<>();
    /// 编译不通过
    // test3.setObj(b);
    
    Test2<A> test4 = new Test2<>(a);
    /// 编译不通过
    // test4.setObj(b);
    

    以上两种情况均出现了编译不通过的情况. 这就是我前言所说看起来就是不法分子的入侵者
    那么有办法, 越过这道防线嘛??
    答案是可以的(场景3).

    场景3-反射操作

    B b = new B();
    A a = new A();
    Test2<A> test6 = new Test2<>(a);
    
    /// 这一步会发现 这样是找不到方法的
    /// Exception in thread "main" java.lang.NoSuchMethodException: cn.net.bale.demo.reflect.Test2.setObj(cn.net.bale.demo.reflect.ModifiedClass$A)
    // Test2.class.getMethod("setObj", A.class);
    
    Method method = Test2.class.getMethod("setObj", Object.class);
    method.setAccessible(true);
    method.invoke(test6, b);
    show("6", test6);
    

    输出结果

    [ 6 ] B
    

    这种方式就非常暴力的用反射把调用 setObj 方法把 b放了进去.
    但是我们可以看到, 反射的是getMethod并不是通过 getMethod("setObj", A.class)
    (进一步佐证, 范型在编译过后被擦除, 验证在后面)

    这种方式太暴力, 一般的场景用不到, 那么什么方式能“委婉”一些呢?
    见 场景4

    场景4-多次范型擦除

    引入工具类

    public class ObjectUtil {
        private ObjectUtil() {}
        /**
         * 类型转换
         * WARNING: 确保强制转换正确无误方可使用
         *
         * @param object
         * @param <T>
         * @return (T) object
         */
        @SuppressWarnings("unchecked")
        public static <T> T cast(Object object) {
            return (T) object;
        }
    }
    

    使用场景

    B b = new B();
    A a = new A();
    Test2<A> test5 = new Test2<>(a);
    test5.setObj(ObjectUtil.cast(b));
    show("5", test5);
    

    输出结果

    [ 5 ] B
    

    我们可以看到, 没有编译出错, 也没有使用反射. 但是就这样, 一路通行的进入了“爪哇小区”??
    这个方法在编译的过程中发生了 2次范型擦除.

    1. Test2方法 中的T都被擦除变为 Object
    2. cast()方法中发生了一次范型擦除 变为 return Object.

    b 首先进入 cast方法成为Object 伪装自己. 然后两次范型擦除 cast()与setObj()打通, 让b成功被set.

    这个过程中, 没有发生我们想象中的, B -> A 发生强制转换报出运行时异常.
    (如果没有发生范型擦除, 那么我们设想的“强转报错”的情况就会发生, 但是事实与设想截然相反)

    场景5-创建实例

    该场景下, 来验证一下, 范型信息会不会存储在object中.

    引入输出方法

    /**
         * 输出 obj.class
         *
         * @param mark 标记区分
         * @param test
         */
        public static void showObjClass(String mark, Test test) {
            try {
                System.out.println("[ " + mark + " ]Test.class: " + test.getClass());
                System.out.println("[ " + mark + " ]toString: " + test);
                System.out.println("[ " + mark + " ]Test.obj.class: " + test.obj.getClass());
            } catch (Exception e) {
                System.out.println("[ " + mark + " ]Test.obj.class: " + e.getMessage());
            } finally {
                System.out.println();
            }
        }
    

    场景应用

    Test test1 = new Test<>("bale");
    Test test2 = new Test<>(1);
    Test test3 = new Test();
    // 强制转换一下
    Test test4 = new Test((String) null);
    Test.showObjClass("1", test1);
    Test.showObjClass("2", test2);
    Test.showObjClass("3", test3);
    System.out.println("Test1.class == Test2.class: " + test1.getClass().equals(test2.getClass()));
    

    输出结果

    [ 1 ]Test.class: class cn.net.bale.demo.reflect.Test
    [ 1 ]toString: bale
    [ 1 ]Test.obj.class: class java.lang.String
    
    [ 2 ]Test.class: class cn.net.bale.demo.reflect.Test
    [ 2 ]toString: 1
    [ 2 ]Test.obj.class: class java.lang.Integer
    
    [ 3 ]Test.class: class cn.net.bale.demo.reflect.Test
    [ 3 ]Test.obj.class: null
    
    Test1.class == Test2.class: true
    

    从最后的结果看到, Test1.class == Test2.class: true.
    Test.class 无论范型是否相同, 但是class都是相同的.

    范型擦除验证

    通过以上场景, 可以看出, 范型在编译之后的class似乎就不起作用了.
    通过反编译来看一下class是否和我们的场景保持一致, 还是在什么地方发生了奇妙的反应, 让范型被擦除.

    引入命令

    // javap 反编译输出class
    javap -v ClassName.class
    

    class 结构参考如下

    // todo jvm文档在补ing
    

    验证1-范型类

    Test.class 反编译之后筛选出有用的信息

    #9 = Utf8               TT;
    #19 = Utf8               (TT;)V
    // ... 基本参数与常量池省略 ...
    
    {
      T obj;
        descriptor: Ljava/lang/Object;
        flags: (0x0000)
        Signature: #9                           // TT;
    
      cn.net.bale.demo.reflect.Test2(T);
        descriptor: (Ljava/lang/Object;)V
        flags: (0x0000)
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: aload_1
             6: putfield      #2                  // Field obj:Ljava/lang/Object;
             9: return
          // 省略临时变量区
                0      10     1   obj   TT;
        Signature: #19                          // (TT;)V
    
      // 省略 无参数构造器
    
      public void setObj(T);
        descriptor: (Ljava/lang/Object;)V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=2, locals=2, args_size=2
             0: aload_0
             1: aload_1
             2: putfield      #2                  // Field obj:Ljava/lang/Object;
             5: return
          LineNumberTable:
            line 19: 0
            line 20: 5
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       6     0  this   Lcn/net/bale/demo/reflect/Test2;
                0       6     1   obj   Ljava/lang/Object;
          LocalVariableTypeTable:
            Start  Length  Slot  Name   Signature
                0       6     0  this   Lcn/net/bale/demo/reflect/Test2<TT;>;
                0       6     1   obj   TT;
        Signature: #19                          // (TT;)V
    
      // 省略 toString方法
    }
    Signature: #24                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
    SourceFile: "Test2.java"
    

    我们通过反编译结果可以看到 所有范型出现的地方 都是 Ljava/lang/Object;

    T obj;
        descriptor: Ljava/lang/Object; // Object属性
        flags: (0x0000)
        Signature: #9 // TT;
        
    cn.net.bale.demo.reflect.Test2(T);  
        descriptor: (Ljava/lang/Object;)V // Object参数无返回值的构造器方法
        Signature: #19 // (TT;)V
        
    public void setObj(T);  
        descriptor: (Ljava/lang/Object;)V // Object参数无返回值的setObject方法
        Signature: #19 // (TT;)V
    

    可以看出, 编译后的结果, 所有范型的位置都被Object擦除

    验证2-调用范型

    引入class用于反编译观察Test<T>的调用情况

    public class NewClass {
        public static void main(String[] args) {
            Test test1 = new Test<>("bale");
            Test test2 = new Test<>(1);
            Test test3 = new Test();
        }
    }
    

    反编并省略无关紧要的信息

    // 省略基础信息以及常量池
    {
      // 省略构造器
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=3, locals=4, args_size=1
             0: new           #2                  // class cn/net/bale/demo/reflect/Test
             3: dup
             4: ldc           #3                  // String bale
             6: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V
             9: astore_1
            10: new           #2                  // class cn/net/bale/demo/reflect/Test
            13: dup
            14: iconst_1
            15: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
            18: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V
            21: astore_2
            22: new           #2                  // class cn/net/bale/demo/reflect/Test
            25: dup
            26: invokespecial #6                  // Method cn/net/bale/demo/reflect/Test."<init>":()V
            29: astore_3
            30: return
          // ...省略...
    }
    SourceFile: "NewClass.java"
    

    重点关注内容如下

    
    // 调用1, 直接调用 Test构造器, 参数为Object
    6: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V
    
    // 调用 valueOf 将数字包装成包装类
    15: invokestatic  #5                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    // 调用2, 直接调用 Test构造器, 参数为Object
    18: invokespecial #4                  // Method cn/net/bale/demo/reflect/Test."<init>":(Ljava/lang/Object;)V
    

    可以看到调用方法使用范型的部分也都被擦除为Object
    (感兴趣的同学可以观察 场景4cast()方法的范型擦除情况, 不过也都大同小异)

    范型防止擦除的应用

    通过以上的场景观察, 以及反编译验证可以了解到范型在编译之后会被擦除, 那如和防止这种情况的发生呢?
    换一种说法, 范型我们如何去使用它, 就目前来看, 范型仅仅做到类型限制的作用, 如何让范型变得很灵活呢??
    (要不我凭什么喜欢范型)
    如果只做到这一步, 岂不是直接用Object也没什么问题, 只要开发的时候注释写好一些不就可以了??
    不是这样的的, 见 应用1

    应用1-extends指定擦除类型

    使用 extend 关键字来指定擦除类

    在不使用extends的时候, 默认使用 Object 作为擦除类(毕竟 超类是万能的)

    引入class

    class C {
    }
    public class Test3<T extends C> {
        T obj;
    }
    

    反编译结果

    // 省略基础信息和常量池
    {
      T obj;
        descriptor: Lcn/net/bale/demo/reflect/C;
        flags: (0x0000)
        Signature: #7                           // TT;
    
      // 省略构造器
    }
    Signature: #17                          // <T:Lcn/net/bale/demo/reflect/C;>Ljava/lang/Object;
    SourceFile: "Test3.java"
    

    结果很明显, extends 关键字之后的范型擦除的位置不在是 Ljava/lang/Object; 而变为指定的 C

    应用2-extend保存范型

    !!! 该处extends 不同于 应用1
    此处为class的 extend继承

    此处应用场景最为广泛且实用

    应用如下

    public class ExtendTest extends Test<C>{
    }
    

    此处通过 extends Test<C> 将范型的信息以 标签的形式存储在 ExtendTest中

    那么如何使用它?

    在普通的情况下 Who extends Test<C> 那么在Who 中一定就知道C是谁. 如何灵活使用这个关系呢?

    引入帮助接口

    package cn.net.bale.demo.reflect;
    
    import cn.net.bale.demo.util.ObjectUtil;
    
    import java.lang.reflect.ParameterizedType;
    
    /**
     * TClass, 用于辅助范型获取 class
     *
     * @author bale 2019-06-11
     */
    public interface Clazz<T> {
        /**
         * 获取 ParameterizedType 辅助获取 T class
         *
         * @return ParameterizedType
         */
        default ParameterizedType getParameterizedType() {
            return ObjectUtil.cast(getClass().getGenericSuperclass());
        }
        /**
         * 获取 T class
         *
         * @return Class<T>
         */
        default Class<T> getTClass() {
            // [0]为<T> 复杂情况, 按需求改写
           return ObjectUtil.cast(getParameterizedType().getActualTypeArguments()[0]);
        }
    }
    

    假设一个场景, 对mongoTemplate 二次封装, 每一个mongoCollection都对应一个 class entity
    这样我们可以通过 class entity的属性名,生成通用的查询方法

    代码如下
    引入辅助注解

    /**
     * Mongo json集(表)
     *
     * @author wentao.liu01@hand-china.com 2019-05-22
     */
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MongoCollection {
        String name() default "";
    }
    
    public abstract class BaseMongoRepositoryImpl<T> implements Clazz<T> {
        // 这里可以根据情况改写 spring 中可以 @Autowired
        private final MongoTemplate mongoTemplate;
        /**
         * 获取 mongo 集合 name
         *
         * @return String
         */
        protected String getCollectionName() {
            Class<T> tClass = getTClass();
            MongoCollection mongoCollection = tClass.getAnnotation(MongoCollection.class);
            if (Objects.nonNull(mongoCollection)) {
                return mongoCollection.name();
            }
            return null;
        }
         /**
         * mongo find 
         * (封装一个最简单的find方法, 更多的黑科技可以在这里使用)
         * @param query
         * @return
         */
        public List<T> find(final Query query) {
            List<T> list = this.mongoTemplate.find(query, getTClass(), getCollectionName());
            setMulFieldFunction(list);
            return list;
        }
    }
    对应接口 
    public interface BaseMongoRepositoryI<T> {
        List<T> find(final Query query);
    }
    

    使用情况:

    entity:

    @MongoCollection(name = "集合名称")
    public class ClassName {
        // ...
    }
    

    impl:

    public class Impl extends BaseMongoRepositoryImpl<ClassName> implements BaseMongoRepository {
    }
    

    使用:

    Impl impl = 想办法获取到 Impl
    Query query= new Query();
    impl.find(Query)
    

    这里已经实现最简单的版本, 让mongoTempalte 自带collectionName, 而不用每次查询都手动指定collectionName

    还有更多的功能, 比如多语言, 根据属性自动生成查询方法等通用的功能, 来方便接下来的开发.

    总结

    范型是一个好东西, 了解范型的使用与擦除, 那么在编写通用的代码的时候是非常有意义的.
    并且可以极大的缩短代码的行数, 让代码更有效.

    相关文章

      网友评论

        本文标题:Java防止范型擦除的方法

        本文链接:https://www.haomeiwen.com/subject/ybycfctx.html