思维导图:
- 什么是泛型擦除?
- 泛型擦除会带来什么样的问题,如何解决?
- 应用场景
- PECS 原则
- 泛型擦除后 retrofit 是怎么获取类型的
Java
泛型(generics
)就是参数化类型,适用于多种数据类型执行相同代码,在使用时才确定真实类型。泛型有泛型类、泛型接口、泛型方法。
泛型擦除:泛型信息只存在于代码编译阶段,在进入 JVM
之前,与泛型相关的信息会被擦除掉。
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>
则会被转译成普通的 Object
类型,如果指定了上限如<T extends String>
则类型参数就被替换成类型上限。
会在字节码保留泛型类型,使用时也就是运行到这段字节码的时候,再将 Object
强制转换成对应类型。
需要注意的一点:
泛型不能声明在泛型类里面的静态方法和静态变量中,
因为泛型类里面的静态方法和静态变量可能比构造方法先执行,导致泛型没有实例化就调用。
在理解了泛型擦除的概念,咱们看下面的例子:
List list = new ArrayList();
List listString = new ArrayList<String>();
List listInteger = new ArrayList<Integer>();
这几段代码简单、粗暴、又带有很浓厚的熟悉感是吧。那我接下来要把一个数字 1
插入到这三段不一样的代码中了。
作为读者的你可能现在已经黑人问号了????你肯定有很多疑问,这明显不一样啊,怎么可能。
public class Main {
public static void main(String[] args) {
List list = new ArrayList();
List listString = new ArrayList<String>();
List listInteger = new ArrayList<Integer>();
try {
list.getClass().getMethod("add", Object.class).invoke(list, 1);
listString.getClass().getMethod("add", Object.class).invoke(listString, 1);
// 给不服气的读者们的测试之处,你可以改成字符串来尝试。
listInteger.getClass().getMethod("add", Object.class).invoke(listInteger, 1);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("list size:" + list.size());
System.out.println("listString size:" + listString.size());
System.out.println("listInteger size:" + listInteger.size());
}
}
不好意思,有图有真相,我就是插进去了,要是你还不信,我还真没办法了。
上述的就是泛型擦除的一种表现了,但是为了更好的理解,当然要更深入了是吧。虽然List很大,但却也不是不能看看。
两个关键点,来验证一下:
- 数据存储类型
- 数据获取
// 先来看看画了一个大饼的List
// 能够过很清楚的看到泛型E
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
// 第一个关键点
// 还没开始就出问题的存储类型
// 难道不应该也是一个泛型E?
transient Object[] elementData;
public E get(int index) {
rangeCheck(index);
return elementData(index); // 1---->
}
// 由1直接调用的函数
// 第二个关键点,强制转化得来的数据
E elementData(int index) {
return (E) elementData[index];
}
}
我想,其实你也能够懂了,这个所谓的泛型T最后会被转化为一个 Object
,最后又通过强制转化来进行一个转变。从这里我们也就能够知道为什么我们的数据从前面过来的时候,String
类型数据能够直接被 Integer
进行接收了。
带来什么样的问题?
(1) 强制类型转化
这个问题的结果我们已经在上述文章中提及到了,通过反射的方式去进行插入的时候,我们的数据就会发生错误。
如果我们在一个 List<Integer>
中在不知情的情况下插入了一个 String
类型的数值,那这种重大错误,我们该找谁去说呢。
(2)引用传递问题
上面的问题中,我们已经说过了 T
将在后期被转义成 Object
,那我们对引用也进行一个转化,是否行得通呢?
List<String> listObject = new ArrayList<Object>();
List<Object> listObject = new ArrayList<String>();
如果你这样写,在我们的检查阶段,会报错。但是从逻辑意义上来说,其实你真的有错吗?
假设说我们的第一种方案是正确的,那么其实就是将一堆 Object
数据存入,然后再由上面所说的强制转化一般,转化成 String
类型,听起来完全 ok
,因为在 List
中本来存储数据的方式就是 Object
。但其实是会出现 ClassCastException
的问题,因为 Object
是万物的基类,但是强转是为子类向父类准备的措施。
再来假设说我们的第二种方案是正确的,这个时候,根据上方的数据 String
存入,但是有什么意义存在呢?最后都还是要成 Object
的,你还不如就直接是 Object
。
解决方案
其实很简单,如果看过一些公开课想来就见过这样的用法。
public class Part<T extends Parent> {
private T val;
public T getVal() {
return val;
}
public void setVal(T val) {
this.val = val;
}
}
相比较于之前的 Part
而言,他多了 <T extends Parent>
的语句,其实这就是将基类重新规划的操作,就算被编译,虚拟机也会知道将数据转化为 Parent
而不是直接用 Object
来直接进行替代。
应用场景
该部分的思路来自于Java泛型中extends和super的区别?
上面我们说过了解决方案,使用 <T extends Parent>
。其实这只是一种方案,在不同的场景下,我们需要加入不同的使用方法。另外官方也是提倡使用这样的方法的,但是我们为了避免我们上述的错误,自然需要给出一些使用场景了。
基于的其实是两种场景,一个是扩展型 super
,一个是继承型 extends
。下面都用一个列表来举例子。
统一继承顺序
// 承载者
class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
// Lev 1
class Food{}
// Lev 2
class Fruit extends Food{}
class Meat extends Food{}
//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}
//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}
<T extends Parent>
继承型的用处是什么呢?
其实他期待的就是这整个列表的数据的基础都是来自我们的 Parent
,这样获取的数据全部人的父类其实都是来自于我们的 Parent
了,你可以叫这个列表为 Parent
家族。所以也可以说这是一个适合频繁读取的方案。
Plate<? extends Fruit> p1=new Plate<Apple>(new Apple());
Plate<? extends Fruit> p2=new Plate<Apple>(new Beef()); // 检查不通过
// 修改数据不通过
p1.set(new Banana());
// 数据获取一切正常
// 但是他只能精确到由我们定义的Fruit
Fruit result = p1.get();
<? extends Fruit>
会使往盘子里放东西的 set( )
方法失效。但取东西 get( )
方法还有效。
原因是编译器只知道容器内是 Fruit
或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是 Apple?
也可能是 Banana,RedApple,GreenApple?
编译器在看到后面用 Plate
赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1
,来表示捕获一个 Fruit
或Fruit
的子类,具体是什么类不知道,代号 CAP#1
。然后无论是想往里插入 Apple
或者 Meat
或者 Fruit
编译器都不知道能不能和这个 CAP#1
匹配,所以就都不允许。
<T super Parent>
扩展型的作用是什么呢?
你可以把它当成一种兼容工具,由 super
修饰,说明兼容这个类,通过这样的方式比较适用于去存放上面所说的 Parent
列表中的数据。这是一个适合频繁插入的方案。
// 填写Food的位置,级别一定要大于或等于Fruit
Plate<? super Fruit> p1=new Plate<Food>(new Apple());
// 和extends 不同可以进行存储
p1.set(new Banana());
// get方法
Banana result1 = p1.get(); // 会报错,一定要经过强制转化,因为返回的只是一个Object
Object result2 = p1.get(); // 返回一个Object数据我们已经属于快要丢失掉全部数据了,所以不适合读取
使用下界 <? super Fruit>
会使从盘子里取东西的 get( )
方法部分失效,只能存放到 Object
对象里。set( )
方法正常。
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是 Fruit
的基类,那往里存粒度比 Fruit
小的都可以。但往外读取元素就费劲了,只有所有类的基类 Object
对象才能装下。但这样的话,元素的类型信息就全部丢失。
PECS原则
最后看一下什么是 PECS(Producer Extends Consumer Super)
原则,已经很好理解了:
- 频繁往外读取内容的,适合用上界
Extends
。 - 经常往里插入的,适合用下界
Super
。
泛型擦除后 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>
的类型信息的?
我们看一下 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
中。
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)
}