思維導圖:
- 什么是泛型擦除葱跋?
- 泛型擦除會帶來什么樣的問題屈雄,如何解決猎荠?
- 應用場景
- PECS 原則
- 泛型擦除后 retrofit 是怎么獲取類型的
Java
泛型(generics
)就是參數(shù)化類型,適用于多種數(shù)據(jù)類型執(zhí)行相同代碼斧拍,在使用時才確定真實類型雀扶。泛型有泛型類、泛型接口肆汹、泛型方法愚墓。
泛型擦除:泛型信息只存在于代碼編譯階段,在進入 JVM
之前昂勉,與泛型相關的信息會被擦除掉浪册。
在泛型類被類型擦除的時候,之前泛型類中的類型參數(shù)部分如果沒有指定上限岗照,如 <T>
則會被轉譯成普通的 Object
類型村象,如果指定了上限如<T extends String>
則類型參數(shù)就被替換成類型上限笆环。
會在字節(jié)碼保留泛型類型,使用時也就是運行到這段字節(jié)碼的時候煞肾,再將 Object
強制轉換成對應類型咧织。
需要注意的一點:
泛型不能聲明在泛型類里面的靜態(tài)方法和靜態(tài)變量中,
因為泛型類里面的靜態(tài)方法和靜態(tài)變量可能比構造方法先執(zhí)行籍救,導致泛型沒有實例化就調(diào)用。
在理解了泛型擦除的概念渠抹,咱們看下面的例子:
List list = new ArrayList();
List listString = new ArrayList<String>();
List listInteger = new ArrayList<Integer>();
這幾段代碼簡單蝙昙、粗暴、又帶有很濃厚的熟悉感是吧梧却。那我接下來要把一個數(shù)字 1
插入到這三段不一樣的代碼中了奇颠。
作為讀者的你可能現(xiàn)在已經(jīng)黑人問號了?放航?烈拒??你肯定有很多疑問广鳍,這明顯不一樣啊荆几,怎么可能。
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());
}
}
不好意思,有圖有真相祖秒,我就是插進去了,要是你還不信诞吱,我還真沒辦法了。
上述的就是泛型擦除的一種表現(xiàn)了竭缝,但是為了更好的理解房维,當然要更深入了是吧。雖然List很大抬纸,但卻也不是不能看看咙俩。
兩個關鍵點,來驗證一下:
- 數(shù)據(jù)存儲類型
- 數(shù)據(jù)獲取
// 先來看看畫了一個大餅的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直接調(diào)用的函數(shù)
// 第二個關鍵點暴浦,強制轉化得來的數(shù)據(jù)
E elementData(int index) {
return (E) elementData[index];
}
}
我想,其實你也能夠懂了晓锻,這個所謂的泛型T最后會被轉化為一個 Object
歌焦,最后又通過強制轉化來進行一個轉變。從這里我們也就能夠知道為什么我們的數(shù)據(jù)從前面過來的時候砚哆,String
類型數(shù)據(jù)能夠直接被 Integer
進行接收了独撇。
帶來什么樣的問題?
(1) 強制類型轉化
這個問題的結果我們已經(jīng)在上述文章中提及到了,通過反射的方式去進行插入的時候纷铣,我們的數(shù)據(jù)就會發(fā)生錯誤卵史。
如果我們在一個 List<Integer>
中在不知情的情況下插入了一個 String
類型的數(shù)值,那這種重大錯誤搜立,我們該找誰去說呢以躯。
(2)引用傳遞問題
上面的問題中,我們已經(jīng)說過了 T
將在后期被轉義成 Object
啄踊,那我們對引用也進行一個轉化忧设,是否行得通呢?
List<String> listObject = new ArrayList<Object>();
List<Object> listObject = new ArrayList<String>();
如果你這樣寫颠通,在我們的檢查階段址晕,會報錯。但是從邏輯意義上來說顿锰,其實你真的有錯嗎谨垃?
假設說我們的第一種方案是正確的,那么其實就是將一堆 Object
數(shù)據(jù)存入硼控,然后再由上面所說的強制轉化一般刘陶,轉化成 String
類型,聽起來完全 ok
淀歇,因為在 List
中本來存儲數(shù)據(jù)的方式就是 Object
易核。但其實是會出現(xiàn) ClassCastException
的問題,因為 Object
是萬物的基類浪默,但是強轉是為子類向父類準備的措施牡直。
再來假設說我們的第二種方案是正確的,這個時候纳决,根據(jù)上方的數(shù)據(jù) 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>
的語句,其實這就是將基類重新規(guī)劃的操作夭织,就算被編譯吭露,虛擬機也會知道將數(shù)據(jù)轉化為 Parent
而不是直接用 Object
來直接進行替代。
應用場景
該部分的思路來自于Java泛型中extends和super的區(qū)別尊惰?
上面我們說過了解決方案讲竿,使用 <T extends Parent>
泥兰。其實這只是一種方案,在不同的場景下题禀,我們需要加入不同的使用方法鞋诗。另外官方也是提倡使用這樣的方法的,但是我們?yōu)榱吮苊馕覀兩鲜龅腻e誤迈嘹,自然需要給出一些使用場景了削彬。
基于的其實是兩種場景,一個是擴展型 super
秀仲,一個是繼承型 extends
吃警。下面都用一個列表來舉例子。
統(tǒng)一繼承順序
// 承載者
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>
繼承型的用處是什么呢啄育?
其實他期待的就是這整個列表的數(shù)據(jù)的基礎都是來自我們的 Parent
,這樣獲取的數(shù)據(jù)全部人的父類其實都是來自于我們的 Parent
了拌消,你可以叫這個列表為 Parent
家族挑豌。所以也可以說這是一個適合頻繁讀取的方案。
Plate<? extends Fruit> p1=new Plate<Apple>(new Apple());
Plate<? extends Fruit> p2=new Plate<Apple>(new Beef()); // 檢查不通過
// 修改數(shù)據(jù)不通過
p1.set(new Banana());
// 數(shù)據(jù)獲取一切正常
// 但是他只能精確到由我們定義的Fruit
Fruit result = p1.get();
<? extends Fruit>
會使往盤子里放東西的 set( )
方法失效墩崩。但取東西 get( )
方法還有效氓英。
原因是編譯器只知道容器內(nèi)是 Fruit
或者它的派生類,但具體是什么類型不知道鹦筹÷敛可能是Fruit?可能是 Apple铐拐?
也可能是 Banana徘键,RedApple,GreenApple遍蟋?
編譯器在看到后面用 Plate
賦值以后吹害,盤子里沒有被標上有“蘋果”。而是標上一個占位符:CAP#1
虚青,來表示捕獲一個 Fruit
或Fruit
的子類它呀,具體是什么類不知道,代號 CAP#1
棒厘。然后無論是想往里插入 Apple
或者 Meat
或者 Fruit
編譯器都不知道能不能和這個 CAP#1
匹配纵穿,所以就都不允許。
<T super Parent>
擴展型的作用是什么呢奢人?
你可以把它當成一種兼容工具谓媒,由 super
修飾,說明兼容這個類达传,通過這樣的方式比較適用于去存放上面所說的 Parent
列表中的數(shù)據(jù)篙耗。這是一個適合頻繁插入的方案迫筑。
// 填寫Food的位置,級別一定要大于或等于Fruit
Plate<? super Fruit> p1=new Plate<Food>(new Apple());
// 和extends 不同可以進行存儲
p1.set(new Banana());
// get方法
Banana result1 = p1.get(); // 會報錯宗弯,一定要經(jīng)過強制轉化脯燃,因為返回的只是一個Object
Object result2 = p1.get(); // 返回一個Object數(shù)據(jù)我們已經(jīng)屬于快要丟失掉全部數(shù)據(jù)了,所以不適合讀取
使用下界 <? super Fruit>
會使從盤子里取東西的 get( )
方法部分失效蒙保,只能存放到 Object
對象里辕棚。set( )
方法正常。
因為下界規(guī)定了元素的最小粒度的下限邓厕,實際上是放松了容器元素的類型控制逝嚎。既然元素是 Fruit
的基類,那往里存粒度比 Fruit
小的都可以详恼。但往外讀取元素就費勁了补君,只有所有類的基類 Object
對象才能裝下。但這樣的話昧互,元素的類型信息就全部丟失挽铁。
PECS原則
最后看一下什么是 PECS(Producer Extends Consumer Super)
原則,已經(jīng)很好理解了:
- 頻繁往外讀取內(nèi)容的敞掘,適合用上界
Extends
叽掘。 - 經(jīng)常往里插入的,適合用下界
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
文件中已經(jīng)將泛型信息給擦除了,那么 Retrofit
是如何拿到 Call<List>
的類型信息的?
我們看一下 retrofit
的源碼:
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
...
Type returnType = method.getGenericReturnType();
...
}
public Type getGenericReturnType() {
// 根據(jù) Signature 信息 獲取 泛型類型
if (getGenericSignature() != null) {
return getGenericInfo().getReturnType();
} else {
return getReturnType();
}
}
可以看出赫冬,retrofit
是通過 getGenericReturnType
來獲取類型信息的
jdk
的 Class 浓镜、Method 、Field
類提供了一系列獲取 泛型類型的相關方法面殖。
以 Method
為例竖哩,getGenericReturnType
獲取帶泛型信息的返回類型 、 getGenericParameterTypes
獲取帶泛型信息的參數(shù)類型脊僚。
問:泛型的信息不是被擦除了嗎相叁?
答:是被擦除了, 但是某些(聲明側的泛型辽幌,接下來解釋) 泛型信息會被 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)
}