背景
平時(shí)在看一些開源框架源碼時(shí)總發(fā)現(xiàn)他們會(huì)或多或少的用到泛型來定義數(shù)據(jù)類型瘫镇。這可以理解鼎兽,畢竟牛逼的開源框架大都是為了解決一類普遍問題而存在的;但看不懂的是铣除,有時(shí)參數(shù)或者返回值會(huì)出現(xiàn)諸如<? extends T>
和<? super T>
這樣帶通配符的泛型參數(shù),這種通配符的泛型是什么意思鹦付?如果直接用指定的T
會(huì)有什么問題尚粘?這樣做是為了解決什么問題?這是我的疑惑敲长。咨詢公司完全做Java開發(fā)的服務(wù)端同學(xué)后郎嫁,也未能完全解惑。于是查找資料后引出今天的主題----Java泛型的協(xié)變(<? extends T>
)祈噪、逆變(<? super T>
)和不變(T
)泽铛。
舉例
- RxJava框架
在定義一個(gè)Observable
后,最終會(huì)通過subscribe()
來訂閱一個(gè)Observer
, 而subscribe()
參數(shù)的定義就使用了逆變(<? super T>
)辑鲤,如下所示:
/**
* 參數(shù)observer用到了逆變<? super T>
*/
public final void subscribe(Observer<? super T> observer) {
try {
//....
subscribeActual(observer); //實(shí)際發(fā)起的訂閱
//...方法
} catch (Throwable e) {
//...
}
}
map
操作符的參數(shù)Function
泛型分別使用協(xié)變和逆變實(shí)現(xiàn)盔腔,如下所示:
/**
* 參數(shù)mapper是一個(gè)Function接口類型,第一個(gè)參數(shù)用到了逆變<? super T>,
* 第二個(gè)參數(shù)用到了協(xié)變<? extends R>
*/
public final <R> Observable<R> map(Function<? super T, ? extends R> mapper) {
//...
return RxJavaPlugins.onAssembly(new ObservableMap<T, R>(this, mapper));
}
- java集合框架
Collections
的工具方法copy()
分別使用協(xié)變和逆變定義了兩個(gè)集合的類型弛随,如下所示:
/**
* 目的列表使用的是逆變瓢喉,源列表使用的是協(xié)變
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
//...
for (int i=0; i<srcSize; i++) {
dest.set(i, src.get(i));
}
//...
}
3.java8中Stream
的超級接口collect()
,其參數(shù)定義使用了不變和逆變舀透,如下所示:
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
面對上面這些源碼的定義栓票,不禁讓人產(chǎn)生疑惑!c倒弧走贪!
概念
假設(shè)Orange
類是Fruit
類的子類,以集合類List<T>
為例:
- 型變:用來描述類型轉(zhuǎn)換后的繼承關(guān)系(即協(xié)變惑芭、逆變和不變的統(tǒng)稱)坠狡。比如:
List<Orange>
是List<Fruit>
的子類型嗎?答案是No强衡,兩者并沒有關(guān)系员萍,并不能相互讀寫數(shù)據(jù)绪商。因此,型變是處理如List<Orange>
(List<? extends Orange>
)和List<Fruit>
子類型關(guān)系的一種表達(dá)方式。 - 協(xié)變(covariance):滿足條件諸如
List<Orange>
是List<? extends Fruit>
的子類型時(shí)壁肋,稱為協(xié)變。 - 逆變(covariance):滿足條件
List<Fruit>
是List<? super Orange>
的子類型時(shí)驱显,稱為逆變拆祈。 - 不變(invariance):表示
List<Orange>
和List<Fruit>
不存在型變關(guān)系。
注:子類(subclass)和子類型(subtype)不是同一個(gè)概念究飞。
先回答文章開頭提出的幾個(gè)問題:
帶通配符的泛型是什么意思置谦?
----這是因?yàn)镴ava泛型本身不支持型變,因此引入通配符來解決泛型類型的類型轉(zhuǎn)換問題亿傅,這是Java型變的通用表達(dá)式(<? extends T>
表示類型轉(zhuǎn)換的上界媒峡;<? super T>
表示類型轉(zhuǎn)換的下界)。如果直接用指定的T會(huì)有什么問題葵擎?
----直接使用T作為參數(shù)的類型不會(huì)有任何問題谅阿,但這會(huì)限制函數(shù)接口調(diào)用的靈活性,導(dǎo)致框架的通用性降低酬滤。這樣做是為了解決什么問題签餐?
----綜上兩點(diǎn),型變處理的最終目的是在保證了運(yùn)行時(shí)類型安全的基礎(chǔ)上盯串,并提高參數(shù)類型的靈活性氯檐。
型變
看一個(gè)不使用型變的例子:
/**
* 1.定義一個(gè)String類型的List
*/
List<String> value1 = new ArrayList<String>();
/**
* 2. 這里編譯器報(bào)錯(cuò),因?yàn)閮烧邲]有型變關(guān)系,無法直接賦值体捏,后續(xù)操作會(huì)導(dǎo)致類型不安全
*/
List<Object> value2 = value1; //error
/**
* 3.假如上面第2步編譯通過了冠摄,那么此時(shí)add()這個(gè)整型數(shù)據(jù)1到value2中
* 是沒問題的糯崎,因?yàn)樗念愋褪荗bject。但是讀取時(shí)會(huì)碰到困難耗拓,如第4步拇颅。
*/
value2.add(1);
/**
* 4.但此時(shí)讀出來的是什么類型呢,上一步add了一個(gè)整型數(shù)據(jù)1,此時(shí)如果用String類
* 型的變量接返回值乔询,肯定不合適樟插,因此運(yùn)行時(shí)會(huì)報(bào)類型轉(zhuǎn)換異常!竿刁!
*/
String result = value1.get(0); //error
上面舉例說明了在不使用型變的情況下黄锤,對泛型數(shù)據(jù)的操作會(huì)面臨種種困難,雖然保證了運(yùn)行時(shí)參數(shù)類型的安全食拜,卻限制了接口的靈活性(編譯器檢查)鸵熟,比如:如果我們只調(diào)用value2
(List<Object>
)的get()
方法,不調(diào)用add()
方法(只讀取數(shù)據(jù)不寫入數(shù)據(jù))负甸,顯然此時(shí)不會(huì)有類型的安全問題流强,那如何限制只能調(diào)用get()
卻不能add()
方法呢?當(dāng)然只能靠編譯器限制了呻待,讓你調(diào)add()
方法的時(shí)候編譯都通不過就可以了打月。通配符就是干這件事的,通知編譯器蚕捉,限制我們對于某些方法的調(diào)用奏篙,以保證運(yùn)行時(shí)的類型安全。
協(xié)變
對于上面不型變的例子迫淹,我們可以做如下調(diào)整秘通,就可以達(dá)到協(xié)變的目的:
/**
* 2. 這里編譯器不會(huì)報(bào)錯(cuò)
*/
List<? extends String> value2 = value1;
/**
* 3.但此處編譯器報(bào)錯(cuò)了,編譯器限制了寫入數(shù)據(jù)的操作
*/
value2.add(1); //error
但上面的簡單例子太過簡單敛熬,缺少繼承關(guān)系肺稀,不能明顯說明問題,下面仍以Orange
類是Fruit
類的子類來舉例說明:
/**
* 1.定義一個(gè)類型上界限定為Fruit的List应民,即協(xié)變
*/
List<? extends Fruit> fruits = new ArrayList<>();
/**
* 2.編譯器報(bào)錯(cuò)盹靴,不能添加任何類型的數(shù)據(jù)
* 原因是:
* List.add(T t)函數(shù)通過上面的類型指定后,參數(shù)會(huì)變成
* <? extends Fruit>,從這個(gè)參數(shù)中瑞妇,編譯器無法知道需要哪個(gè)具體的Fruit子類型,
* Orange梭冠、Banana甚至Fruit都可以辕狰,因此,為了保證類型安全控漠,編譯器拒絕任何類型蔓倍。
*/
//fruits.add(new Orange());
//fruits.add(new Fruit());
//fruits.add(new Object());
/**
* 3.此處正常P! 由于我們定義是指定了上界為Fruit偶翅,因此此處的返回值肯定至少是Fruit類型默勾,
* 而基類型可以引用子類型
*/
Fruit f = fruits.get(0);
通過上面代碼的注釋可以看出,協(xié)變限制了參數(shù)中帶T的方法調(diào)用聚谁,比如上面的add(T t)
方法(我們稱之為消費(fèi)者方法)母剥,而允許生產(chǎn)者方法的調(diào)用如T get(int position)
,以此來保證類型的安全形导。
逆變
協(xié)變的反方向是逆變环疼,在協(xié)變中我們可以安全地從泛型類中讀取(從一個(gè)方法中返回)朵耕,而在逆變中我們可以安全地向泛型類中寫入(傳遞給一個(gè)方法)炫隶。
/**
* 1.定義一個(gè)Object的List,作為原始數(shù)據(jù)列表
*/
List<Object> objects = new ArrayList<>();
objects.add(new Object()); //添加數(shù)據(jù)沒有問題
objects.add(new Orange()); //仍然沒有問題,
/**
* 2.定義一個(gè)類型下界限定為Fruit的List,并將objects賦值給它阎曹。
* 此時(shí)編譯不會(huì)報(bào)錯(cuò)伪阶,因?yàn)闈M足逆變的條件。
*/
List<? super Fruit> fruits = objects;
/**
* 3.add(T t)函數(shù)处嫌,編譯器不會(huì)報(bào)錯(cuò)栅贴,因?yàn)閒ruits接受Fruit的基類類型,
* 而該類型可以引用其子類型(多態(tài)性)
*/
fruits.add(new Orange());
fruits.add(new Fruit());
fruits.add(new RedApple());
/**
* 4.此處編譯器報(bào)錯(cuò)锰霜,因?yàn)閒ruits限定的是下界是Friut類型,因此筹误,
* 編譯器并不知道確切的類型是什么,沒法找到一個(gè)合適的類型接受返回值
*/
Fruit f = fruits.get(0);
/**
* 5.此處不會(huì)報(bào)錯(cuò)癣缅,因?yàn)镺bject是Fruit的最頂層基類厨剪,滿足下界的限定
*/
//Object obj = fruits.get(0);
通過上面代碼的注釋可以看出,逆變限制了讀取方法的調(diào)用友存,比如上面的T get(int position)
方法(我們稱之為生產(chǎn)者方法)祷膳,而允許消費(fèi)者方法的調(diào)用如add(T t)
,依次來保證類型的安全屡立。
總結(jié)
extends限定了通配符類型的上界直晨,所以我們可以安全地從其中讀取膨俐;而super限定了通配符類型的下界勇皇,所以我們可以安全地向其中寫入。
我們把那些只能從中讀取的對象稱為生產(chǎn)者(Producer)焚刺,我們可以從生產(chǎn)者中安全地讀攘舱;只能寫入的對象稱為消費(fèi)者(Consumer)乳愉。
因此這里就是著名的PECS原則:Producer-Extends, Consumer-Super兄淫。
源碼分析實(shí)戰(zhàn)
結(jié)合上文總結(jié)的PECS原則屯远,來看文章開頭提到的框架源碼(這里就不貼重復(fù)的源碼了),不難看出其含義了:
- RxJava中的
subscribe(Observer<? super T> observer)
函數(shù)由于并沒有返回T類型的數(shù)據(jù)捕虽,因此是一個(gè)消費(fèi)者方法慨丐,根據(jù)PECS原則,此處參數(shù)應(yīng)使用逆變來提高靈活性泄私。 - RxJava的
map
操作符函數(shù)map(Function<? super T, ? extends R> mapper)
房揭,他最終的調(diào)用在MapObserver
類中的onNext()
中執(zhí)行R v = mapper.apply(t)
,仍根據(jù)PECS原則挖滤,T僅做為傳入?yún)?shù)類型崩溪,因此是個(gè)消費(fèi)者參數(shù),可以使用逆變斩松;而R僅在返回值中出現(xiàn)伶唯,因此是個(gè)生產(chǎn)者參數(shù),可以使用協(xié)變惧盹,來保證類型類型安全乳幸。 - java集合框架
Collections
的工具方法copy(List<? super T> dest, List<? extends T> src)
,它具體的實(shí)現(xiàn)是如下:
for (int i=0; i<srcSize; i++) {
dest.set(i, src.get(i));
}
可以看出,src
調(diào)用了get(i)
,這是一個(gè)生產(chǎn)者的過程钧椰,因此這里使用了協(xié)變參數(shù)粹断;而dest
調(diào)用了set(i, t)
,這是一個(gè)消費(fèi)者的過程嫡霞,因此這里使用了逆變參數(shù)瓶埋。
數(shù)組的協(xié)變
Java中數(shù)組是協(xié)變的:可以向子類型的數(shù)組賦予基類型的數(shù)組引用,由于數(shù)組在Java中是完全定義的诊沪,因此內(nèi)建了編譯期和運(yùn)行時(shí)的檢查养筒,具體參見如下代碼注釋:
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
/**
* 創(chuàng)建了一個(gè)Apple數(shù)組,并將其賦值給一個(gè)Fruit數(shù)組引用,編譯器和運(yùn)行時(shí)都允許
*/
Fruit[] fruits = new Apple[10];
/**
* 將子類對象放置到父類數(shù)組中,編譯器和運(yùn)行時(shí)都允許
*/
fruits[0] = new Apple();
fruits[1] = new Jonathan();
try {
/**
* 將Apple的父類對象放置到子類數(shù)組中端姚,編譯器允許晕粪,但運(yùn)行時(shí)檢查拋出異常
*/
fruits[2] = new Fruit();
} catch (Exception e) {
Log.i(TAG, "array exception!", e);
}
try {
/**
* 將Apple的兄弟對象放置到數(shù)組中,編譯器允許渐裸,但運(yùn)行時(shí)檢查拋出異常
*/
fruits[3] = new Orange();
} catch (Exception e) {
Log.i(TAG, "array exception!", e);
}
自限定與協(xié)變
Java中一個(gè)常見的自限定寫法是:
class Base<T extends Base<T>> {
T element;
T get() {
return element;
}
void set(T t) {
element = t;
}
}
這種語法定義了一個(gè)基類巫湘,這個(gè)基類能夠使用子類作為其參數(shù)、返回類型昏鹃、作用域尚氛。
- 協(xié)變參數(shù)類型
在非泛型代碼中,參數(shù)類型不能隨子類型發(fā)生變化洞渤。方法只能重載不能重寫怠褐。在使用自限定類型時(shí),方法接受子類型而不是基類型為參數(shù):
/**
* 自限定協(xié)變參數(shù)類型
* 方法接受只能接受子類型而不是基類型為參數(shù)
* @param <T>
*/
interface SetInterface<T extends SetInterface<T>> {
void set(T arg);
}
/**
* 具體的子類型
* 避免重寫基類的方法
*/
interface SubSetInterface extends SetInterface<SubSetInterface> {}
public void test5(SubSetInterface s1, SubSetInterface s2, SetInterface sb) {
/**
* 編譯通過
*/
s1.set(s2);
/**
* 只能接受具體的子類型您宪,不能接受SetInterface基類型
*/
//s1.set(sb); //error
}
- 協(xié)變返回類型
繼承自限定基類的子類奈懒,將產(chǎn)生確切的子類型作為其返回值.不過,這種實(shí)現(xiàn)java的多態(tài)性已經(jīng)可以達(dá)到目的(基類引用子類):
/**
* 自限定協(xié)變返回類型
* @param <T>
*/
interface GetInterface<T extends GetInterface<T>> {
T get();
}
/**
* 具體的子類型
* 避免重寫基類的方法
*/
interface SubGetInterface extends GetInterface<SubGetInterface> {}
public void test4(SubGetInterface g) {
GetInterface s1 = g.get();
SubGetInterface s2 = g.get();
}
參考文檔
http://www.reibang.com/p/0c2948f7e656
https://www.cnblogs.com/en-heng/p/5041124.html
http://www.reibang.com/p/2bf15c5265c5