歡迎關注 二師兄Kotlin
轉載請注明出處 二師兄kotlin
跟Java一樣,在Kotlin中的類也可以擁有泛型參數:
class Box<T>(t: T) {
var value = t
}
通常,為了創建這樣一個類的實例,我們需要提供類型參數:
val box: Box<Int> = Box<Int>(1)
但是如果這個參數是可以被推斷出來的,我們就可以忽略類型:
val box = Box(1) // 1 has type Int, so the compiler figures out that we are talking
about Box<Int>
泛型之可變性
Java類型系統最復雜的特性之一,就是通配符。但是Kotlin中一個都沒有,取而代之的是兩種其他實現:: declaration-site可變
和 類型預測(type projections)
。
首先,讓我們想一下,為什么Java需要如此難以理解的通配符。這個問題在Effective Java
的第28節有解釋:利用有限制的通配符來提高API的靈活性
。首先,Java中泛型的參數化類型是不可變的(invariant)
,這意味著List<String>
并不是List<Object>
的子類型。為什么會這樣? 如果List
是不可變的,那么它對比于Java中數組(arrays)
就沒有任何優勢了,下面這段代碼將會帶來編譯異常以及運行時異常:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!! 這就是即將引入的問題的原因。Java禁止這樣!
objs.add(1); // 我們向一個包含`String`的列表添加了一個`Integer`
String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
所以,Java禁止這樣做其實是為了保證運行時安全。但是它有這樣的一些啟示,比如,考慮一下Collection
接口的addAll()
方法,這個方法的簽名是什么呢?直觀來看覺得可能會是這樣的:
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
但是,我們卻不能做下面這件很簡單的事(盡管它看起來已經相當安全了):
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from); // !!! Would not compile with the naive declaration of addAll:
// Collection<String> is not a subtype of Collection<Object>
}
(在Java中, 我們學習這一節非常之不容易, 參見Effective Java
, 第25節:列表優先于數組( Prefer lists to arrays))
這就是為什么addAll()
的實際的簽名函數會變成下面這個樣子:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
通配符類型參數(wildcard type argument)? extends T
表明這個方法接受 T
子類型的對象集合,并非T
本身。這意味著,可以從列表中安全的讀取T
(集合中所有的元素都是T
的一個子類),但是我們無法寫入因為我們并不知道哪些類是T
的子類。因為有了這種限制,我們渴望這種行為: Collection<String>
是 Collection<? extends Object>
的子類。從表面意義來看,通過extends-bound
(向上限制)修飾的通配符使得類型可協變
。
理解這個技巧什么能工作的關鍵其實很簡單:如果你僅僅是從一個集合中取元素, 比如使用一個String
集合并且從中讀取Object
是工作正常的。相反的,如果你需要向集合中添加元素,比如使用一個Object
的集合并且向其中添加String
,你可以利用Java中的另一個通配符 List<? super String>
是List<Object>
的超類。
這被成為 逆變(contravariance)
,你只能使用String
作為參數在List<? super String>
上調用方法( 你可以調用add(String)
或者 set(int, String)
),然而,如果當你調用List<T>
的一些函數來返回T
的話,你將會得到一個Object
,而不是String
。
Joshua Bloch稱:這些對象你只能從生產者(Producers)
中讀取,只能在消費者(Consumers)
中寫入,他如此推薦:“為了最大程度的靈活性,在輸入參數時使用通配符類型來表示生產者或者消費者”,可以使用下面的方法來提高記憶:
PECS stands for Producer-Extends, Consumer-Super.
注意:如果你使用一個Object生產者,List<? extends Foo>
,你將不會被允許在這個對象上調用add()
和set()
,但是這并不意味著這個對象是不可變得(immutable
):比如,沒有什么可以阻止你調用clear()
來移除這個list的所有item,因為clear()
并不需要任何參數。通配符唯一保證的事情就是類型安全(type safety
)。不可變性(Immutability
)完全是另外一件事。