登录

去注册

登录

注册

去登录

注册

每日一问 大家应该都有泛型在编译期会被擦除的概念,那么为什么我们在运行时还能读取到呢? 3/3

xiaoyang   2019-08-04   收藏

如果说泛型在编译器会被擦除;

那么我们在还能做类似的写法:

HttpUtils.doGet(url,new CallBack<List<User> users){
    void onSuccess(List<User> users);
}

我们声明的List<User>可以在运行时,正确的获取到该泛型类型,并利用Gson等正确的转为实际的对象。

这是为什么呢?

3

https://juejin.im/post/5d3dc406f265da1b740245e4
我前一阵子写的一篇文章中有介绍。

我直接复制里边的内容啦~

如何通过反射获取泛型类型?
既然泛型类型在运行时会被擦除那么我们怎么获取到泛型类型呢?

其实在泛型擦除时并不会将所有的泛型类型都擦除掉,它只会擦除运行时的泛型类型,编译时类中定义的泛型类型是不会被擦除的,对应的泛型类型会被保存在Signature中。
我们如果想获取对应对象中的泛型类型只需将动态创建的对象改为匿名内部类即可获取,因为内部类实在编译时创建的,泛型类型是会保存下来的。
对应API getGeneric...都是获取泛型类型的。
下边举两个例子:

List<Integer> list = new ArrayList<>();
list.getClass().getGenericSuperclass(); //获取不到泛型信息
List<Integer> list1 = new ArrayList() {};
list1.getClass().getGenericSuperclass(); //可以获取到泛型信息

可以看到第一个list由于是在运行时创建的对象所以由于泛型擦除是无法获取泛型信息的,因为运行时对象本质是方法的调用(真正调用了以后才会创建),在运行时创建的对象是没有办法通过反射获取其中的类型的。
第二个是可以获取的,因为后边加了{},这就使得这个list成为了一个匿名内部类且父类是List,子类是可以调用父类的构造方法的,加了之后这个list1就不是运行时创建的对象了而是编译时创建的,所以是可以获取泛型类型的。
下边以一道题为例:

public class Demo {

        public static void main(String[] args) throws Exception {

                ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass();
                System.out.println(type.getActualTypeArguments()[0]);

                ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType();
                System.out.println(fieldType.getActualTypeArguments()[0]);

                ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class).getGenericParameterTypes()[0];
                System.out.println(paramType.getActualTypeArguments()[0])

                System.out.println(Foo.class.getTypeParameters()[0].getBounds()[0]);
        }

        class Foo<T extends CharSequence> {

                public List<Bar> children = new ArrayList<Bar>();

                public List<StringBuilder> foo(List<String> foo) {return null}

                public void bar(List<? extends String> param) {}
         }

        class Bar extends Foo<String> {}
}

运行结果如下。
class java.lang.String
class Demo$Bar
class java.lang.String
interface java.lang.CharSequence

通过上面例子会发现泛型类型的每一个类型参数都被保留了,而且在运行期可以通过反射机制获取到,因为泛型的擦除机制实际上擦除的是除结构化信息外的所有东西(结构化信息指与类结构相关的信息,而不是与程序执行流程有关的,即与类及其字段和方法的类型参数相关的元数据都会被保留下来通过反射获取到)。

回复
2

之前刚好看到过这一块,记了点笔记,这是其中的一部分:
原因是因为编译器帮我们完成了自动类型转换,因为类型擦除的问题,泛型类型变量最后得到的都是原始类型,比如ArrayList<String>最终得到的是ArrayList,那么为什么我们在get的时候为什么不需要经过类型转换呢?实际上这里是Java自动的帮我们进行了一个类型转换,如下代码:

class FanXing<T>{
    private T t;
    public void set(T t){
        this.t = t;
    }
    public T get(){
        return t;
    }
}
// test:
FanXing<Integer> f = new FanXing();
f.set(1);
Integer i = f.get();

// 反编译字节码指令:
  public static void main(java.lang.String[]);
    Code:
       0: new           #2// class FanXing
       3: dup
       4: invokespecial #3// Method FanXing."<init>":()V
       7: astore_1
       8: aload_1
       9: iconst_1
      //...
      20: checkcast     #7// class java/lang/Integer
      23: astore_2
      24: return

20处的checkcast指令所做的事情就是类型转换,程序员不需要在源代码中再进行类型转换,改由编译器自动实现。而该转换也不是在get方法里头,是在get方法执行完成后,将操作数栈顶的元素强转为指定类型。

注意这里是在get方法之后将操作数栈顶元素进行强转,更通俗的理解就是,泛型进入的地方因为类型擦除,默认转型成为了Object对象,然后再泛型离开的地方,编译器为我们自动加上了将Object转型成Target对象的指令。
再结合@lvzishen的回答,理解起来就非常直接了。

回复
0

泛型的使用只是为了我们编写代码时,省去了强制转换,其实最终还是会强转成对应类型的

Java语言进阶篇:泛型原理与Android网络应用

我是小黑 来关注我吧

回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定