重學泛型

前言

不知道讀者們平時使用泛型多不多,自認為對泛型了解多少呢?本文筆者帶你重學一下泛型,不只從語法的角度绸吸,盡可能從本質(zhì)的角度上去理解它鼻弧,并用實例代碼去解釋,主要內(nèi)容如下:

  • 泛型的聲明和實例化
  • extends 和 super 的使用
  • 泛型的協(xié)變和逆變
  • 泛型的類型擦除
  • 泛型方法和類型推斷
  • 泛型的嵌套和重復
  • Kotlin泛型

如果你對以上有不是很清楚的知識點锦茁,建議看完以下內(nèi)容攘轩,相信你會有所收獲!

泛型的創(chuàng)建

泛型的聲明和實例化

假設(shè)有個商店码俩,它只賣某種類型的商品度帮,這時候我們就可以通過聲明一個泛型類型來限制它。

public interface Shop<T> {
    void sale(T item);
}

如上面代碼所示稿存,聲明的泛型T就指代商店出售的商品類型笨篷,這一步可稱其為泛型的聲明。

繼續(xù)這個場景栗子:

假設(shè)有個水果店只賣水果瓣履,那么就可以這么寫

class Fruit {}

class FruitShop implements Shop<Fruit>{
    @Override
    public void sale(Fruit item) {

    }
}

這個時候我們Shop接口的泛型T就被確定了率翅,是個水果類型。這一步可稱其為泛型實例化袖迎。

繼續(xù)這個場景栗子:

假設(shè)這個水果店比較專一冕臭,每天只賣一種水果。

由于上面FruitShop類的泛型已經(jīng)被實例化了燕锥,這個商店可以賣所有類型的水果辜贵。要想實現(xiàn)這個需求,就需要把FruitShop類也聲明一個泛型归形。這個泛型的要求該店只售賣水果托慨,并且每天可更換水果品種

public interface Fruit {}
public class FruitShop<F extends Fruit> implements Shop<F>{
    @Override
    public void sale(F item) {
    }
}

//今日只賣蘋果
public class Apple implements Fruit {}
new FruitShop<Apple>()

可以看到用到extends關(guān)鍵字,它可以限制邊界暇榴,把泛型F的上邊界限制成了Fruit榴芳。這樣泛型F的實例化就只允許是Fruit接口的實現(xiàn)類或者繼承它接口嗡靡。

不知道你有沒有這樣的疑惑 ,Fruit是個接口窟感,F在實例化的時候都是具體的類讨彼,而extends不是表示繼承嘛,實現(xiàn)類怎么可以繼承接口?

extends 和 super 的使用

這個extends在泛型中的使用和我們平常在類關(guān)系中使用的不太一樣,所以不能用繼承的角度去理解它柿祈,在泛型中它起到的是限制邊界的作用哈误。

我們經(jīng)常在集合中使用泛型,用此來舉例子
躏嚎,先看下面這行代碼有沒有問題:

ArrayList<Fruit> fruits = new ArrayList<Apple>();

直覺上看這行代碼沒有問題蜜自,我要一堆水果,你給我一堆蘋果卢佣,沒毛病重荠。

但這一行代碼放在編譯器上直接飄紅,編譯器會告訴你 我要的是 ArrayList <Fruit>你不能給我ArrayList<Apple>虚茶,也就是說聲明和實例化的泛型類型必須一樣戈鲁。這是為啥呀?嘹叫?婆殿?

我們可以從寫代碼的角度看,假設(shè)這么寫編譯器不報錯的話罩扇,就意味著如果我們不小心把橘子加入到這個水果集合里婆芦,卻也沒有任何錯誤提醒。它就只能在運行的時候拋出錯誤喂饥。(當然消约,根本原因并不是這樣,而是泛型擦除的特性员帮,這個后面詳細說)

fruits.add(new Orange());  //加一個橘子到實例化成蘋果的集合荆陆,編譯器它不出錯

但實際上我們經(jīng)常會遇到這樣的需求,我們需要把多種不同水果集侯,放到這個水果集合里被啼。我們可以按照編譯器提示,把聲明和實例化的泛型寫成一致的棠枉,都用Fruit

 ArrayList<Fruit> fruits2 = new ArrayList<Fruit>();
 fruits.add(new Orange());
 fruits.add(new Apple());

這么寫在這種場景下完全沒問題浓体,但是聲明和實例化的泛型一致就必須一致嗎?從直觀感覺上看辈讶,如果這兩個泛型有父子關(guān)系命浴,不一樣的寫法也沒有問題啊,很容易理解。

其實是可以的生闲,我們可以借助extends來限制上邊界媳溺,下面寫就沒有問題了,可以解除限制泛型一致的問題:

ArrayList<? extends Fruit> fruits = new ArrayList<Apple>();
fruits.add(new Orange());  //報錯碍讯,提示加入集合的元素類型不對

先加入一個橘子到實例化的蘋果集合里悬蔽,發(fā)現(xiàn)提示報錯,很友好捉兴。這樣就可以避免誤寫代碼蝎困。

那么現(xiàn)在加一個蘋果到集合里:

fruits.add(new Apple());  //報錯了?

報錯信息:
Required type: capture of ? extends Fruit
Provided:Apple

what倍啥?為什么報錯了禾乘,我是蘋果,自己人啊虽缕,都不給加始藕,編譯器出問題了吧,傻了?

其實在這里 extends的作用是一種約定氮趋,就是說如果你用了 extends伍派,雖然解除了聲明和實例化需泛型一致的限制,但同時也添加了一種限制凭峡。

這個限制就是:為了避免類型的亂入,就不讓你往里面加入元素决记。

ArrayList<? extends Fruit> fruits = new ArrayList<Apple>(); //1.可能是 Apple類型
ArrayList<? extends Fruit> fruits = new ArrayList<Orange>(); //2. 可能是 Orange類型

在編譯期摧冀,編譯器只確定容器可存儲類型是? extends Fruit 類型,也就是Fruit或其子類系宫。如上面代碼所示索昂,當你加入元素時,容器有可能是Apple類型扩借。也有可能是Orange類型的椒惨。它們都屬于 ? extends Fruit,既然不能被確定那就都不給加了潮罪,不讓你寫入數(shù)據(jù)康谆。

既然寫入數(shù)據(jù)不能,那我讀取數(shù)據(jù)可行不嫉到?像這樣:

Fruit getFruit =  fruits3.get(0);

這樣就沒有問題沃暗,其實還比較好理解,因為上邊界已經(jīng)被限制了何恶,數(shù)據(jù)肯定屬于Fruit或其子類數(shù)據(jù)孽锥。所以獲取Fruit肯定是沒問題的。當然如果要獲取具體子類對象,就需要強轉(zhuǎn)了惜辑。

在所舉例子的這種場景下唬涧,用<? extends Fruit>這種方式就很雞肋,不是很合適盛撑,它的使用場景更多是作為方法的參數(shù)來使用碎节,并且只要去獲取數(shù)據(jù)

舉個栗子:

假設(shè)每個水果都有個名字(硬湊一個奇奇怪怪的假設(shè)),需要提供一個方法去打印所有水果的名字

因為你不知道調(diào)用這個方法的人會想要什么類型的水果名字撵彻,你還不能直接用Fruit钓株,因為那樣只能這樣使用:

//獲取某種水果集合里它們的名字
void getFruitName(ArrayList<Fruit> fruits){
  for (Fruit fruit: fruits){
     System.out.println("name is " +  fruit.getName());
  }
}
//只能水果集合
ArrayList<Fruit> fruits = new ArrayList<Fruit>();
getFruitName(fruits);

這樣就強制了調(diào)用者必須使用Fruit集合,而這時使用<? extends Fruit>就可以解除限制陌僵,可以傳遞具體的實例化對象集合轴合,比如蘋果集合:

//獲取某種水果集合里它們的名字
void getFruitName(ArrayList<? extends Fruit> fruits){
    for (Fruit fruit: fruits){
        System.out.println("name is " +  fruit.getName());
    }
}
//蘋果集合
ArrayList<Apple> appList= new ArrayList<Apple>()
getFruitName(appList);

所以針對 ? extends有它適合的使用場景

既然上邊界可以被限制,那么下邊界能不能被限制呢碗短?

也是可以的受葛,用super關(guān)鍵字,和extends方式使用一樣偎谁,但它和extends的限制剛好相反总滩,它是可以寫入數(shù)據(jù),但只能讀取部分數(shù)據(jù)(只允許存放到Object對象)巡雨。

關(guān)于super這里不細說了闰渔,貼下代碼,如果上面的內(nèi)容你看懂了铐望,下面的代碼還是很容易理解的:

ArrayList<? super GreenApple> appleList = new ArrayList<Fruit>();
apples.add(new GreenApple());  //可以正常添加冈涧,GreenApple 是 Apple 子類
apples.add(new Apple());       //可以正常添加

GreenApple greenApple = appleList.get(0); //get失敗
Fruit fruit = appleList.get(0);           //get失敗
Object object = appleList.get(0);         //get成功

因為下界規(guī)定了元素的最小粒度的下限,實際上是放松了容器元素的類型控制正蛙,所以只要符合類型條件都可以寫入督弓,但是讀取就受到了限制,因為不能確定具體類型了乒验,所以只能用Object來抽象的存儲愚隧,但這樣就失去了具體類型信息,很大程度上也就失去了使用的意義锻全。

如果還存有疑惑狂塘,建議看下這篇文章:

Java 泛型 <? super T> 中 super 怎么 理解?與 extends 有何不同鳄厌?-知乎

泛型的協(xié)變和逆變

這里再提兩個概念: 泛型的協(xié)變和逆變

對于協(xié)變和逆變的解釋是這亞子的:

協(xié)變和逆變都是術(shù)語睹耐,前者指能夠使用比原始指定的派生類型的派生程度更大(更具體的)的類型,后者指能夠使用比原始指定的派生類型的派生程度更胁壳獭(不太具體的)的類型

具體到剛剛的例子硝训,協(xié)變就是這樣:

ArrayList<? extends Fruit> fruits = new ArrayList<Apple>()

協(xié)變派生的類型比原始類型Fruit更加具體,是個更明確的水果

逆變就是這樣:

ArrayList<? super GreenApple> appleList = new ArrayList<Fruit>()

逆變派生的類型比原始類型GreenApple就更加寬泛,可能是個水果窖梁,可能是個食物赘风,可是是個東西...

泛型的類型擦除

上面提到的聲明和實例化需泛型一致的限制,我們還可以通過下面這樣的代碼解除

 ArrayList<Fruit> fruits4 = (ArrayList) new ArrayList<Apple>();
 fruits4.add(new Apple());   //可以添加蘋果
 fruits4.add(new Orange());  //添加橘子也沒有問題

發(fā)現(xiàn)通過強轉(zhuǎn)就可以解除纵刘。關(guān)鍵是還可以添加非蘋果類型的水果邀窃。這本質(zhì)上是因為泛型的類型擦除

泛型類型擦除是理解泛型必須理解的概念假哎,概念很簡單

Java中的泛型基本上都是在編譯器這個層次來實現(xiàn)的瞬捕。使用泛型時加上的類型參數(shù)信息,只存在編譯階段舵抹,會在運行階段去掉肪虎。這個過程就稱為類型擦除。

簡單點說就是在運行階段惧蛹,泛型類型就被消除了扇救。比如上面的代碼消除掉泛型后等價于

 ArrayList fruits5 = (ArrayList) new ArrayList();

這行代碼我們就很好理解了,它就變成了一個不受限制的普通容器香嗓,你想往里面加什么都可以迅腔。

為什么需要泛型類型擦除呢?

  1. 主要是因為兼容性,因為泛型是JDK 5 中引入的一個新特性靠娱,如果沒有泛型擦除沧烈,就會有兩種不同的類型,比如
    List<String>List像云。假設(shè)只用新版本的JVM锌雀,可以定義規(guī)則,把兩者視為不同的類型苫费,但對于舊版本的JVM就無法辨識這兩種類型汤锨,編譯就會出現(xiàn)問題双抽。所以為了兼容舊版本JVM百框,就只能通過泛型擦除將其變成一種類型。
  2. 另外還有一個原因是因為性能牍汹,如果泛型不擦除铐维,就意味著要增加泛型化后的類來支持,內(nèi)存就會受到影響慎菲。

那么問題來了嫁蛇,我們經(jīng)常會通過反射去拿泛型的信息,那既然剛剛說泛型類型在運行時被擦除露该,為什么我們卻可以通過反射去拿到泛型的類型睬棚,不矛盾?

其實是因為這些信息在編譯成字節(jié)碼的時候被保留下來了,口說無憑抑党,證據(jù)哪里來包警,看字節(jié)碼文件:

還是用上面那個商店的例子:

public class Shop<T> {
    void sale(T item){}
}

Shop shop = new Shop<Fruit>();

按之前的說法泛型類型擦除后,就變成了這樣:

public class Shop {
    void sale(Object item){}
}

可最終結(jié)果真的是這樣嗎底靠,我們接下來驗證一下是否如此害晦,反編譯一下編譯后的字節(jié)碼:

{
  com.example.androidpromoteroad.generic.shop.Shop();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

  void sale(T);                         //1. 可以看到`T`還在
    descriptor: (Ljava/lang/Object;)V   //2. 這里是對`T`的類型描述Object
    flags:
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 11: 0
    Signature: #11      // (TT;)V
}

可以看出來,類型擦除的類型信息(1處)暑中,在字節(jié)碼階段還是存在的壹瘟。descriptor這一行可以看到最終的結(jié)果是Object,驗證我們的猜想是正確的鳄逾。

泛型方法和類型推斷

假設(shè)一個水果商店有交換物品的功能稻轨,你給他一個物品,它會返回一個物品(具體由外面決定)严衬,我們就可以這么實現(xiàn)

public class FruitShop implements Shop{
    Object changeItem(Object item){
        //假設(shè)一個object對象
        Object newItem = new Object();
        return newItem;
    }
}

若想用蘋果換橘子就可以這么寫

//調(diào)用泛型方法
FruitShop changeShop = new FruitShop();
Orange orange2 = (Orange) changeShop.changeItem(new Apple());

這么寫完全是可以的澄者,但是這樣的問題在于,雖然用Object來避免對象類型的具體化请琳,但實際使用時需要強轉(zhuǎn)粱挡,這就需要寫很多這樣的強轉(zhuǎn)樣板代碼。

這時我們可以用泛型方法來解決(ps:為什么要啰嗦上面一段不直接介紹泛型方法俄精,目的是為了讓讀者們想清楚為什么需要用這個询筏,而不僅僅學它的語法),這么寫泛型方法:

<T,E> T change(E item) {
    //省略具體實現(xiàn)
}

這么去調(diào)用

 Apple apple = new Apple();
 Orange orange = changeShop.<Orange,Apple>change(apple);

因為泛型的類型推斷功能竖慧,它會根據(jù)實例化的類型嫌套,自動推斷出聲明的泛型類型。

因此我們還可以省略<Orange,Apple>圾旨,這么寫:

Orange orange = changeShop.change(apple);

反之也是可以的踱讨,如果你聲明了泛型類型,實例化的泛型類型如果和聲明一樣就可以省略砍的。例如:

FruitShop<Fruit> changeShop = new FruitShop<Fruit>();
簡寫成 ==>
FruitShop<Fruit> changeShop = new FruitShop();

泛型方法一般只和方法有關(guān)聯(lián)痹筛,就是說它的泛型參數(shù)類型,和在類上的泛型參數(shù)沒有關(guān)系廓鞠。

泛型的嵌套和重復

泛型的重復

創(chuàng)建一個商店集合帚稠,并加上泛型T,這T就表示泛型的重復

class ShopList<T> extends ArrayList<T> {
     void sale(T item){}
}

當然這個概念本身并不重要床佳,兩個泛型T自身也不重要滋早,我們要關(guān)注的點是兩處T各自發(fā)揮作用的地方,比如 ShopList<T>Tsale()方法中表示出售的T物品砌们,而ArrayList<T>T杆麸,表示只能存儲或操作T元素

所以這里想表達的是搁进,它們重復的關(guān)系并不重要,更多的是要關(guān)注它們所發(fā)揮作用的那個類內(nèi)部使用它的地方到底干了啥昔头。

泛型的嵌套

泛型的嵌套拷获,顧名思義,泛型類型里面嵌套一個泛型類型

比如上面的商店集合是一種只賣蘋果的水果商店减细,就可以這么寫

class ShopList<T extends List<FruitShop<Apple>>> extends ArrayList<T> {
  void sale(T item){}  
}

仔細看ShopList的泛型類型匆瓜,變成了<T extends List<FruitShop<Apple>>>,它是一個"經(jīng)典的"泛型嵌套類型未蝌。這個嵌套是可以無窮盡的驮吱,不過實際場景我們很少會嵌套很多層。

Kotlin泛型

在你不了解Java泛型的時候萧吠,去看Kotlin泛型左冬,會很難理解。這也是為什么要花前面這么長的篇幅來介紹Java泛型纸型,而不是直接介紹拇砰。不過在你看完上面的內(nèi)容,再看Kotlin泛型狰腌,就非常容易理解了除破。

基本使用和Java泛型一樣,這里主要介紹一下Kotlin中如何使用泛型的協(xié)變和逆變

依然用上述使用的商店琼腔,改寫成Kotlin語法

class KotlinShop<T> {
    fun sale(): T {
        return null as T //此處不合理瑰枫,純屬為了方便編譯通過
    }
    fun buy(item: T) {
    }
}
  • 協(xié)變和可以借助out 替換 java中的 ? extends
  • 逆變可以借助in 替換 java中的 ? super

像這樣:

val outShop: KotlinShop<out KotlinApple> = KotlinShop()  //協(xié)變
val inShop: KotlinShop<in KotlinApple> = KotlinShop()    //逆變

那么逆變和協(xié)變在kotlin中是否有和java泛型一樣的特性呢

  • 協(xié)變只能讀不能寫入?
  • 逆變只能寫入部分讀丹莲?
val outSale = outShop.sale()  // 返回 KotlinApple
val inSale = inShop.sale()    //  返回 Any
outShop.buy(KotlinApple()) //編譯報錯
inShop.buy(KotlinApple())   //編譯通過

從上面的例子可以看出來光坝,確實是和java泛型一樣的。

后語

筆者定下了一個重學Android知識的博客計劃甥材,目的從更深入盯另,更全的層次去重新學習那些一知半解且非常重要的知識點,本篇博客是重學系列的第一篇洲赵,之后的內(nèi)容就慢慢更吧鸳惯,立個flage:至少月更一篇。
立flag的目的如果不是為了打臉板鬓,都將毫無意義[手動狗頭]

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末悲敷,一起剝皮案震驚了整個濱河市究恤,隨后出現(xiàn)的幾起案子俭令,更是在濱河造成了極大的恐慌,老刑警劉巖部宿,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抄腔,死亡現(xiàn)場離奇詭異瓢湃,居然都是意外死亡,警方通過查閱死者的電腦和手機赫蛇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門绵患,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悟耘,你說我怎么就攤上這事落蝙。” “怎么了暂幼?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵筏勒,是天一觀的道長。 經(jīng)常有香客問我旺嬉,道長管行,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任邪媳,我火速辦了婚禮捐顷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雨效。我一直安慰自己迅涮,他們只是感情好,可當我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布徽龟。 她就那樣靜靜地躺著逗柴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪顿肺。 梳的紋絲不亂的頭發(fā)上戏溺,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天,我揣著相機與錄音屠尊,去河邊找鬼旷祸。 笑死,一個胖子當著我的面吹牛讼昆,可吹牛的內(nèi)容都是我干的托享。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼浸赫,長吁一口氣:“原來是場噩夢啊……” “哼闰围!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起既峡,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤羡榴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后运敢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體校仑,經(jīng)...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡忠售,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了迄沫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稻扬。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖羊瘩,靈堂內(nèi)的尸體忽然破棺而出泰佳,到底是詐尸還是另有隱情,我是刑警寧澤尘吗,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布乐纸,位于F島的核電站,受9級特大地震影響摇予,放射性物質(zhì)發(fā)生泄漏汽绢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧剔氏,春花似錦、人聲如沸积仗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寂曹。三九已至,卻和暖如春回右,著一層夾襖步出監(jiān)牢的瞬間隆圆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工翔烁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留渺氧,地道東北人。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓蹬屹,卻偏偏與公主長得像侣背,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子慨默,可洞房花燭夜當晚...
    茶點故事閱讀 45,876評論 2 361