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