纵横研究院后端基础技术专题社区java学习笔记

Java防止范型擦除的方法

2019-06-11  本文已影响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

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

总结

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

上一篇下一篇

猜你喜欢

热点阅读