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);//編譯器不通過
複製程式碼
總的來說,泛型帶來以下好處:
- 在編譯期檢查型別,讓型別更安全
- 自動型別轉換
- 提高程式碼通用性
型別引數約束
上界
型別引數約束可以限制作為泛型類和泛型函式的型別實參的型別。
把一個型別指定為泛型的型別形參的上界約束,在泛型型別具體的初始化中,對應的型別實參必須是這個具體型別或它的子型別。
換句話說就是,某泛型函式(例如求和函式)可以用在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>
等。
子類 和 子型別
我們一般將一個類的派生類稱為子類,該類稱為父類(基類)。例如:Int
是Number
的派生類,Number
作為父類,Int
作為子類。
而子型別與子類的定義不一樣,子型別的定義:
任何時候期望A型別的值時,可以使用B型別的值,則B就是A的子型別
超型別是子型別的反義詞。如果B是A的子型別,那麼反過來A就是B的超型別。
- Int是Number的子型別
對於非泛型類,其型別會沿襲該類的繼承關係,當A是B的父類,同時A型別也是B型別的超型別。當期望A型別的物件時,可以使用B型別的物件進行傳遞。
- String是String?的子型別
所有類的 非空型別 都是該類的 可空型別 的子型別,但反過來不可以。例如:在接收String?
型別的地方,可以使用String
型別的值來替換。但不能將String?
型別的值儲存到String
型別的值中,因為null
不是非空型別變數可以接收的值。(除非進行判空或非空斷言,編譯器將可空型別轉換為非空型別,這時原可空型別的值可以儲存到非空型別的變數中)
- Int不是String的子型別
作為非泛型類,Int
和String
沒有繼承關係,兩者間不存在子型別或超型別的關係。
為什麼存在型變
我們都知道非泛型類其型別會沿襲該類的繼承關係。但對於泛型類,這是行不通的。例如以下程式碼,是無法編譯成功的:
#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 Number
和 in Number
的"取值範圍"可以用一張圖概括(暫時只考慮由非泛型類的繼承帶來的子型別化關係):
Number
類具有Int
、Long
等派生類,同時也擁有Any
這個基類。當需要依據Number
進行協變時(即<out Number>
),泛型的型別形參只能選取Number
自身以及其子類(子型別)。當需要依據Number
進行逆變時(即<in Number>
),泛型的型別形參只能選取Number
自身以及其基類(超型別)。
當某方法中需要List<out Number>
型別的引數時,將<out Number>
轉換為<? extends Number>
,表示泛型的型別形參可以為Number
自身以及其子類(子型別)。即List<Number>
協變的子型別集合有:List<Number>
、List<Int>
等。
List<Int>
在List<Number>
協變的子型別集合中。意味著當需要List<Number>
時,可以使用List<Int>
來替換,List<Int>
是List<Number>
的子型別。符合協變的要求: Int
是 Number
的子型別,以致List<Int>
也是List<Number>
的子型別。
而如果協變的是List<Int>
,那麼將<out Int>
轉換為<? extends Int>
。表示泛型的型別形參可以為Int
自身以及其子類(子型別)。即List<Int>
協變的子型別集合只有:List<Int>
。
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>
等。
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>
。
MutableList<Number>
在MutableList<Int>
逆變的子型別集合中。意味著當需要MutableList<Int>
時,可以使用MutableList<Number>
來替換,MutableList<Number>
是MutableList<Int>
的子型別。符合逆變的要求: Int
是 Number
的子型別,但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?>
。
當你試圖將MutableList<Any>
做為子型別傳遞給接收MutableList<in Any?>
型別引數的方法時,編譯器將報錯,編譯不通過。因為MutableList<Any?>
逆變的子型別集合中沒有MutableList<Any>
。
當某方法中需要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?>
。
當你試圖將List<Any?>
做為子型別傳遞給接收List<out Any>
型別引數的方法時,編譯器將報錯,編譯不通過。因為List<Any>
協變的子型別集合中沒有List<Any?>
。
in位置 和 out位置
到這裡或許有個疑問,我該依據什麼來選擇協變或者逆變呢?這就涉及關鍵字out
和in
的第二層含義了。
關鍵字out的兩層含義:
- 子型別化被保留。
- T 只能用在out位置。
關鍵in的兩層含義:
- 子型別化被反轉。
- T 只能用在in位置。
out位置是指:該函式生產型別為T
的值,泛型T只能作為函式的返回值。而in位置是指:該函式消費型別T的值,泛型T作為函式的形參型別。
消費者 和 生產者
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
修飾符確保型別引數 T
從 Comparable<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
例項。以超型別的形式返回子型別例項,型別安全。
in位置
配合逆變分析,也可以清楚in
為什麼扮演消費者角色:
- 1、由於逆變的關係,
Consumer<Number>
、Consumer<Any>
等子型別可以替代Consumer<Number>
,傳遞給接收Consumer<Number>
型別的方法。 - 2、當對其進行寫入操作時,可以接收Number的任何子型別。不管接收的是Number的什麼子型別,對外始終是
Consumer<Number>
(Consumer<Int>
、Consumer<Long>
等不能傳遞進來)。以超型別的形式消費子型別例項,型別安全。 - 3、當對其進行讀取操作時,由於不知道該泛型類實際的型別形參是什麼(是
Number
呢,還是Any
呢?)。只有使用Any
返回(生產)才能確保型別安全,所以讀取受限。(也就是說在逆變中,泛型T
為Number
時,你返回的不是Number
,而是Any
。)
UnSafeVariance註解
那是否意味著out
關鍵字修飾的泛型引數是不是不能出現在in
位置 ?當然不是,只要函式內部能保證不會對泛型引數存在寫操作的行為,可以使用UnSafeVariance
註解使編譯器停止警告,就可以將其放在in
位置。out
關鍵字修飾的泛型引數也是同理。
例如Kotlin的List
中contains
函式等,就是應用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<*>只是通配某種型別,因為不知道其具體什麼型別,所以不允許向該列表中新增元素,否則會造成型別不安全。
參考資料:
- 《Kotlin實戰》
- Kotlin官網