本期做者:java
視頻:扔物線(朱凱)git
文章:Bruce(鄭嘯天)github
你們好,我是扔物線朱凱。你在看的是碼上開學項目的 Kotlin 高級部分的第 1 篇:Kotlin 的泛型。首當其衝的固然仍是香香的視頻香香的我啦:數組
由於我一直沒有學會怎麼在掘金貼視頻,因此請點擊 這裏 去嗶哩嗶哩看,或者點擊 這裏 去 YouTube 看。安全
如下內容來自文章做者Bruce。ide
這期是碼上開學 Kotlin 系列的獨立技術點部分的第一期,咱們來聊一聊泛型。函數
提到 Kotlin 的泛型,一般離不開 in
和 out
關鍵字,但泛型這門武功須要些基本功才能修煉,不然容易走火入魔,待筆者慢慢道來。post
下面這段 Java 代碼在平常開發中應該很常見了:網站
☕️
List<TextView> textViews = new ArrayList<TextView>();
複製代碼
其中 List<TextView>
表示這是一個泛型類型爲 TextView
的 List
。ui
那到底什麼是泛型呢?咱們先來說講泛型的由來。
如今的程序開發大都是面向對象的,平時會用到各類類型的對象,一組對象一般須要用集合來存儲它們,於是就有了一些集合類,好比 List
、Map
等。
這些集合類裏面都是裝的具體類型的對象,若是每一個類型都去實現諸如 TextViewList
、ActivityList
這樣的具體的類型,顯然是不可能的。
所以就誕生了「泛型」,它的意思是把具體的類型泛化,編碼的時候用符號來指代類型,在使用的時候,再肯定它的類型。
前面那個例子,List<TextView>
就是泛型類型聲明。
既然泛型是跟類型相關的,那麼是否是也能適用類型的多態呢?
先看一個常見的使用場景:
☕️
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 裏用數組作相似的事情,是不會報錯的,這是由於數組並無在編譯時擦除類型:
☕️ TextView[] textViews = new TextView[10]; 複製代碼
可是在實際使用中,咱們的確會有這種相似的需求,須要實現上面這種賦值。
Java 提供了「泛型通配符」 ? extends
和 ? super
來解決這個問題。
? extends
在 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
的子類,知足了泛型類型的限制條件,於是可以成功賦值。
根據剛纔的描述,下面幾種狀況都是能夠的:
☕️
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
,它的具體定義以下:
☕️
public interface List<E> extends Collection<E>{
E get(int index);
boolean add(E e);
...
}
複製代碼
上面的代碼中,E
就是表示泛型類型的符號(用其餘字母甚至單詞均可以)。
咱們看看在使用了上界通配符以後,List
的使用上有沒有什麼問題:
☕️
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>
。那我乾脆不要 extends TextView
,只用通配符 ?
呢?
這樣使用 List<?>
實際上是 List<? extends Object>
的縮寫。
☕️
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
。
? super
先看一下它的寫法:
☕️
👇
List<? super Button> buttons = new ArrayList<TextView>();
複製代碼
這個 ? super
叫作「下界通配符」,可使 Java 泛型具備「逆變性 Contravariance」。
與上界通配符對應,這裏 super 限制了通配符 ? 的子類型,因此稱之爲下界。
它也有兩層意思:
?
表示 List
的泛型類型是一個未知類型。super
限制了這個未知類型的下界,也就是泛型類型必須知足這個 super
的限制條件。
super
咱們在類的方法裏面常常用到,這裏的範圍不只包括 Button
的直接和間接父類,也包括下界 Button
自己。super
一樣支持 interface
。上面的例子中,TextView
是 Button
的父類型 ,也就可以知足 super
的限制條件,就能夠成功賦值了。
根據剛纔的描述,下面幾種狀況都是能夠的:
☕️
List<? super Button> buttons = new ArrayList<Button>(); // 👈 自己
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父類
List<? super Button> buttons = new ArrayList<Object>(); // 👈 間接父類
複製代碼
對於使用了下界通配符的 List
,咱們再看看它的 get
和 add
操做:
☕️
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 中的泛型,有如練完九陽神功再練乾坤大挪移,就比較容易了。
out
和 in
和 Java 泛型同樣,Kolin 中的泛型自己也是不可變的。
out
來支持協變,等同於 Java 中的上界通配符 ? extends
。in
來支持逆變,等同於 Java 中的下界通配符 ? super
。🏝️
var textViews: List<out TextView>
var textViews: List<in TextView>
複製代碼
換了個寫法,但做用是徹底同樣的。out
表示,我這個變量或者參數只用來輸出,不用來輸入,你只能讀我不能寫我;in
就反過來,表示它只用來輸入,不用來輸出,你只能寫我不能讀我。
你看,咱們 Android 工程師學不會 out
和 in
,其實並非由於這兩個關鍵字多難,而是由於咱們應該先學學 Java 的泛型。是吧?
說了這麼多 List
,其實泛型在非集合類的使用也很是普遍,就以「生產者-消費者」爲例子:
🏝️
class Producer<T> {
fun produce(): T {
...
}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // 👈 至關於 'List' 的 `get`
複製代碼
再來看看消費者:
🏝️
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
了。
🏝️ 👇
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
只用來輸入。
🏝️ 👇
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
。
🏝️ 👇
var list: List<*>
複製代碼
和 Java 不一樣的地方是,若是你的類型定義裏已經有了 out
或者 in
,那這個限制在變量聲明時也依然在,不會被 *
號去掉。
好比你的類型定義裏是 out T : Number
的,那它加上 <*>
以後的效果就不是 out Any
,而是 out Number
。
where
關鍵字Java 中聲明類或接口的時候,可使用 extends
來設置邊界,將泛型類型參數限制爲某個類型的子集:
☕️
// 👇 T 的類型必須是 Animal 的子類型
class Monster<T extends Animal>{
}
複製代碼
注意這個和前面講的聲明變量時的泛型類型聲明是不一樣的東西,這裏並無 ?
。
同時這個邊界是能夠設置多個,用 &
符號鏈接:
☕️
// 👇 T 的類型必須同時是 Animal 和 Food 的子類型
class Monster<T extends Animal & Food>{
}
複製代碼
Kotlin 只是把 extends
換成了 :
冒號。
🏝️ 👇
class Monster<T : Animal>
複製代碼
設置多個邊界可使用 where
關鍵字:
🏝️ 👇
class Monster<T> where T : Animal, T : Food
複製代碼
有人在看文檔的時候以爲這個 where
是個新東西,其實雖然 Java 裏沒有 where
,但它並無帶來新功能,只是把一個老功能換了個新寫法。
不過筆者以爲 Kotlin 裏 where
這樣的寫法可讀性更符合英文裏的語法,尤爲是若是 Monster
自己還有繼承的時候:
🏝️
class Monster<T> : MonsterParent<T>
where T : Animal
複製代碼
reified
關鍵字因爲 Java 中的泛型存在類型擦除的狀況,任何在運行時須要知道泛型確切類型信息的操做都無法用了。
好比你不能檢查一個對象是否爲泛型類型 T
的實例:
☕️
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // 👈 IDE 會提示錯誤,illegal generic type for instanceof
System.out.println(item);
}
}
複製代碼
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
方法來檢查:
☕️ 👇
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
👆
System.out.println(item);
}
}
複製代碼
Kotlin 中一樣能夠這麼解決,不過還有一個更方便的作法:使用關鍵字 reified
配合 inline
來解決:
🏝️ 👇 👇
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // 👈 這裏就不會在提示錯誤了
println(item)
}
}
複製代碼
這具體是怎麼回事呢?等到後續章節講到 inline
的時候會詳細說明,這裏就不過多延伸了。
還記得第二篇文章中,提到了兩個 Kotlin 泛型與 Java 泛型不一致的地方,這裏做一下解答。
Java 裏的數組是支持協變的,而 Kotlin 中的數組 Array
不支持協變。
這是由於在 Kotlin 中數組是用 Array
類來表示的,這個 Array
類使用泛型就和集合類同樣,因此不支持協變。
Java 中的 List
接口不支持協變,而 Kotlin 中的 List
接口支持協變。
Java 中的 List
不支持協變,緣由在上文已經講過了,須要使用泛型通配符來解決。
在 Kotlin 中,實際上 MutableList
接口才至關於 Java 的 List
。Kotlin 中的 List
接口實現了只讀操做,沒有寫操做,因此不會有類型安全上的問題,天然能夠支持協變。
fill
函數,傳入一個 Array
和一個對象,將對象填充到 Array
中,要求 Array
參數的泛型支持逆變(假設 Array
size 爲 1)。copy
函數,傳入兩個 Array
參數,將一個 Array
中的元素複製到另外個 Array
中,要求 Array
參數的泛型分別支持協變和逆變。(提示:Kotlin 中的 for
循環若是要用索引,須要使用 Array.indices
)Bruce(鄭嘯天) ,即刻 Android 工程師。2018 年加入即刻,參與了即刻多個版本的迭代。多年 Android 開發經驗,如今負責即刻客戶端中臺基礎建設。