---
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());
}
}
```
> 
透過反編譯後,我們可以看到 `List`#`add` 方法的接收參數轉為 Object,而不是在源碼中的泛型指定類
> 
### Java 數組類型:不擦除
* 我們看另外一個擦除與否的案例「**數組**, `Array`」;以結論來講 Java 在編譯時 **不會將數組類型擦除**,也就是數組類型會保留到運行時!
:::warning
* Java 不支援泛型數組,所以要使用原生類型的數組
> 
:::
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());
}
}
```
> 
我們將編譯過後的 `.class` 檔反編譯,會發現 Java 簽名會呈現為 Array 的形式,並不會擦除(仍保留指定類型)
> 
### 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);
}
}
}
}
```
> 
**使用 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)
}
}
}
}
}
```
> 
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 類會保存泛型的相關資訊
:::
> 
**使用 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)
}
}
}
}
}
```
> 
### 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,數組會保持簽名
> 
:::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)
}
}
```
> 
:::warning
* **沒有 `reified` 就無法透過 泛型 取得 class**
> 
:::
## 通配符:型變
通配符以 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;
}
}
```
如下圖,我們可以看到,**==類有關係(上面案例中是繼承關係)不代表類型有關係==**!
> 
要讓「**類 & 類型**」產生關係就要使用 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;
}
}
```
> 
* **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 列表**
> 
使用 `<? 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
}
}
```
> 
:::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)
}
```
> 
:::danger
* 如果改成 `out` 就不能安全取值,編譯就會出錯
> 
:::
## Kotlin 泛型約束
上面我們提到的是泛型的通配符(主要是讓類型 Type 產生繼承關係),而這裡要講的則是泛型的 **約束**
Java 中使用 `<T extends E>` 來約束能接收的參數只能是 E 或 E 的子類;**在 Kotlin 中使用 `:` 符號來進行約束**
:::warning
* 不能使用 super 限制,只能使用 extends
> 
:::
### 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)
}
```
> 
## 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
* 投影時必須直接指定類型,不可寫在指定屬性時
> 
:::
### 限制類型:星號投影
* 如果我們要在 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);
}
}
}
```
> 
* 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)
}
```
> 
:::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`