「Java 路線」| 泛型(含Kotlin)

點(diǎn)贊關(guān)注甘凭,不再迷路捶枢,你的支持對(duì)我意義重大沉噩!

?? Hi,我是丑丑柱蟀。本文 「Java 路線」| 導(dǎo)讀 —— 他山之石,可以攻玉 已收錄蚜厉,這里有 Android 進(jìn)階成長(zhǎng)路線筆記 & 博客长已,歡迎跟著彭丑丑一起成長(zhǎng)。(聯(lián)系方式在 GitHub)

前言

  • 泛型(Generic Type)無論在哪一門語言里昼牛,都是最難語法的存在术瓮,細(xì)節(jié)之繁雜、理解之困難贰健,令人切齒胞四;
  • 在這個(gè)系列里,我將總結(jié)Java & Kotlin中泛型的知識(shí)點(diǎn)伶椿,帶你從語法 & 原理全面理解泛型辜伟。追求簡(jiǎn)單易懂又不失深度,如果能幫上忙脊另,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注导狡!
  • 首先,嘗試回答這些面試中容易出現(xiàn)的問題:
1偎痛、下列代碼中旱捧,編譯出錯(cuò)的是:
public class MyClass<T> {
    private T t0; // 0
    private static T t1; // 1 
    private T func0(T t) { return t; } // 2
    private static T func1(T t) { return t; } // 3
    private static <T> T func2(T t) { return t; } // 4
}
2、泛型的存在是用來解決什么問題踩麦?
3枚赡、請(qǐng)說明泛型的原理,什么是泛型擦除機(jī)制谓谦,具體是怎樣實(shí)現(xiàn)的贫橙?

相關(guān)文章


目錄


1. 泛型基礎(chǔ)

  • 問:什么是泛型,有什么作用反粥?

答:在定義類料皇、接口和方法時(shí),可以附帶類型參數(shù)星压,使其變成泛型類践剂、泛型接口和泛型方法。與非泛型代碼相比娜膘,使用泛型有三大優(yōu)點(diǎn):更健壯(在編譯時(shí)進(jìn)行更強(qiáng)的類型檢查)逊脯、更簡(jiǎn)潔(消除強(qiáng)轉(zhuǎn),編譯后自動(dòng)會(huì)增加強(qiáng)轉(zhuǎn))竣贪、更通用(代碼可適用于多種類型)

  • 問:什么是類型擦除機(jī)制军洼?

答:泛型本質(zhì)上是 Javac 編譯器的一顆 語法糖巩螃,這是因?yàn)椋悍盒褪?JDK1.5 中引進(jìn)的新特性,為了 向下兼容匕争,Java 虛擬機(jī)和 Class 文件并沒有提供泛型的支持避乏,而是讓編譯器擦除 Code 屬性中所有的泛型信息,需要注意的是甘桑,泛型信息會(huì)保留在類常量池的屬性中拍皮。

  • 問:類型擦除的具體步驟?

答:類型擦除發(fā)生在編譯時(shí)跑杭,具體分為以下 3 個(gè)步驟:

  • 1:擦除所有類型參數(shù)信息铆帽,如果類型參數(shù)是有界的,則將每個(gè)參數(shù)替換為其第一個(gè)邊界德谅;如果類型參數(shù)是無界的爹橱,則將其替換為 Object
  • 2:(必要時(shí))插入類型轉(zhuǎn)換,以保持類型安全
  • 3:(必要時(shí))生成橋接方法以在子類中保留多態(tài)性

舉個(gè)例子:

源碼:
public class Parent<T> {
    public void func(T t){
    }
}

public class Child<T extends Number> extends Parent<T> {
    public T get() {
        return null;
    }
    public void func(T t){
    }
}

void test(){
    Child<Integer> child = new Child<>();
    Integer i = child.get();
}
---------------------------------------------------------
字節(jié)碼:
public class Parent {
    public void func(Object t){
    }
}

public class Child extends Parent {
    public Number get() {
        return null;
    }
    public void func(Number t) {
    }
    
    橋方法 - synthetic
    public void func(Object t){
        func((Number)t);
    }
}

void test() {
    Child<Integer> child = new Child();
    // 插入強(qiáng)制類型轉(zhuǎn)換
    Integer i = (Integer) child.get();
}

步驟1:Parent 中的類型參數(shù) T 被擦除為 Object窄做,而 Child 中的類型參數(shù) T 被擦除為 Number愧驱;

步驟2:child.get(); 插入了強(qiáng)制類型轉(zhuǎn)換

步驟3:在 Child 中生成橋方法,橋方法是編譯器生成的椭盏,所以會(huì)帶有 synthetic 標(biāo)志位冯键。為什么子類中需要增加橋方法呢,可以先思考這個(gè)問題:假如沒有橋方法庸汗,會(huì)怎么樣惫确?你可以看看下列代碼調(diào)用的是子類還是父類方法:

Parent<Integer> child = new Child<>();
Parent<Integer> parent = new Parent<>();
        
child.func(new Object()); // Parent#func(Object); 
parent.func(new Object()); // Parent#func(Object); 

這兩句代碼都會(huì)調(diào)用到 Parent#func(),如果你看過之前我寫過的一篇文章蚯舱,相信難不到你:《Java | 深入理解方法調(diào)用的本質(zhì)(含重載與重寫區(qū)別)》改化。在這里我簡(jiǎn)單分析下:

1、方法調(diào)用的本質(zhì)是根據(jù)方法的符號(hào)引用確定方法的直接引用(入口地址)

2枉昏、這兩句代碼調(diào)用的方法符號(hào)引用為:

child.func(1) => com/xurui/Child.func(Object)
parent.func(1) => com/xurui/Parent.func(Object)

3陈肛、這兩句方法調(diào)用的字節(jié)碼指令為 invokevirtual

4、類加載解析階段解析類的繼承關(guān)系兄裂,生成類的虛方法表

5句旱、調(diào)用階段(動(dòng)態(tài)分派):Child 沒有重寫 func(Object),所以 Child 的虛方法表中存儲(chǔ)的是Parent#func(Object)晰奖;Parent 的虛方法表中存儲(chǔ)的是Parent#func(Object);

可以看到谈撒,即使使用對(duì)象的實(shí)際類型為 Child ,這里調(diào)用的依舊是父類的方法匾南。這樣就失去了多態(tài)性啃匿。因此,才需要在泛型子類中添加橋方法。

  • 問:為什么擦除后溯乒,反編譯還是看到類型參數(shù) T 夹厌?
反編譯Parent.class,可以看到 T 裆悄,不是已經(jīng)擦除了嗎矛纹?

public class Parent<T> {
    public Parent() {
    }

    public void func(T t) {
    }
}

答:泛型中所謂的類型擦除,其實(shí)只是擦除Code 屬性中的泛型信息光稼,在類常量池屬性(Signature屬性或南、LocalVariableTypeTable屬性)中其實(shí)還保留著泛型信息,這也是在運(yùn)行時(shí)可以反射獲取泛型信息的根本依據(jù)钟哥,我在第 4 節(jié)說。

  • 問:泛型的限制 & 類型擦除會(huì)帶來什么影響瞎访?

由于類型擦除的影響腻贰,在運(yùn)行期是不清楚類型實(shí)參的實(shí)際類型的。為了避免程序的運(yùn)行結(jié)果與程序員語義不一致的情況扒秸,泛型在使用上存在一些限制播演。好處是類型擦除不會(huì)為每種參數(shù)化類型創(chuàng)建新的類,因此泛型不會(huì)增大內(nèi)存消耗伴奥。

泛型的限制

2. Kotlin的實(shí)化類型參數(shù)

前面我們提到写烤,由于類型擦除的影響,在運(yùn)行期是不清楚類型實(shí)參的實(shí)際類型的拾徙。例如下面的代碼是不合法的洲炊,因?yàn)?code>T并不是一個(gè)真正的類型,而僅僅是一個(gè)符號(hào):

在這個(gè)函數(shù)里尼啡,我們傳入一個(gè)List暂衡,企圖從中過濾出 T 類型的元素:

Java:
<T> List<T> filter(List list) {
    List<T> result = new ArrayList<>();
    for (Object e : list) {
        if (e instanceof T) { // compiler error
            result.add(e);
        }
    }
    return result;
}
---------------------------------------------------
Kotlin:
fun <T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
        if (e is T) { // cannot check for instance of erased type: T
            result.add(e)
        }
    }
    return result
}

Kotlin中,有一種方法可以突破這種限制崖瞭,即:帶實(shí)化類型參數(shù)的內(nèi)聯(lián)函數(shù)

Kotlin:
inline fun <reified T> filter(list: List<*>): List<T> {
    val result = ArrayList<T>()
    for (e in list) {
        if (e is T) {
            result.add(e)
        }
    }
    return result
}

關(guān)鍵在于inlinereified狂巢,這兩者的語義是:

  • inline(內(nèi)聯(lián)函數(shù)):Kotlin編譯器將內(nèi)聯(lián)函數(shù)的字節(jié)碼插入到每一次調(diào)用方法的地方
  • reified(實(shí)化類型參數(shù)):在插入的字節(jié)碼中,使用類型實(shí)參的確切類型代替類型實(shí)參

規(guī)則很好理解书聚,對(duì)吧唧领。很明顯,當(dāng)發(fā)生方法內(nèi)聯(lián)時(shí)雌续,方法體字節(jié)碼就變成了:

調(diào)用:
val list = listOf("", 1, false)
val strList = filter<String>(list)
---------------------------------------------------
內(nèi)聯(lián)后:
val result = ArrayList<String>()
for (e in list) {
    if (e is String) {
        result.add(e)
    }
}

需要注意的是斩个,內(nèi)聯(lián)函數(shù)整個(gè)方法體字節(jié)碼會(huì)被插入到調(diào)用位置,因此控制內(nèi)聯(lián)函數(shù)體的大小驯杜。如果函數(shù)體過大萨驶,應(yīng)該將不依賴于T的代碼抽取到單獨(dú)的非內(nèi)聯(lián)函數(shù)中。

注意艇肴,無法從 Java 代碼里調(diào)用帶實(shí)化類型參數(shù)的內(nèi)聯(lián)函數(shù)

實(shí)化類型參數(shù)的另一個(gè)妙用是代替 Class 對(duì)象引用腔呜,例如:

fun Context.startActivity(clazz: Class<*>) {
    Intent(this, clazz).apply {
        startActivity(this)
    }
}

inline fun <reified T> Context.startActivity() {
    Intent(this, T::class.java).apply {
        startActivity(this)
    }
}

調(diào)用方:
context.startActivity(MainActivity::class.java)
context.startActivity<MainActivity>() // 第二種方式會(huì)簡(jiǎn)化一些

3. 變型:協(xié)變 & 逆變 & 不變

變型(Variant)描述的是相同原始類型的不同參數(shù)化類型之間的關(guān)系叁温。說起來有點(diǎn)繞,其實(shí)就是說:IntegerNumber的子類型核畴,問你List<Integer>是不是List<Number>的子類型膝但?

變型的種類具體分為三種:協(xié)變型 & 逆變型 & 不變型

  • 協(xié)變型(covariant):子類型關(guān)系被保留
  • 逆變型(contravariant):子類型關(guān)系被翻轉(zhuǎn)
  • 不變型(invariant):子類型關(guān)系被消除

在 Java 中,類型參數(shù)默認(rèn)是不變型的谤草,例如:

List<Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // compiler error

相比之下跟束,數(shù)組是支持協(xié)變型的:

Number[] nums;
Integer[] ints = new Integer[10]; 
nums = ints; // OK 協(xié)變,子類型關(guān)系被保留

那么丑孩,當(dāng)我們需要將List<Integer>類型的對(duì)象冀宴,賦值給List<Number>類型的引用時(shí),應(yīng)該怎么做呢温学?這個(gè)時(shí)候我們需要限定通配符

  • <? extends> 上界通配符

要想類型參數(shù)支持協(xié)變略贮,需要使用上界通配符,例如:

List<? extends Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK

但是這會(huì)引入一個(gè)編譯時(shí)限制:不能調(diào)用參數(shù)包含類型參數(shù) E 的方法仗岖,也不能設(shè)置類型參數(shù)的字段逃延,簡(jiǎn)單來說,就是只能訪問不能修改(非嚴(yán)格):

// ArrayList.java
public boolean add(E e) {
    ...
}

l1.add(1); // compiler error
  • <? super> 下界通配符

要想類型參數(shù)支持逆變轧拄,需要使用下界通配符揽祥,例如:

List<? super Integer> l1;
List<Number> l2 = new ArrayList<>();
l1 = l2; // OK

同樣,這也會(huì)引入一個(gè)編譯時(shí)限制檩电,但是與協(xié)變相反:不能調(diào)用返回值為類型參數(shù)的方法拄丰,也不能訪問類型參數(shù)的字段,簡(jiǎn)單來說俐末,就是只能修改不能訪問(非嚴(yán)格):

// ArrayList.java
public E get(int index) {
    ...
}

Integer i = l1.get(0); // compiler error
  • <?> 無界通配符

<?>其實(shí)很簡(jiǎn)單愈案,很多資料其實(shí)都解釋得過于復(fù)雜了。<?> 其實(shí)就是 <? extends Object>的縮寫鹅搪,就是這樣站绪,沒了,例如:

List<?> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK

理解了這點(diǎn)丽柿,這個(gè)問題就很好回答了:

  • 問:List 與 List<?>有什么區(qū)別恢准?

答:List 是原生類型,可以添加或訪問元素甫题,不具備編譯期安全性馁筐,而 List<?> 其實(shí)是 List<? extends Object>的縮寫,是協(xié)變型的(可引出協(xié)變型的特點(diǎn)與限制)坠非;從語義上敏沉,List<?> 表明使用者清楚變量是類型安全的,而不是因?yàn)槭韬龆褂昧嗽愋?List。

泛型代碼的設(shè)計(jì)盟迟,應(yīng)遵循PECS原則(Producer extends Consumer super):

  • 如果只需要獲取元素秋泳,使用 <? extends T>
  • 如果只需要存儲(chǔ),使用<? super T>

舉例:

// Collections.java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
}

在 Kotlin 中攒菠,變型寫法會(huì)有些不同迫皱,但是語義是完全一樣的:

協(xié)變:
val l0: MutableList<*> 相當(dāng)于MutableList<out Any?>
val l1: MutableList<out Number>
val l2 = ArrayList<Int>()
l0 = l2 // OK
l1 = l2 // OK
---------------------------------------------------
逆變:
val l1: MutableList<in Int>
val l2 = ArrayList<Number>()
l1 = l2 // OK

另外,Kotlin 的in & out不僅僅可以用在類型實(shí)參上辖众,還可以用在泛型類型聲明的類型參數(shù)上卓起。其實(shí)這是一種簡(jiǎn)便寫法,表示類設(shè)計(jì)者知道類型參數(shù)在整個(gè)類上只能協(xié)變或逆變凹炸,避免在每個(gè)使用的地方增加戏阅,例如 Kotlin 的List被設(shè)計(jì)為不可修改的協(xié)變型:

public interface List<out E> : Collection<E> {
    ...
}

注意:在 Java 中,只支持使用點(diǎn)變型啤它,不支持 Kotlin 類似的聲明點(diǎn)變型

小結(jié)一下:


4. 使用反射獲取泛型信息

前面提到了奕筐,編譯期會(huì)進(jìn)行類型擦除,Code 屬性中的類型信息會(huì)被擦除蚕键,但是在類常量池屬性(Signature屬性救欧、LocalVariableTypeTable屬性)中還保留著泛型信息衰粹,因此我們可以通過反射來獲取這部分信息锣光。

獲取泛型類型實(shí)參:需要利用Type體系

4.1 獲取泛型類 & 泛型接口聲明

TypeVariable
ParameterizedType
GenericArrayType
WildcardType

Gson TypeToken

Editting....


5. 總結(jié)

  • 應(yīng)試建議
    • 1、第 1 節(jié)非常非常重點(diǎn)铝耻,著重記憶:泛型的本質(zhì)和設(shè)計(jì)緣由誊爹、泛型擦除的三個(gè)步驟、限制和優(yōu)點(diǎn)瓢捉,已經(jīng)總結(jié)得很精華了频丘,希望能幫到你;
    • 2泡态、著重理解變型(Variant)的概念搂漠,以及各種限定符的含義;
    • 3某弦、Kotlin 相關(guān)的部分桐汤,作為知識(shí)積累和思路擴(kuò)展為主,非應(yīng)試重點(diǎn)靶壮。

參考資料

  • 《Kotlin實(shí)戰(zhàn)》 (第9怔毛、10章)—— [俄] Dmitry Jemerov,Svetlana Isakova 著
  • 《Java編程思想》 (第19腾降、20拣度、23章)—— [美] Bruce Eckel 著
  • 《深入理解Java虛擬機(jī)(第3版本)》(第10章)—— 周志明 著

創(chuàng)作不易,你的「三連」是丑丑最大的動(dòng)力,我們下次見抗果!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筋帖,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子窖张,更是在濱河造成了極大的恐慌幕随,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件宿接,死亡現(xiàn)場(chǎng)離奇詭異赘淮,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)睦霎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門梢卸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人副女,你說我怎么就攤上這事蛤高。” “怎么了碑幅?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵戴陡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我沟涨,道長(zhǎng)恤批,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任裹赴,我火速辦了婚禮喜庞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棋返。我一直安慰自己延都,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布睛竣。 她就那樣靜靜地躺著晰房,像睡著了一般。 火紅的嫁衣襯著肌膚如雪射沟。 梳的紋絲不亂的頭發(fā)上殊者,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音躏惋,去河邊找鬼幽污。 笑死,一個(gè)胖子當(dāng)著我的面吹牛簿姨,可吹牛的內(nèi)容都是我干的距误。 我是一名探鬼主播簸搞,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼准潭!你這毒婦竟也來了趁俊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤刑然,失蹤者是張志新(化名)和其女友劉穎寺擂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泼掠,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怔软,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了择镇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挡逼。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖腻豌,靈堂內(nèi)的尸體忽然破棺而出家坎,到底是詐尸還是另有隱情,我是刑警寧澤吝梅,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布虱疏,位于F島的核電站,受9級(jí)特大地震影響苏携,放射性物質(zhì)發(fā)生泄漏做瞪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一兜叨、第九天 我趴在偏房一處隱蔽的房頂上張望穿扳。 院中可真熱鬧衩侥,春花似錦国旷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至峦萎,卻和暖如春屡久,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爱榔。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工被环, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人详幽。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓筛欢,卻偏偏與公主長(zhǎng)得像浸锨,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子版姑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353