泛型機(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
,說明泛型類型String
和Integer
都被擦除掉了馆里,只剩下原始類型
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)不了”的問題。
舉個例子,Java
到1.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.直接把已有的類型泛型化,讓所有需要泛型化的已有類型都原地泛型化姊氓,不添加任何平行于已有類型的泛型版丐怯。
.NET
在1.1 -> 2.0
的時(shí)候選擇了上面選項(xiàng)的1,而Java
則選擇了2翔横。
從Java
設(shè)計(jì)者的角度看读跷,這個取舍很明白。
.NET
在1.1 -> 2.0
的時(shí)候禾唁,實(shí)際的應(yīng)用代碼量還很少(相對Java
來說)效览,而且整個體系都在微軟的控制下,要做變更比較容易荡短;
而Java
在1.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
來獲取類型信息的
jdk
的Class
锭沟、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
。
也就是說java
的class
文件會保存繼承的父類或者接口的泛型信息呼寸。
所以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
讓我們通過一個典型的例子理解一下到底什么是Producer
和Consumer
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然Fruit
是Apple
的父類污它,但是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í)可以插入Apple
與Apple
的子類箍镜,因?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í)筆記慕趴!
本文如果對您有所幫助痪蝇,歡迎點(diǎn)贊,謝謝~