點(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)文章
- 《Java | 這是一篇全面的注解使用攻略(含 Kotlin)》
- 《Java | 反射:在運(yùn)行時(shí)訪問類型信息(含 Kotlin)》
- 《Java | 請(qǐng)概述一下 Class 文件的結(jié)構(gòu)》
- 《Java | 深入理解方法調(diào)用的本質(zhì)(含重載與重寫區(qū)別)》
目錄
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)鍵在于inline
和reified
狂巢,這兩者的語義是:
-
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í)就是說:Integer
是Number
的子類型核畴,問你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)力,我們下次見抗果!