更多精彩內容,歡迎關注我的微信公眾號——Android機動車。
無論是Java還是Kotlin,泛型都是一個非常重要的概念,簡單的泛型應用很容易理解,不過也有理解起來麻煩的時候。
泛型基礎
在瞭解Kotlin的泛型之前,先來看看Java中的泛型:
舉個例子:在JDK中,有一類列表物件,這些物件對應的類都實現了List介面。List中可以儲存任何物件:
List list=new ArrayList();
list.add(55);
list.add("hello");
複製程式碼
上面的程式碼中,List中儲存了Integer和String兩種型別值。儘管這樣做是可以儲存任意型別的物件,但每個列表元素就失去了原來物件的特性,因為在Java中任何類都是Object的子類,這樣做的弊端就是原有物件型別的屬性和方法都不能再使用了。
但在定義List時,可以指定元素的資料型別,那麼這個List就不再是通用的了,只能儲存一種型別的資料。JDK1.5之後引入了一個新的概念:泛型。
所謂泛型,就是指在定義資料結構時,只指定型別的佔位符,待到使用該資料結構時再指定具體的資料型別:
public class Box<T> {
private T t;
public Box(T t) {
this.t = t;
}
}
Box<Integer> box=new Box(2);
複製程式碼
在Kotlin中同樣也支援泛型,下面是Kotlin實現上面同樣的功能:
class Box<T>(t: T) {
var value = t
}
var box: Box<String> = Box("haha")
複製程式碼
型別變異
Java中
Java泛型中有型別萬用字元這一機制,不過在Kotlin泛型中,沒有萬用字元。
先看一個Java的栗子:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1; // 編譯錯誤
複製程式碼
以上程式碼編譯錯誤。這裡有兩個List物件,很明顯String是Object的子類,但遺憾的是,Java編譯器並不認為List < String >和List < Object> 有任何關係,直接將list1賦值給list2是會編譯報錯的,這是由於List的父介面是Collection:
public interface Collection<E> extends Iterable<E> {..}
複製程式碼
為了解決這個問題,Java泛型提供了問號(?)萬用字元來解決這個問題。例如Collection介面中的addAll方法定義如下:
boolean addAll(Collection<? extends E> var1);
複製程式碼
? extends E 表示什麼呢,表示任何父類是E(或者E的任何子類和自己)都滿足條件,這樣就解決了List < String > 給List < Object> 賦值的問題。
出了extend還有super,這裡不再過多介紹。
Kotlin中
Kotlin泛型並沒有提供萬用字元,取而代之的是out和in關鍵字。用out宣告的泛型佔位符只能在獲取泛型型別值得地方,如函式的返回值。用in宣告的泛型佔位符只能在設定泛型型別值的地方,如函式的引數。
我們習慣將只能讀取的物件稱為生產者,將只能設定的物件稱為消費者。如果你使用一個生產者物件,將無法對這個物件呼叫add或set等方法,但這並不代表這個物件的值是不變的。例如,你完全可以呼叫clear方法來刪除List中的所有元素,因為clear方法不需要任何引數。
萬用字元型別(或者其他任何的型別變異),唯一能夠確保的僅僅是型別安全。
abstract class Source<out T> {
abstract fun func(): T
}
abstract class Comparable<in T> {
abstract fun func(t: T)
}
複製程式碼
型別投射
如果將泛型型別T宣告為out,就可以將其子類化(List < String > 是List < Object> 的子型別),這是非常方便的。如果我們的類能夠僅僅只返回T型別的值,那麼的確可以將其子類化。但如果在宣告泛型時未使用out宣告T呢?
現在有一個Array類如下:
class Array<T>(val size: Int) {
fun get(index: Int): T {
}
fun set(index: Int, t: T) {
}
}
複製程式碼
此類中的T既是get方法的返回值,又是set方法的引數,也就是說Array類既是T的生產者,也是T的消費者,這樣的類就無法進行子類化。
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for(i in from.indices){
to[i]=from[i]
}
}
複製程式碼
這個copy方法,就是將一個Array複製到另一個Array中,現在嘗試使用一下:
val ints: Array<Int> = arrayOf(1, 2, 3)
val any: Array<Any> = Array(3)
copy(ints, any) // 編譯錯誤,因為Array<Int> 不是Array<Any>的子型別
複製程式碼
Array< T > 對於型別引數T是不可變的,因此Array< Int> 和Array< Any>他們沒有任何關係,為什麼呢?因為copy可能會進行一些不安全的操作,也就是說,這個函式可能會試圖向from中寫入資料,這樣可能會拋型別轉換異常。
可以這樣:
fun copy(from: Array<out Any>, to: Array<Any>) {
...
}
複製程式碼
將from的泛型使用out修飾。
這種宣告在Kotlin中稱為型別投射:from不是一個單純的陣列,而是一個被限制(投射)的陣列,我們只能對這個陣列呼叫那些返回值為型別引數T的函式,在這個例子中,我們只能呼叫get方法,這就是我們事先使用處的型別變異的方案。
in關鍵字也是同理。
泛型函式
不僅類可以有泛型引數,函式一樣可以有泛型引數。泛型引數放在函式名稱之前。
fun <T> getList(item: T): List<T> {
...
}
複製程式碼
呼叫泛型函式時,應該在函式名稱之後指定呼叫端型別引數。
val value = getList<Int>(1)
複製程式碼
泛型約束
對於一個給定的泛型引數,所允許使用的型別,可以通過泛型約束來限制,最常見的約束是上界,與Java中的extends類似。
fun <T : Any> sort(list: List<T>) {
}
複製程式碼
冒號之後指定的型別就是泛型引數的上界:對於泛型引數T,允許使用Any的子型別。如果沒有指定,則預設使用的上界型別是“Any?”,在定義泛型引數的尖括號內,值允許定義唯一一個上界。
小結
Kotlin泛型是在Java泛型的基礎上進行了改進,變得更好用,更安全,儘管上述的泛型技術不一定都用得上,但對於全面瞭解Kotlin泛型會起到很大作用。
更多精彩內容,歡迎關注我的微信公眾號——Android機動車