Java基礎:泛型擦除

思維導圖:

  • 什么是泛型擦除葱跋?
  • 泛型擦除會帶來什么樣的問題屈雄,如何解決猎荠?
  • 應用場景
  • 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虚青,來表示捕獲一個 FruitFruit的子類它呀,具體是什么類不知道,代號 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 來獲取類型信息的

jdkClass 浓镜、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)
}

參考:

面試官問我:“泛型擦除是什么,會帶來什么問題乌企?”
Android開發(fā)面試——Java泛型機制7連問

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末虑润,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子加酵,更是在濱河造成了極大的恐慌拳喻,老刑警劉巖哭当,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異冗澈,居然都是意外死亡钦勘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門亚亲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來彻采,“玉大人,你說我怎么就攤上這事捌归「叵欤” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵惜索,是天一觀的道長特笋。 經(jīng)常有香客問我,道長巾兆,這世上最難降的妖魔是什么雹有? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮臼寄,結果婚禮上,老公的妹妹穿的比我還像新娘溜宽。我一直安慰自己吉拳,他們只是感情好,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布适揉。 她就那樣靜靜地躺著留攒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嫉嘀。 梳的紋絲不亂的頭發(fā)上炼邀,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天,我揣著相機與錄音剪侮,去河邊找鬼拭宁。 笑死,一個胖子當著我的面吹牛瓣俯,可吹牛的內(nèi)容都是我干的杰标。 我是一名探鬼主播,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼彩匕,長吁一口氣:“原來是場噩夢啊……” “哼腔剂!你這毒婦竟也來了?” 一聲冷哼從身側響起驼仪,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤掸犬,失蹤者是張志新(化名)和其女友劉穎袜漩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湾碎,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡宙攻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了胜茧。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粘优。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖呻顽,靈堂內(nèi)的尸體忽然破棺而出雹顺,到底是詐尸還是另有隱情,我是刑警寧澤廊遍,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布嬉愧,位于F島的核電站,受9級特大地震影響喉前,放射性物質(zhì)發(fā)生泄漏没酣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一卵迂、第九天 我趴在偏房一處隱蔽的房頂上張望裕便。 院中可真熱鬧,春花似錦见咒、人聲如沸偿衰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽下翎。三九已至,卻和暖如春宝当,著一層夾襖步出監(jiān)牢的瞬間视事,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工庆揩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留俐东,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓订晌,卻偏偏與公主長得像犬性,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子腾仅,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容