原文章出處:[Kotlin 的泛型](https://kaixue.io/kotlin-generics/)
## 從 Kotlin 的 in 和 out 說起
這期是碼上開學 Kotlin 系列的獨立技術點部分的第一期,我們來聊一聊泛型。
提到 Kotlin 的泛型,通常離不開`in` 和`out`關鍵字,但泛型這門武功需要些基本功才能修煉,否則容易走火入魔,待筆者慢慢道來。
下面這段 Java 代碼在日常開發中應該很常見了:
~~~java
??
List<TextView> textViews = new ArrayList<TextView>();
~~~
其中`List<TextView>`表示這是一個泛型類型為`TextView`的`List`。
那到底什么是泛型呢?我們先來講講泛型的由來。
現在的程序開發大都是面向對象的,平時會用到各種類型的對象,一組對象通常需要用集合來存儲它們,因而就有了一些集合類,比如`List`、`Map`等。
這些集合類里面都是裝的具體類型的對象,如果每個類型都去實現諸如`TextViewList`、`ActivityList`這樣的具體的類型,顯然是不可能的。
因此就誕生了「泛型」,它的意思是把具體的類型泛化,編碼的時候用符號來指代類型,在使用的時候,再確定它的類型。
前面那個例子,`List<TextView>`就是泛型類型聲明。
既然泛型是跟類型相關的,那么是不是也能適用類型的多態呢?
先看一個常見的使用場景:
~~~java
??
TextView textView = new Button(context);
// ?? 這是多態
List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = buttons;
// ?? 多態用在這里會報錯 incompatible types: List<Button> cannot be converted to List<TextView>
~~~
我們知道`Button`是繼承自`TextView`的,根據 Java 多態的特性,第一處賦值是正確的。
但是到了`List<TextView>`的時候 IDE 就報錯了,這是因為 Java 的泛型本身具有「不可變性 Invariance」,Java 里面認為`List<TextView>`和`List<Button>`類型并不一致,也就是說,子類的泛型(`List<Button>`)不屬于泛型(`List<TextView>`)的子類。
> Java 的泛型類型會在編譯時發生**類型擦除**,為了保證類型安全,不允許這樣賦值。至于什么是類型擦除,這里就不展開了。
你可以試一下,在 Java 里用數組做類似的事情,是不會報錯的,這是因為數組并沒有在編譯時擦除類型:
> ~~~java
> ??
> TextView[] textViews = new TextView[10];
>
> ~~~
但是在實際使用中,我們的確會有這種類似的需求,需要實現上面這種賦值。
Java 提供了「泛型通配符」`? extends`和`? super`來解決這個問題。
## Java 中的 ? extends
在 Java 里面是這么解決的:
~~~java
??
List<Button> buttons = new ArrayList<Button>();
??
List<? extends TextView> textViews = buttons;
~~~
這個`? extends`叫做「上界通配符」,可以使 Java 泛型具有「協變性 Covariance」,協變就是允許上面的賦值是合法的。
> 在繼承關系樹中,子類繼承自父類,可以認為父類在上,子類在下。`extends`限制了泛型類型的父類型,所以叫上界。
它有兩層意思:
* 其中`?`是個通配符,表示這個`List`的泛型類型是一個**未知類型**。
* `extends`限制了這個未知類型的上界,也就是泛型類型必須滿足這個`extends`的限制條件,這里和定義`class`的`extends`關鍵字有點不一樣:
* 它的范圍不僅是所有直接和間接子類,還包括上界定義的父類本身,也就是`TextView`。
* 它還有`implements`的意思,即這里的上界也可以是`interface`。
這里`Button`是`TextView`的子類,滿足了泛型類型的限制條件,因而能夠成功賦值。
根據剛才的描述,下面幾種情況都是可以的:
~~~java
??
List<? extends TextView> textViews = new ArrayList<TextView>(); // ?? 本身
List<? extends TextView> textViews = new ArrayList<Button>(); // ?? 直接子類
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // ?? 間接子類
~~~
一般集合類都包含了`get`和`add`兩種操作,比如 Java 中的`List`,它的具體定義如下:
~~~java
??
public interface List<E> extends Collection<E>{
E get(int index);
boolean add(E e);
...
}
~~~
上面的代碼中,`E`就是表示泛型類型的符號(用其他字母甚至單詞都可以)。
我們看看在使用了上界通配符之后,`List`的使用上有沒有什么問題:
~~~java
??
List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // ?? get 可以
textViews.add(textView);
// ?? add 會報錯,no suitable method found for add(TextView)
~~~
前面說到`List<? extends TextView>`的泛型類型是個未知類型`?`,編譯器也不確定它是啥類型,只是有個限制條件。
由于它滿足`? extends TextView`的限制條件,所以`get`出來的對象,肯定是`TextView`的子類型,根據多態的特性,能夠賦值給`TextView`,啰嗦一句,賦值給`View`也是沒問題的。
到了`add`操作的時候,我們可以這么理解:
* `List<? extends TextView>`由于類型未知,它可能是`List<Button>`,也可能是`List<TextView>`。
* 對于前者,顯然我們要添加 TextView 是不可以的。
* 實際情況是編譯器無法確定到底屬于哪一種,無法繼續執行下去,就報錯了。
那我干脆不要`extends TextView`,只用通配符`?`呢?
這樣使用`List<?>`其實是`List<? extends Object>`的縮寫。
~~~java
??
List<Button> buttons = new ArrayList<>();
List<?> list = buttons;
Object obj = list.get(0);
list.add(obj); // ?? 這里還是會報錯
~~~
和前面的例子一樣,編譯器沒法確定`?`的類型,所以這里就只能`get`到`Object`對象。
同時編譯器為了保證類型安全,也不能向`List<?>`中添加任何類型的對象,理由同上。
由于`add`的這個限制,使用了`? extends`泛型通配符的`List`,只能夠向外提供數據被消費,從這個角度來講,向外提供數據的一方稱為「生產者 Producer」。對應的還有一個概念叫「消費者 Consumer」,對應 Java 里面另一個泛型通配符`? super`。
## Java 中的 ? super
先看一下它的寫法:
~~~java
??
??
List<? super Button> buttons = new ArrayList<TextView>();
~~~
這個`? super`叫做「下界通配符」,可以使 Java 泛型具有「逆變性 Contravariance」。
> 與上界通配符對應,這里 super 限制了通配符 ? 的子類型,所以稱之為下界。
它也有兩層意思:
* 通配符`?`表示`List`的泛型類型是一個**未知類型**。
* `super`限制了這個未知類型的下界,也就是泛型類型必須滿足這個`super`的限制條件。
* `super`我們在類的方法里面經常用到,這里的范圍不僅包括`Button`的直接和間接父類,也包括下界`Button`本身。
* `super`同樣支持`interface`。
上面的例子中,`TextView`是`Button`的父類型 ,也就能夠滿足`super`的限制條件,就可以成功賦值了。
根據剛才的描述,下面幾種情況都是可以的:
~~~java
??
List<? super Button> buttons = new ArrayList<Button>(); // ?? 本身
List<? super Button> buttons = new ArrayList<TextView>(); // ?? 直接父類
List<? super Button> buttons = new ArrayList<Object>(); // ?? 間接父類
~~~
對于使用了下界通配符的`List`,我們再看看它的`get`和`add`操作:
~~~java
??
List<? super Button> buttons = new ArrayList<TextView>();
Object object = buttons.get(0); // ?? get 出來的是 Object 類型
Button button = ...
buttons.add(button); // ?? add 操作是可以的
~~~
解釋下,首先`?`表示未知類型,編譯器是不確定它的類型的。
雖然不知道它的具體類型,不過在 Java 里任何對象都是`Object`的子類,所以這里能把它賦值給`Object`。
`Button`對象一定是這個未知類型的子類型,根據多態的特性,這里通過`add`添加`Button`對象是合法的。
使用下界通配符`? super`的泛型`List`,只能讀取到`Object`對象,一般沒有什么實際的使用場景,通常也只拿它來添加數據,也就是消費已有的`List<? super Button>`,往里面添加`Button`,因此這種泛型類型聲明稱之為「消費者 Consumer」。
* * *
小結下,Java 的泛型本身是不支持協變和逆變的。
* 可以使用泛型通配符`? extends`來使泛型支持協變,但是「只能讀取不能修改」,這里的修改僅指對泛型集合添加元素,如果是`remove(int index)`以及`clear`當然是可以的。
* 可以使用泛型通配符`? super`來使泛型支持逆變,但是「只能修改不能讀取」,這里說的不能讀取是指不能按照泛型類型讀取,你如果按照`Object`讀出來再強轉當然也是可以的。
根據前面的說法,這被稱為 PECS 法則:「*Producer-Extends, Consumer-Super*」。
理解了 Java 的泛型之后,再理解 Kotlin 中的泛型,有如練完九陽神功再練乾坤大挪移,就比較容易了。
## 說回 Kotlin 中的 out 和 in
和 Java 泛型一樣,Kolin 中的泛型本身也是不可變的。
* 使用關鍵字`out`來支持協變,等同于 Java 中的上界通配符`? extends`。
* 使用關鍵字`in`來支持逆變,等同于 Java 中的下界通配符`? super`。
~~~kotlin
???
var textViews: List<out TextView>
var textViews: List<in TextView>
~~~
換了個寫法,但作用是完全一樣的。`out`表示,我這個變量或者參數只用來輸出,不用來輸入,你只能讀我不能寫我;`in`就反過來,表示它只用來輸入,不用來輸出,你只能寫我不能讀我。
你看,我們 Android 工程師學不會`out`和`in`,其實并不是因為這兩個關鍵字多難,而是因為我們應該先學學 Java 的泛型。是吧?
說了這么多`List`,其實泛型在非集合類的使用也非常廣泛,就以「生產者-消費者」為例子:
~~~kotlin
???
class Producer<T> {
fun produce(): T {
...
}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // ?? 相當于 'List' 的 `get`
~~~
再來看看消費者:
~~~kotlin
???
class Consumer<T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context)) // ?? 相當于 'List' 的 'add'
~~~
## 聲明處的 out 和 in
在前面的例子中,在聲明`Producer`的時候已經確定了泛型`T`只會作為輸出來用,但是每次都需要在使用的時候加上`out TextView`來支持協變,寫起來很麻煩。
Kotlin 提供了另外一種寫法:可以在聲明類的時候,給泛型符號加上`out`關鍵字,表明泛型參數`T`只會用來輸出,在使用的時候就不用額外加`out`了。
~~~kotlin
??? ??
class Producer<out T> {
fun produce(): T {
...
}
}
val producer: Producer<TextView> = Producer<Button>() // ?? 這里不寫 out 也不會報錯
val producer: Producer<out TextView> = Producer<Button>() // ?? out 可以但沒必要
~~~
與`out`一樣,可以在聲明類的時候,給泛型參數加上`in`關鍵字,來表明這個泛型參數`T`只用來輸入。
~~~kotlin
??? ??
class Consumer<in T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<Button> = Consumer<TextView>() // ?? 這里不寫 in 也不會報錯
val consumer: Consumer<in Button> = Consumer<TextView>() // ?? in 可以但沒必要
~~~
## \* 號(泛型通配符)
前面講到了 Java 中單個`?`號也能作為泛型通配符使用,相當于`? extends Object`。
**它在 Kotlin 中有等效的寫法:`*`號,相當于`out Any`**。
~~~kotlin
??? ??
var list: List<*>
~~~
和 Java 不同的地方是,如果你的類型定義里已經有了`out`或者`in`,那這個限制在變量聲明時也依然在,不會被`*`號去掉。
比如你的類型定義里是`out T : Number`的,那它加上`<*>`之后的效果就不是`out Any`,而是`out Number`。
## where 關鍵字(多重上界)
Java 中聲明類或接口的時候,可以使用`extends`來設置邊界,將泛型類型參數限制為某個類型的子集:
~~~java
??
// ?? T 的類型必須是 Animal 的子類型
class Monster<T extends Animal>{
}
~~~
>[info]【注意】這個和前面講的聲明變量時的泛型類型聲明是不同的東西,這里并沒有`?`。
同時這個邊界是可以設置多個,用`&`符號連接:
~~~java
??
// ?? T 的類型必須同時是 Animal 和 Food 的子類型
class Monster<T extends Animal & Food>{
}
~~~
Kotlin 只是把`extends`換成了`:`冒號。
~~~kotlin
??? ??
class Monster<T : Animal>
~~~
設置多個邊界可以使用`where`關鍵字:
~~~kotlin
??? ??
class Monster<T> where T : Animal, T : Food
~~~
有人在看文檔的時候覺得這個`where`是個新東西,其實雖然 Java 里沒有`where`,但它并沒有帶來新功能,只是把一個老功能換了個新寫法。
不過筆者覺得 Kotlin 里`where`這樣的寫法可讀性更符合英文里的語法,尤其是如果`Monster`本身還有繼承的時候:
~~~kotlin
???
class Monster<T> : MonsterParent<T>
where T : Animal
~~~
## reified 關鍵字
由于 Java 中的泛型存在類型擦除的情況,任何在運行時需要知道泛型確切類型信息的操作都沒法用了。Java泛型里的類型參數,也就是這個T,它不是真正的類型,只是一個代號,所以你不能把它當成一個普通的類型來用,比如你不能在方法里檢查一個對象是不是一個T的實例。這個在kotlin和Java都是一樣的。
比如你不能檢查一個對象是否為泛型類型`T`的實例:
~~~java
??
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // ?? IDE 會提示錯誤,illegal generic type for instanceof
System.out.println(item);
}
}
~~~
Kotlin 里同樣也不行:
~~~kotlin
???
fun <T> printIfTypeMatch(item: Any) {
if (item is T) { // ?? IDE 會提示錯誤,Cannot check for instance of erased type: T
println(item)
}
}
~~~
這個問題,在 Java 中的解決方案通常是額外傳遞一個`Class<T>`類型的參數,然后通過`Class#isInstance`方法來檢查:
~~~java
?? ??
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
??
System.out.println(item);
}
}
~~~
Kotlin 中同樣可以這么解決,不過還有一個更方便的做法:使用關鍵字`reified`配合`inline`來解決:
~~~kotlin
??? ?? ??
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // ?? 這里就不會在提示錯誤了
println(item)
}
}
~~~
這具體是怎么回事呢?等到后續章節講到`inline`的時候會詳細說明,這里就不過多延伸了。
還記得第二篇文章中,提到了兩個 Kotlin 泛型與 Java 泛型不一致的地方,這里作一下解答。
1. Java 里的數組是支持協變的,而 Kotlin 中的數組`Array`不支持協變。
這是因為在 Kotlin 中數組是用`Array`類來表示的,這個`Array`類使用泛型就和集合類一樣,所以不支持協變。
2. Java 中的`List`接口不支持協變,而 Kotlin 中的`List`接口支持協變。
Java 中的`List`不支持協變,原因在上文已經講過了,需要使用泛型通配符來解決。
在 Kotlin 中,實際上`MutableList`接口才相當于 Java 的`List`。Kotlin 中的`List`接口實現了只讀操作,沒有寫操作,所以不會有類型安全上的問題,自然可以支持協變。
## 練習題
1. 實現一個`fill`函數,傳入一個`Array`和一個對象,將對象填充到`Array`中,要求`Array`參數的泛型支持逆變(假設`Array`size 為 1)。
2. 實現一個`copy`函數,傳入兩個`Array`參數,將一個`Array`中的元素復制到另外個`Array`中,要求`Array`參數的泛型分別支持協變和逆變。(提示:Kotlin 中的`for`循環如果要用索引,需要使用`Array.indices`)
- 前言
- 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