泛型擦除后 Retrofit
是怎么获取类型的?
Retrofit
是如何传递泛型信息的?
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
使用 jad
查看反编译后的 class 文件:
可以看到 class 文件中已经将泛型信息给擦除了,那么
Retrofit
是如何拿到<ListList<Repo>>
的类型信息的?
import retrofit2.Call;
public interface GitHubService
{
public abstract Call listRepos(String s);
}
我们看一下 Retrofit
的源码:
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
...
Type returnType = method.getGenericReturnType();
...
}
public Type getGenericReturnType() {
// 根据 Signature 信息 获取 泛型类型
if (getGenericSignature() != null) {
return getGenericInfo().getReturnType();
} else {
return getReturnType();
}
}
可以看出,Retrofit
是通过 getGenericReturnType()
来获取类型信息的,jdk 的 Class 、Method 、Field 类提供了一系列获取泛型类型的相关方法。以 Method
为例,getGenericReturnType
获取带泛型信息的返回类型,getGenericParameterTypes
获取带泛型信息的参数类型。
泛型的信息不是被擦除了吗?
答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被 class 文件 以 Signature
的形式 保留在 Class 文件的 Constant pool
中。
通过 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)
}
这就是我们 Retrofit
中能够获取泛型类型的原因。
Gson
解析为什么要传入内部类?
我们这里可以提出两个问题
1,Gson 是怎么获取泛型类型的,也是通过 Signature 吗?
2,为什么 Gson 解析要传入匿名内部类?
public List<String> parse(String jsonStr) {
List<String> topNews = new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
return topNews;
}
Gson 解析时传入的参数属于 使用侧泛型
,因此不能通过 Signature 解析。
Gson 是如何获取到 List<String> 的泛型信息 String 的呢?
Class 类提供了一个方法 public Type getGenericSuperclass()
,可以获取到带泛型信息的父类 Type。也就是说 Java 的 class 文件会保存 父类 或者 接口 的泛型信息。
所以 Gson 使用了一个巧妙的方法来获取泛型类型:
-
创建一个泛型抽象类
TypeToken <T>
,这个抽象类不存在抽象方法,因为匿名内部类必须继承自抽象类或者接口。所以才定义为抽象类; -
创建一个 继承自
TypeToken
的匿名内部类, 并实例化泛型参数TypeToken<String>
; -
通过 class 类的
public Type getGenericSuperclass()
方法,获取带泛型信息的父类 Type,也就是TypeToken<String>
。
总结:Gson 利用子类会保存父类 class 的泛型参数信息的特点。 通过匿名内部类实现了泛型参数的传递。
PECS 介绍
PECS 的意思是 Producer Extend Consumer Super
,简单理解为如果是生产者则使用 Extend
,如果是消费者则使用 Super
,不过,这到底是啥意思呢?
PECS 是从集合的角度出发的:
-
如果你只是从集合中取数据,那么它是个生产者,你应该用
extend
; -
如果你只是往集合中加数据,那么它是个消费者,你应该用
super
; -
如果你往集合中既存又取,那么你不应该用
extend
或者super
。
让我们通过一个典型的例子理解一下到底什么是 Producer
和 Consumer
?
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
dest.set(i, src.get(i));
}
}
}
为什么需要 PECS?
使用 PECS 主要是为了实现集合的多态。
List<? extends Fruit>
,同时兼容了 List<Fruit>
和 List<Apple>
,我们可以理解为 List<? extends Fruit>
现在是 List<Fruit>
和 List<Apple>
的超类型(父类型),通过这种方式就实现了泛型集合的多态。
public static void getOutFruits(List<? extends Fruit> basket){
for (Fruit fruit : basket) {
System.out.println(fruit);
//...do something other
}
}
小结
-
在
List<? extends Fruit>
的泛型集合中,对于元素的类型,编译器只能知道元素是继承自Fruit
,具体是Fruit
的哪个子类是无法知道的。 所以「向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的」。但是由于知道元素是继承自Fruit
,所以从这个泛型集合中取Fruit
类型的元素是可以的。 -
在List<? super Apple>的泛型集合中,元素的类型是
Apple
的父类,但无法知道是哪个具体的父类,因此「读取元素时无法确定以哪个父类进行读取」。 插入元素时可以插入Apple
与Apple
的子类,因为这个集合中的元素都是Apple
的父类,子类型是可以赋值给父类型的。
有一个比较好记的口诀:
-
只读不可写时,使用
List<? extends Fruit>: Producer
-
只写不可读时,使用
List<? super Apple>: Consumer
总得来说,List<Fruit>
和 List<Apple>
之间没有任何继承关系。API 的参数想要同时兼容2者,则只能使用 PECS 原则。这样做提升了 API 的灵活性,实现了泛型集合的多态。
参考:
网友评论