關(guān)于Java泛型機(jī)制無非就這7個問題

泛型機(jī)制是我們開發(fā)中的常用技巧,也是面試常見問題
不過泛型機(jī)制這個知識點(diǎn)也比較繁雜又不成體系,學(xué)了容易忘
本文從幾個問題出發(fā)梳理Java泛型機(jī)制知識點(diǎn),如果對你有用禽篱,歡迎點(diǎn)贊~

本文主要包括以下內(nèi)容
1.我們?yōu)槭裁葱枰盒?
2.什么是泛型擦除及泛型擦除帶來的一些問題,如retrofit怎么獲得擦除后的類型,Gson怎么獲得擦除后的類型?
3.什么是PECS原則

本文目錄如下

1.我們?yōu)槭裁葱枰盒?

我們?yōu)槭裁葱枰盒停捶盒陀惺裁从?
首先舉兩個例子

1.1 求和函數(shù)

實(shí)際開發(fā)中闲孤,經(jīng)常有數(shù)值類型求和的需求谆级,例如實(shí)現(xiàn)int類型的加法, 有時(shí)候還需要實(shí)現(xiàn)long類型的求和 如果還需要double類型的求和,又需要重新在重載一個輸入是double類型的add方法讼积。

public int addInt(int x,int y){
    return x+y;
}

public float addFloat(float x,float y){
    return x+y;
}
復(fù)制代碼

如果沒有泛型肥照,我們需要寫不少重復(fù)代碼

1.2 List中添加元素

List list = new ArrayList();
list.add("mark");
list.add("OK");
list.add(100);

for (int i = 0; i < list.size(); i++) {
        String name = list.get(i); // 1
        System.out.println("name:" + name);
    }
復(fù)制代碼

1.list默認(rèn)是Object類型,因此可以存任意類型數(shù)據(jù)
2.但是當(dāng)取出來時(shí)勤众,我們并不知道取出元素的類型舆绎,就需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換了,并且容易出錯

1.3 泛型機(jī)制的優(yōu)點(diǎn)

從上面的兩個例子我們可以直觀的得出泛型機(jī)制的優(yōu)點(diǎn)
1.使用泛型可以編寫模板代碼來適應(yīng)任意類型们颜,減少重復(fù)代碼
2.使用時(shí)不必對類型進(jìn)行強(qiáng)制轉(zhuǎn)換,方便且減少出錯機(jī)會

2.泛型擦除

2.1 什么是泛型擦除?

大家都知道吕朵,Java的泛型是偽泛型,這是因?yàn)?code>Java在編譯期間窥突,所有的泛型信息都會被擦掉努溃,正確理解泛型概念的首要前提是理解類型擦除。
Java的泛型基本上都是在編譯器這個層次上實(shí)現(xiàn)的阻问,在生成的字節(jié)碼中是不包含泛型中的類型信息的崔慧,使用泛型的時(shí)候加上類型參數(shù)均蜜,在編譯器編譯的時(shí)候會去掉,這個過程就是泛型擦除。

舉個例子:

public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());
    }

}
復(fù)制代碼

如上list1.getClass==list2.getClass返回true,說明泛型類型StringInteger都被擦除掉了馆里,只剩下原始類型
Java的泛型也可以被稱作是偽泛型

  • 真泛型:泛型中的類型是真實(shí)存在的冰抢。
  • 偽泛型:僅于編譯時(shí)類型檢查犬金,在運(yùn)行時(shí)擦除類型信息份蝴。

看到這里我們可以自然地引出下一個問題,為什么Java中的泛型是偽泛型衡未,為什么要這樣實(shí)現(xiàn)尸执?

2.2 為什么需要泛型擦除?

泛型擦除看起來有些反直覺,有些奇怪缓醋。為什么Java不能像C#一樣實(shí)現(xiàn)真正的泛型呢?為什么Java的泛型要用"擦除"實(shí)現(xiàn)
單從技術(shù)來說剔交,Java是完全100%能實(shí)現(xiàn)我們所說的真泛型,而之所以選擇使用泛型擦除主要是從API兼容的角度考慮的
導(dǎo)致Java 5引入的泛型采用擦除式實(shí)現(xiàn)的根本原因是兼容性上的取舍改衩,而不是“實(shí)現(xiàn)不了”的問題。

舉個例子,Java1.4.2都沒有支持泛型驯镊,而到Java 5突然支持泛型了葫督,要讓以前編譯的程序在新版本的JRE還能正常運(yùn)行竭鞍,就意味著以前沒有的限制不能突然冒出來。
假如在沒有泛型的Java里橄镜,我們有程序使用了java.util.ArrayList類偎快,而且我們利用了它可以存異質(zhì)元素的特性:

ArrayList things = new ArrayList();
things.add(Integer.valueof(42));
things.add("Hello World")
復(fù)制代碼

為了這段代碼在Java 5引入泛型之后還必須要繼續(xù)可以運(yùn)行,有兩種設(shè)計(jì)思路
1.需要泛型化的類型(主要是容器(Collections)類型),以前有的就保持不變洽胶,然后平行地加一套泛型化版本的新類型晒夹;
2.直接把已有的類型泛型化,讓所有需要泛型化的已有類型都原地泛型化姊氓,不添加任何平行于已有類型的泛型版丐怯。

.NET1.1 -> 2.0的時(shí)候選擇了上面選項(xiàng)的1,而Java則選擇了2翔横。

Java設(shè)計(jì)者的角度看读跷,這個取舍很明白。
.NET1.1 -> 2.0的時(shí)候禾唁,實(shí)際的應(yīng)用代碼量還很少(相對Java來說)效览,而且整個體系都在微軟的控制下,要做變更比較容易荡短;
Java1.4.2 -> 5.0的時(shí)候丐枉,Java已經(jīng)有大量程序部署在生產(chǎn)環(huán)境中,已經(jīng)有很多應(yīng)用和庫程序的代碼掘托。
如果這些代碼在新版本的Java中瘦锹,為了使用Java的新功能(例如泛型)而必須做大量源碼層修改,那么新功能的普及速度就會大受影響烫映。

2.3 泛型擦除后retrofit是怎么獲取類型的?

Retrofit是如何傳遞泛型信息的沼本?
上一段常見的網(wǎng)絡(luò)接口請求代碼:

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}
復(fù)制代碼

使用jad查看反編譯后的class文件:

import retrofit2.Call;

public interface GitHubService
{

    public abstract Call listRepos(String s);
}
復(fù)制代碼

可以看到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();
      }
    }
復(fù)制代碼

可以看出,retrofit是通過getGenericReturnType來獲取類型信息的
jdkClass 锭沟、Method 抽兆、Field 類提供了一系列獲取 泛型類型的相關(guān)方法。
Method為例,getGenericReturnType獲取帶泛型信息的返回類型 族淮、 getGenericParameterTypes獲取帶泛型信息的參數(shù)類型辫红。

問:泛型的信息不是被擦除了嗎?
答:是被擦除了祝辣, 但是某些(聲明側(cè)的泛型贴妻,接下來解釋) 泛型信息會被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)
}
復(fù)制代碼

這就是我們retrofit中能夠獲取泛型類型的原因

2.4 Gson解析為什么要傳入內(nèi)部類

Gson是我們常用的json解析庫,一般是這樣使用的

    // Gson 常用的情況
    public  List<String> parse(String jsonStr){
        List<String> topNews =  new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
        return topNews;
    }
復(fù)制代碼

我們這里可以提出兩個問題
1.Gson是怎么獲取泛型類型的名惩,也是通過Signature嗎?
2.為什么Gson解析要傳入匿名內(nèi)部類?這看起來有些奇怪

2.4.1 那些泛型信息會被保留孕荠,哪些是真正的擦除了娩鹉?

上面我們說了攻谁,聲明側(cè)泛型會被記錄在Class文件的Constant pool中,使用側(cè)泛型則不會

聲明側(cè)泛型主要指以下內(nèi)容
1.泛型類,或泛型接口的聲明 2.帶有泛型參數(shù)的方法 3.帶有泛型參數(shù)的成員變量

使用側(cè)泛型
也就是方法的局部變量,方法調(diào)用時(shí)傳入的變量弯予。

Gson解析時(shí)傳入的參數(shù)屬于使用側(cè)泛型戚宦,因此不能通過Signature解析

2.4.2 為什么Gson解析要傳入匿名內(nèi)部類

根據(jù)以上的總結(jié),方法的局部變量的泛型是不會被保存的
Gson是如何獲取到List<String>的泛型信息String的呢锈嫩?
Class類提供了一個方法public Type getGenericSuperclass() 受楼,可以獲取到帶泛型信息的父類Type
也就是說javaclass文件會保存繼承的父類或者接口的泛型信息呼寸。

所以Gson使用了一個巧妙的方法來獲取泛型類型:
1.創(chuàng)建一個泛型抽象類TypeToken <T> 艳汽,這個抽象類不存在抽象方法,因?yàn)槟涿麅?nèi)部類必須繼承自抽象類或者接口等舔。所以才定義為抽象類骚灸。
2.創(chuàng)建一個 繼承自TypeToken的匿名內(nèi)部類, 并實(shí)例化泛型參數(shù)TypeToken<String>
3.通過class類的public Type getGenericSuperclass()方法慌植,獲取帶泛型信息的父類Type甚牲,也就是TypeToken<String>

總結(jié):Gson利用子類會保存父類class的泛型參數(shù)信息的特點(diǎn)。 通過匿名內(nèi)部類實(shí)現(xiàn)了泛型參數(shù)的傳遞蝶柿。

3.什么是PECS原則?

3.1 PECS介紹

PECS的意思是Producer Extend Consumer Super丈钙,簡單理解為如果是生產(chǎn)者則使用Extend,如果是消費(fèi)者則使用Super交汤,不過雏赦,這到底是啥意思呢?

PECS是從集合的角度出發(fā)的
1.如果你只是從集合中取數(shù)據(jù)芙扎,那么它是個生產(chǎn)者星岗,你應(yīng)該用extend
2.如果你只是往集合中加數(shù)據(jù),那么它是個消費(fèi)者戒洼,你應(yīng)該用super
3.如果你往集合中既存又取俏橘,那么你不應(yīng)該用extend或者super

讓我們通過一個典型的例子理解一下到底什么是ProducerConsumer

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)); 
      }
  } 
}
復(fù)制代碼

上面的例子中將src中的數(shù)據(jù)復(fù)制到dest中,這里src就是生產(chǎn)者圈浇,它「生產(chǎn)」數(shù)據(jù)寥掐,dest是消費(fèi)者,它「消費(fèi)」數(shù)據(jù)磷蜀。

3.2 為什么需要PECS

使用PECS主要是為了實(shí)現(xiàn)集合的多態(tài)
舉個例子召耘,現(xiàn)在有這樣一個需求,將水果籃子中所有水果拿出來(即取出集合所有元素并進(jìn)行操作)

public static void getOutFruits(List<Fruit> basket){
    for (Fruit fruit : basket) {
        System.out.println(fruit);
        //...do something other
    }
}

List<Fruit> fruitBasket = new ArrayList<Fruit>();
getOutFruits(fruitBasket);//成功

List<Apple> appleBasket = new ArrayList<Apple>();
getOutFruits(appleBasket);//編譯錯誤
復(fù)制代碼

如上所示:
1.將List<Apple>傳遞給List<Fruit>會編譯錯誤褐隆。
2.因?yàn)殡m然FruitApple的父類污它,但是List<Apple>List<Fruit>之間沒有繼承關(guān)系
3.因?yàn)檫@種限制,我們不能很好的完成取出水果籃子中的所有水果需求,總不能每個類型都寫一遍一樣的代碼吧轨蛤?

使用extend可以方便地解決這個問題

/**參數(shù)使用List<? extends Fruit>**/
public static void getOutFruits(List<? extends Fruit> basket){
    for (Fruit fruit : basket) {
        System.out.println(fruit);
        //...do something other
    }
}
public static void main(String[] args) {
    List<Fruit> fruitBasket = new ArrayList<>();
    fruitBasket.add(new Fruit());
    getOutFruits(fruitBasket);

    List<Apple> appleBasket = new ArrayList<>();
    appleBasket.add(new Apple());
    getOutFruits(appleBasket);//編譯正確
}
復(fù)制代碼

List<? extends Fruit>蜜宪,同時(shí)兼容了List<Fruit>List<Apple>,我們可以理解為List<? extends Fruit>現(xiàn)在是List<Fruit>List<Apple>的超類型(父類型)
通過這種方式就實(shí)現(xiàn)了泛型集合的多態(tài)

3.3 小結(jié)

  • List<? extends Fruit>的泛型集合中祥山,對于元素的類型,編譯器只能知道元素是繼承自Fruit掉伏,具體是Fruit的哪個子類是無法知道的缝呕。 所以「向一個無法知道具體類型的泛型集合中插入元素是不能通過編譯的」。但是由于知道元素是繼承自Fruit斧散,所以從這個泛型集合中取Fruit類型的元素是可以的供常。
  • List<? super Apple>的泛型集合中,元素的類型是Apple的父類鸡捐,但無法知道是哪個具體的父類栈暇,因此「讀取元素時(shí)無法確定以哪個父類進(jìn)行讀取」。 插入元素時(shí)可以插入AppleApple的子類箍镜,因?yàn)檫@個集合中的元素都是Apple的父類源祈,子類型是可以賦值給父類型的。

有一個比較好記的口訣:
1.只讀不可寫時(shí),使用List<? extends Fruit>:Producer
2.只寫不可讀時(shí),使用List<? super Apple>:Consumer

總得來說色迂,List<Fruit>List<Apple>之間沒有任何繼承關(guān)系香缺。API的參數(shù)想要同時(shí)兼容2者,則只能使用PECS原則歇僧。這樣做提升了API的靈活性图张,實(shí)現(xiàn)了泛型集合的多態(tài)
當(dāng)然,為了提升了靈活性诈悍,自然犧牲了部分功能祸轮。魚和熊掌不能兼得。

總結(jié)

本文梳理了Java泛型機(jī)制這個知識點(diǎn)侥钳,回答了如下幾個問題
1.我們?yōu)槭裁葱枰盒?
2.什么是泛型擦除?
3.為什么需要泛型擦除适袜?
4.泛型擦除后retrofit怎么獲得類型的?
5.Gson解析為什么要傳入內(nèi)部類
6.什么是PECS原則?
7.為什么需要PECS原則?

更多Android筑基學(xué)習(xí)知識,可以后臺私信我獲取pdf學(xué)習(xí)筆記慕趴!

架構(gòu)師筑基基礎(chǔ)目錄

本文如果對您有所幫助痪蝇,歡迎點(diǎn)贊,謝謝~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冕房,一起剝皮案震驚了整個濱河市躏啰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌耙册,老刑警劉巖给僵,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡帝际,警方通過查閱死者的電腦和手機(jī)蔓同,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹲诀,“玉大人斑粱,你說我怎么就攤上這事「Γ” “怎么了则北?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長痕慢。 經(jīng)常有香客問我尚揣,道長,這世上最難降的妖魔是什么掖举? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任快骗,我火速辦了婚禮,結(jié)果婚禮上塔次,老公的妹妹穿的比我還像新娘方篮。我一直安慰自己,他們只是感情好俺叭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布恭取。 她就那樣靜靜地躺著,像睡著了一般熄守。 火紅的嫁衣襯著肌膚如雪蜈垮。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天裕照,我揣著相機(jī)與錄音攒发,去河邊找鬼。 笑死晋南,一個胖子當(dāng)著我的面吹牛惠猿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播负间,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼偶妖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了政溃?” 一聲冷哼從身側(cè)響起趾访,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎董虱,沒想到半個月后扼鞋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體申鱼,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年云头,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捐友。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡溃槐,死狀恐怖匣砖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昏滴,我是刑警寧澤脆粥,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站影涉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏规伐。R本人自食惡果不足惜蟹倾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猖闪。 院中可真熱鬧鲜棠,春花似錦、人聲如沸培慌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吵护。三九已至盒音,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間馅而,已是汗流浹背祥诽。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓮恭,地道東北人雄坪。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像屯蹦,于是被迫代替她去往敵國和親维哈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

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

  • Java泛型登澜,算是一個比較容易產(chǎn)生誤解的知識點(diǎn)阔挠,因?yàn)镴ava的泛型基于擦除實(shí)現(xiàn),在使用Java泛型時(shí)帖渠,往往會受到泛...
    三好碼農(nóng)閱讀 640評論 1 4
  • 泛型的目的 在編譯階段完成類型的轉(zhuǎn)換的工作谒亦,避免在運(yùn)行時(shí)強(qiáng)制類型轉(zhuǎn)換而出現(xiàn)ClassCastException,類...
    風(fēng)月寒閱讀 347評論 0 2
  • 一份招、泛型 泛型在java中有很重要的地位切揭,在面向?qū)ο缶幊碳案鞣N設(shè)計(jì)模式中有非常廣泛的應(yīng)用。 什么是泛型锁摔?為什么要使...
    脆皮雞大蝦閱讀 262評論 0 0
  • 1 泛型基礎(chǔ) 1.1 什么是泛型(Generics) 官方是這樣介紹的: JDK 5.0 introduces s...
    Eager01閱讀 254評論 0 0
  • 表情是什么廓旬,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達(dá)很多信息谐腰。高興了當(dāng)然就笑了孕豹,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,192評論 2 7