java 的泛型擦除与 TypeToken

来自Retrofit 源码的一个疑问

Retrofit 是如何传递泛型信息的?

上一段常见的网络接口请求代码:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

使用jad 查看反编译 后的class 文件:

import retrofit2.Call;

public interface GitHubService
{

    public abstract Call listRepos(String s);
}

可以看到 class文件中已经将泛型信息给擦除了,那么Retrofit 是如何拿到 Call<List> 的类型信息,并通过 Gson 进行反序列化 输出List 对象的呢?
实际上可以通过反射,获取方法的带泛型参数类型:

    public static void main(String args[]) {
        try {
            Class clz = Class.forName("com.example.diva.leet.GitHubService");
            Method method = clz.getMethod("listRepos", String.class);
            System.out.println("getGenericReturnType     " + method.getGenericReturnType());
            System.out.println("getReturnType     " + method.getReturnType());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

输出:

getGenericReturnType     retrofit2.Call<java.util.List<com.example.diva.leet.Repo>>
getReturnType     interface retrofit2.Call

关于反射获取的Type 类

Type类详解:https://blog.csdn.net/wakewakewake/article/details/105039765

java的泛型擦除与泛型信息保留

jdk 的 Class 、Method 、Field 类提供了一系列获取 泛型类型的相关方法。以Method 为例
getGenericReturnType 获取带泛型信息的返回类型 、 getGenericParameterTypes 获取带泛型信息的参数类型。

问:泛型的信息不是被擦除了吗?
答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被class文件 以 Signature 的形式 保留在Class文件的Constant pool中。

    public Type getGenericReturnType() {
    	// 根据 Signature 信息 获取 泛型类型 
      if (getGenericSignature() != null) {
        return getGenericInfo().getReturnType();
      } else { return getReturnType();}
    }

通过 javap 命令 可以看到 在Constant pool 中 #5 Signature 记录 了泛型的类型。

Constant pool:
   #1 = Class              #16            //  com/example/diva/leet/GitHubService
   #2 = Class              #17            //  java/lang/Object
   #3 = Utf8               listRepos
   #4 = Utf8               (Ljava/lang/String;)Lretrofit2/Call;
   #5 = Utf8               Signature
   #6 = Utf8               (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
   #7 = Utf8               RuntimeVisibleAnnotations
   #8 = Utf8               Lretrofit2/http/GET;
   #9 = Utf8               value
  #10 = Utf8               users/{user}/repos
  #11 = Utf8               RuntimeVisibleParameterAnnotations
  #12 = Utf8               Lretrofit2/http/Path;
  #13 = Utf8               user
  #14 = Utf8               SourceFile
  #15 = Utf8               GitHubService.java
  #16 = Utf8               com/example/diva/leet/GitHubService
  #17 = Utf8               java/lang/Object
{
  public abstract retrofit2.Call<java.util.List<com.example.diva.leet.Repo>> listRepos(java.lang.String);
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #6                           // (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
    RuntimeVisibleAnnotations:
      0: #8(#9=s#10)
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #12(#9=s#13)
}

如果通过 Bytecode Viewer 工具查看纯正的 class 的 raw 文件,可以发现是泛型信息是记录在class文件中的。
class Hex 文件

那些泛型会被保留,哪些是真正的擦除了?

声明侧泛型会被记录在 Class 文件的 Constant pool 中 :

  • 泛型类,或泛型接口的声明
  • 带有泛型参数的方法
  • 带有泛型参数的成员变量

使用侧泛型 :也就是方法的局部变量, 方法调用时传入的变量。

Gson 的TypeToken 原理

	// Gson 常用的情况
    public  List<String> parse(String jsonStr){
        List<String> topNews =  new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
        return topNews;
    }

根据以上的总结,方法的局部变量的泛型是不会被保存的,Gson 是如何获取到
List< String > 的泛型信息 String 的呢?
由于通过Class.forName( ) 方法、ClassName.class 这种方式获取到 的是 Raw 类型,是不带泛型类型的。
但是,转折点来了:Class 类提供了一个方法 public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type。 也就是说 java 的class 文件会保存继承的父类或者接口的泛型信息。
所以 Gson 使用了一个巧妙的方法:

  1. 创建一个泛型抽象类 TypeToken < T > ,这个抽象类不存在抽象方法,因为匿名内部类必须继承自抽象类或者接口。所以才定义为抽象类。
  2. 创建一个 继承 自 TypeToken 的 匿名内部类, 并实例化 泛型参数 TypeToken < String >
  3. 通过class 类的 public Type getGenericSuperclass() 方法,获取带泛型信息的父类Type,也就是TypeToken < String >

一个最简单的TypeToken 代码 如下:

public abstract class TypeToken<T> {
    private final Type type;
    public TypeToken() {
        Type genericSuperclass = getClass().getGenericSuperclass();
        if(genericSuperclass instanceof Class){
            throw new RuntimeException("Missing type parameter.");
        }
        ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
        Type[] typeArguments = parameterizedType.getActualTypeArguments();
        type = typeArguments[0];
    }
    
    public Type getType() {
        return type;
    }
    
    public static void main(String args[]){
        TypeToken<List<String>> sToken = new TypeToken<List<String>>(){};
        System.out.println(sToken.getType());
		// 匿名内部类,也可以使用非匿名的方式。
        class TypeToken$1 extends TypeToken<List<String>>{};
        TypeToken$1 typeToken$1 = new TypeToken$1();
        System.out.println(typeToken$1.getType());
    }
    
}

输出结果:

java.util.List<java.lang.String>
java.util.List<java.lang.String>

总结:Gson 利用子类 会 保存父类 class 的 泛型参数信息的特点。 通过匿名内部类和反射 实现了泛型参数的传递。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值