Kotlin知識歸納(十二) —— 泛型

大棋發表於2019-07-16

Java為什麼引入泛型

      眾所周知,Java 5才最大的亮點就是引入泛型,那麼Java引入泛型的目的是什麼?這就需要檢視Java 5引入泛型前的程式碼:(因為Java向後相容,現在這段程式碼還能編譯成功)

#daqiJava.java
List list = new ArrayList();
list.add("");
String str = (String) list.get(0);
//新增錯誤型別
list.add(1);
複製程式碼

      由於ArrayList底層是依靠Object陣列實現的,這使得任何型別都可以新增到同一個ArrayList物件中。且取出來時是Object型別,需要強制型別轉換後才能進行相應的操作。但由於ArrayList物件能接受任何型別,無法保證型別轉換總是正確的,很容易造成ClassCastException異常。

      但泛型的出現,讓這一切都迎刃而解。單個ArrayList物件只能儲存特定型別的物件,如果不是存入該型別或者該型別子類的物件,編譯器會報錯提醒,規範了ArrayList中物件的型別。同時,取出來時可以安心的依據泛型的具體型別進行強制型別轉換,並且這是在ArrayList中自動完成強轉的,省去了開發者進行強制型別轉換帶來的繁瑣。

#daqiJava.java
List<String> list = new ArrayList();
list.add("");
String str = list.get(0);

list.add(1);//編譯器不通過
複製程式碼

總的來說,泛型帶來以下好處:

  • 在編譯期檢查型別,讓型別更安全
  • 自動型別轉換
  • 提高程式碼通用性

型別引數約束

上界

型別引數約束可以限制作為泛型類和泛型函式的型別實參的型別。

      把一個型別指定為泛型的型別形參的上界約束,在泛型型別具體的初始化中,對應的型別實參必須是這個具體型別或它的子型別。

Kotlin知識歸納(十二) —— 泛型

      換句話說就是,某泛型函式(例如求和函式)可以用在List<Int>List<Double>上,但不可以用在List<String>上。這時可以指定泛型型別型參的上界為Number,使型別引數必須使數字。

fun <T:Number> sum(num1:T,num2:T):T{
    
}
複製程式碼

一旦指定上界,只有 Number 的子類(子型別)可以替代 T。

尖括號中只能指定一個上界,如果同一型別引數需要多個上界,需要使用 where-子句

fun <T> daqi(list: List<T>) 
    where T : CharSequence,T : Comparable<T> {
    
}
複製程式碼

型別形參非空

      型別引數約束預設的上界是 Any?。意味著泛型函式接收的引數可空,儘管泛型T並沒有標記? 。這時可以使用<T : Any>替換預設上界,確保泛型T永遠為非空型別。

類、型別 和 子型別

類 與 型別

      學習泛型的型變之前,需要先學習本小節的內容,以便更好的理解後面的泛型的型變。在Java中,我們往往會把型別當作相同的概念來使用,但其實它們是兩種不同概念。區分類和型別這兩種概念的同時,也需要分情況討論:

  • 非泛型類

      非泛型類的名稱可以直接當作型別使用。而在Kotlin中,一個非泛型類至少可以分為兩種型別:非空型別可空型別。例如String類,可以分為可空型別String? 和 非空型別String.

  • 泛型類

      而對於泛型類就變得更為複雜了。一個泛型類想得到合法的型別,必須用一個具體的型別作為泛型的型別形參。因此一個泛型類可以衍生出無限數量的型別。例如:Kotlin的List是一個類,不是一個型別。其合法型別:List<String>List<Int>等。

子類 和 子型別

      我們一般將一個類的派生類稱為子類,該類稱為父類(基類)。例如:IntNumber的派生類,Number作為父類,Int作為子類。

而子型別與子類的定義不一樣,子型別的定義:

任何時候期望A型別的值時,可以使用B型別的值,則B就是A的子型別

超型別是子型別的反義詞。如果B是A的子型別,那麼反過來A就是B的超型別。

Kotlin知識歸納(十二) —— 泛型

  • Int是Number的子型別

      對於非泛型類,其型別會沿襲該類的繼承關係,當A是B的父類,同時A型別也是B型別的超型別。當期望A型別的物件時,可以使用B型別的物件進行傳遞。

  • String是String?的子型別

      所有類的 非空型別 都是該類的 可空型別 的子型別,但反過來不可以。例如:在接收String?型別的地方,可以使用String型別的值來替換。但不能將String?型別的值儲存到String型別的值中,因為null不是非空型別變數可以接收的值。(除非進行判空或非空斷言,編譯器將可空型別轉換為非空型別,這時原可空型別的值可以儲存到非空型別的變數中)

  • Int不是String的子型別

      作為非泛型類,IntString沒有繼承關係,兩者間不存在子型別或超型別的關係。

Kotlin知識歸納(十二) —— 泛型

為什麼存在型變

      我們都知道非泛型類其型別會沿襲該類的繼承關係。但對於泛型類,這是行不通的。例如以下程式碼,是無法編譯成功的:

#daqiJava.java
List<String> strList = new ArrayList();
List<Object> objList = new ArrayList();
objList = strList;
複製程式碼

      List<Object>List<String>是兩個相互獨立的型別,不存在子型別的關係。即便String類的基類是Object類。

      因為當你期望List<Object>時,允許賦值一個List<String>過來,也就意味著其他的型別(如List<Int>等)也能賦值進來。這就造成了型別不一致的可能性,無法確保型別安全,違背了泛型引入的初衷 —— 確保型別安全

      到這裡你或許會想,對於接收泛型類物件的方法,這不就"削了"泛型類的程式碼通用性(靈活性)的能力?Java提供了有限制的萬用字元來確保型別安全,允許泛型類構建相應的子型別化關係,提高程式碼的通用性(靈活性)。與之對應的,便是Kotlin的型變。Kotlin中存在協變逆變兩種概念,統稱為宣告處型變

宣告處型變

      Kotlin的宣告處型變包含了協變逆變。協變和逆變都是用於規範泛型的型別形參的範圍,確保型別安全。

協變

  • 協變主要概念:

保留子型別化關係

具體意思是:當 B 是 A 的子型別,那麼List<B>就是List<A>的子型別。協變類保留了泛型的型別形參的子型別化關係。

  • 基本定義 使用out關鍵字
public fun Out(list: List<out String>) {

}
複製程式碼

逆變

  • 逆變主要概念:

反轉子型別化關係

具體意思是:當 B 是 A 的子型別,那麼List<A>就是List<B>的子型別。逆變類反轉了泛型的型別形參的子型別化關係。

  • 基本定義 使用in關鍵字
public fun In(list: MutableList<in String>) {

}
複製程式碼

圖解協變和逆變

      對於協變的定義普遍很容易理解,但對於逆變往往比較費解。所以我決定退一步,藉助Java的有限制的萬用字元進行了解。從官方文件中瞭解到,協變、逆變和Java的萬用字元型別引數有以下關係:

  • out A 對應Java的萬用字元型別引數為:? extends A

      萬用字元型別引數 ? extends A 表示接受 A 或者 A 的子型別。

  • in A 對應Java的萬用字元型別引數為:? super A

      萬用字元型別引數 ? super A 表示接受 A 或者 A 的超型別。

所以,out Numberin Number的"取值範圍"可以用一張圖概括(暫時只考慮由非泛型類的繼承帶來的子型別化關係):

Kotlin知識歸納(十二) —— 泛型

      Number類具有IntLong等派生類,同時也擁有Any這個基類。當需要依據Number進行協變時(即<out Number>),泛型的型別形參只能選取Number自身以及其子類(子型別)。當需要依據Number進行逆變時(即<in Number>),泛型的型別形參只能選取Number自身以及其基類(超型別)。

      當某方法中需要List<out Number>型別的引數時,將<out Number>轉換為<? extends Number>,表示泛型的型別形參可以為Number自身以及其子類(子型別)。即List<Number>協變的子型別集合有:List<Number>List<Int>等。

Kotlin知識歸納(十二) —— 泛型

      List<Int>List<Number>協變的子型別集合中。意味著當需要List<Number>時,可以使用List<Int>來替換,List<Int>List<Number>的子型別。符合協變的要求: IntNumber 的子型別,以致List<Int>也是List<Number>的子型別。

      而如果協變的是List<Int>,那麼將<out Int>轉換為<? extends Int>。表示泛型的型別形參可以為Int自身以及其子類(子型別)。即List<Int>協變的子型別集合只有:List<Int>

Kotlin知識歸納(十二) —— 泛型

      List<Number>不在List<Int>協變的子型別集合中。意味著當需要List<Int>時,不可以使用List<Number>來替換,List<Number>不是List<Int>的子型別。

      這種思路對於逆變也是可行的。某方法中需要MutableList<in Number>型別的引數時,將<in Number>轉換為<? super Number>,表示泛型的型別形參可以為Number自身以及其基類(超型別)。即MutableList<Number>逆變的子型別集合有:MutableList<Number>MutableList<Any>等。

Kotlin知識歸納(十二) —— 泛型

      MutableList<Int>不在MutableList<Number>逆變的子型別集合中。意味著當需要MutableList<Number>時,不可以使用MutableList<Int>來替換,MutableList<Int>不是MutableList<Number>的子型別。

      而如果逆變的是MutableList<Int>,那麼將<in Int>轉換為<? super Int>。表示泛型的型別形參可以為Int自身以及其基類(超型別)。即MutableList<Int>逆變的子型別集合有:MutableList<Int>MutableList<Number>MutableList<Any>

Kotlin知識歸納(十二) —— 泛型

      MutableList<Number>MutableList<Int>逆變的子型別集合中。意味著當需要MutableList<Int>時,可以使用MutableList<Number>來替換,MutableList<Number>MutableList<Int>的子型別。符合逆變的要求: IntNumber 的子型別,但MutableList<Number>MutableList<Int>的子型別。

可空型別與非空型別的宣告處型變

      眾所周知,Kotlin中一個非泛型類有著對應的可空型別和非空型別,而且非空型別是可空型別的子型別。因為當需要可空型別的物件時,可以使用非空型別的物件來替換。

      關於可空型別和非空型別間的協變與逆變,也可以使用剛才的方法進行理解,只是這次不再侷限於子類和父類,而是擴充套件到子型別和超型別

  • 當需要依據型別A進行協變時(即<out A>),泛型的型別形參只能選取A自身以及其子型別。
  • 當需要依據型別A進行逆變時(即<in A>),泛型的型別形參只能選取A自身以及其超型別。

      當某方法中需要List<out Any?>型別的引數時,將<out Any?>轉換為<? extends Any?>,表示泛型的型別形參可以為Any?自身以及其子型別。即List<Any?>協變的子型別集合有:List<Any?>List<Any>等。

      而如果逆變的是MutableList<Any?>,那麼將<in Any?>轉換為<? super Any?>。表示泛型的型別形參可以為Any?自身以及其超型別。即MutableList<Any?>逆變的子型別集合有:MutableList<Any?>

Kotlin知識歸納(十二) —— 泛型

      當你試圖將MutableList<Any>做為子型別傳遞給接收MutableList<in Any?>型別引數的方法時,編譯器將報錯,編譯不通過。因為MutableList<Any?>逆變的子型別集合中沒有MutableList<Any>

Kotlin知識歸納(十二) —— 泛型

      當某方法中需要List<out Any>型別的引數時,將<out Any>轉換為<? extends Any>,表示泛型的型別形參可以為Any自身以及其子型別。即List<Any>協變的子型別集合有:List<Any>

      而如果逆變的是MutableList<Any>,那麼將<in Any>轉換為<? super Any>。表示泛型的型別形參可以為Any自身以及其超型別。即MutableList<Any>逆變的子型別集合有:MutableList<Any>MutableList<Any?>

Kotlin知識歸納(十二) —— 泛型

      當你試圖將List<Any?>做為子型別傳遞給接收List<out Any>型別引數的方法時,編譯器將報錯,編譯不通過。因為List<Any>協變的子型別集合中沒有List<Any?>

Kotlin知識歸納(十二) —— 泛型

in位置 和 out位置

      到這裡或許有個疑問,我該依據什麼來選擇協變或者逆變呢?這就涉及關鍵字outin的第二層含義了。

關鍵字out的兩層含義:

  • 子型別化被保留。
  • T 只能用在out位置。

關鍵in的兩層含義:

  • 子型別化被反轉。
  • T 只能用在in位置。

       out位置是指:該函式生產型別為T的值,泛型T只能作為函式的返回值。而in位置是指:該函式消費型別T的值,泛型T作為函式的形參型別。

Kotlin知識歸納(十二) —— 泛型

消費者 和 生產者

      Kotlin的型變遵從《Effective Java》中的 PECS (Producer-Extends, Consumer-Super)。只能讀取的物件作為生產者,只能寫入的物件作為消費者。

  • out關鍵字使得一個型別引數協變:只可以被生產而不可以被消費。

      out修飾符確保型別引數 T 從 Iterator<T> 成員中返回(生產),並從不被消費。

public interface Iterator<out T> {
    public operator fun next(): T
    public operator fun hasNext(): Boolean
}
複製程式碼
  • in關鍵字使得一個型別引數逆變:只可以被消費而不可以被生產。

      in 修飾符確保型別引數 TComparable<T> 成員中寫入(消費),並從不被生產。

interface Comparable<in T> {
    operator fun compareTo(other: T): Int
}
複製程式碼

out位置

      配合協變分析,可以清楚out為什麼扮演生產者角色:

  • 1、由於協變的關係,List<Int>List<Long>等子型別可以替代List<Number>,傳遞給接收List<Number>型別的方法。而對外仍是List<Number>,但並不知道該泛型類實際的型別形參是什麼。
  • 2、當對其進行寫入操作時,可以接收Number的任何子型別。但由於不知道該泛型類實際的型別形參是什麼。對其進行寫入會造成型別不安全。(例如:可能接收的是一個List<Int>,如果你對其寫入一個Long,這時就會造成型別不安全。)
  • 3、當對其進行讀取操作時,不管它原本接收的是什麼型別形參的泛型例項(不管是List<Int>,還是List<Long>等),返回(生產)的是Number例項。以超型別的形式返回子型別例項,型別安全。

Kotlin知識歸納(十二) —— 泛型

in位置

      配合逆變分析,也可以清楚in為什麼扮演消費者角色:

  • 1、由於逆變的關係,Consumer<Number>Consumer<Any>等子型別可以替代Consumer<Number>,傳遞給接收Consumer<Number>型別的方法。
  • 2、當對其進行寫入操作時,可以接收Number的任何子型別。不管接收的是Number的什麼子型別,對外始終是Consumer<Number>Consumer<Int>Consumer<Long>等不能傳遞進來)。以超型別的形式消費子型別例項,型別安全。
  • 3、當對其進行讀取操作時,由於不知道該泛型類實際的型別形參是什麼(是Number呢,還是Any呢?)。只有使用Any返回(生產)才能確保型別安全,所以讀取受限。(也就是說在逆變中,泛型 TNumber時,你返回的不是Number,而是Any。)

Kotlin知識歸納(十二) —— 泛型

UnSafeVariance註解

      那是否意味著out關鍵字修飾的泛型引數是不是不能出現在in位置 ?當然不是,只要函式內部能保證不會對泛型引數存在寫操作的行為,可以使用UnSafeVariance註解使編譯器停止警告,就可以將其放在in位置。out關鍵字修飾的泛型引數也是同理。

      例如Kotlin的Listcontains函式等,就是應用UnSafeVariance註解使泛型引數存在於in位置,其內部沒有寫操作。

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    public operator fun get(index: Int): E
    public fun indexOf(element: @UnsafeVariance E): Int
    public fun lastIndexOf(element: @UnsafeVariance E): Int
    public fun listIterator(): ListIterator<E>
    public fun listIterator(index: Int): ListIterator<E>
    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}
複製程式碼

其他

      構造方法的引數既不在in位置也不在out位置。同時該位置規則只對對外公開的API有效。(即對private修飾的函式無效)

宣告處型變總結

協變 逆變 不變
結構 Producer<out T> Consumer<in T> MutableList<T>
Java實現 Producer<? extends T> Consumer<? super T> MutableList<T>
子型別化關係 保留子型別化關係 逆轉子型別化關係 無子型別化關係
位置 out位置 in位置 in位置和out位置
角色 生產者 消費者 生產者和消費者
表現 只讀 只寫,讀取受限 即可讀也可寫

選擇逆變、協變和不變

那麼使用泛型時,逆變、協變和不變如何選擇呢?

  • 首先需要考慮泛型形參的位置:只讀操作(協變或不變)、只寫讀操作(逆變或不變)、又讀又寫操作(不變)。

      Array中存在又讀又寫的操作,如果為其指定協變或逆變,都會造成型別不安全:

class Array<T>(val size: Int) {
    fun get(index: Int): T { …… }
    fun set(index: Int, value: T) { …… }
}
複製程式碼
  • 最後判斷是否需要子型別化關係,子型別化關係主要用於提高API的靈活度。

      如果需要子型別化關係,則只讀操作(協變或不變)選擇協變,否則不變;只寫讀操作(逆變或不變),選擇逆變,否則不變。

星點投射

      Kotlin的型變分為 宣告處型變星點投射。所謂的星點投射就是使用 * 代替型別引數。表示你不知道關於泛型實參的任何資訊,但仍然希望以安全的方式使用它。

Kotlin 為此提供了以下星點投射的語法:

  • 對於 Foo <T : TUpper>,其中 T 是一個具有上界 TUpper 的不型變型別引數,Foo<*>讀取值時等價於 Foo<out TUpper> ,而寫值時等價於 Foo<in Nothing>

  • 對於 Foo <out T : TUpper>,其中 T 是一個具有上界 TUpper的協變型別引數,Foo <*> 等價於 Foo <out TUpper>。 這意味著當 T 未知時,你可以安全地從 Foo <*>讀取 TUpper的值。

  • 對於 Foo <out T>,其中 T 是一個協變型別引數,Foo <*> 等價於 Foo <out Any?>。 因為 T 未知時,只有讀取 Any? 型別的元素是安全的。

  • 對於 Foo <in T>,其中 T 是一個逆變型別引數,Foo <*> 等價於 Foo <in Nothing>。 因為 T 未知時,沒有什麼可以以安全的方式寫入 Foo <*>

  • 對於普通的 Foo <T>,這其中沒有任何泛型實參的資訊。Foo<*>讀取值時等價於 Foo<out Any?>,因為讀取 Any? 型別的元素是安全的;Foo<*>寫入值是等價於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?>。

MutableList<*>和MutableList<Any?>的區別

      可以向MutableList<Any?>中新增任何資料,但MutableList<*>只是通配某種型別,因為不知道其具體什麼型別,所以不允許向該列表中新增元素,否則會造成型別不安全。

參考資料:

android Kotlin系列:

Kotlin知識歸納(一) —— 基礎語法

Kotlin知識歸納(二) —— 讓函式更好呼叫

Kotlin知識歸納(三) —— 頂層成員與擴充套件

Kotlin知識歸納(四) —— 介面和類

Kotlin知識歸納(五) —— Lambda

Kotlin知識歸納(六) —— 型別系統

Kotlin知識歸納(七) —— 集合

Kotlin知識歸納(八) —— 序列

Kotlin知識歸納(九) —— 約定

Kotlin知識歸納(十) —— 委託

Kotlin知識歸納(十一) —— 高階函式

Kotlin知識歸納(十二) —— 泛型

Kotlin知識歸納(十三) —— 註解

Kotlin知識歸納(十四) —— 反射

Kotlin知識歸納(十二) —— 泛型

相關文章