轉向Kotlin——泛型

Android機動車發表於2018-05-17

更多精彩內容,歡迎關注我的微信公眾號——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機動車

這裡寫圖片描述

相關文章