# 泛型
[TOC]
>[info]注意:本篇文章,初次閱讀很難理解,建議可參考這篇文章,[泛型](https://c.lioil.win/2017/06/23/Kotlin-generics.html)
與 Java 類似,Kotlin 中的類也可以有類型參數:
```kotlin
class Box<T>(t: T) {
var value = t
}
```
一般來說,要創建這樣類的實例,我們需要提供類型參數:
```kotlin
val box: Box<Int> = Box<Int>(1)
```
但是**如果類型參數可以推斷出來**,例如從構造函數的參數或者從其他途徑,**允許省略類型參數**:
```kotlin
val box = Box(1) // 1 具有類型 Int,所以編譯器知道我們說的是 Box<Int>。
```
## 型變
Java 類型系統中最棘手的部分之一是通配符類型(參見 [Java Generics FAQ](http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html))。而 Kotlin 中沒有。 相反,它有兩個其他的東西:聲明處型變(declaration-site variance)與類型投影(type projections)。
首先,讓我們思考為什么 Java 需要那些神秘的通配符。在 [《Effective Java》第三版](http://www.oracle.com/technetwork/java/effectivejava-136174.html) 解釋了該問題——第 31 條:*利用有限制通配符來提升 API 的靈活性*。
首先,Java 中的泛型是**不型變的**,這意味著 `List<String>` 并**不是** `List<Object>` 的子類型。
為什么這樣? 如果 List 不是**不型變的**,它就沒比 Java 的數組好到哪去,因為如下代碼會通過編譯然后導致運行時異常:
``` java
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即將來臨的問題的原因就在這里。Java 禁止這樣!
objs.add(1); // 這里我們把一個整數放入一個字符串列表
String s = strs.get(0); // !!! ClassCastException:無法將整數轉換為字符串
```
因此,Java 禁止這樣的事情以保證運行時的安全。但這樣會有一些影響。例如,考慮 `Collection` 接口中的 `addAll()`
方法。該方法的簽名應該是什么?直覺上,我們會這樣:
``` java
// Java
interface Collection<E> …… {
void addAll(Collection<E> items);
}
```
但隨后,我們將無法做到以下簡單的事情(這是完全安全):
``` java
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!!對于這種簡單聲明的 addAll 將不能編譯:
// Collection<String> 不是 Collection<Object> 的子類型
}
```
(在 Java 中,我們艱難地學到了這個教訓,參見[《Effective Java》第三版](http://www.oracle.com/technetwork/java/effectivejava-136174.html),第 28 條:*列表優先于數組*)
這就是為什么 `addAll()` 的實際簽名是以下這樣:
``` java
// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
```
**通配符類型參數** `? extends E` 表示此方法接受 `E` *或者 `E` 的 一些子類型*對象的集合,而不只是 `E` 自身。這意味著我們可以安全地從其中(該集合中的元素是 E 的子類的實例)**讀取** `E`,但**不能寫入**,因為我們不知道什么對象符合那個未知的 `E` 的子類型。反過來,該限制可以讓`Collection<String>`表示為`Collection<? extends Object>`的子類型。簡而言之,帶 **extends** 限定(**上界**)的通配符類型使得類型是**協變的(covariant)**。
理解為什么這個技巧能夠工作的關鍵相當簡單:如果只能從集合中獲取項目,那么使用 `String` 的集合,
并且從其中讀取 `Object` 也沒問題 。反過來,如果只能向集合中 _放入_ 項目,就可以用`Object` 集合并向其中放入 `String`:在 Java 中有 `List<? super String>` 是 `List<Object>` 的一個**超類**。
后者稱為**逆變性(contravariance)**,并且對于 `List <? super String>` 你只能調用接受 String 作為參數的方法(例如,你可以調用 `add(String)` 或者 `set(int, String)`),當然如果調用函數返回 `List<T>` 中的 `T`,你得到的并非一個 `String` 而是一個 `Object`。
Joshua Bloch 稱那些你只能從中**讀取**的對象為**生產者**,并稱那些你只能**寫入**的對象為**消費者**。他建議:“*為了靈活性最大化,在表示生產者或消費者的輸入參數上使用通配符類型*”,并提出了以下助記符:
*****
*PECS 代表生產者-Extens,消費者-Super(Producer-Extends, Consumer-Super)。*
*****
>[info]*注意*:如果你使用一個生產者對象,如 `List<? extends Foo>`,在該對象上不允許調用 `add()` 或 `set()`。但這并不意味著該對象是**不可變的**:例如,沒有什么阻止你調用 `clear()`從列表中刪除所有項目,因為 `clear()`根本無需任何參數。通配符(或其他類型的型變)保證的唯一的事情是**類型安全**。不可變性完全是另一回事。
### 聲明處型變
假設有一個泛型接口 `Source<T>`,該接口中不存在任何以 `T` 作為參數的方法,只是方法返回 `T` 類型值:
``` java
// Java
interface Source<T> {
T nextT();
}
```
那么,在 `Source <Object>` 類型的變量中存儲 `Source <String>` 實例的引用是極為安全的——沒有消費者-方法可以調用。但是 Java 并不知道這一點,并且仍然禁止這樣操作:
``` java
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允許
// ……
}
```
為了修正這一點,我們必須聲明對象的類型為 `Source<? extends Object>`,這是毫無意義的,因為我們可以像以前一樣在該對象上調用所有相同的方法,所以更復雜的類型并沒有帶來價值。但編譯器并不知道。
在 Kotlin 中,有一種方法向編譯器解釋這種情況。這稱為**聲明處型變**:我們可以標注 `Source` 的**類型參數** `T` 來確保它僅從 `Source<T>` 成員中**返回**(生產),并從不被消費。為此,我們提供 **out** 修飾符:
```kotlin
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 這個沒問題,因為 T 是一個 out-參數
// ……
}
```
一般原則是:當一個類 `C` 的類型參數 `T` 被聲明為 **out** 時,它就只能出現在 `C` 的成員的**輸出**\-位置,但回報是 `C<Base>` 可以安全地作為`C<Derived>`的超類。
簡而言之,他們說類 `C` 是在參數 `T` 上是**協變的**,或者說 `T` 是一個**協變的**類型參數。你可以認為 `C` 是 `T` 的**生產者**,而不是 `T` 的**消費者**。
**out**修飾符稱為**型變注解**,并且由于它在類型參數聲明處提供,所以我們稱之為**聲明處型變**。這與 Java 的**使用處型變**相反,其類型用途通配符使得類型協變。
另外除了 **out**,Kotlin 又補充了一個型變注釋:**in**。它使得一個類型參數**逆變**:只可以被消費而不可以被生產。逆變類型的一個很好的例子是 `Comparable`:
```kotlin
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 擁有類型 Double,它是 Number 的子類型
// 因此,我們可以將 x 賦給類型為 Comparable <Double> 的變量
val y: Comparable<Double> = x // OK!
}
```
我們相信 **in** 和 **out** 兩詞是自解釋的(因為它們已經在 C# 中成功使用很長時間了),因此上面提到的助記符不是真正需要的,并且可以將其改寫為更高的目標:
**[存在性(The Existential)](https://zh.wikipedia.org/wiki/%E5%AD%98%E5%9C%A8%E4%B8%BB%E4%B9%89) 轉換:消費者 in, 生產者 out\!** :-)
## 類型投影
### 使用處型變:類型投影
將類型參數 T 聲明為 *out* 非常方便,并且能避免使用處子類型化的麻煩,但是有些類實際上**不能**限制為只返回 `T`!
一個很好的例子是 Array:
```kotlin
class Array<T>(val size: Int) {
fun get(index: Int): T { …… }
fun set(index: Int, value: T) { …… }
}
```
該類在 `T` 上既不能是協變的也不能是逆變的。這造成了一些不靈活性。考慮下述函數:
```kotlin
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
```
這個函數應該將項目從一個數組復制到另一個數組。讓我們嘗試在實踐中應用它:
```kotlin
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// ^ 其類型為 Array<Int> 但此處期望 Array<Any>
```
這里我們遇到同樣熟悉的問題:`Array <T>` 在 `T` 上是**不型變的**,因此 `Array <Int>` 和 `Array <Any>` 都不是另一個的子類型。為什么? 再次重復,因為 copy **可能**做壞事,也就是說,例如它可能嘗試**寫**一個 String 到 `from`,并且如果我們實際上傳遞一個 `Int` 的數組,一段時間后將會拋出一個 `ClassCastException` 異常。
那么,我們唯一要確保的是 `copy()` 不會做任何壞事。我們想阻止它**寫**到 `from`,我們可以:
```kotlin
fun copy(from: Array<out Any>, to: Array<Any>) { …… }
```
這里發生的事情稱為**類型投影**:我們說`from`不僅僅是一個數組,而是一個受限制的(**投影的**)數組:我們只可以調用返回類型為類型參數`T` 的方法,如上,這意味著我們只能調用 `get()`。這就是我們的**使用處型變**的用法,并且是對應于 Java 的 `Array<? extends Object>`、但使用更簡單些的方式。
你也可以使用 **in** 投影一個類型:
```kotlin
fun fill(dest: Array<in String>, value: String) { …… }
```
`Array<in String>` 對應于 Java 的 `Array<? super String>`,也就是說,你可以傳遞一個 `CharSequence` 數組或一個 `Object` 數組給 `fill()` 函數。
### 星投影
有時你想說,你對類型參數一無所知,但仍然希望以安全的方式使用它。
這里的安全方式是定義泛型類型的這種投影,該泛型類型的每個具體實例化將是該投影的子類型。
Kotlin 為此提供了所謂的**星投影**語法:
- 對于 `Foo <out T : TUpper>`,其中 `T` 是一個具有上界 `TUpper` 的協變類型參數,`Foo <*>` 等價于 `Foo <out TUpper>`。 這意味著當 `T` 未知時,你可以安全地從 `Foo <*>` *讀取* `TUpper` 的值。
- 對于 `Foo <in T>`,其中 `T` 是一個逆變類型參數,`Foo <*>` 等價于 `Foo <in Nothing>`。 這意味著當 `T` 未知時,沒有什么可以以安全的方式*寫入* `Foo <*>`。
- 對于 `Foo <T : TUpper>`,其中 `T` 是一個具有上界 `TUpper` 的不型變類型參數,`Foo<*>` 對于讀取值時等價于 `Foo<out TUpper>` 而對于寫值時等價于 `Foo<in Nothing>`。
如果泛型類型具有多個類型參數,則每個類型參數都可以單獨投影。例如,如果類型被聲明為 `interface Function <in T, out U>`,我們可以想象以下星投影:
- `Function<*, String>` 表示 `Function<in Nothing, String>`;
- `Function<Int, *>` 表示 `Function<Int, out Any?>`;
- `Function<*, *>` 表示 `Function<in Nothing, out Any?>`。
*注意*:星投影非常像 Java 的原始類型,但是安全。
## 泛型函數
不僅類可以有類型參數。函數也可以有。類型參數要放在函數名稱**之前**:
```kotlin
fun <T> singletonList(item: T): List<T> {
// ……
}
fun <T> T.basicToString(): String { // 擴展函數
// ……
}
```
要調用泛型函數,在調用處函數名**之后**指定類型參數即可:
```kotlin
val l = singletonList<Int>(1)
```
可以省略能夠從上下文中推斷出來的類型參數,所以以下示例同樣適用:
```kotlin
val l = singletonList(1)
```
## 泛型約束
能夠替換給定類型參數的所有可能類型的集合可以由**泛型約束**限制。
### 上界
最常見的約束類型是與 Java 的 *extends* 關鍵字對應的 **上界**:
```kotlin
fun <T : Comparable<T>> sort(list: List<T>) { …… }
```
冒號之后指定的類型是**上界**:只有 `Comparable<T>` 的子類型可以替代 `T`。 例如:
```kotlin
sort(listOf(1, 2, 3)) // OK。Int 是 Comparable<Int> 的子類型
sort(listOf(HashMap<Int, String>())) // 錯誤:HashMap<Int, String> 不是 Comparable<HashMap<Int, String>> 的子類型
```
默認的上界(如果沒有聲明)是 `Any?`。在尖括號中只能指定一個上界。如果同一類型參數需要多個上界,我們需要一個單獨的 **where**-子句:
```kotlin
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
}
```
所傳遞的類型必須同時滿足 `where` 子句的所有條件。在上述示例中,類型 `T` 必須*既*實現了 `CharSequence` *也*實現了 `Comparable`。
## 類型擦除
Kotlin 為泛型聲明用法執行的類型安全檢測僅在編譯期進行。運行時泛型類型的實例不保留關于其類型實參的任何信息。其類型信息稱為被*擦除*。例如,`Foo<Bar>` 與 `Foo<Baz?>` 的實例都會被擦除為`Foo<*>`。
因此,并沒有通用的方法在運行時檢測一個泛型類型的實例是否通過指定類型參數所創建,并且編譯器[禁止這種 *is*檢測](http://www.kotlincn.net/docs/reference/typecasts.html#%E7%B1%BB%E5%9E%8B%E6%93%A6%E9%99%A4%E4%B8%8E%E6%B3%9B%E5%9E%8B%E6%A3%80%E6%B5%8B)。
類型轉換為帶有具體類型參數的泛型類型,如 `foo as List<String>` 無法在運行時檢測。當高級程序邏輯隱含了類型轉換的類型安全而無法直接通過編譯器推斷時,可以使用這種[非受檢類型轉換](http://www.kotlincn.net/docs/reference/typecasts.html#%E9%9D%9E%E5%8F%97%E6%A3%80%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2)。編譯器會對非受檢類型轉換發出警告,并且在運行時只對非泛型部分檢測(相當于 `foo as List<*>`)。
泛型函數調用的類型參數也同樣只在編譯期檢測。在函數體內部,類型參數不能用于類型檢測,并且類型轉換為類型參數(`foo as T`)也是非受檢的。然而,內聯函數的[具體化的類型參數](http://www.kotlincn.net/docs/reference/inline-functions.html#%E5%85%B7%E4%BD%93%E5%8C%96%E7%9A%84%E7%B1%BB%E5%9E%8B%E5%8F%82%E6%95%B0)會由調用處內聯函數體中的類型實參所代入,因此可以用于類型檢測與轉換,與上述泛型類型的實例具有相同限制。
- 前言
- Kotlin簡介
- IntelliJ IDEA技巧總結
- idea設置類注釋和方法注釋模板
- 像Android Studion一樣創建工程
- Gradle
- Gradle入門
- Gradle進階
- 使用Gradle創建一個Kotlin工程
- 環境搭建
- Androidstudio平臺搭建
- Eclipse的Kotlin環境配置
- 使用IntelliJ IDEA
- Kotlin學習路線
- Kotlin官方中文版文檔教程
- 概述
- kotlin用于服務器端開發
- kotlin用于Android開發
- kotlin用于JavaScript開發
- kotlin用于原生開發
- Kotlin 用于數據科學
- 協程
- 多平臺
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 開始
- 基本語法
- 習慣用法
- 編碼規范
- 基礎
- 基本類型
- 包與導入
- 控制流
- 返回與跳轉
- 類與對象
- 類與繼承
- 屬性與字段
- 接口
- 可見性修飾符
- 擴展
- 數據類
- 密封類
- 泛型
- 嵌套類
- 枚舉類
- 對象
- 類型別名
- 內嵌類
- 委托
- 委托屬性
- 函數與Lambda表達式
- 函數
- Lambda表達式
- 內聯函數
- 集合
- 集合概述
- 構造集合
- 迭代器
- 區間與數列
- 序列
- 操作概述
- 轉換
- 過濾
- 加減操作符
- 分組
- 取集合的一部分
- 取單個元素
- 排序
- 聚合操作
- 集合寫操作
- List相關操作
- Set相關操作
- Map相關操作
- 多平臺程序設計
- 平臺相關聲明
- 以Gradle創建
- 更多語言結構
- 解構聲明
- 類型檢測與轉換
- This表達式
- 相等性
- 操作符重載
- 空安全
- 異常
- 注解
- 反射
- 作用域函數
- 類型安全的構造器
- Opt-in Requirements
- 核心庫
- 標準庫
- kotlin.test
- 參考
- 關鍵字與操作符
- 語法
- 編碼風格約定
- Java互操作
- Kotlin中調用Java
- Java中調用Kotlin
- JavaScript
- 動態類型
- kotlin中調用JavaScript
- JavaScript中調用kotlin
- JavaScript模塊
- JavaScript反射
- JavaScript DCE
- 原生
- 并發
- 不可變性
- kotlin庫
- 平臺庫
- 與C語言互操作
- 與Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 調試
- FAQ
- 協程
- 協程指南
- 基礎
- 取消與超時
- 組合掛起函數
- 協程上下文與調度器
- 異步流
- 通道
- 異常處理與監督
- 共享的可變狀態與并發
- Select表達式(實驗性)
- 工具
- 編寫kotlin代碼文檔
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin與OSGI
- 編譯器插件
- 編碼規范
- 演進
- kotlin語言演進
- 不同組件的穩定性
- kotlin1.3的兼容性指南
- 常見問題
- FAQ
- 與Java比較
- 與Scala比較(官方已刪除)
- Google開發者官網簡介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin樣品
- Kotlin零基礎到進階
- 第一階段興趣入門
- kotlin簡介和學習方法
- 數據類型和類型系統
- 入門
- 分類
- val和var
- 二進制基礎
- 基礎
- 基本語法
- 包
- 示例
- 編碼規范
- 代碼注釋
- 異常
- 根類型“Any”
- Any? 可空類型
- 可空性的實現原理
- kotlin.Unit類型
- kotlin.Nothing類型
- 基本數據類型
- 數值類型
- 布爾類型
- 字符型
- 位運算符
- 變量和常量
- 語法和運算符
- 關鍵字
- 硬關鍵字
- 軟關鍵字
- 修飾符關鍵字
- 特殊標識符
- 操作符和特殊符號
- 算術運算符
- 賦值運算符
- 比較運算符
- 邏輯運算符
- this關鍵字
- super關鍵字
- 操作符重載
- 一元操作符
- 二元操作符
- 字符串
- 字符串介紹和屬性
- 字符串常見方法操作
- 字符串模板
- 數組
- 數組介紹創建及遍歷
- 數組常見方法和屬性
- 數組變化以及下標越界問題
- 原生數組類型
- 區間
- 正向區間
- 逆向區間
- 步長
- 類型檢測與類型轉換
- is、!is、as、as-運算符
- 空安全
- 可空類型變量
- 安全調用符
- 非空斷言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函數
- 函數式編程概述
- OOP和FOP
- 函數式編程基本特性
- 組合與范疇
- 在Kotlin中使用函數式編程
- 函數入門
- 函數作用域
- 函數加強
- 命名參數
- 默認參數
- 可變參數
- 表達式函數體
- 頂層、嵌套、中綴函數
- 尾遞歸函數優化
- 函數重載
- 控制流
- if表達式
- when表達式
- for循環
- while循環
- 循環中的 Break 與 continue
- return返回
- 標簽處返回
- 集合
- list集合
- list集合介紹和操作
- list常見方法和屬性
- list集合變化和下標越界
- set集合
- set集合介紹和常見操作
- set集合常見方法和屬性
- set集合變換和下標越界
- map集合
- map集合介紹和常見操作
- map集合常見方法和屬性
- map集合變換
- 集合的函數式API
- map函數
- filter函數
- “ all ”“ any ”“ count ”和“ find ”:對集合應用判斷式
- 別樣的求和方式:sumBy、sum、fold、reduce
- 根據人的性別進行分組:groupBy
- 扁平化——處理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 區間、數組、集合之間轉換
- 面向對象
- 面向對象-封裝
- 類的創建及屬性方法訪問
- 類屬性和字段
- 構造器
- 嵌套類(內部類)
- 枚舉類
- 枚舉類遍歷&枚舉常量常用屬性
- 數據類
- 密封類
- 印章類(密封類)
- 面向對象-繼承
- 類的繼承
- 面向對象-多態
- 抽象類
- 接口
- 接口和抽象類的區別
- 面向對象-深入
- 擴展
- 擴展:為別的類添加方法、屬性
- Android中的擴展應用
- 優化Snackbar
- 用擴展函數封裝Utils
- 解決煩人的findViewById
- 擴展不是萬能的
- 調度方式對擴展函數的影響
- 被濫用的擴展函數
- 委托
- 委托類
- 委托屬性
- Kotlin5大內置委托
- Kotlin-Object關鍵字
- 單例模式
- 匿名類對象
- 伴生對象
- 作用域函數
- let函數
- run函數
- with函數
- apply函數
- also函數
- 標準庫函數
- takeIf 與 takeUnless
- 第二階段重點深入
- Lambda編程
- Lambda成員引用高階函數
- 高階函數
- 內聯函數
- 泛型
- 泛型的分類
- 泛型約束
- 子類和子類型
- 協變與逆變
- 泛型擦除與實化類型
- 泛型類型參數
- 泛型的背后:類型擦除
- Java為什么無法聲明一個泛型數組
- 向后兼容的罪
- 類型擦除的矛盾
- 使用內聯函數獲取泛型
- 打破泛型不變
- 一個支持協變的List
- 一個支持逆變的Comparator
- 協變和逆變
- 第三階段難點突破
- 注解和反射
- 聲明并應用注解
- DSL
- 協程
- 協程簡介
- 協程的基本操作
- 協程取消
- 管道
- 慕課霍丙乾協程筆記
- Kotlin與Java互操作
- 在Kotlin中調用Java
- 在Java中調用Kotlin
- Kotlin與Java中的操作對比
- 第四階段專題練習
- 朱凱Kotlin知識點總結
- Kotlin 基礎
- Kotlin 的變量、函數和類型
- Kotlin 里那些「不是那么寫的」
- Kotlin 里那些「更方便的」
- Kotlin 進階
- Kotlin 的泛型
- Kotlin 的高階函數、匿名函數和 Lambda 表達式
- Kotlin協程
- 初識
- 進階
- 深入
- Kotlin 擴展
- 會寫「18.dp」只是個入門——Kotlin 的擴展函數和擴展屬性(Extension Functions / Properties)
- Kotlin實戰-開發Android