與 Java 類似,Kotlin 中的類也可以有類型參數:
class Box<T>(t: T) {
var value = t
}
一般來說,要創(chuàng)建這樣類的實例,我們需要提供類型參數:
val box: Box<Int> = Box<Int>(1)
但是如果類型參數可以推斷出來,例如從構造函數的參數或者從其他途徑,允許省略類型參數:
val box = Box(1) // 1 具有類型 Int,所以編譯器知道我們說的是 Box<Int>。
Java 類型系統(tǒng)中最棘手的部分之一是通配符類型(參見 Java Generics FAQ)。
而 Kotlin 中沒有。 相反,它有兩個其他的東西:聲明處型變(declaration-site variance)與類型投影(type projections)。
首先,讓我們思考為什么 Java 需要那些神秘的通配符。在 Effective Java 解釋了該問題——第28條:利用有限制通配符來提升 API 的靈活性。
首先,Java 中的泛型是不型變的,這意味著 List<String>
并不是 List<Object>
的子類型。
為什么這樣? 如果 List 不是不型變的,它就沒
比 Java 的數組好到哪去,因為如下代碼會通過編譯然后導致運行時異常:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // ?。?!即將來臨的問題的原因就在這里。Java 禁止這樣!
objs.add(1); // 這里我們把一個整數放入一個字符串列表
String s = strs.get(0); // !??! ClassCastException:無法將整數轉換為字符串
因此,Java 禁止這樣的事情以保證運行時的安全。但這樣會有一些影響。例如,考慮 Collection
接口中的 addAll()
方法。該方法的簽名應該是什么?直覺上,我們會這樣:
// Java
interface Collection<E> …… {
void addAll(Collection<E> items);
}
但隨后,我們將無法做到以下簡單的事情(這是完全安全):
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // !??!對于這種簡單聲明的 addAll 將不能編譯:
// Collection<String> 不是 Collection<Object> 的子類型
}
(在 Java 中,我們艱難地學到了這個教訓,參見Effective Java,第25條:列表優(yōu)先于數組)
這就是為什么 addAll()
的實際簽名是以下這樣:
// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
通配符類型參數 ? extends E
表示此方法接受 E
的 一些子類型對象的集合,而不是 E
本身。
這意味著我們可以安全地從其中(該集合中的元素是 E 的子類的實例)讀取 E
,但不能寫入,
因為我們不知道什么對象符合那個未知的 E
的子類型。
反過來,該限制可以讓Collection<String>
表示為Collection<? extends Object>
的子類型。
簡而言之,帶 extends 限定(上界)的通配符類型使得類型是協(xié)變的(covariant)。
理解為什么這個技巧能夠工作的關鍵相當簡單:如果只能從集合中獲取項目,那么使用 String
的集合,
并且從其中讀取 Object
也沒問題 。反過來,如果只能向集合中 放入 項目,就可以用Object
集合并向其中放入 String
:在 Java 中有 List<? super String>
是 List<Object>
的一個超類。
后者稱為逆變性(contravariance),并且對于 List <? super String>
你只能調用接受 String 作為參數的方法
(例如,你可以調用 add(String)
或者 set(int, String)
),當然
如果調用函數返回 List<T>
中的 T
,你得到的并非一個 String
而是一個 Object
。
Joshua Bloch 稱那些你只能從中讀取的對象為生產者,并稱那些你只能寫入的對象為消費者。他建議:“為了靈活性最大化,在表示生產者或消費者的輸入參數上使用通配符類型”,并提出了以下助記符:
PECS 代表生產者-Extens,消費者-Super(Producer-Extends, Consumer-Super)。
注意:如果你使用一個生產者對象,如 List<? extends Foo>
,在該對象上不允許調用 add()
或 set()
。但這并不意味著
該對象是不可變的:例如,沒有什么阻止你調用 clear()
從列表中刪除所有項目,因為 clear()
根本無需任何參數。通配符(或其他類型的型變)保證的唯一的事情是類型安全。不可變性完全是另一回事。
假設有一個泛型接口 Source<T>
,該接口中不存在任何以 T
作為參數的方法,只是方法返回 T
類型值:
// Java
interface Source<T> {
T nextT();
}
那么,在 Source <Object>
類型的變量中存儲 Source <String>
實例的引用是極為安全的——沒有消費者-方法可以調用。但是 Java 并不知道這一點,并且仍然禁止這樣操作:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !?。≡?Java 中不允許
// ……
}
為了修正這一點,我們必須聲明對象的類型為 Source<? extends Object>
,這是毫無意義的,因為我們可以像以前一樣在該對象上調用所有相同的方法,所以更復雜的類型并沒有帶來價值。但編譯器并不知道。
在 Kotlin 中,有一種方法向編譯器解釋這種情況。這稱為聲明處型變:我們可以標注 Source
的類型參數 T
來確保它僅從 Source<T>
成員中返回(生產),并從不被消費。
為此,我們提供 out 修飾符:
abstract class Source<out T> {
abstract fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 這個沒問題,因為 T 是一個 out-參數
// ……
}
一般原則是:當一個類 C
的類型參數 T
被聲明為 out 時,它就只能出現在 C
的成員的輸出-位置,但回報是 C<Base>
可以安全地作為C<Derived>
的超類。
簡而言之,他們說類 C
是在參數 T
上是協(xié)變的,或者說 T
是一個協(xié)變的類型參數。
你可以認為 C
是 T
的生產者,而不是 T
的消費者。
out修飾符稱為型變注解,并且由于它在類型參數聲明處提供,所以我們講聲明處型變。
這與 Java 的使用處型變相反,其類型用途通配符使得類型協(xié)變。
另外除了 out,Kotlin 又補充了一個型變注釋:in。它使得一個類型參數逆變:只可以被消費而不可以
被生產。逆變類的一個很好的例子是 Comparable
:
abstract class Comparable<in T> {
abstract fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 擁有類型 Double,它是 Number 的子類型
// 因此,我們可以將 x 賦給類型為 Comparable <Double> 的變量
val y: Comparable<Double> = x // OK!
}
我們相信 in 和 out 兩詞是自解釋的(因為它們已經在 C# 中成功使用很長時間了),
因此上面提到的助記符不是真正需要的,并且可以將其改寫為更高的目標:
存在性(The Existential) 轉換:消費者 in, 生產者 out! :-)
將類型參數 T 聲明為 out 非常方便,并且能避免使用處子類型化的麻煩,但是有些類實際上不能限制為只返回 T
!
一個很好的例子是 Array:
class Array<T>(val size: Int) {
fun get(index: Int): T { ///* …… */ }
fun set(index: Int, value: T) { ///* …… */ }
}
該類在 T
上既不能是協(xié)變的也不能是逆變的。這造成了一些不靈活性。考慮下述函數:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
這個函數應該將項目從一個數組復制到另一個數組。讓我們嘗試在實踐中應用它:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3)
copy(ints, any) // 錯誤:期望 (Array<Any>, Array<Any>)
這里我們遇到同樣熟悉的問題:Array <T>
在 T
上是不型變的,因此 Array <Int>
和 Array <Any>
都不是
另一個的子類型。為什么? 再次重復,因為 copy 可能做壞事,也就是說,例如它可能嘗試寫一個 String 到 from
,
并且如果我們實際上傳遞一個 Int
的數組,一段時間后將會拋出一個 ClassCastException
異常。
那么,我們唯一要確保的是 copy()
不會做任何壞事。我們想阻止它寫到 from
,我們可以:
fun copy(from: Array<out Any>, to: Array<Any>) {
// ……
}
這里發(fā)生的事情稱為類型投影:我們說from
不僅僅是一個數組,而是一個受限制的(投影的)數組:我們只可以調用返回類型為類型參數T
的方法,如上,這意味著我們只能調用 get()
。這就是我們的使用處型變的用法,并且是對應于 Java 的 Array<? extends Object>
、
但使用更簡單些的方式。
你也可以使用 in 投影一個類型:
fun fill(dest: Array<in String>, value: String) {
// ……
}
Array<in String>
對應于 Java 的 Array<? super String>
,也就是說,你可以傳遞一個 CharSequence
數組或一個 Object
數組給 fill()
函數。
有時你想說,你對類型參數一無所知,但仍然希望以安全的方式使用它。
這里的安全方式是定義泛型類型的這種投影,該泛型類型的每個具體實例化將是該投影的子類型。
Kotlin 為此提供了所謂的星投影語法:
Foo <out T>
,其中 T
是一個具有上界 TUpper
的協(xié)變類型參數,Foo <*>
等價于 Foo <out TUpper>
。 這意味著當 T
未知時,你可以安全地從 Foo <*>
讀取 TUpper
的值。Foo <in T>
,其中 T
是一個逆變類型參數,Foo <*>
等價于 Foo <in Nothing>
。 這意味著當 T
未知時,沒有什么可以以安全的方式寫入 Foo <*>
。Foo <T>
,其中 T
是一個具有上界 TUpper
的不型變類型參數,Foo<*>
對于讀取值時等價于 Foo<out TUpper>
而對于寫值時等價于 Foo<in Nothing>
。如果泛型類型具有多個類型參數,則每個類型參數都可以單獨投影。
例如,如果類型被聲明為 interface Function <in T, out U>
,我們可以想象以下星投影:
Function<*, String>
表示 Function<in Nothing, String>
;Function<Int, *>
表示 Function<Int, out Any?>
;Function<*, *>
表示 Function<in Nothing, out Any?>
。注意:星投影非常像 Java 的原始類型,但是安全。
不僅類可以有類型參數。函數也可以有。類型參數要放在函數名稱之前:
fun <T> singletonList(item: T): List<T> {
// ……
}
fun <T> T.basicToString() : String { // 擴展函數
// ……
}
要調用泛型函數,在調用處函數名之后指定類型參數即可:
val l = singletonList<Int>(1)
能夠替換給定類型參數的所有可能類型的集合可以由泛型約束限制。
最常見的約束類型是與 Java 的 extends 關鍵字對應的 上界:
fun <T : Comparable<T>> sort(list: List<T>) {
// ……
}
冒號之后指定的類型是上界:只有 Comparable<T>
的子類型可以替代 T
。 例如
sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子類型
sort(listOf(HashMap<Int, String>())) // 錯誤:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子類型
默認的上界(如果沒有聲明)是 Any?
。在尖括號中只能指定一個上界。
如果同一類型參數需要多個上界,我們需要一個單獨的 where-子句:
fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<T>
where T : Comparable,
T : Cloneable {
return list.filter { it > threshold }.map { it.clone() }
}