前言
不知道讀者們平時使用泛型多不多,自認為對泛型了解多少呢?本文筆者帶你重學一下泛型,不只從語法的角度绸吸,盡可能從本質(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();
這行代碼我們就很好理解了,它就變成了一個不受限制的普通容器香嗓,你想往里面加什么都可以迅腔。
為什么需要泛型類型擦除呢?
-
主要是因為兼容性,因為泛型是JDK 5 中引入的一個新特性靠娱,如果沒有泛型擦除沧烈,就會有兩種不同的類型,比如
List<String>
和List
像云。假設(shè)只用新版本的JVM锌雀,可以定義規(guī)則,把兩者視為不同的類型苫费,但對于舊版本的JVM就無法辨識這兩種類型汤锨,編譯就會出現(xiàn)問題双抽。所以為了兼容舊版本JVM百框,就只能通過泛型擦除將其變成一種類型。 - 另外還有一個原因是因為性能牍汹,如果泛型不擦除铐维,就意味著要增加泛型化后的類來支持,內(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>
的T
在sale()
方法中表示出售的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的目的如果不是為了打臉板鬓,都將毫無意義[手動狗頭]