java泛型擦除

Java泛型在编译后会被擦除,但其信息可通过Signature保存。反射可以绕过编译时类型检查添加非泛型元素,但存在类型转换风险。泛型信息可以通过匿名内部类如Gson的TypeToken来保存实参泛型。
摘要由CSDN通过智能技术生成

泛型擦除

  • Java泛型擦除后,能够添加不是泛型类型的元素吗?------答案是可以的,通过反射
  • 泛型擦除后是如何获取泛型信息?-----匿名内部类

1 泛型擦除后能否添加非泛型类型的元素?

下面看一段代码:

public static void main(String[] args) throws Exception {
		Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);//true

        ArrayList<String> strings = new ArrayList<>();
        strings.add("aa");
//        strings.add(1);//此处会报错,因为泛型检查此处需要String类型
    }

打印输出为true,验证了泛型擦除机制,两个泛型不一致的集合,比较class对象的时候,是一样的。

然后声明了一个泛型为String的ArrayList集合,在调用string.add(1)的时候,编译出错,提示我们需要一个String类型的元素,但是这里却传入了一个int类型的。


那么我们有没有办法向ArrayList中添加一个非String类型的元素呢?下面看一段代码演示:

public static void main(String[] args) throws Exception {
        ArrayList<String> strings = new ArrayList<>();
        strings.add("aa");
//        strings.add(1);//此处会报错,因为泛型检查此处需要String类型

        Method methodAdd = strings.getClass().getDeclaredMethod("add", Object.class);
        methodAdd.setAccessible(true);
        methodAdd.invoke(strings, 2);
        System.out.println(strings.size());
        for (Object o : strings) {
            System.out.println(o);
        }
    }

我们知道泛型是一种编译时类型安全检查机制,因为要兼容之前的JDK1.6之前的版本,所以泛型在编译为class字节码文件时候,泛型就被擦除了。

那么说到这里,我们就应该知道,可以通过反射来非泛型元素,上面的代码也正是这个思路实现的。往集合strings里添加了 数字 2.

但是反射本来就是一个非常规的操作,明明知道不能添加非泛型类型的元素,却非要通过反射添加的话,就有类型转换异常的风险,需要自己承担。

2 泛型擦除后是如何保存泛型信息的?

我们通过如下两段代码来发现一些端倪:

public class Sign {
    public static void test(List list) {
        System.out.println("test");
    }
}

public class Sign {
    public static void test(List<String> list) {
        System.out.println("test");
    }
}

上面两段代码唯一的区别是下面比上面的加了泛型为String的泛型信息。我们通过javac Sign.java后再通过javap -v Sign.class对比下面两段反编译后的class字节码。


public static void test(java.util.List);
    descriptor: (Ljava/util/List;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String test
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8


public static void test(java.util.List<java.lang.String>);
    descriptor: (Ljava/util/List;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String test
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 11: 0
        line 12: 8
    Signature: #14                          // (Ljava/util/List<Ljava/lang/String;>;)V

我们能看到加了泛型的反编译后多了一个最后一行的签名文件Signature,保存到就是泛型的信息。而字节码层面没有什么差别


泛型类型只会在类,字段,以及方法形参保存泛型信息,正是因为有了Signature对泛型信息的保存,我们才能获取他们,下面通过一段代码演示获取字段和方法泛型信息:

public class Sign {
    private List<String> list;

    public static void test(List<String> list) {
        System.out.println("test");
    }

    public static void main(String[] args) throws Exception {
        Field list = Sign.class.getDeclaredField("list");
        list.setAccessible(true);
        Type genericType = list.getGenericType();
        System.out.println(genericType);//java.util.List<java.lang.String>
        ParameterizedType parameterizedType = (ParameterizedType) genericType;
        System.out.println(parameterizedType.getActualTypeArguments()[0]);//class java.lang.String

        System.out.println("------------------");
        Method methodTest = Sign.class.getDeclaredMethod("test", List.class);
        methodTest.setAccessible(true);
        System.out.println(methodTest.getGenericParameterTypes()[0]);
        ParameterizedType parameterizedType1 = (ParameterizedType) methodTest.getGenericParameterTypes()[0];//java.util.List<java.lang.String>
        System.out.println(parameterizedType1.getActualTypeArguments()[0]);//class java.lang.String
    }
}


但是上面的泛型获取有一个问题,就是不能获取到实参的泛型信息,比如我们要进行序列化和反序列化的话,获取不到泛型信息的话,就很难进行序列化和反序列化了,这个时候我们可以参考Gson的解决方案,使用TypeToken匿名内部类的方式来保存泛型信息。我们模拟写出了一个MyTypeToken类,如下:

实例代码如下:

public class Deserialize {
    static class Response<T> {
        T data;
        int code;
        String message;

        @Override
        public String toString() {
            return "Response{" +
                    "data=" + data +
                    ", code=" + code +
                    ", message='" + message + '\'' +
                    '}';
        }

        public Response(T data, int code, String message) {

            this.data = data;
            this.code = code;
            this.message = message;
        }
    }

    static class Data {
        String result;

        public Data(String result) {
            this.result = result;
        }

        @Override
        public String toString() {
            return "Data{" +
                    "result=" + result +
                    '}';
        }
    }

    public static void main(String[] args) {
        Response<Data> dataResponse = new Response(new Data("数据"), 1, "成功");

        Gson gson = new Gson();
        String json = gson.toJson(dataResponse);
        System.out.println(json);
        System.out.println("---------------");

        /**
         *  有花括号: 代表是匿名内部类,创建一个匿名内部类的实例对象
         *  没花括号:创建实例对象
         */
        Type type1 = new TypeToken<Response<Data>>() {
        }.getType();
        Type type = new MyTypeToken<Response<Data>>() {

        }.getType();

        System.out.println(type1);
        System.out.println(type);
        Response<Data> response = gson.fromJson(json, type);
        System.out.println(response);
        System.out.println(response.data);
        System.out.println(response.data.getClass());
    }
}

//运行结果
{"data":{"result":"数据"},"code":1,"message":"成功"}
---------------
com.oman.lib.gson.Deserialize$Response<com.oman.lib.gson.Deserialize$Data>
com.oman.lib.gson.Deserialize.com.oman.lib.gson.Deserialize$Response<com.oman.lib.gson.Deserialize$Data>
Response{data=Data{result=数据}, code=1, message='成功'}
Data{result=数据}
class com.oman.lib.gson.Deserialize$Data


上面使用匿名内部类满足了我们的需求,我们可以反编译一下看看加{}和不加括号的区别:

public static void main(String[] args) {
    Type type = new MyTypeToken<Response<Data>>().getType();
}

public static void main(String[] args) {
    Type type = new MyTypeToken<Response<Data>>(){

    }.getType();
}

他们的区别在于加了花括号的时候,反编译的时候多了下面一行,从而保存了实参的泛型信息。


Signature: #15  // Lcom/oman/lib/gson/Deserialize$MyTypeToken<Lcom/oman/lib/gson/Deserialize$Response<Lcom/oman/lib/gson/Deserialize$Data;>;>;

3 总结

  • Java泛型擦除后可以通过反射添加元素,不过最好不要这么做,会有类型转换异常的风险。
  • 泛型擦除后,类,字段和方法的形参泛型信息是会保存到Signature中的,另外可以通过匿名内部类的方式获取泛型的实参类型,比如Gson中的使用。

转载 #面试题:你知道泛型擦除后是如何获取泛型信息的吗?_Zhou Jiang的博客-CSDN博客_java泛型擦除后保存在哪里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值