但是很抱歉,由于種種原因,Java 并不支持。但是,Java 并不是完全抹殺了泛型的型變特性,Java 提供了 extends T> 和 super T> 使泛型擁有協(xié)變和逆變的特性。
extends T> 與 super T>
extends T> 稱為上界通配符, super T> 稱為下界通配符。使用上界通配符可以使泛型協(xié)變,而使用下界通配符可以使泛型逆變。
比如之前舉的例子
List l = new ArrayList<>();
List
如果使用上界通配符,
List l = new ArrayList<>();
List extends Object> o = l;// 可以通過編譯復(fù)制代碼
這樣,List extends Object> 的類型就大于 List 的類型了,也就實(shí)現(xiàn)了協(xié)變。這也就是所謂的“子類的泛型是泛型的子類”。
同樣,下界通配符 super T> 可以實(shí)現(xiàn)逆變,如:
public List super Integer> fun(){
List
上述代碼怎么就實(shí)現(xiàn)逆變了呢?首先,Object > Integer;另外,從前言我們知道,函數(shù)返回值類型必須大于實(shí)際返回值類型,在這里就是 List super Integer> > List,和 Object > Integer 剛好相反。也就是說,經(jīng)過泛型變化后,Object 和 Integer 的類型關(guān)系翻轉(zhuǎn)了,這就是逆變,而實(shí)現(xiàn)逆變的就是下界通配符 super T>。
從上面可以看出, extends T> 中的上界是 T,也就是說 extends T> 所泛指的類型都是 T 的子類或 T 本身,所以 T 大于 extends T> 。 super T> 中的下界是 T,也就是說 super T> 所泛指的類型都是 T 的父類或 T 本身,所以 super T> 大于 T。
代碼 (3) 編譯通過,它把 String 類型賦值給 super String>, super String> 泛指 String 的父類或 String,所以這是可以通過編譯的。
代碼 (4) 編譯報(bào)錯(cuò),因?yàn)樗鼑L試把 super String> 賦值給 String,而 super String> 大于 String,所以不能賦值。事實(shí)上,編譯器完全不知道該用什么類型去接受 c.get() 的返回值,因?yàn)樵诰幾g器眼里 super String> 是一個(gè)泛指的類型,所有 String 的父類和 String 本身都有可能。
同樣從上面代碼可以看出,對(duì)于使用了 super T> 的類型,是不能讀取元素的,不然就會(huì)像代碼 (4) 處一樣編譯報(bào)錯(cuò)。但是可以寫入元素,比如代碼 (3)。該類型只能寫入元素,這就是所謂的“消費(fèi)者”,即只能寫入元素的就是消費(fèi)者,消費(fèi)者就使用 super T> 通配符。
綜上,這就是 PECS 原則。
kotlin 中的協(xié)變與逆變
kotlin 拋棄了 Java 中的通配符,轉(zhuǎn)而使用了聲明處型變與類型投影。
聲明處型變
首先讓我們回頭看看 Container 的定義:
public class Container { private T item; public void set(T t) {
item = t;
} public T get() { return item;
}
}復(fù)制代碼
class Container { // (1)
private var item: T? = null
fun get(): T? = item
}
val c: Container = Container()// (2)編譯通過,因?yàn)?T 是一個(gè) out-參數(shù)復(fù)制代碼
(1) 處直接使用 指定 T 類型只能出現(xiàn)在生產(chǎn)者的位置上。雖然多了一些限制,但是,在 kotlin 編譯器在知道了 T 的角色以后,就可以像 (2) 處一樣將 Container 直接賦值給 Container,好像泛型直接可以協(xié)變了一樣,而不需要再使用 Java 當(dāng)中的通配符 extends String>。
同樣的,對(duì)于消費(fèi)者來說,
class Container { // (1)
private var item: T? = null
fun set(t: T) {
item = t
}
}val c: Container = Container() // (2) 編譯通過,因?yàn)?T 是一個(gè) in-參數(shù)復(fù)制代碼
代碼 (1) 處使用 指定 T 類型只能出現(xiàn)在消費(fèi)者的位置上。代碼 (2) 可以編譯通過, Any > String,但是 Container 可以被 Container 賦值,意味著 Container 大于 Container ,即它看上去就像 T 直接實(shí)現(xiàn)了泛型逆變,而不需要借助 super String> 通配符來實(shí)現(xiàn)逆變。如果是 Java 代碼,則需要寫成 Container super String> c = new Container(); 。
這就是聲明處型變,在類聲明的時(shí)候使用 out 和 in 關(guān)鍵字,在使用時(shí)可以直接寫出泛型型變的代碼。
有時(shí)一個(gè)類既可以作生產(chǎn)者又可以作消費(fèi)者,這種情況下,我們不能直接在 T 前面加 in 或者 out 關(guān)鍵字。比如:
class Container { private var item: T? = null
fun set(t: T?) {
item = t
} fun get(): T? = item
}復(fù)制代碼
考慮這個(gè)函數(shù):
fun copy(from: Container, to: Container) {
to.set(from.get())
}復(fù)制代碼
當(dāng)我們實(shí)際使用該函數(shù)時(shí):
val from = Container()val to = Container()
copy(from, to) // 報(bào)錯(cuò),from 是 Container 類型,而 to 是 Container 類型復(fù)制代碼
這樣使用的話,編譯器報(bào)錯(cuò),因?yàn)槲覀儼褍蓚€(gè)不一樣的類型做了賦值。用 kotlin 官方文檔的話說,copy 函數(shù)在”干壞事“, 它嘗試寫一個(gè) Any 類型的值給 from, 而我們用 Int 類型來接收這個(gè)值,如果編譯器不報(bào)錯(cuò),那么運(yùn)行時(shí)將會(huì)拋出一個(gè) ClassCastException 異常。
所以應(yīng)該怎么辦?直接防止 from 被寫入就可以了!
將 copy 函數(shù)改為如下所示:
fun copy(from: Container, to: Container) { // 給 from 的類型加了 out
to.set(from.get())
}val from = Container()val to = Container()
copy(from, to) // 不會(huì)再報(bào)錯(cuò)了復(fù)制代碼
同理,如果 from 的泛型是用 in 來修飾的話,則 from 只能被當(dāng)作消費(fèi)者使用,它只能調(diào)用 set() 方法,上述代碼就會(huì)報(bào)錯(cuò):
fun copy(from: Container, to: Container) { // 給 from 的類型加了 in
to.set(from.get())
}val from = Container()val to = Container()
copy(from, to) // 報(bào)錯(cuò)復(fù)制代碼
其實(shí)從上面可以看到,類型投影和 Java 的通配符很相似,也是一種使用時(shí)型變。
為什么要這么設(shè)計(jì)?
為什么 Java 的數(shù)組是默認(rèn)型變的,而泛型默認(rèn)不型變呢?其實(shí) kolin 的泛型默認(rèn)也是不型變的,只是使用 out 和 in 關(guān)鍵字讓它看起來像泛型型變。
// Illegal code - because otherwise life would be BadList dogs = new List();
List animals = dogs; // Awooga awoogaanimals.add(new Cat());// (1)Dog dog = dogs.get(0); //(2) This should be safe, right?復(fù)制代碼
如果上述代碼可以通過編譯,即 List 可以賦值給 List,List 是協(xié)變的。接下來往 List 中 add 一個(gè) Cat(),如代碼 (1) 處。這樣就有可能造成代碼 (2) 處的接收者 Dog dog 和 dogs.get(0) 的類型不匹配的問題。會(huì)引發(fā)運(yùn)行時(shí)的異常。所以 Java 在編譯期就要阻止這種行為,把泛型設(shè)計(jì)為默認(rèn)不型變的。
總結(jié)
1、Java 泛型默認(rèn)不型變,所以 List 不是 List 的子類。如果要實(shí)現(xiàn)泛型型變,則需要 extends T> 與 super T> 通配符,這是一種使用處型變的方法。使用 extends T> 通配符意味著該類是生產(chǎn)者,只能調(diào)用 get(): T 之類的方法。而使用 super T> 通配符意味著該類是消費(fèi)者,只能調(diào)用 set(T t)、add(T t) 之類的方法。
2、Kotlin 泛型其實(shí)默認(rèn)也是不型變的,只不過使用 out 和 in 關(guān)鍵字在類聲明處型變,可以達(dá)到在使用處看起來像直接型變的效果。但是這樣會(huì)限制類在聲明時(shí)只能要么作為生產(chǎn)者,要么作為消費(fèi)者。
使用類型投影可以避免類在聲明時(shí)被限制,但是在使用時(shí)要使用 out 和 in 關(guān)鍵字指明這個(gè)時(shí)刻類所充當(dāng)?shù)慕巧窍M(fèi)者還是生產(chǎn)者。類型投影也是一種使用處型變的方法。