Kotlin 泛型 VS Java 泛型

建議先閱讀我的上一篇文章 -- Java 泛型

和 Java 泛型一樣,Kotlin 泛型也是 Kotlin 語言中較難理解的一個(gè)部分。Kotlin 泛型的本質(zhì)也是參數(shù)化類型,并且提供了編譯時(shí)強(qiáng)類型檢查,實(shí)際上也是偽泛型,和 Java 泛型類型一樣。這篇文章將介紹 Kotlin 泛型里中的重要概念,以及與 Java 泛型的對(duì)比。

1. 泛型類型與泛型函數(shù)

Kotlin 下泛型類型與泛型函數(shù)的寫法,與 Java 差不多,直接看下面的例子:

// 泛型類型
class Box<T> {
    var t: T? = null
}

// 泛型函數(shù),類型參數(shù)在函數(shù)名之前
object Util {
    fun <K, V> compare(p1: Pair<K, V>, p2: Pair<K, V>): Boolean {
         return p1.first == p1.first && p2.second == p2.second
    }
}

Kotlin 中泛型的類型參數(shù)如果可以推斷出來,例如從構(gòu)造函數(shù)的參數(shù)或者其他途徑,允許省略類型參數(shù):

val p1 = Pair(1, "1")
val p2 = Pair(2, "2")
Util.compare(p1, p2)

通過 Tools -> Kotlin -> Show Kotlin Bytecode, 然后點(diǎn)擊字節(jié)碼上面的 Decompile 出 Java 代碼可以看出與 Java 泛型的原理是一樣的,都進(jìn)行了類型擦除。

2. 泛型約束

Java 中可以通過有界類型參數(shù)來限制參數(shù)類型的邊界,Kotlin 下泛型約束也可以限制參數(shù)類型的上界:

fun <T: Comparable<T>> compare(t1: T, t2: T) = t1.compareTo(t2)

默認(rèn)的上界是Any?,是可空類型,如果確定為非空類型的話,應(yīng)該使用<T: Any>。

泛型約束中的尖括號(hào)中只能指定一個(gè)上界,如果需要多個(gè)上界,需要一個(gè)單獨(dú)的 where 子句:

fun <T> cloneWhenGreater(list: List<T>, threshold: T): List<Any>
        where T : Comparable<T>,
              T : CharSequence {
    return list.filter { it > threshold }
}

3. 使用處型變:類型投影

在 Java 泛型的通配符中有一個(gè)“Producer Extends, Consumer Super”原則,簡(jiǎn)稱 PECS 原則:只讀類型使用上界通配符? extends T,只寫類型使用下界通配符? super T。Kotlin 中提供了類似功能的兩個(gè)操作符outin,分別生產(chǎn)和消費(fèi)。

先看_Collection.kt中一個(gè)擴(kuò)展函數(shù):

public operator fun <T> Collection<T>.plus(elements: Array<out T>): List<T>

// list 為 ArrayList<Number> 類型
val list = arrayListOf<Number>(1, 2, 3)
// array 為 Array<Float> 類型
val array = arrayOf(1f, 2f)
val list1: List<Number> = list.plus(array)

所以Array<out T>相當(dāng)于 Java 中的Array<? extends T>,而Array<in T>相當(dāng)于 Java 中的Array<? super T>,out 表示生產(chǎn),用于只讀類型,in 表示消費(fèi),用于只寫類型。

類型投影和 Java 的上界通配符和下界通配符一樣,只能用于參數(shù)、屬性、局部變量或返回值的類型,但是不能用于泛型類型和泛型函數(shù)聲明的類型,所以稱之使用處型變。

4. 聲明處型變

與 Java 有界通配符不能用于泛型聲明時(shí)使用不同的是,Kotlin 中outin兩個(gè)型變注解還可以用于泛型聲明時(shí),更加靈活。下面通過 Java 和 Kotlin 中對(duì) Collection 的定義來分析:

// Java 中 Collection 的定義,元素是可讀寫的
public interface Collection<E> extends Iterable<E> { ... }
public interface List<E> extends Collection<E> { ... }

// Collections 的 copy 方法
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

// 但是下面聲明在 Java 中是不允許的
public interface IllegalList<? extends T> extends Collection<E> { ... }

但是 Kotlin 中可以通過聲明處型變(型變的概念在后面會(huì)詳細(xì)解釋)定義只讀的集合:

// A generic collection of elements. Methods in this interface support only read-only access to the collection
public interface Collection<out E> : Iterable<E>

// A generic collection of elements that supports adding and removing elements.
public interface MutableCollection<E> : Collection<E>, MutableIterable<E>

Collection<out E>使得 Collection 里面的元素是只讀的,也使得 Collection<Number>Collection<Int> 的父類,在 Kotlin 中稱 Collection 的元素類型是協(xié)變的。對(duì)于協(xié)變的類型,通常不允許泛型類型作為函數(shù)的傳入?yún)?shù)。

in型變注解可以使得元素類型是逆變的,只能被消費(fèi),與協(xié)變相反,通常不允許泛型類型作為函數(shù)的返回值,看Comparable的定義:

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
    // 1.0 類型為 Double,是 Number 的子類型
    x.compareTo(1.0)
    // 因?yàn)?Comparable 只能被消費(fèi),所以可以賦值給 Comoparable<Double> 的變量
    val y: Comparable<Double> = x
}

4.1 UnsafeVariance 注解

上面提到過對(duì)于協(xié)變的類型,通常不允許泛型類型作為函數(shù)的傳入?yún)?shù),對(duì)于逆變類型,通常不允許泛型類型作為函數(shù)的返回值,但是有時(shí)我們可以通過@UnsafeVariance 注解告訴 Kotlin 編譯器:“我保證不會(huì)干壞事”,例如 Collection 的 contains 函數(shù):

public interface Collection<out E> : Iterable<E> {
    ...
    public operator fun contains(element: @UnsafeVariance E): Boolean
    ...
}

5. 星投影

使用泛型的過程中,如果參數(shù)類型未知時(shí),在 Java 中可以使用原始類型(Raw Types),但是 Java 的原始類型是類型不安全的:

ArrayList<String> list = new ArrayList<>(5);

ArrayList unkownList = list;
Object first = unkownList.get(0);
unkownList.add(1);  // warning: Unchecked call to 'add(E)'
unkownList.add("1"); // warning: Unchecked call to 'add(E)'

而在 Kotlin 中,在參數(shù)類型未知時(shí),可以用星投影來安全的使用泛型:

val list = ArrayList<Int>(5)
val unkownList: ArrayList<*> = list
val first: Any = unkownList[0]
unkownList.add(1)  // error
unkownList.add("1") // error

對(duì)于ArrayList<*>來說,因?yàn)椴恢谰唧w的參數(shù)類型,對(duì)于add(e E)這種不安全的操作,Kotlin 編譯器會(huì)直接報(bào)錯(cuò),比 Java 的原始類型更安全。

Kotlin 中具體的星投影語法如下:

  • 對(duì)于Foo<out T>,其中T是一個(gè)具有上界TUpper的協(xié)變類型參數(shù),Foo<*>等價(jià)于Foo<out TUpper>。這意味著當(dāng)T未知時(shí),你可以安全地從Foo<*>讀取TUpper的值。

  • 對(duì)于Foo<in T>,其中T是一個(gè)逆變類型參數(shù),Foo<*>等價(jià)于Foo<in Nothing>。這意味著當(dāng)T未知時(shí),沒有什么可以以安全的方式寫入Foo<*>。

  • 對(duì)于Foo<T>,其中T是一個(gè)具有TUpper的不型變類型參數(shù),Foo<*>對(duì)于讀取值時(shí)等價(jià)于Foo<out TUpper>,而對(duì)于寫值時(shí)等價(jià)于Foo<in Nothing>

如果泛型類型具有多個(gè)類型參數(shù),則每個(gè)類型參數(shù)都可以單獨(dú)投影。例如,如果類型被聲明為interface Function<in T, out U>,我們可以想象以下星投影:

  • Function<*, String>表示Function<in Nothing, String>

  • Function<Int, *>表示Function<Int, out Any?>

  • Function<*, *>表示Function<in Nothing, out Any?>

6. 型變的概念

在上面提到過使用處型變和聲明處型變,那具體型變指什么呢?型變:是否允許對(duì)參數(shù)類型進(jìn)行子類型轉(zhuǎn)換。例如在 Java 中List<Integer>List<Number>沒有直接的類型關(guān)系,就是說 Java 中的泛型是不可以直接型變的。

為了提高代碼的靈活性,Java 中可以通配符在使用時(shí)實(shí)現(xiàn)型變,例如void addNumbers(List<? super Number> list)方法中可以傳List<Integer>,List<Integer>List<? super Number>的子類型,而 Integer 也是 Number 的子類型,這也稱之為協(xié)變。另外,List<Number>List<? super Integer>的子類型,和 Integer 與 Number 之間的類型關(guān)系相反,稱之為逆變。

Kotlin 中outin操作符可以更簡(jiǎn)潔地實(shí)現(xiàn) Java 的使用處型變,而且還支持聲明處型變,這也使得 Kotlin 中的泛型是可以直接型變的。

Kotlin 下協(xié)變:interface List<out E>,List<Int>是``List<Number>`的子類型。

Kotlin 下逆變:interface Comparable<in T>Comparable<Double>Comparable<Number>的父類型。

7. 具體化的類型參數(shù)

Kotlin 與 Java 中泛型都會(huì)進(jìn)行類型擦除,泛型的具體類型在運(yùn)行時(shí)是未知的,例如在解析 json 字符串時(shí):

public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { ... } 

還必須傳泛型的 Class 類型,不能直接使用T.class獲取類型,除非使用反射。

而在 Kotlin 中可以使用reified修飾符將內(nèi)聯(lián)函數(shù)的泛型類型當(dāng)作具體的類型來使用,不需要再額外傳一個(gè) class 對(duì)象:

inline fun <reified T> Gson.fromJson(json: String): T? {
    return fromJson(json, T::class.java)
}

對(duì)于具體化的類型參數(shù),可以當(dāng)做一個(gè)普通的類型一樣,as!as操作符也可以使用。因?yàn)?Kotlin 編譯器會(huì)把內(nèi)聯(lián)函數(shù)的代碼插入到調(diào)用者的地方,所以可以在編譯期就確定泛型的類型。需要注意的是,Kotlin 中的reified的內(nèi)聯(lián)函數(shù)不能被 Java 代碼調(diào)用。

8. 小結(jié)

回顧 Kotlin 和 Java 中的泛型,Kotlin 泛型擴(kuò)展了 Java 中的泛型,添加了使用處型變和更安全的星投影,還支持具體化的類型參數(shù)。我整理了下面表格對(duì)比兩者:

Java 泛型 Java 中代碼示例 Kotlin 中代碼示例 Kotlin 泛型
泛型類型 class Box<T> class Box<T> 泛型類型
泛型方法 <K, V> boolean method(Pair<K, V> p) fun <K, V> function(p: Pair<K, V>) 泛型函數(shù)
有界類型參數(shù) class Box<T extends Comparable<T> class Box<T : Comparable<T>> 泛型約束
上界通配符 void sumOfList(List<? extends Number> list) fun sumOfList(list: List<out Number>) 使用處協(xié)變
下界通配符 void addNumbers(List<? super Integer> list) fun addNumbers(list: List<in Int>) 使用處逆變
interface Collection<out E> : Iterable<E> 聲明處協(xié)變
interface Comparable<in T> 聲明處逆變
原始類型 ArrayList unkownList = new ArrayList<String>(5) val unkownList: ArrayList<*> = ArrayList<Int>(5) 星投影

總的來說,Kotlin 泛型更加簡(jiǎn)潔安全,但是和 Java 一樣都是有類型擦除的,都屬于編譯時(shí)泛型。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 人生苦多,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,759評(píng)論 9 118
  • 寫在開頭:本人打算開始寫一個(gè)Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,551評(píng)論 1 3
  • 前言 泛型(Generics)的型變是Java中比較難以理解和使用的部分,“神秘”的通配符,讓我看了幾遍《Java...
    珞澤珈群閱讀 8,140評(píng)論 12 51
  • 本文是在學(xué)習(xí)和使用kotlin時(shí)的一些總結(jié)與體會(huì),一些代碼示例來自于網(wǎng)絡(luò)或Kotlin官方文檔,持續(xù)更新... 對(duì)...
    竹塵居士閱讀 3,494評(píng)論 0 8
  • 我是一只孤獨(dú)的鳥 我想有一個(gè)男朋友。 可我不敢和別人談戀愛。 我確實(shí)害怕寂寞,更害怕失去。 夜那么深那么涼。我想逼...
    小阿徐閱讀 403評(píng)論 13 4

友情鏈接更多精彩內(nèi)容