在更深入的了解之前,讓我們先從一些例子看起:
讓我們先寫一個簡單的泛型類:
class Box<T>(t: T) {
private var value = t
}
open class Animal
//繼承Animal的Dog
open class Dog : Animal()
fun test(){
var animal = Animal()
var boxAnimal = Box<Animal>(animal)
var dog = Dog()
var boxDog = Box<Dog>(dog)
}
代碼很簡單,代碼邏輯沒有問題,編譯也能通過。但是如果把boxDog賦值給 boxAnimal 呢?是不是也能通過呢?見下圖:

編譯不通過,報錯
Type mismatch Required: Box<Animal> Found: Box<Dog>,也就是說Box<Dog>并不是Box<Animal>子類。邏輯上感覺有點不通。再來看一個例子:
var listOfAnimal = listOf<Animal>()
var listOfDog = listOf<Dog>()
listOfAnimal = listOfDog
這里是可以通過編譯的,沒有報錯,這似乎是符合邏輯的。那么再來一個例子:
var mutableListOfAnimal = mutableListOf<Animal>() //注意這里是mutableListOf區(qū)別于上面的listOf
var mutableListOfDog = mutableListOf<Dog>()
mutableListOfAnimal = mutableListOfDog
這里又報錯了,具體看下面截圖:

怎么一會兒感覺符合邏輯,一會兒編譯報錯!

在具體解釋一會兒編譯過一會兒編譯不過之前,先來簡單介紹下不變、協(xié)變和逆變的概念。
- 不變(invariant) --- 例如上面的mutableListOf<Animal>()對象不可以被mutableListOf<Dog>對象賦值,亦即mutableListOf<Dog>不是mutableListOf<Animal>的子類
- 協(xié)變(covariant) --- 比如上面的List<Animal>對象可以被List<Dog>對象賦值,即List<Dog>是List<Animal>的子類
- 逆變(contravariance) --- Contravariance describes a relationship between two sets of types where they subtype in opposite directions. 大意是和通常理解的類從屬關(guān)系是相反的,這個不太好理解,先簡單記一下和協(xié)變相反即可。
終于要說到in、out了,在即將介紹之前,先把前面的Box<T>類做一點小小的修改:
class Box<T>(t: T) {
private var value = t
fun getItem(): T = value
fun setItem(t: T) {
value = t
}
}
-
out --- 表示聲明的類中只能有返回該類型的方法,不能有接受該類型的方法。以上面的Box為例,如果改成 Box<out T>會有哪些影響呢?
- 編譯不通過,報錯提示本該是in類型出現(xiàn)的地方,卻被聲明成了out。結(jié)合上面的out解釋,可以理解。
image.png
2.之前Box<Dog>賦值給Box<Animal>編譯報錯沒有了。這就是說out產(chǎn)生了協(xié)變效果,Box<Dog>成為了Box<Animal>子類,讓本來不變 的類型產(chǎn)生了協(xié)變(符合邏輯了)。 - 如果這時刪掉了value的private修飾,也會報錯,報錯的原因同1中相同,也就是value會被外部修改。
- 編譯不通過,報錯提示本該是in類型出現(xiàn)的地方,卻被聲明成了out。結(jié)合上面的out解釋,可以理解。
-
in --- 表示聲明的類中只能有接受該類型的方法,不能有返回該類型的方法。
同樣以上面的Box為例,如果改成 Box<in T>會有哪些影響呢?-
編譯不通過,提示本該是out類型的地方,卻被聲明成了in。結(jié)合上面in的解釋也可以理解。
image.png - 之前
Box<Dog>賦值給Box<Animal>編譯報錯又出現(xiàn)了。之前out的協(xié)變 沒有了。 - 如果這時刪掉value的private修飾,也會報錯,報錯提示value是in類型,但是卻出現(xiàn)在了不變 的位置。(這里可以理解成value會被外部訪問到,換言之,只要被in、out任一修飾,該類型變量都不希望被外部直接訪問到)。
- 如果把boxDog = boxAnimal會怎么樣?注意!這里是把我們印象中的父類賦值給了子類!邏輯上類比dog = animal,但是編譯卻能通過。這就有點和印象不符了,不是說只有子類能賦值給父類,哪有父類能賦值給子類的。所以這里就需要思考一下,到底誰是誰的父類。其實這里確實是子類賦值給父類,也就是說boxDog是boxAnimal的父類。這就是前面講的 逆變 。
-

有點懵,理一下思路,如果泛型什么都不加就是不變,如果加了out就是協(xié)變,如果加了in就是逆變。如果說加了out產(chǎn)生 協(xié)變 更符合邏輯直覺,那么加in產(chǎn)生 逆變 是為了什么?不是為了把人搞懵逼吧?
可以這樣理解,如果boxAnimal = boxDog成立,即out的情況,那么只能輸出不能出入泛型對象。也就是說變成boxAnimal后只能輸出Animal,因為Dog本身就是Animal的子類,所以這樣沒有問題。如果boxDog = boxAnimal成立(其實這里boxDog = boxAny也成立,感興趣的可以試一下),即in的情況,那么boxDog能接受Animal或者Any類。但是因為不會輸出,所以不會有類型轉(zhuǎn)換異常!但是為什么boxDog能接受Animal或者Any呢?因為泛型擦除機制,其實所有的泛型都會被擦除成Any,那么無論放什么進去,只要不取出來就不會有類型轉(zhuǎn)換異常。個人認(rèn)為理解這里的關(guān)鍵就是把boxAnimal = boxDog和boxDog = boxAnimal賦值后給boxAnimal 和 boxDog分別設(shè)置item或者取出item,如果設(shè)置或者取出item不會發(fā)生邏輯異常,就算是理解了in、out設(shè)計的用意了。
但是到這里對上面講的List、mutableList能賦值和不能賦值也有了初步的理解了。說白了,就是List的代碼泛型加了out,mutableList沒有加。但是目前為止關(guān)于in、out的邏輯還是很不清晰。
那么Java是怎么處理泛型問題的?(協(xié)變和通配符)
首先Java中的泛型也是 不變 的,這意味著List<String>也不是List<Object>的子類??匆幌孪旅娴拇a:
// Java
List<String> strs = new ArrayList<String>();
// Java 報錯 type mismatch.
List<Object> objs = strs;
// 假如上面的代碼不報錯會怎樣?
// 我們就能在一個List<String>中放一個Integer.
objs.add(1);
// 下面的代碼就會在運行時拋出一個類型轉(zhuǎn)化異常: Integer cannot be cast to String
String s = strs.get(0);
Java會在List<Object> objs = strs; 編譯不通過,來阻止后續(xù)的類型轉(zhuǎn)換異常。假如要自己實現(xiàn) Collections 的 addAll 方法,直覺上會寫成這樣:
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
但是,這樣寫就會導(dǎo)致下面的完全安全的代碼無法編譯通過:
// Java
// addAll方法會報錯:
// Collection<String> is not a subtype of Collection<Object>
// 但是這段代碼是完全安全的,即把Collection<String>賦值給Collection<Object>
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
}
真實的addAll 方法是怎么實現(xiàn)的呢?
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
這里的增加了通配符 ? extends E 來顯示,該方法能夠接受E和E的子類對象的集合,而不僅僅是E。那么從這個集合中可以被安全的讀取的類型就是E,但是不能向該集合寫入,因為對于未知類型的E,一個對象是無法確認(rèn)是否是E的子類。作為這個限制的回報,就可以產(chǎn)生預(yù)期的行為: Collection<String> 是 Collection<? extends Object> 的子類。換言之,通配符通過產(chǎn)生一個擴展邊界(上界)讓類型產(chǎn)生了協(xié)變。
理解這為什么能work的關(guān)鍵是:如果你只能從一個集合中取出對象,那么從一個String的集合讀取Object是安全的。相對應(yīng)的,如果你只能將對象放入一個集合中,把String放入Object的集合也是ok的。Java中List<? super String>接受String或者他的超類。
后者List<? super String>就是 逆變 , 外部只能調(diào)用String作為入?yún)⒌姆椒ǎɡ纾嚎梢哉{(diào)用addAll(String) 或者 set(int, String))。如果想要從List<T> 中調(diào)用return T的方法,那么不會得到String,只能得到Object(下界)。
通過使用邊界通配符來增加API的擴展性。通常使用生產(chǎn)者來表示只能讀取,使用消費者表示只能寫入。為了最大程度的提高擴展性,使用通配符來表示生產(chǎn)者(Producer --- ? extends Object)和消費者(Consumer --- ? super String)??s寫:PECS(Producer-Extends,Consumer-Super)。
如果使用一個生產(chǎn)者模型List<? extends Foo>,那么不允許調(diào)用add()或者set()方法,但是這并不意味著這個集合中的內(nèi)容是永遠不變的,例如:可以調(diào)用clear() 來移除所有的內(nèi)容,因為這個方法沒有任何傳參。通配符或者其他類型的協(xié)變的唯一關(guān)注點是類型安全,而不是內(nèi)容是否是可變的。
根據(jù)上面講的Java泛型的原則,寫一個例子:
// Java
interface Source<T> {
T nextT();
}
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!! Java里面是不行的?。。。。? // ...
}
上面的代碼邏輯上是沒有問題的,但是Java是無法編譯通過的。解決的方案就是:Source<? extends Object>。但是這么做看起來就沒啥意義,因為你只能調(diào)用 T nextT(); 方法,根本不會添加其他類型。但是Java編譯器不管這些,就是編譯不通過。
但是在Kotlin里面,就可以通過一種方式告訴編譯器我們的使用方式。就是是被稱為聲明時協(xié)變(declaration-site variance):可以通過在泛型上增加注解的方式(上面這個例子中就是指out)來確保只返回T類型(即是生產(chǎn)者),不接受T類型(即不是消費者)。具體見下面代碼:
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // OK, 因為T被標(biāo)注了out
// ...
}
通常的規(guī)則是:當(dāng)泛型T在Class Box中被聲明成了out,那么T就只能出現(xiàn)在方法返回類型里,不能出現(xiàn)在方法的入?yún)⒗?。并?Box<Animal> 是 Box<Dog>的父類。(注意,這里漸漸開始和開頭的相關(guān)概念產(chǎn)生了關(guān)聯(lián)!)
out修飾符被稱為 協(xié)變 注解,并且因為出現(xiàn)在類型聲明時,被稱為聲明時協(xié)變。與之相對應(yīng)的是Java的使用時協(xié)變,在使用時通過通配符的方式產(chǎn)生協(xié)變。
除了out外,Kotlin還提供了與之相對應(yīng)的in。它會讓泛型產(chǎn)生逆變,這表明這個類只消費這個泛型,不產(chǎn)生泛型。一個很好的例子就是Comparable中的逆變:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, you can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}
in、out從名字就能很好的理解他們的用途(C#已經(jīng)使用很久了),類似上面的PECS,這里有個POCI(Producer-Out,Consumer-In)。
到了這里對in、out就有了大概的理解了。in、out對比java就是把使用時的通配符協(xié)變替換成了聲明時的協(xié)變,方便使用。
類型投影(Type Projections)
使用時協(xié)變:類型投影
將泛型T聲明成out可以很方便的解決使用時泛型子類的問題,但是就限制了Box類中只能返回T。下面來舉一個Array的例子:
class Array<T>(val size: Int) {
operator fun get(index: Int): T { ... }
operator fun set(index: Int, value: T) { ... }
}
這個類現(xiàn)在是不變 的。那么根據(jù)前文內(nèi)容,就會帶來一些擴展性的問題,例如Array<Dog>不再是Array<Animal>子類。那么看一下接下來的方法:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
這個方法將from的內(nèi)容copy到to中,接下來調(diào)用這個方法:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// ^ type is Array<Int> but Array<Any> was expected
這里又會遇到上文中提過的報錯:type is Array<Int> but Array<Any> was expected。因為當(dāng)前的泛型是 不變 的,所以 Array<Int> 和 Array<Any> 沒有任何子類從屬關(guān)系。為什么這樣做不行? 因為可能對from做一些預(yù)期之外的行為,比如向 from中寫入String。注意,因為copy方法的入?yún)⑹莊rom: Array<Any>,如果不采取任何編譯限制,就可以向from: Array<Any>中寫入String。后續(xù)的如果從ints中讀取數(shù)據(jù),可能會發(fā)生ClassCastException。
如果要禁止向from中寫入數(shù)據(jù),可以用下面的代碼:
fun copy(from: Array<out Any>, to: Array<Any>) { ... }
這就是類型投影。通過添加out告訴編譯器這不是一個隨意的array,是一個有限制的array(有投射類型)。只能從這個from中讀取T。這是Kotlin的使用時協(xié)變,對比起Java中的Array<? extends Object> 使用起來要更加簡明。
也可以使用in來做類型投影:
fun fill(dest: Array<in String>, value: String) { ... }
Array<in String> 和Java中的Array<? super String>對應(yīng)。這個方法只能傳入字符數(shù)組或者Object(畢竟,將String放入一個Object數(shù)組當(dāng)然是可以的)。

但是看一下下面這個情況:
class Box<out T>(private var item: T) {
fun get(): T = item
fun has(other: T) = item == other
}
因為這里被標(biāo)記了out,所以 has(other: T) 方法是無法通過編譯的。沒有修改T的對象item,但是方法又必須傳入other的T類型對象。這時可以改成 has(other: @UnsafeVariance T),告訴編譯器這里明確是要傳入T類型的對象,不要發(fā)出編譯錯誤。事實上這也是Kotlin庫中indexOf的實現(xiàn)方式。
*星投影(Star-projections)
如果不知道具體的類型,比如通過方法傳遞過來一個包含未知泛型的參數(shù),但是仍想在使用過程中保證安全。這時候*星投影就可以保證實例化的對象就是傳入泛型的投影。
這么講概念非常不好理解,可以看下下面的例子:
class Box<out T>(t: T) {//注意這里的out
private var value = t
fun getItem(): T = value
}
var animal: Animal = Animal()
var boxAnimal = Box<Animal>(animal)
var dog: Dog = Dog()
var boxDog = Box<Dog>(dog)
var starBox:Box<*> = boxDog
val item = starBox.getItem()
上面的代碼中,不用關(guān)心Box里面的泛型到底是什么,直接傳遞給Box<*>,上面的代碼可以通過編譯:

并且可以調(diào)用相應(yīng)的方法,但是返回不再是boxDog中的Dog了,而是Any?。因為這里抹除了類型信息,Box<out T>這里T的 上界Upper Bound 是Any?,所以取出來就是Any。但是可能會有疑惑,Dog的 上界 不是Animal嗎?從邏輯繼承的角度看確實是這樣,但是單從泛型里無法看出,如果想要取出來的類型是Animal就需要在Box的泛型上指出上界,可以看下下面的代碼:
class Box<out T:Animal>(t: T) {//注意,這里從out T變成了out T:Animal
private var value = t
fun getItem(): T = value
}
那么相應(yīng)的,取出來的就是Animal,見下圖:

上面講完了out,如果是in,星投影是什么效果呢?先看代碼:
class Box<in T>(t: T) { //注意,這里改成了in
private var value = t
fun setItem(t: T) {
value = t
}
}
var starBox:Box<*> = boxDog
val item = starBox.setItem(dog)
上面的代碼在編譯結(jié)果是什么樣的?見下圖:

不管什么Dog還是Animal,這里直接提示Required:Nothing。Nothing是什么意思?
package kotlin
/**
* Nothing has no instances. You can use Nothing to represent "a value that never exists": for example,
* if a function has the return type of Nothing, it means that it never returns (always throws an exception).
*/
public class Nothing private constructor()
Nothing沒有實例,用于表示一個不存在的值。這就是說上面的setItem 方法不能傳入任何值。
看完了兩個例子,可以總結(jié)下星投影:
在不知道T的具體類型的情況下:
- 對于Box<out T:TUpper> ,T 是帶有上界TUpper 的協(xié)變,Box<*>就相當(dāng)于Box<out TUpper> 。也就是說只能從Box<*>中讀取TUpper。
- 對于Box<in T>,T 是逆變 的,Box<*>等同于Box<in Nothing> ,那么原本只能接受寫入的Box變成了不能接受寫入。
- 丟與Box<T:TUpper> ,T是 不變 的,T的上界是TUpper,Box<*>在讀取的時候等同于Box<out TUpper>,在寫入的時候等同于Box<in Nothing>。只能讀取上界,不能寫入。
如果有多個泛型,那么每個泛型會獨立遵從對應(yīng)的規(guī)則。例如,有一個方法Function<in T, out U>,那么可以有三種組合,第一個T用星號代替,第二個U用星號代替,第三個兩個都用星號代替,舉例:
- Function<*, String> 等同于 Function<in Nothing, String>.
- Function<Int, *> 等同于 Function<Int, out Any?>.
- Function<*, *> 等同于 Function<in Nothing, out Any?>.
通過這樣的限制可以更加安全的使用泛型。
上面提到了泛型的上界Upper Bound。class Box<out T:Animal>表示T的上界是Animal,如果我想要多個上界呢?也就是進一步約束泛型。這里就引出了最后一個修飾符 where。
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
上面T的上界必須同時滿足CharSequence和Comparable。String就是一個符合條件的類型。
到這里基本可以對in、out和where有了一個大概的了解。
但是,大家有沒有想過,為什么Kotlin要設(shè)計這樣一個機制?或者說Java為什么要設(shè)計協(xié)變和通配符機制?
核心的原因就在于泛型擦除。所有看到的通配符,in、out都存在于編譯階段。一旦進入到運行階段,泛型實際上不會存儲任何關(guān)于類型的信息,即類型被擦除了。例如Box<Dog> 和 Box<Animal?> 都會被擦除成Box<*>。 所以為了防止運行過程中的異常,就必須在編譯階段嚴(yán)格的檢查類型。
再講一個情況,假如要在運行時檢查某個類對象是否是某個泛型的對象,按直覺怎么寫?
fun <A, B> Pair<*, *>.asPairOf(): Pair<A, B>? {
if (first !is A || second !is B) return null
return first as A to second as B
}
報錯無法對擦除類型進行檢查和Uchecked cast lint提示:


里面有個提示 Make type parameter reified and function inline ,按提示修改代碼:
inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {//多了inline 和 reified
if (first !is A || second !is B) return null
return first as A to second as B
}
代碼編譯通過。首先得內(nèi)聯(lián)(可以理解為代碼替換,即將內(nèi)聯(lián)函數(shù)的代碼直接copy到調(diào)用的位置),然后要加reified。因為只有內(nèi)聯(lián)到對應(yīng)的代碼中,才能知道泛型代表的實際類型,從而將泛型替換成真正的類型,才能做類型檢查和轉(zhuǎn)換。

