kotlin之泛型的使用

fb0122發表於2018-08-28

kotlin之泛型的使用

泛型

我們最先了解到的泛型應該是來自於Java,在Java SE 1.5的時候,首次提出了泛型的概念,泛型的本質是引數化的型別,也就是說傳遞操作的資料型別被指定為一個引數,泛型可以被應用於類(泛型類)、介面(泛型介面)、方法(泛型方法)。Java引入泛型的好處就是安全簡單。在Java SE 1.5之前,沒有泛型的情況下,對引數的“任意化”是通過Object的引用來實現的,然而用這種方式去實現引數的任意化的缺點是總是要進行強制型別轉換,這種轉換是要求開發者對實際引數型別可以預知的情況下進行的,然而隨著專案的規模、參與人員的數量慢慢增加,要做到對每個實際引數型別都可以預知幾乎不太可能,而強制型別轉換錯誤在編譯期不會報錯,只有在執行時會丟擲型別轉換的異常,這讓程式變得不穩定且不可控制,所以泛型的引入解決了這些問題。

下面首先通過一段Java程式碼瞭解一下泛型:

public interface List<E> extends Collection<E>{
    ...
    boolean add(E e);
    ...
}
複製程式碼

上面這段程式碼是來自於Java的集合List的原始碼,可以看到List是一個泛型介面,一般情況下當我們想要初始化一個list的時候,應該都是這麼寫: List<Integer> list = new ArrayList<>(); 當想要向集合中新增一個元素的時候,只需要呼叫list.add()方法就可以了。那麼假如我建立的是一個int的集合那麼呼叫add()方法的時候傳的引數就應該是int,那如果建立了一個String的集合那麼add()的引數就應該是String。那麼設想一下,如果沒有泛型的存在的話,我們需要寫兩個引數不同的add()方法,那麼對應每一種資料型別就需要多寫一遍,顯然不是一個很好的實現方法。

但是當我們運用泛型的時候,比如:List<Integer> list = new ArrayList<>(); 通過原始碼可以看到,我們將集合List的泛型引數E設定為Integer,那麼此時對於list來說,add()方法內的引數E就是Integer型別。同理如果建立語句為:List<String> list = new ArrayList<>(); 那麼此時E型別就會是String。

Kotlin泛型

上面對泛型做了一個簡單的解釋,舉例說明了泛型最簡單的用法,實際上遠遠不止於此。kotlin中的泛型定義與Java中類似,在類名之後宣告:

class Box<T>(t: T){
    val vlaue = t
}

//在建立box物件的時候宣告瞭泛型引數為Int
val box: Box<Int> = Box<Int>(1)

//一般來說,如果型別是能夠被推斷出來的,我們也可以省去宣告泛型引數的步驟
val box = Box(1)
複製程式碼

對於Java來說,常見的泛型使用中,萬用字元也是最為常見的一種方式,比如有 ?、? extends Number、? super Integer等等。那麼T和萬用字元之間有什麼區別呢?實際上T更多的是表示了一個限定約束,如宣告瞭 “T” 類,那麼該泛型類中用 “T”宣告的物件就必須是T型別,如上述List中的E;如果是 ? 就表示該型別可以是任意的型別,並不會起到限定約束作用。

kotlin中並沒有上述Java中的萬用字元型別,如Java中用 ? extends Number 表示了引數的上界,只能是Number的子型別,用 ? super Integer 說明了引數的下界,只能是Integer的超型別。這樣我們可以在使用萬用字元的時候也對引數進行一個約束,然而kotlin中拋棄了這一個概念,在kotlin中,類似的概念稱之為生產者和消費者。

生產者:只能讀取資料的物件

消費者:只能寫入資料的物件

之所以會有生產者和消費者概念的引入,和泛型的型變有關,在Java中泛型是沒有型變的,例如String是Object的子類,但是List<String> 並不是 List<Object > 的子類。這種設計會讓我們的List變的安全,如果是可以型變的,那麼將發生一些錯誤。比如我們看下面的程式碼:

List<String> strs = new ArrayList<String>();
List<Object> objs = strs; 
objs.add(1); 
String s = strs.get(0);
複製程式碼

如果Java是可以型變的,那麼上述程式碼將會編譯通過,然而我們最後是想得到一個String, 但是卻向List內寫入了一個int型的資料,這在執行時會發生ClassCastException(型別轉換異常)而導致程式crash。

通過Java的上述特性,可以考慮一下集合List的addAll()方法的引數,順理成章的應該是類似下面這樣:

boolean addAll(Collection<E> c);
複製程式碼

但如果addAll方法是上述那樣的話,當編寫了如下程式碼的時候:

void copyAll(Collection<Object> to, Collection<String> from) {
  to.addAll(from); 
}
複製程式碼

這個操作看上去很安全,但是編譯器會報錯:Collection<String> 不是 Collection<Object> 的子型別,因此實際上集合的原始碼如下:

public interface List<E> extends Collection<E>{
    ...
    boolean add(E e);
    boolean addAll(Collection<? extends E> c);
    ...
}
複製程式碼

我們可以看到,addAll()方法的引數是Collection<? extends E> 而不是Collection<E>, 通過指定萬用字元引數的上界來使得向Collection<Object>中新增Collection<String>變得合法。

因此當對於一個集合Collection<A> , 從中讀取一個元素,他可能是A型別,也可能是A型別的子類。這種情況被稱為協變;反之,如果向一個集合Collection<A> 寫入元素時,可以寫入A型別,也可以寫入A型別的超類,這種情況被稱為逆變。(舉例說明:如果只需要讀取的話,那麼我們可以從一個String的集合中讀取Object,這種操作是安全的; 如果需要寫入,那麼我們應當向一個Object的集合內寫入String,而不是像一個Number的集合內寫入Object。)

在kotlin中,用out和in來表示生產者和消費者的行為,一言以蔽之:out T 表示 Java 中的 ? extends T, in T 表示Java中的 ? super T。 * 用來表示Java中的 ?(萬用字元)

Kotlin宣告處型變

Kotlin 對 Java 泛型的一項改動就是新增了宣告處型變。看下面的例子:

interface Source<T> { 
T nextT(); 
} 

void demo(Source<String> str) { 
// Java 中這種寫法是不允許的 
Source<Object> obj = str; 
...
} 

複製程式碼

因為 Java 泛型是不型變的,Source<String> 不是Source<Object> 的子型別,所以不能把 Source<String> 型別變數賦給 Source<Object>型別變數。

現在用 Kotlin 改寫上面的介面宣告:

interface Source<out T> { 
T nextT(); 
} 
複製程式碼

我們在介面的宣告處用 out T 做了生產者宣告,因為這個介面只有一個讀取資料的 nextT() 方法,可以視為生產者。把這個介面的型別引數宣告為生產者後,就可以實現安全的型別協變了:

fun demo(Source<String> str) { 

val obj: Source<Any> = str // 合法的型別協變 
} 
複製程式碼

Kotlin 中有大量的宣告處協變,比如 Iterable 介面的宣告:

public interface Iterable<out T> { 
public operator fun iterator(): Iterator<T> 
} 
複製程式碼

因為 Collection 介面和 Map 介面都繼承了 Iterable 介面,而 Iterable 介面被宣告為生產者介面,所以所有的 Collection 和 Map 物件都可以實現安全的型別協變:**

val c: List<Number> = listOf(1, 2, 3) 
複製程式碼

這裡的 listOf() 函式返回 List<Int> 型別,因為在kotlin中List <out T>介面實現了安全的型別協變,所以可以安全地把 List<Int> 型別賦給 List<Number>型別變數。 在kotlin中List是指可以讀不可以寫的,因此上述程式碼是安全的。(可讀寫的List為MutableList<E>)

使用處型變:型別投影。

考慮之前講到的在宣告處型變,將T設定為生產者out T,可以使其安全的產生型變。然而有些類我們不能夠限制它就返回T。比如一個Array:

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}
複製程式碼

可以看到其中set方法,並不會返回T,我們無法將其設定為生產者。那麼當我們去對兩個陣列進行copy操作的時候:

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)
複製程式碼

顯然上述程式碼是錯誤的,因為它在嘗試將一個String的Array賦值給Int的Array。因此可以將copy方法的from陣列設定為生產者,如下:

fun copy(from: Array<out Any>, to: Array<Any>)
複製程式碼

這個時候,我們只能呼叫from的返回值為T的方法,即get()方法,這時to陣列將不會被寫入到from中去,這可以被稱為是使用處型變,也可以稱為型別投影(因為此時的from陣列就像是一個受到限制的Array<Any>)。

相關文章