引子
相信總是有很多同學,總是在抱怨泛型無論怎麼學習,都只是停留在一個簡單使用的水平,所以一直為此而備受苦惱。
Kotlin 作為一門能和 Java 相互呼叫的語言,自然也支援泛型,不過 Kotlin 的新關鍵字 in
和 out
卻總能繞暈一部分人,歸根結底,還是因為 Java 的泛型基本功沒有足夠紮實。
很多同學總是會產生這些疑問:
- Kotlin 泛型和 Java 泛型到底有何區別?
- Java 泛型存在的意義到底是什麼?
- Java 的型別擦除到底是指什麼?
- Java 泛型的上界、下界、萬用字元到底有何區別?它們可以實現多重限制麼?
- Java 的
<? extends T>
、<? super T>
、<?>
到底對應了什麼?有哪些使用場景? - Kotlin 的
in
、out
、*
、where
到底有何魔力? - 泛型方法又是什麼?
今天,就用一篇文章為大家解除上述疑惑。
泛型:型別安全的利刃
總所周知,Java 在 1.5 之前,是沒有泛型這個概念的。那時候的 List
還只是一個可以裝下一切的集合。所以我們難免會寫上這樣的程式碼:
List list = new ArrayList();
list.add(1);
list.add("nanchen2251");
String str = (String) list.get(0);
上面的程式碼編譯並沒有任何問題,但執行的時候一定會出現常見的 ClassCastException
異常:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
這個體驗非常糟糕,我們真正需要的是在程式碼編譯的時候就能發現錯誤,而不是讓錯誤的程式碼釋出到生產環境中。
而如果上述程式碼我們增加上泛型,就會在編譯期就能看到明顯的錯誤啦。
List<String> list = new ArrayList<>();
list.add(1);
// ? 報錯 Required type:String but Provided:int
list.add("nanchen2251");
String str = list.get(0);
很明顯,泛型的出現,讓型別更加安全,使我們在使用 List
、Map
等不再需要去專門編寫 StringList
、StringMap
了,只需要在宣告 List
的同時指定引數型別即可。
總的來說,泛型具備以下優勢:
- 型別檢查,能在編譯時就幫開發檢查出錯誤;
- 更加語義化,比如我們宣告一個
LIst<String>
,我們可以很直接知道里面儲存的是String
物件; - 能自動進行型別轉換,獲取資料的時候不需要再做強轉操作;
- 能寫出更加通用化的程式碼。
型別擦除
可能有些同學思考過這樣一個問題,既然泛型是和型別相關的,那麼是不是也能使用型別的多型呢?
我們知道,一個子型別是可以賦值給父型別的,比如:
Object obj = "nanchen2251";
// ? 這是多型
Object
作為 String
的父類,自然可以接受 String
物件的賦值,這樣的程式碼我們早已司空見慣,並沒有什麼問題。
但當我們寫下這串程式碼:
List<String> list = new ArrayList<String>();
List<Object> objects = list;
// ? 多型用在這裡會報錯 Required type:List<Object> Provided: List<String>
上面發生了賦值錯誤,這是因為 Java 的泛型本身具有「不可變性 Invariance」,Java 裡面認為 List<String>
和 List<Object>
型別並不一致,也就是說,子類的泛型 List<String>
不屬於泛型 List<Object>
的子類。
由於 Java 的泛型本身是一種 「偽泛型」,Java 為了相容 1.5 以前的版本,不得以在泛型底層實現上使用 Object
引用,所以我們宣告的泛型在編譯時會發生「型別擦除」,泛型型別會被 Object
型別取代。比如:
class Demo<T> {
void func(T t){
// ...
}
}
會被編譯成:
class Demo {
void func(Object t){
// ...
}
}
可能你會好奇,在編譯時發生型別擦除後,我們的泛型都被更換成了 Object
,那為什麼我們在使用的時候,卻不需要強轉操作呢?比如:
List<String> list = new ArrayList<>();
list.add("nanchen2251");
String str = list.get(0);
// ? 這裡並沒有要求我們把 list.get(0) 強轉為 String
這是因為編譯器會根據我們宣告的泛型型別進行提前的型別檢查,然後再進行型別擦除,擦除為 Object
,但在位元組碼中其實還儲存了我們的泛型的型別資訊,在使用到泛型型別的時候會把擦除後的 Object
自動做型別強轉操作。所以上面的 list.get(0)
本身就是一個經過強轉的 String
物件了。
這個技術看起來還蠻好的,但卻有一個弊端。就是既然擦成 Object
了,那麼在執行的時候,你根本不能確定這個物件到底是什麼型別,雖然你可以通過編譯器幫你插入的 checkcast
來獲得此物件的型別。但是你並不能把 T
真正的當作一個型別使用:比如這條語句在 Java 中是非法的。
T a = new T();
// ? 報錯:Type parameter 'T' cannot be instantiated directly
同理,因為都被擦成了 Object
,你就不能根據型別來做某種區分。
比如 instanceof
:
if("nanchen2251" instanceof T.class){
// ? 報錯:Identifier expected Unexpected token
}
比如過載:
void func(T t){
// ? 報錯:'func(T)' clashes with 'func(E)'; both methods have same erasure
}
void func(E e){
}
同樣,因為基本資料型別不屬於 oop
,所以也不能被擦除為 Object
,所以 Java 的泛型也不能用於基本型別:
List<int> list;
// ? 報錯:Type argument cannot be of primitive type
oop:物件導向的程式設計(Object Oriented Programming)
到這裡,是不是可以回答上面的第 3 個問題了:Java 的型別擦除到底是指什麼?
首先你要明白一點,一個物件的型別永遠不會被擦出的,比如你用一個 Object
去引用一個 Apple
物件,你還是可以獲得到它的型別的。比如用 RTTI。
RTTI:執行時型別資訊,執行時型別識別 (Run Time Type Identification)
Object object = new Apple();
System.out.println(object.getClass().getName());
// ? will print Apple
哪怕它是放到泛型裡的。
class FruitShop<T>{
private T t;
public void set(T t){
this.t = t;
}
public void showFruitName(){
System.out.println(t.getClass().getName());
}
}
FruitShop<Apple> appleShop = new FruitShop<Apple>();
appleShop.set(new Apple());
appleShop.showFruitName();
// ? will print Apple too
為啥?因為引用就是一個用來訪問物件的標籤而已,物件一直在堆上放著呢。
所以不要斷章取義認為型別擦除就是把容器內物件的型別擦掉了,所謂的型別擦除,是指容器類FruitShop<Apple>
,對於 Apple
的型別宣告在編譯期的型別檢查之後被擦掉,變為和 FruitShop<Object>
等同效果,也可以說是 FruitShop<Apple>
和 FruitShop<Banana>
被擦為和 FruitShop<Object>
等價,而不是指裡面的物件本身的型別被擦掉!
那,Kotlin 中有型別擦除麼?
C# 和 Java 在一開始都是不支援泛型的。Java 在 1.5 開始才加入了泛型。為了讓一個不支援泛型的語言支援泛型,只有兩條路可以走:
- 以前的非泛型容器保持不變,然後平行的增加一套泛型化的型別。
- 直接把已有的非泛型容器擴充套件為泛型,不新增任何新的泛型版本。
Java 由於 1.5 之前市面上一句有大量的程式碼,所以不得以選擇了第 2 種方式,而 C# 比較機智就選擇了第一種。
而 Kotlin 本身就是基於 Java 1.6 編寫的,一開始就有泛型,不存在相容老版本程式碼的問題,那 Kotlin 實現的泛型還具備型別擦除麼?
當然具備。上面其實已經說的很清楚了,Kotlin 本身就是基於 Java 1.6 編寫的,而且 Kotlin 和 Java 有極強的互調能力,當然也存在型別擦除。
不過...
你還是會發現有意思的點:
val list = ArrayList()
// ? 報錯:Not enough information to infer type variable E
在 Java 中,不指定泛型型別是沒問題的,但 Kotlin 這樣不好使了。想來也簡單,畢竟在 Java 1.5 之前是肯定不存在上述類似程式碼的,而泛型的設計初衷就不是用來裝預設的 Kotlin Any
的。
泛型的上界萬用字元
前面說到:因為 Java 的泛型本身具有「不可變性 Invariance」,所以即使 Fruit
類是 Apple
類的父類,但 Java 裡面認為 List<Fruit>
和 List<Apple>
型別並不一致,也就是說,子類的泛型 List<Apple>
不屬於泛型 List<Fruit>
的子類。
所以這樣的程式碼並不被執行。
List<Apple> apples = new ArrayList<Apple>();
List<Fruit> fruits = apples;
// ? 多型用在這裡會報錯 Required type:List<Fruit> Provided: List<Apple>
那假如我們想突破這層限制,怎麼辦?使用上界萬用字元 ? extends
。
List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
// ?使用上界萬用字元後,編譯不再報錯
「上界萬用字元」,可以使 Java 泛型具有「協變性 Covariance」,協變就是允許上面的賦值是合法的。
在繼承關係樹中,子類繼承自父類,可以認為父類在上,子類在下。extends 限制了泛型型別的父型別,所以叫上界。
它有兩層意思:
- 其中
?
是個萬用字元,表示這個List
的泛型型別是一個未知型別。 extends
限制了這個未知型別的上界,也就是泛型型別必須滿足這個extends
的限制條件,這裡和定義class
的extends
關鍵字有點不一樣:- 它的範圍不僅是所有直接和間接子類,還包括上界定義的父類本身,也就是
Fruit
。 - 它還有
implements
的意思,即這裡的上界也可以是interface
。
- 它的範圍不僅是所有直接和間接子類,還包括上界定義的父類本身,也就是
這個突破限制有意義麼?
有的有的。
假如我們有一個介面 Fruit
:
interface Fruit {
float getWeight();
}
有兩個水果類實現了 Fruit
介面:
class Banana implements Fruit {
@Override
public float getWeight() {
return 0.5f;
}
}
class Apple implements Fruit {
@Override
public float getWeight() {
return 1f;
}
}
假設我們有個需求是需要給水果稱重:
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// ? 報錯:Required type: List<Fruit> Provided: List<Apple>
private float getTotalWeight(List<Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}
想來這也是一個非常正常的需求,秤可以稱各種水果的重量,但也可以只稱蘋果。你不能因為我只買蘋果就不給我稱重吧。所以把上面的程式碼加上上界萬用字元就可以啦。
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
float totalWeight = getTotalWeight(apples);
// ? 不再報錯
// ? 增加了上界萬用字元 ? extends
private float getTotalWeight(List<? extends Fruit> fruitList) {
float totalWeight = 0;
for (Fruit fruit : fruitList) {
totalWeight += fruit.getWeight();
}
return totalWeight;
}
不過,上面使用 ? extends
上界萬用字元突破了一層限制,卻被施加了另一層限制:只可輸出不可輸入。
什麼意思呢?
比如:
List<Apple> apples = new ArrayList<Apple>();
List<? extends Fruit> fruits = apples;
Fruit fruit = fruits.get(0);
fruits.add(new Apple());
// ? 報錯:Required type: capture of ? extends Fruit Provided: Apple
宣告瞭上界萬用字元泛型的集合,不再允許 add
新的物件,Apple
不行,Fruit
也不行。擴充開來說:不止是集合,自己編寫一個泛型做輸入也不行。
interface Shop<T> {
void showFruitName(T t);
T getFruit();
}
Shop<? extends Fruit> apples = new Shop<Apple>(){
@Override
public void showFruitName(Apple apple) { }
@Override
public Apple getFruit() {
return null;
}
};
apples.getFruit();
apples.showFruitName(new Apple());
// ? 報錯:Required type: capture of ? extends Fruit Provided: Apple
泛型的下界萬用字元
泛型有上界萬用字元,那有沒有下界萬用字元呢?
有的有的。
與上界萬用字元 ? extends
對應的就是下界萬用字元 ? super
下界萬用字元 ? super
所有情況和 ? extends
上界萬用字元剛剛相反:
- 萬用字元
?
表示 List 的泛型型別是一個 未知型別。 super
限制了這個未知型別的下界,也就是泛型型別必須滿足這個 super 的限制條件- 它的範圍不僅是所有直接和間接子父類,還包括下界定義的子類本身。
super
同樣支援interface
。
它被施加的新限制是:只可輸入不可輸出。
Shop<? super Apple> apples = new Shop<Fruit>(){
@Override
public void showFruitName(Fruit apple) { }
@Override
public Fruit getFruit() {
return null;
}
};
apples.showFruitName(new Apple());
Apple apple = apples.getFruit();
// ? 報錯:Required type: Apple Provided: capture of ? super Apple
解釋下,首先 ?
表示未知型別,編譯器是不確定它的型別的。
雖然不知道它的具體型別,不過在 Java 裡任何物件都是 Object
的子類,所以這裡只能把apples.getFruit()
獲取出來的物件賦值給 Object
。由於型別未知,所以直接賦值給一個 Apple
物件肯定是不負責任的,需要我們做一層強制轉換,不過強制轉換本身可能發生錯誤。
而 Apple
物件一定是這個未知型別的子型別,根據多型的特性,這裡通過 showFruitName
輸入 Button
物件是合法的。
小結下,Java 的泛型本身是不支援協變和逆變的:
- 可以使用泛型萬用字元
? extends
來使泛型支援協變,但是「只能讀取不能修改」,這裡的修改僅指對泛型集合新增元素,如果是remove(int index)
以及clear
當然是可以的。 - 可以使用泛型萬用字元
? super
來使泛型支援逆變,但是「只能修改不能讀取」,這裡說的不能讀取是指不能按照泛型型別讀取,你如果按照Object
讀出來再強轉當然也是可以的。
理解了 Java 的泛型之後,再理解 Kotlin 中的泛型,就比較容易了。
Kotlin 的 out 和 in
和 Java 泛型一樣,Kolin 中的泛型本身也是不可變的。
不過換了一種表現形式:
- 使用關鍵字
out
來支援協變,等同於 Java 中的上界萬用字元? extends
。 - 使用關鍵字
in
來支援逆變,等同於 Java 中的下界萬用字元? super
。
val appleShop: Shop<out Fruit>
val fruitShop: Shop<in Apple>
它們完全等價於:
Shop<? extends Fruit> appleShop;
Shop<? super Apple> fruitShop;
換了個寫法,但作用是完全一樣的。out
表示,我這個變數或者引數只用來輸出,不用來輸入,你只能讀我不能寫我;in
就反過來,表示它只用來輸入,不用來輸出,你只能寫我不能讀我。
泛型的上下界約束
上面講的都是在使用的時候再對泛型進行限制,我們稱之為「上界萬用字元」和「下界萬用字元」。那我們可以在函式設計的時候,就設定這個限制麼?
可以的可以的。
比如:
open class Animal
class PetShop<T : Animal?>(val t: T)
等同於 Java 的:
class PetShop<T extends Animal> {
private T t;
PetShop(T t) {
this.t = t;
}
}
這樣,我們在設計寵物店類 PetShop
就給支援的泛型設定了上界約束,支援的泛型型別必須是 Animal
的之類。所以我們使用的話:
class Cat : Animal()
val catShop = PetShop(Cat())
val appleShop = PetShop(Apple())
// ? 報錯:Type mismatch. Required: Animal? Found: Apple
很明顯,Apple
並不是 Animal
的子類,當然不滿足 PetShop
泛型型別的上界約束。
那....可以設定多個上界約束麼?
當然可以,在 Java 中,給一個泛型引數宣告多個約束的方式是,使用 &
:
class PetShop<T extends Animal & Serializable> {
// ? 通過 & 實現了兩個上界,必須是 Animal 和 Serializable 的子類或實現類
private T t;
PetShop(T t) {
this.t = t;
}
}
而在 Kotlin 中捨棄了 &
這種方式,而是增加了 where
關鍵字:
open class Animal
class PetShop<T>(val t: T) where T : Animal?, T : Serializable
通過上面的方式,就實現了多個上界的約束。
Kotlin 的萬用字元 *
前面我們說的泛型型別都是在我們需要知道引數型別是什麼型別的,那如果我們對泛型引數的型別不感興趣,有沒有一種方式處理這個情況呢?
有的有的。
在 Kotlin 中,可以用萬用字元 *
來替代泛型引數。比如:
val list: MutableList<*> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// ? 報錯:Type mismatch. Required: Nothing Found: String
這個報錯確實讓人匪夷所思,上面用萬用字元代表了 MutableList
的泛型引數型別。初始化裡面也加入了 String
型別,但在新 add
字串的時候,卻發生了編譯錯誤。
而如果是這樣的程式碼:
val list: MutableList<Any> = mutableListOf(1, "nanchen2251")
list.add("nanchen2251")
// ? 不再報錯
看來,所謂的萬用字元作為泛型引數並不等價於 Any
作為泛型引數。MutableList<*>
和 MutableList<Any>
並不是同一種列表,後者的型別是確定的,而前者的型別並不確定,編譯器並不能知道這是一種什麼型別。所以它不被允許新增元素,因為會導致型別不安全。
不過細心的同學肯定發現了,這個和前面泛型的協變非常類似。其實萬用字元 *
不過是一種語法糖,背後也是用協變來實現的。所以:MutableList<*>
等價於 MutableList<out Any?>
,使用萬用字元與協變有著一樣的特性。
在 Java 中,也有一樣意義的萬用字元,不過使用的是 ?
作為通配。
List<?> list = new ArrayList<Apple>();
Java 中的萬用字元 ?
也等價於 ? extends Object
。
多個泛型引數宣告
那可以宣告多個泛型麼?
可以的可以的。
HashMap
不就是一個典型的例子麼?
class HashMap<K,V>
多個泛型,可以通過 ,
進行分割,多個宣告,上面是兩個,實際上多個都是可以的。
class HashMap<K: Animal, V, T, M, Z : Serializable>
泛型方法
上面講的都是都是在類上宣告泛型型別,那可以宣告在方法上麼?
可以的可以的。
如果你是一名 Android 開發,View
的 findViewById
不就是最好的例子麼?
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
很明顯,View
是沒有泛型引數型別的,但其 findViewById
就是典型的泛型方法,泛型宣告就在方法上。
上述寫法改寫成 Kotlin 也非常簡單:
fun <T : View?> findViewById(@IdRes id: Int): T? {
return if (id == View.NO_ID) {
null
} else findViewTraversal(id)
}
Kotlin 的 reified
前面有說到,由於 Java 中的泛型存在型別擦除的情況,任何在執行時需要知道泛型確切型別資訊的操作都沒法用了。比如你不能檢查一個物件是否為泛型型別 T
的例項:
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) { // ? IDE 會提示錯誤,illegal generic type for instanceof
}
}
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)) {
?
}
}
Kotlin 中同樣可以這麼解決,不過還有一個更方便的做法:使用關鍵字 reified
配合 inline
來解決:
? ?
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) { // ? 這裡就不會在提示錯誤了
}
}
上面的 Gson 解析的時候用的非常廣泛,比如我們們專案裡就有這樣的擴充套件方法:
inline fun <reified T> String?.toObject(type: Type? = null): T? {
return if (type != null) {
GsonFactory.GSON.fromJson(this, type)
} else {
GsonFactory.GSON.fromJson(this, T::class.java)
}
}
總結
本文花了非常大的篇幅來講 Kotlin 的泛型和 Java 的泛型,現在再回過頭去回答文首的幾個問題,同學你有譜了嗎?如果還是感覺一知半解,不妨多看幾遍。
文章中有比較多的參考「碼上開學」的文章:Kotlin 的泛型
甚至有一部分直接擷取過來,主要本意是不想重複造輪子。文章中如有疏漏,歡迎在評論區進行留言。