--- title: 'Kotlin 泛型' disqus: kyleAlien --- Kotlin 泛型 === ## Overview of Content 以下參考,Kotlin 進階實戰 如有引用參考本文章請詳註出處,感謝 :smile: Java 泛型可以參考 [**Java 泛型篇**](https://hackmd.io/Dlm0Ov0URya2t6mbTgNQkQ?view#Java-%E6%B3%9B%E5%9E%8B) :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入探究 Kotlin 與 Java 泛型:擦除、取得泛型類型、型變、投影 | 協變、逆變**](https://devtechascendancy.com/explore-kotlin-java-generics_type_erasure/) ::: [TOC] ## Java 泛型擦除 Java 的泛型是 **偽泛型**,這是 Java 因為兼容性的考慮,**泛型只會存在於 `.java` 階段**,在進入 JVM 之前會將泛型轉為 Object 類(也就是在 JVM 運行中,是不會有泛型的) :::success **Kotlin 同樣使用泛型擦除機制**,所以以下舉例的 Java 例子也可以使用在 Kotlin 中 ::: ### Java 泛型:擦除 * 當泛型指定為基礎類別(`Integer`, `Long`, `String`... 等等),在編譯成 `.class` 之後就會被擦除,指定的泛型類別會轉為 Object 類(這個特點);範例如下… ```java= public class JavaGeneral_1 { public static void main(String[] args) { // 指定泛型類為 String List<String> list1 = new ArrayList<>(); list1.add("Alien"); // 指定泛型類為 Integer List<Integer> list2 = new ArrayList<>(); list2.add(123); System.out.println(list1.getClass()); System.out.println(list2.getClass()); } } ``` > ![](https://i.imgur.com/7KTk36R.png) 透過反編譯後,我們可以看到 `List`#`add` 方法的接收參數轉為 Object,而不是在源碼中的泛型指定類 > ![](https://i.imgur.com/BZoqbzj.png) ### Java 數組類型:不擦除 * 我們看另外一個擦除與否的案例「**數組**, `Array`」;以結論來講 Java 在編譯時 **不會將數組類型擦除**,也就是數組類型會保留到運行時! :::warning * Java 不支援泛型數組,所以要使用原生類型的數組 > ![](https://i.imgur.com/hok430n.png) ::: Java 數組類型範例如下 ```java= public class JavaGeneral_2 { public static void main(String[] args) { String[] strArray = new String[10]; Integer[] intArray = new Integer[10]; System.out.println(strArray.getClass()); System.out.println(intArray.getClass()); } } ``` > ![](https://i.imgur.com/mYJxjmw.png) 我們將編譯過後的 `.class` 檔反編譯,會發現 Java 簽名會呈現為 Array 的形式,並不會擦除(仍保留指定類型) > ![](https://i.imgur.com/xhrd3pg.png) ### JVM 識別泛型:Class 常量池 * 前我們知道在經過編譯後 Java 會將泛型擦除(轉為 Object 類),那 JVM 如何知道該 Object 類指定的泛型是哪個呢?明明已經被擦除了呀? Java 在運行時使用泛型時已經被擦除,但編譯完畢的 `.class` 文件中還是會保存了泛型相關的訊息 (**保存在 class 的常量池中**) :::info * 使用泛型的程式在編譯後會產生簽名 (`signature`) 字段,**簽名會指向在常量池中實際的類型,之後就可以透過該類型強制轉型** ::: ## Kotlin 取得泛型 ### Java/Kotlin 類:Class 獲得具體泛型 * 不管是匿名類,或是實體類都可以透過 Class 物件來獲取泛型的資訊 | 取得泛型資訊的函數 | 簡介 | | - | - | | Class#getGenericSuperclass | 取得類的類型(包含泛型資訊) | | ParameterizedType#getActualTypeArguments | ParameterizedType 是 Type 的子類,表示 **參數的類型** (可取得泛型的具體類型) | 1. **Java 匿名類取得泛型資訊**:範例如下 ```java= class Generic_1 { private static class GenericClz<T, R> { } public static void main(String[] args) { // 匿名類 GenericClz<Integer, String> genericClz = new GenericClz<>() { }; // 取得類的泛型的類型 Type genericSuperclass = genericClz.getClass().getGenericSuperclass(); System.out.println("Generic class: " + genericSuperclass); if(genericSuperclass instanceof ParameterizedType) { // 轉譯為 ParameterizedType // 取得實際的泛型類型 Type[] actualTypeArguments = ((ParameterizedType) genericSuperclass).getActualTypeArguments(); for(Type t: actualTypeArguments) { // 取得泛型類型 Generic type System.out.println("actualTypeArguments class: " + t); } } } } ``` > ![](https://i.imgur.com/79TdahX.png) **使用 Kotlin 匿名類,也可以達到同樣的效果**,範例如下 ```kotlin= object Generic_1_kt { open class GenericClz<T, S> @JvmStatic fun main(args: Array<String>) { // 匿名類 val genericClz = object : GenericClz<Int, String>() { } val typeClz = genericClz.javaClass.genericSuperclass println(typeClz) when(typeClz) { is ParameterizedType -> { for (i in typeClz.actualTypeArguments) { println(i) } } } } } ``` > ![](https://i.imgur.com/tmLoWd6.png) 2. **Java 繼承泛型**:當有一個類繼承於泛型類,並且有指定泛型類型時,也可以透過相同的 API 來取得泛型資訊;範例如下 ```java= class Generic_2 { // 基礎泛型 private static class GenericClz_2<T, R> { } // 繼承泛型 private static class ChildGeneric extends GenericClz_2<String, Long> { } public static void main(String[] args) { ChildGeneric genericClz = new ChildGeneric(); Type genericSuperclass = genericClz.getClass().getGenericSuperclass(); System.out.println("Generic class: " + genericSuperclass); if(genericSuperclass instanceof ParameterizedType) { Type[] actualTypeArguments = ((ParameterizedType) genericSuperclass).getActualTypeArguments(); for(Type t: actualTypeArguments) { // 取得 Generic type System.out.println("actualTypeArguments class: " + t); } } } } ``` :::info * 這也就證明了 Class 類會保存泛型的相關資訊 ::: > ![](https://i.imgur.com/lXqTKKC.png) **使用 Kotlin 也可以達到同樣的效果**,範例如下 ```kotlin= object Generic_2_kt { // 基礎泛型 open class GenericClz_2<T, S> // 繼承泛型 class ChildGenericClz : GenericClz_2<String, Long>() @JvmStatic fun main(args: Array<String>) { val genericClz = ChildGenericClz() val typeClz = genericClz.javaClass.genericSuperclass println(typeClz) when(typeClz) { is ParameterizedType -> { for (i in typeClz.actualTypeArguments) { println(i) } } } } } ``` > ![](https://i.imgur.com/S13cbwK.png) ### Kotlin 泛型數組 * 我們知道 Java 不支援泛型數組(上面小節有說到),不過 **Kotlin 支持泛性數組!!** ```kotlin= fun main() { // 創建數組, java/kotlin 都保留簽名 val array1 = arrayOf(1, 2, 3, 4, 5) val array2 = arrayOf("A", "B", "C", "D", "E") println(array1.javaClass) println(array2.javaClass) // 創建泛型數組, java 不能這樣寫!不過 kotlin 可以! val list1 = listOf<Int>(1, 2, 3, 4, 5) val list2 = listOf<String>("A", "B", "C", "D", "E") println(list1.javaClass) println(list2.javaClass) } ``` 如同 Java,數組會保持簽名 > ![](https://i.imgur.com/AKbKonT.png) :::info * 從上面可以發現 `arrayOf` 沒有擦除, 而 `listOf` 被擦除,而它們有以下差異 | 函數 | 是否使用 `inline` | 是否使用 `reified` (具體化) | | - | - | - | | arrayOf | Y | Y | | listOf | N | N | 可以看到 `arrayOf` 有使用 `inline`、`reified` 關鍵字,而這兩個關鍵字就可以用來保留泛型類型(以下小節會說明這兩個關鍵字) ```kotlin= // arrayOf 原型 public inline fun <reified @PureReifiable T> arrayOf(vararg elements: T) : Array<T> // listOf 原型 public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList() ``` ::: ### Kotlin 泛型方法:inline / reified 關鍵字 * 如果要 **保留類型不被擦除就需要使用 `inline` + `reified` 描述**,這樣才能 **不透過實例(`instance`)** 直接取得泛型 Class! ```kotlin= inline fun <reified T : Any> Gson.formJson(jsonStr: String) : T = // 這樣才能直接取得泛型 Class Gson().formJson(json, T::class.java) ``` * 再一個 `inline` + `reified` 的使用範例,加強印象 ```kotlin= class Test { inline fun <reified T> toClassArray(vararg elements: T) : List<Class<*>> { val res = mutableListOf<Class<*>>() // 直接取得泛型 T 的實際類型 val clz = T::class.java for (i in elements) { res.add(i!!::class.java) } return res.toList() } } fun main() { val list = Test().toClassArray<Int>(1, 2, 3, 4) for(i in list) { println(i) } } ``` > ![](https://i.imgur.com/Ue39ndq.png) :::warning * **沒有 `reified` 就無法透過 泛型 取得 class** > ![](https://i.imgur.com/gVMDBBV.png) ::: ## 通配符:型變 通配符以 Java 來說就是 `?` 符號,可以用在讓 **泛型產生繼承關係** 的指定,加強靜態語言的檢查特性 > Kotlin 中是指 ++類型++ 轉換後的繼承關係 ### 類 Class & 類型 Type * **在 Kotlin 中,==類 & 類型是兩種不同的概念==**(或是說它強調了兩者的不同概念) * **類型(`Type`)**:具有相同特性的物件,強調了「**特性**」的概念,在程式中的表達方式通常可以使用 `interface`、`abstract` 描述特性 * **類(`Class`)**:類是類型的實現,提供「**具體**」的數據結構、方法集合,提供共同方法、操作,**它說明了如何使用物件** :::warning * **所以子類(`SubClass`) & 子類型(`SubType`)有很大的區別** 所有出現 Type 的地方可被另一個 SubType 取代;它們有相同的特性。在程式中,任何出現類型的地方都可以被另一個子類型取代 > 以 Java 泛型來舉例:`List<? extends Hello>`,可使用取代 `?` 符號的類型都是 Hello 或是 Hello 的子類 ::: * **接著我們再來看 Kotlin 的「可空類型, `Nullable Type`」與類之間的關係** Kotlin 的「可空類型」代表的是一種類型(`Type`),它說明的是該類型是「**可空的特性**」,可空類型本身並不代表一個具體的類,而是類型的一種特性,但這個 **可空類型並非代表一個類**!!如下表所示 | \ | Class 類 | Type 類型 | | ------------- |:-------- |:--------- | | String | Y | Y | | String? | N | Y | | List | Y | Y | | List<String\> | N | Y | ### Kotlin 泛型通配符:`Covariance` 協變 out * 先來看看 Java 的「**通配符上限, `extends`**」;**如果沒有通配符限制,每個類型都是單獨個體**,不會有任何關係 ```java= public class JavaType { // 父類 private static class Animal { } // 子類 private static class Dog extends Animal { } public static void main(String[] args) { List<Animal> animalList = new ArrayList<>(); List<Dog> dogList = new ArrayList<>(); // Error animalList = dogList; } } ``` 如下圖,我們可以看到,**==類有關係(上面案例中是繼承關係)不代表類型有關係==**! > ![](https://i.imgur.com/NSkpM06.png) 要讓「**類 & 類型**」產生關係就要使用 Java 的通配符 `?`,這樣就可以建立「類」與「類型」的產生關係 ```java= public class JavaType { // 父類 private static class Animal { } // 子類 private static class Dog extends Animal { } public static void main(String[] args) { // 使用通配符! List<? extends Animal> animalList = new ArrayList<>(); List<Dog> dogList = new ArrayList<>(); // OK animalList = dogList; } } ``` > ![](https://i.imgur.com/aBQMGX0.png) * **Kotlin 中實現同樣的效果則是使用 `out` 關鍵字**;像是 `List<out E>` 就有使用到 `out` 關鍵字 Kotlin 的 List 源碼:可以看到 `<out E>` 關鍵字,它與 Java 的 `<? extends E>` 有相同的功能,它代表了可以 **安全取值**! ```kotlin= // List 源碼 public interface List<out E> : Collection<E> { // Query Operations override val size: Int override fun isEmpty(): Boolean override fun contains(element: @UnsafeVariance E): Boolean override fun iterator(): Iterator<E> // Bulk Operations override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean public operator fun get(index: Int): E public fun indexOf(element: @UnsafeVariance E): Int ... 省略部分 } ``` :::warning * **`@UnsafeVariance` 是幹嘛的**? 主要因為這個操作可能違反規則,**用來告訴編譯器這個操作由自己負責** > 違反規則的操作:像是我可以在 `contains` 函數中修改原來元素的內容,這就不安全 ::: 同上面 Java 的程式,但我們現在用 Kotlin 來撰寫,可以看到編譯結果是完全正常不會報錯的! ```kotlin= fun main() { open class Animal class Dog : Animal() // 由於協變 // 所以 List<Animal> 相當於寫了 List<? extends Animal> var animalList : List<Animal> = ArrayList() val dogList = ArrayList<Dog>() // Okay, Pass ~ animalList = dogList } ``` 這是因為在 Kotlin 中,對於擁有不同類型參數的泛型類型,如果 **泛型參數的類型是協變的(`out`)**,**則泛型類型本身也是協變的** > 換句話說 **`List<Dog>` 是 `List<Animal>` 的子類型,因為 List 在 Kotlin 中被聲明為 `<out E>` 的形式**,這也就是為什麼我們可以將 dogList 分配給 animalList,而不需要使用通配符 ```kotlin= // 由於協變的關係,Kotlin 會讓 List<Animal>、 List<Dog> 自動產生關係 List<Animal> List<Dog> ``` :::success * **協變(`Covariance`)結論**: 如果 A 是 B 的子類(Class 關係),則在 Kotlin 中,協變使用 `out` 關鍵字表示泛型;這意味著,將 `List<A>` 視為 `List<B>` 的子類型(Type 關係)是合法的 ::: ### Kotlin 泛型通配符:`Contravariance` 逆變 in * 一樣先來看看 Java 的「**通配符下限, `super`**」;如果沒有通配符限制,則每個類型都是單獨個體,不會有任何關係;我們先來看看 Java 範例… ```java= class JavaType_2 { private static class Animal { } private static class Dog extends Animal { } public static void main(String[] args) { List<? extends Animal> animalList = new ArrayList<>(); // 無法安全的設定 animalList.add(new Dog()); // Error } } ``` 為甚麽 `add` 會被編譯器警告?這是 **因為不知道加入 `AnimalList` 的具體子類型,所以無法加入到 List 列表** > ![](https://i.imgur.com/RgwyPM7.png) 使用 `<? surper E>` 修正就可正常設定進列表;**這保證了設定進列表中的元素一定是 `E` or `E 的父類`!** 這相當於 OOP 類設計原則中的 **里式原則的一個展現** ```java= class JavaType_2 { private static class Animal { } private static class Dog extends Animal { } public static void main(String[] args) { // 修正 List<? super Animal> animalList = new ArrayList<>(); animalList.add(new Dog()); // Ok } } ``` > ![](https://i.imgur.com/GyUAVTb.png) :::info * 從這邊也可以看出 `<? extends>`、`<? super>` 是相互對應(正交)關係 * `<? extends>` 用來安全取值 * `<? super>` 用來安全的設定值 ::: * Kotlin 也可以使用 `in` 關鍵字達到上述 `super` 的效果 ```kotlin= interface IPrint<in E> { fun print(e: E) } class AddInfoPrint : IPrint<String> { override fun print(e: String) { println("Hello: $e") } } class CountPrint : IPrint<Int> { override fun print(e: Int) { for (i in 0 until e) { println("times: ${i + 1}") } } } fun main() { val info = AddInfoPrint() info.print("123") val count = CountPrint() count.print(3) } ``` > ![](https://i.imgur.com/ygryQiy.png) :::danger * 如果改成 `out` 就不能安全取值,編譯就會出錯 > ![](https://i.imgur.com/ixagG2d.png) ::: ## Kotlin 泛型約束 上面我們提到的是泛型的通配符(主要是讓類型 Type 產生繼承關係),而這裡要講的則是泛型的 **約束** Java 中使用 `<T extends E>` 來約束能接收的參數只能是 E 或 E 的子類;**在 Kotlin 中使用 `:` 符號來進行約束** :::warning * 不能使用 super 限制,只能使用 extends > ![](https://i.imgur.com/UbRtuBL.png) ::: ### Kotlin 泛型限制範例 * Kotlin 使用 `:` 符號限制傳入的類型(就像是 `extends`) ```kotlin= fun <T : Number> sum(vararg param: T) = param.sumOf { it.toDouble() } fun main() { val res = sum(1, 2, 3, 0.1) // 所有傳入的參數都是 Number 的子類 println(res) } ``` > ![](https://i.imgur.com/2fux3d2.png) ## Kotlin 投影 投影是 Kotlin 特殊的概念,可以用來 **限制泛型類型**,而不影響原先的類型! ### 限制類型:類型投影 * Kotlin 的 `MutalbleList<T>` 是泛型類,並且沒有用通配符(`<in>`、`<out>`)限制;這時我們可以透過對泛型 `<T>` 限制來達到泛型限制 ```kotlin= fun main() { val list1 : MutableList<String> = mutableListOf() list1.add("Hello") list1.add("World") // 可安全的取值 val list2 : MutableList<out String> = mutableListOf() list2.add("111") // Error list2.add("831") // Error // 可安全的設定值 val list3 : MutableList<in String> = mutableListOf() list3.add("555") list3.add("666") lateinit var list4 : MutableList<String> list4 = list3 // Error } ``` 1. 由於 `list2` 使用 `<out>` 來描述,所以不可以設定值 2. 進行投影過限制過的屬性不等於原先類型 :::info * 投影時必須直接指定類型,不可寫在指定屬性時 > ![](https://i.imgur.com/kU4SMwR.png) ::: ### 限制類型:星號投影 * 如果我們要在 Java 的通配符中不指定任何類型,我們就會使用 `<?>` 符號來表示,用來接收任何類型的泛型;範例如下 ```java= public class Java_Main { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("AAA"); list.add("BBB"); list.add("CCC"); showList(list); List<Integer> list2 = new ArrayList<>(); list2.add(111); list2.add(222); list2.add(333); showList(list2); } // 不限定類型 private static void showList(List<?> list) { for (Object o : list) { System.out.println(o); } } } ``` > ![](https://i.imgur.com/FdU1xDb.png) * Kotlin 中使用 `<*>` 符號來取代 `<?>` 符號 ```kotlin= fun showList_1(list: MutableList<*>) { for (i in list) { print(i) } println() } // `<*>` 意思也等同於 `<out Any?>` fun showList_2(list: MutableList<out Any?>) { for (i in list) { print(i) } println() } fun main() { val intList = mutableListOf<Int>(1, 2, 3, 4, 5) val strList = mutableListOf<String>("A", "B", "C", "D", "E") showList_1(intList) showList_1(strList) showList_2(intList) showList_2(strList) } ``` > ![](https://i.imgur.com/BryysFX.png) :::danger 像是這種不限定類型的投影,**只可以用來取得,不可用在寫入** (`out` 跟 `extends` 的概念類似) ::: ## 更多的 Kotlin 語言相關文章 在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容! ### Kotlin 語言基礎 * **Kotlin 語言基礎**:想要建立堅實的 Kotlin 基礎?以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念,幫你打造更堅固的 Kotlin 語言基礎 :::info * [**Kotlin 函數、類、屬性 | DataClass、Sealed、Object 關鍵字 | Enum、Companion、NPE**](https://devtechascendancy.com/kotlin-functions_oop_dataclass_sealed_object/) * [**深入探究 Kotlin 與 Java 泛型:擦除、取得泛型類型、型變、投影 | 協變、逆變**](https://devtechascendancy.com/explore-kotlin-java-generics_type_erasure/) * [**深入 Kotlin 函數特性:Inline、擴展、標準函數全解析 | 提升程式碼效能與可讀性**](https://devtechascendancy.com/kotlin_inline_extensions_standards-func/) * [**Kotlin DSL、操作符、中綴表達式 Infix | DSL 詳解 | DSL 設計與應用**](https://devtechascendancy.com/kotlin-dsl-operators-infix-explained/) ::: ### Kotlin 特性、特點 * **Kotlin 特性、特點**:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用 :::warning * [**Kotlin 代理與懶加載機制:使用、lazy 深度解析**](https://devtechascendancy.com/kotlin-delegate_java-proxy_lateinit_lazy/) * [**Kotlin Lambda 編程 & Bytecode | Array & Collections 集合 | 集合函數式 API**](https://devtechascendancy.com/kotlin-lambda-bytecode-array-collections-functional/) * [**深入理解 Kotlin:智能推斷與 Contact 規則**](https://devtechascendancy.com/kotlin-smart-inference-contract-rules-guide/) ::: ### Kotlin 進階:協程、響應式、異步 * **Kotlin 進階:協程、響應式、異步**:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章 :::danger * [**應用 Kotlin 協程:對比 Thread、創建協程、任務掛起 | Dispatcher、CoroutineContext、CoroutineScope**](https://devtechascendancy.com/applied-kotlin-coroutines-in-depth-guide/) * [**Kotlin Channel 使用介紹 | Select、Actor | 生產者消費者**](https://devtechascendancy.com/kotlin-channel_select_actor_cs/) * [**探索 Kotlin Flow:基本使用、RxJava 對比、背壓機制 | Flow 細節**](https://devtechascendancy.com/kotlin-flow-usage_compare-rx_backpressure/) ::: ## Appendix & FAQ :::info ::: ###### tags: `Kotlin`