第8章 泛型
通常情況的類(lèi)和函數(shù),我們只需要使用具體的類(lèi)型即可:要么是基本類(lèi)型赖草,要么是自定義的類(lèi)学少。但是在集合類(lèi)的場(chǎng)景下,我們通常需要編寫(xiě)可以應(yīng)用于多種類(lèi)型的代碼秧骑,我們最簡(jiǎn)單原始的做法是版确,針對(duì)每一種類(lèi)型,寫(xiě)一套刻板的代碼乎折。這樣做绒疗,代碼復(fù)用率會(huì)很低,抽象也沒(méi)有做好骂澄。我們能不能把“類(lèi)型”也抽象成參數(shù)呢吓蘑?是的,當(dāng)然可以坟冲。
Java 5 中引入泛型機(jī)制磨镶,實(shí)現(xiàn)了“參數(shù)化類(lèi)型”(Parameterized Type)。參數(shù)化類(lèi)型健提,顧名思義就是將類(lèi)型由原來(lái)的具體的類(lèi)型參數(shù)化棋嘲,類(lèi)似于方法中的變量參數(shù),此時(shí)類(lèi)型也定義成參數(shù)形式矩桂,我們稱(chēng)之為類(lèi)型參數(shù)沸移,然后在使用時(shí)傳入具體的類(lèi)型(類(lèi)型實(shí)參)。
我們知道侄榴,在數(shù)學(xué)中泛函是以函數(shù)為自變量的函數(shù)雹锣。類(lèi)比的來(lái)理解,編程中的泛型就是以類(lèi)型為變量的類(lèi)型癞蚕,即參數(shù)化類(lèi)型蕊爵。這樣的變量參數(shù)就叫類(lèi)型參數(shù)(Type Parameters)。
本章我們來(lái)一起學(xué)習(xí)一下Kotlin泛型的相關(guān)知識(shí)桦山。
8.1 為何引入泛型
《Java編程思想 》(第4版)中提到:有許多原因促成了泛型的出現(xiàn)攒射,而最引人注意的一個(gè)原因,就是為了創(chuàng)建容器類(lèi) (集合類(lèi))恒水。
集合類(lèi)可以說(shuō)是我們?cè)趯?xiě)代碼過(guò)程中最最常用的類(lèi)之一会放。我們先來(lái)看下沒(méi)有泛型之前,我們的集合類(lèi)是怎樣持有對(duì)象的钉凌。在Java中咧最,Object類(lèi)是所有類(lèi)的根類(lèi)。為了集合類(lèi)的通用性,把元素的類(lèi)型定義為Object矢沿,當(dāng)放入具體的類(lèi)型的時(shí)候滥搭,再作相應(yīng)的強(qiáng)制類(lèi)型轉(zhuǎn)換。
這是一個(gè)示例代碼:
class RawArrayList {
public int length = 0;
private Object[] elements; // 把元素的類(lèi)型定義為Object
public RawArrayList(int length) {
this.length = length;
this.elements = new Object[length];
}
public Object get(int index) {
return elements[index];
}
public void add(int index, Object element) {
elements[index] = element;
}
}
一個(gè)簡(jiǎn)單的測(cè)試代碼如下
public class RawTypeDemo {
public static void main(String[] args) {
RawArrayList rawArrayList = new RawArrayList(4);
rawArrayList.add(0, "a");
rawArrayList.add(1, "b");
System.out.println(rawArrayList);
String a = (String)rawArrayList.get(0);
System.out.println(a);
String b = (String)rawArrayList.get(1);
System.out.println(b);
rawArrayList.add(2, 200);
rawArrayList.add(3, 300);
System.out.println(rawArrayList);
int c = (int)rawArrayList.get(2);
int d = (int)rawArrayList.get(3);
System.out.println(c);
System.out.println(d);
String x = (String)rawArrayList.get(2); //Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
System.out.println(x);
}
}
我們可以看出捣鲸,在使用原生態(tài)類(lèi)型(raw type)實(shí)現(xiàn)的集合類(lèi)中瑟匆,我們使用的是Object[]數(shù)組。這種實(shí)現(xiàn)方式栽惶,存在的問(wèn)題有兩個(gè):
向集合中添加對(duì)象元素的時(shí)候愁溜,沒(méi)有對(duì)元素的類(lèi)型進(jìn)行檢查,也就是說(shuō)媒役,我們往集合中添加任意對(duì)象,編譯器都不會(huì)報(bào)錯(cuò)宪迟。
當(dāng)我們從集合中獲取一個(gè)值的時(shí)候酣衷,我們不能都使用Object類(lèi)型,需要進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換次泽。而這個(gè)轉(zhuǎn)換過(guò)程由于在添加元素的時(shí)候沒(méi)有作任何的類(lèi)型的限制跟檢查穿仪,所以容易出錯(cuò)。例如上面代碼中的:
String x = (String)rawArrayList.get(2); //Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
對(duì)于這行代碼意荤,編譯時(shí)不會(huì)報(bào)錯(cuò)啊片,但是運(yùn)行時(shí)會(huì)拋出類(lèi)型轉(zhuǎn)換錯(cuò)誤。能不能讓編譯器來(lái)解決這樣的樣板化的類(lèi)型轉(zhuǎn)換代碼呢玖像?當(dāng)我們向rawArrayList 添加元素的時(shí)候
rawArrayList.add(0, "a");
就限定其元素類(lèi)型只能為String紫谷,那么在后面的獲取元素的時(shí)候,自動(dòng)強(qiáng)制轉(zhuǎn)型為String 呢捐寥?
String a = (String)rawArrayList.get(0);
這個(gè)元素類(lèi)型 String 的信息笤昨,我們存放到 一個(gè)“類(lèi)型參數(shù)”中,然后在編譯器層面引入相應(yīng)的類(lèi)型檢查和自動(dòng)轉(zhuǎn)換機(jī)制握恳,這樣就可以解決這個(gè)類(lèi)型安全使用的問(wèn)題瞒窒。這也正是引入的泛型的基本思想。
泛型最主要的優(yōu)點(diǎn)就是讓編譯器追蹤參數(shù)類(lèi)型乡洼,執(zhí)行類(lèi)型檢查和類(lèi)型轉(zhuǎn)換崇裁。因?yàn)橛删幾g器來(lái)保證類(lèi)型轉(zhuǎn)換不會(huì)失敗。如果依賴(lài)我們程序員自己去追蹤對(duì)象類(lèi)型和執(zhí)行轉(zhuǎn)換束昵,那么運(yùn)行時(shí)產(chǎn)生的錯(cuò)誤將很難去定位和調(diào)試拔稳,然而有了泛型,編譯器 可以幫助我們執(zhí)行大量的類(lèi)型檢查锹雏,并且可以檢測(cè)出更多的編譯時(shí)錯(cuò)誤壳炎。在這一點(diǎn)上,泛型跟我們第3章中所講到的“可空類(lèi)型”實(shí)現(xiàn)的空指針安全,在思想上有著異曲同工之妙匿辩。
8.2 在類(lèi)腰耙、接口和函數(shù)上使用泛型
泛型類(lèi)、泛型接口和泛型方法具備可重用性铲球、類(lèi)型安全和高效等優(yōu)點(diǎn)挺庞。在集合類(lèi)API中大量地使用了泛型。在Java 中我們可以為類(lèi)稼病、接口和方法分別定義泛型參數(shù)选侨,在Kotlin中也同樣支持。本節(jié)我們分別介紹Kotlin中的泛型接口然走、泛型類(lèi)和泛型函數(shù)援制。
8.2.1 泛型接口
我們舉一個(gè)簡(jiǎn)單的Kotlin泛型接口的例子。
interface Generator<T> { // 類(lèi)型參數(shù)放在接口名稱(chēng)后面: <T>
operator fun next(): T // 接口函數(shù)中直接使用類(lèi)型 T
}
測(cè)試代碼
fun testGenerator() {
val gen = object : Generator<Int> { // 對(duì)象表達(dá)式
override fun next(): Int {
return Random().nextInt(10)
}
}
println(gen.next())
}
這里我們使用object 關(guān)鍵字來(lái)聲明一個(gè)Generator實(shí)現(xiàn)類(lèi)芍瑞,并在lambda表達(dá)式中實(shí)現(xiàn)了next() 函數(shù)晨仑。
Kotlin 中 Map 和 MutableMap 接口的定義也是一個(gè)典型的泛型接口的例子。
public interface Map<K, out V> {
...
public fun containsKey(key: K): Boolean
public fun containsValue(value: @UnsafeVariance V): Boolean
public operator fun get(key: K): V?
...
public val keys: Set<K>
public val values: Collection<V>
public val entries: Set<Map.Entry<K, V>>
}
public interface MutableMap<K, V> : Map<K, V> {
public fun put(key: K, value: V): V?
public fun remove(key: K): V?
public fun putAll(from: Map<out K, V>): Unit
...
}
例如拆檬,我們使用 mutableMapOf 函數(shù)來(lái)實(shí)例化一個(gè)可變Map
>>> val map = mutableMapOf<Int,String>(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
其中洪己,mutableMapOf 函數(shù)簽名如下
fun <K, V> mutableMapOf(vararg pairs: Pair<K, V>): MutableMap<K, V>
這里類(lèi)型參數(shù) K,V 當(dāng)泛型類(lèi)型被實(shí)例化和使用時(shí)竟贯,它將被一個(gè)實(shí)際的類(lèi)型參數(shù)所替代答捕。在 mutableMapOf<Int,String> 中,放置K屑那, V 的位置被具體的Int 和 String 類(lèi)型所替代拱镐。
泛型可以用來(lái)限制集合類(lèi)持有的對(duì)象類(lèi)型,這樣使得類(lèi)型更加安全持际。當(dāng)我們?cè)谝粋€(gè)集合類(lèi)里面放入了錯(cuò)誤類(lèi)型的對(duì)象痢站,編譯器就會(huì)報(bào)錯(cuò):
>>> map.put("5","e")
error: type mismatch: inferred type is String but Int was expected
map.put("5","e")
^
Kotlin中有類(lèi)型推斷的功能,有些類(lèi)型參數(shù)可以直接省略不寫(xiě)选酗。mutableMapOf<Int,String> 后面的類(lèi)型參數(shù) <Int,String> 可以省掉不寫(xiě):
>>> val map = mutableMapOf(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
8.2.2 泛型類(lèi)
我們直接聲明一個(gè)帶類(lèi)型參數(shù)的 Container 類(lèi)
class Container<K, V>(var key: K, var value: V)
為了方便測(cè)試阵难,我們重寫(xiě) toString() 函數(shù)
class Container<K, V>(var key: K, var value: V){ // 在類(lèi)名后面聲明泛型參數(shù)<K, V> , 多個(gè)泛型使用逗號(hào)隔開(kāi)
override fun toString(): String {
return "Container(key=$key, value=$value)"
}
}
測(cè)試代碼
fun testContainer() {
val container = Container<Int, String>(1, "A") // <K, V> 被具體化為<Int, String>
println(container) // container = Container(key=1, value=A)
}
8.2.3 泛型函數(shù)
在泛型接口和泛型類(lèi)中芒填,我們都在類(lèi)名和接口名后面聲明了泛型參數(shù)呜叫。而實(shí)際上,我們也可以直接在類(lèi)或接口中的函數(shù)殿衰,或者直接在包級(jí)函數(shù)中直接聲明泛型參數(shù)朱庆。代碼示例如下
class GenericClass {
fun <T> console(t: T) { // 類(lèi)中的泛型函數(shù)
println(t)
}
}
interface GenericInterface {
fun <T> console(t: T) // 接口中的泛型函數(shù)
}
fun <T : Comparable<T>> gt(x: T, y: T): Boolean { // 包中的泛型函數(shù)
return x > y
}
8.3 類(lèi)型上界
在上面的例子中,我們有看到 gt(x:T, y:T) 函數(shù)的簽名中有個(gè) T : Comparable<T>
fun <T : Comparable<T>> gt(x: T, y: T): Boolean
這里的 T : Comparable<T> 闷祥,表示 Comparable<T>是類(lèi)型 T 的上界娱颊。也就是告訴編譯器傲诵,類(lèi)型參數(shù) T 代表的都是實(shí)現(xiàn)了 Comparable<T> 接口的類(lèi),這樣等于告訴編譯器它們都實(shí)現(xiàn)了compareTo方法箱硕。如果沒(méi)有這個(gè)類(lèi)型上界聲明拴竹,我們就無(wú)法直接使用 compareTo ( > )操作符。也就是說(shuō)剧罩,下面的代碼編譯不通過(guò)
fun <T> gt(x: T, y: T): Boolean {
return x > y // 編譯不通過(guò)
}
8.4 協(xié)變與逆變
我們來(lái)看一個(gè)問(wèn)題場(chǎng)景栓拜。首先,我們有下面的存在父子關(guān)系的類(lèi)型
open class Food
open class Fruit : Food()
class Apple : Fruit()
class Banana : Fruit()
class Grape : Fruit()
然后惠昔,我們有下面的兩個(gè)函數(shù)
object GenericTypeDemo {
fun addFruit(fruit: MutableList<Fruit>) {
// TODO
}
fun getFruit(fruit: MutableList<Fruit>) {
// TODO
}
}
這個(gè)時(shí)候幕与,我們可以這樣調(diào)用上面的兩個(gè)函數(shù)
val fruits: MutableList<Fruit> = mutableListOf(Fruit(), Fruit(), Fruit())
GenericTypeDemo.addFruit(fruits)
GenericTypeDemo.getFruit(fruits)
現(xiàn)在,我們又有一個(gè)存放Apple的List
val apples: MutableList<Apple> = mutableListOf(Apple(), Apple(), Apple())
由于Kotlin中的泛型跟Java一樣是非協(xié)變的镇防,下面的調(diào)用是編譯不通過(guò)的
GenericTypeDemo.addFruit(apples) // type mismatch
GenericTypeDemo.getFruit(apples) // type mismatch
如果沒(méi)有協(xié)變啦鸣,那么我們不得不再添加兩個(gè)函數(shù)
object GenericTypeDemo {
fun addFruit(fruit: MutableList<Fruit>) {
// TODO
}
fun getFruit(fruit: MutableList<Fruit>) {
// TODO
}
fun addApple(apple: MutableList<Apple>) {
// TODO
}
fun getApple(apple: MutableList<Apple>) {
// TODO
}
}
我們一眼就能看出,這是重復(fù)的樣板代碼来氧。我們能不能讓 MutableList<Fruit> 成為 MutableList<Apple> 的父類(lèi)型呢诫给? Java泛型中引入了類(lèi)型通配符的概念來(lái)解決這個(gè)問(wèn)題。Java 泛型的通配符有兩種形式:
子類(lèi)型上界限定符 ? extends T 指定類(lèi)型參數(shù)的上限(該類(lèi)型必須是類(lèi)型T或者它的子類(lèi)型)饲漾。也就是說(shuō)MutableList<? extends Fruit> 是 MutableList<Apple> 的父類(lèi)型蝙搔。 Kotlin中使用 MutableList<out Fruit> 來(lái)表示缕溉。
超類(lèi)型下界限定符 ? super T 指定類(lèi)型參數(shù)的下限(該類(lèi)型必須是類(lèi)型T或者它的父類(lèi)型)考传。也就是說(shuō)MutableList<? super Fruit> 是 MutableList<Object>的父類(lèi)型。Kotlin中使用 MutableList<in Fruit> 來(lái)表示证鸥。
這里的問(wèn)號(hào) (?) , 我們稱(chēng)之為類(lèi)型通配符(Type Wildcard)僚楞。通配符在類(lèi)型系統(tǒng)中具有重要的意義,它們?yōu)橐粋€(gè)泛型類(lèi)所指定的類(lèi)型集合提供了一個(gè)有用的類(lèi)型范圍枉层。
Number 類(lèi)型(簡(jiǎn)記為F) 是 Integer 類(lèi)型(簡(jiǎn)記為C)的父類(lèi)型泉褐,我們把這種父子類(lèi)型關(guān)系簡(jiǎn)記為:C => F (C 繼承 F);而List<Number>, List<Integer>的代表的泛型類(lèi)型信息鸟蜡,我們分別簡(jiǎn)記為 f(F), f(C)膜赃。
那么我們可以這么來(lái)描述協(xié)變和逆變:
當(dāng) C => F 時(shí), 如果有 f(C) => f(F), 那么 f 叫做協(xié)變;
當(dāng) C => F 時(shí), 如果有 f(F) => f(C), 那么 f 叫做逆變揉忘。
如果上面兩種關(guān)系都不成立則叫做不變跳座。
協(xié)變與逆變可以用下圖來(lái)簡(jiǎn)單說(shuō)明
協(xié)變和逆協(xié)變都是類(lèi)型安全的。
8.4.1 協(xié)變
在Java中數(shù)組是協(xié)變的泣矛,下面的代碼是可以正確編譯運(yùn)行的:
Integer[] ints = new Integer[3];
ints[0] = 0;
ints[1] = 1;
ints[2] = 2;
Number[] numbers = new Number[3];
numbers = ints;
for (Number n : numbers) {
System.out.println(n);
}
在Java中疲眷,因?yàn)?Integer 是 Number 的子類(lèi)型,數(shù)組類(lèi)型 Integer[] 也是 Number[] 的子類(lèi)型汇荐,因此在任何需要 Number[] 值的地方都可以提供一個(gè) Integer[] 值熬苍。Java中數(shù)組協(xié)變的意思可以用下圖簡(jiǎn)單說(shuō)明
Java中泛型是非協(xié)變的。如下圖所示
也就是說(shuō)真屯, List<Integer> 不是 List<Number> 的子類(lèi)型几颜,試圖在要求 List<Number> 的位置提供 List<Integer> 是一個(gè)類(lèi)型錯(cuò)誤倍试。下面的代碼,編譯器是會(huì)直接報(bào)錯(cuò)的:
List<Integer> integerList = new ArrayList<>();
integerList.add(0);
integerList.add(1);
integerList.add(2);
List<Number> numberList = new ArrayList<>();
numberList = integerList; // 編譯錯(cuò)誤:類(lèi)型不兼容
編譯器報(bào)錯(cuò)提示如下:
Java中泛型和數(shù)組的不同行為菠剩,的確引起了許多混亂易猫。就算我們使用通配符,這樣寫(xiě):
List<? extends Number> list = new ArrayList<Number>();
list.add(new Integer(1)); //error
仍然是報(bào)錯(cuò)的:
這通常會(huì)讓我們感到困惑:為什么Number的對(duì)象可以由Integer實(shí)例化具壮,而ArrayList<Number>的對(duì)象卻不能由ArrayList<Integer>實(shí)例化准颓?list中的<? extends Number>聲明其元素是Number或Number的派生類(lèi),為什么不能add Integer? 為了弄清楚這些問(wèn)題棺妓,我們需要了解Java中的逆變和協(xié)變以及泛型中通配符用法攘已。
List<? extends Number> list = new ArrayList<>();
這里的子類(lèi)型 C 就是 Number類(lèi)及其子類(lèi)(例如Number、Integer怜跑、Float等) 样勃,表示的是 Number 類(lèi)或其子類(lèi)。父類(lèi) F 就是上界通配符: ? extends Number性芬。
當(dāng) C => F 峡眶,這個(gè)關(guān)系成立:f(C) => f(F) , 這就是協(xié)變。我們把 f(F) 具體化為 List<? extends Number>植锉, f(C) 具體化為 List<Integer> 辫樱、List<Float>等。 協(xié)變代表的意義就是: List<? extends Number> 是 List<Integer> 俊庇、List<Float>等的父類(lèi)型狮暑。如下圖所示
代碼示例
List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Float>();
但是這里不能向list1、list2添加除null以外的任意對(duì)象辉饱。
list1.add(null); // ok
list2.add(null);// ok
list1.add(new Integer(1)); // error
list2.add(new Float(1.1f)); // error
List<Integer>可以添加Interger及其子類(lèi)搬男;
List<Float>可以添加Float及其子類(lèi);
List<Integer>彭沼、List<Float>等都是 List<? extends Number>的子類(lèi)型缔逛。
現(xiàn)在問(wèn)題來(lái)了,如果能將Float的子類(lèi)添加到 List<? extends Number>中姓惑,那么也能將Integer的子類(lèi)添加到 List<? extends Number>中, 那么這時(shí)候 List<? extends Number> 里面將會(huì)持有各種Number子類(lèi)型的對(duì)象(Byte褐奴,Integer,F(xiàn)loat挺益,Double等)歉糜。而這個(gè)時(shí)候,當(dāng)我們?cè)偈褂眠@個(gè)list的時(shí)候望众,元素的類(lèi)型就會(huì)混亂匪补。我們不知道哪個(gè)元素會(huì)是Integer或者Float 伞辛。Java為了保護(hù)其類(lèi)型一致,禁止向List<? extends Number>添加任意對(duì)象夯缺,不過(guò)可以添加空對(duì)象null蚤氏。
8.4.2 逆變
我們先用一段代碼舉例
List<? super Number> list = new ArrayList<Object>();
這里的子類(lèi)型 C 是 ? super Number , 父類(lèi)型 F 是 Number 的父類(lèi)型(例如:Object類(lèi))。
當(dāng) C => F , 有 f(F) => f(C) 踊兜, 這就是逆變竿滨。我們把 f (C) 具體化為 List<? super Number> ,f(F) 具體化為L(zhǎng)ist<Object> 捏境。逆變的意思就是說(shuō) List<? super Number> 是 List<Object> 的父類(lèi)型于游。如下圖所示
代碼示例:
List<? super Number> list3 = new ArrayList<Number>();
List<? super Number> list4 = new ArrayList<Object>();
list3.add(new Integer(3));
list4.add(new Integer(4));
在逆變類(lèi)型中,我們可以向其中添加元素垫言。例如贰剥,我們可以向 List<? super Number > list4 變量中添加Number及其子類(lèi)對(duì)象。
8.4.3 PECS
現(xiàn)在問(wèn)題來(lái)了:我們什么時(shí)候用extends什么時(shí)候用super呢筷频?《Effective Java》給出了答案:
PECS: producer-extends, consumer-super
下面我們通過(guò)實(shí)例來(lái)說(shuō)明PECS的具體含義蚌成。
首先,我們聲明一個(gè)簡(jiǎn)單的Stack 泛型類(lèi)如下
public class Stack<E>{
public Stack();
public void push(E e):
public E pop();
public boolean isEmpty();
}
要實(shí)現(xiàn)pushAll(Iterable<E> src)方法凛捏,將src的元素逐一入棧
public void pushAll(Iterable<E> src){
for(E e : src)
push(e)
}
假設(shè)有一個(gè)Stack<Number>(類(lèi)型參數(shù)E 具體化為 Number 類(lèi)型)實(shí)例化的對(duì)象stack担忧,src有 Iterable<Integer> 與 Iterable<Float>,那么在調(diào)用pushAll方法時(shí)會(huì)發(fā)生type mismatch錯(cuò)誤坯癣,因?yàn)镴ava中泛型是不可變的瓶盛,Iterable<Integer>與 Iterable<Float>都不是Iterable<Number>的子類(lèi)型。
因此坡锡,pushAll(Iterable<E> src)方法簽名應(yīng)改為
// Wildcard type for parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src) // out T, 從src中讀取數(shù)據(jù)蓬网,producer-extends
push(e);
}
這樣就實(shí)現(xiàn)了泛型的協(xié)變窒所。同時(shí)鹉勒,我們從src中讀取的數(shù)據(jù)都能保證是E類(lèi)型及其子類(lèi)型的對(duì)象。
現(xiàn)在吵取,我們?cè)倏?popAll(Collection<E> dst)方法禽额,該方法將Stack中的元素依次取出add到dst中,如果不用通配符實(shí)現(xiàn):
// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
同樣地皮官,假設(shè)有一個(gè)實(shí)例化Stack<Number>的對(duì)象stack脯倒,dst為Collection<Object>;調(diào)用popAll方法是會(huì)發(fā)生type mismatch錯(cuò)誤捺氢,因?yàn)镃ollection<Object>不是Collection<Number>的子類(lèi)型藻丢。
因而,popAll(Collection<E> dst) 方法應(yīng)改為:
// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) { // 保證dst中的元素都是E類(lèi)型或者E的父類(lèi)型
while (!isEmpty())
dst.add(pop()); // in T, 向dst中寫(xiě)入數(shù)據(jù)摄乒, consumer-super
}
因?yàn)?pop() 返回的數(shù)據(jù)類(lèi)型是E悠反, 而dst中的元素都是E類(lèi)型或者E的父類(lèi)型残黑,所以我們可以安全地寫(xiě)入E類(lèi)型的數(shù)據(jù)。
Naftalin與Wadler將PECS稱(chēng)為 Get and Put Principle斋否。
在java.util.Collections
的copy
方法中(JDK1.7)完美地詮釋了PECS:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator(); // in T, 寫(xiě)入dest數(shù)據(jù)
ListIterator<? extends T> si=src.listIterator(); // out T梨水, 讀取src數(shù)據(jù)
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
8.5 out T 與 in T
正如上文所講的,在 Java 泛型里茵臭,有通配符這種東西疫诽,我們要用? extends T
指定類(lèi)型參數(shù)的上限,用 ? super T
指定類(lèi)型參數(shù)的下限旦委。
而Kotlin 拋棄了這個(gè)東西奇徒,直接實(shí)現(xiàn)了上文所講的PECS的規(guī)則。Kotlin 引入了投射類(lèi)型 out T 代表生產(chǎn)者對(duì)象缨硝,投射類(lèi)型 in T 代表消費(fèi)者對(duì)象逼龟。Kotlin使用了投射類(lèi)型( projected type ) out T 和 in T 來(lái)實(shí)現(xiàn)了類(lèi)型通配符同樣的功能。
我們用代碼示例簡(jiǎn)單講解一下:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
ListIterator<? super T> di=dest.listIterator(); // in T, 寫(xiě)入dest數(shù)據(jù)
ListIterator<? extends T> si=src.listIterator(); // out T追葡, 讀取src數(shù)據(jù)
...
}
List<? super T> dest 是消費(fèi)數(shù)據(jù)的對(duì)象腺律,數(shù)據(jù)會(huì)被寫(xiě)入到 dest 對(duì)象中,這些數(shù)據(jù)該對(duì)象被“吃掉”了(Kotlin中叫in T
)宜肉。
List<? extends T> src 是生產(chǎn)提供數(shù)據(jù)的對(duì)象匀钧。src 會(huì)“吐出”數(shù)據(jù)(Kotlin中叫out T
)。
在Kotlin中谬返,我們把那些只能保證讀取數(shù)據(jù)時(shí)類(lèi)型安全的對(duì)象叫做生產(chǎn)者之斯,用 out T
標(biāo)記;把那些只能保證寫(xiě)入數(shù)據(jù)安全時(shí)類(lèi)型安全的對(duì)象叫做消費(fèi)者遣铝,用 in T
標(biāo)記佑刷。
如果你覺(jué)得太晦澀難懂,就這么記吧:
out T
等價(jià)于? extends T
in T
等價(jià)于? super T
8.6 類(lèi)型擦除
Java和Kotlin 的泛型實(shí)現(xiàn)酿炸,都是采用了運(yùn)行時(shí)類(lèi)型擦除的方式瘫絮。也就是說(shuō),在運(yùn)行時(shí)填硕,這些類(lèi)型參數(shù)的信息將會(huì)被擦除麦萤。
泛型是在編譯器層次上實(shí)現(xiàn)的。生成的 class 字節(jié)碼文件中是不包含泛型中的類(lèi)型信息的扁眯。例如在代碼中定義的List<Object>和List<String>等類(lèi)型壮莹,在編譯之后都會(huì)變成List。JVM看到的只是List姻檀,而由泛型附加的類(lèi)型信息對(duì)JVM來(lái)說(shuō)是不可見(jiàn)的命满。
關(guān)于泛型的很多奇怪特性都與這個(gè)類(lèi)型擦除的存在有關(guān),比如:泛型類(lèi)并沒(méi)有自己獨(dú)有的Class類(lèi)對(duì)象绣版。比如Java中并不存在List<String>.class或是List<Integer>.class胶台,而只有List.class狭莱。對(duì)應(yīng)地在Kotlin中并不存在MutableList<Fruit>::class, 而只有 MutableList::class 概作。
類(lèi)型擦除的基本過(guò)程也比較簡(jiǎn)單:
首先腋妙,找到用來(lái)替換類(lèi)型參數(shù)的具體類(lèi)。這個(gè)具體類(lèi)一般是Object讯榕。如果指定了類(lèi)型參數(shù)的上界的話骤素,則使用這個(gè)上界。
其次愚屁,把代碼中的類(lèi)型參數(shù)都替換成具體的類(lèi)济竹。同時(shí)去掉出現(xiàn)的類(lèi)型聲明,即去掉<>的內(nèi)容霎槐。比如送浊, T get() 就變成了Object get(), List<String> 就變成了List丘跌。
最后袭景,根據(jù)需要生成一些橋接方法。這是由于擦除了類(lèi)型之后的類(lèi)可能缺少某些必須的方法闭树。這個(gè)時(shí)候就由編譯器來(lái)動(dòng)態(tài)生成這些方法耸棒。
當(dāng)了解了類(lèi)型擦除機(jī)制之后,我們就會(huì)明白是編譯器承擔(dān)了全部的類(lèi)型檢查工作报辱。編譯器禁止某些泛型的使用方式与殃,也正是為了確保類(lèi)型的安全性。
本章小結(jié)
泛型是一個(gè)非常有用的東西碍现。尤其在集合類(lèi)中幅疼。我們可以發(fā)現(xiàn)大量的泛型代碼。有了泛型昼接,我們可以擁有更強(qiáng)大更安全的類(lèi)型檢查爽篷、無(wú)需手工進(jìn)行類(lèi)型轉(zhuǎn)換,并且能夠開(kāi)發(fā)更加通用的泛型算法辩棒。
Kotlin 開(kāi)發(fā)者社區(qū)
國(guó)內(nèi)第一Kotlin 開(kāi)發(fā)者社區(qū)公眾號(hào)狼忱,主要分享膨疏、交流 Kotlin 編程語(yǔ)言一睁、Spring Boot、Android佃却、React.js/Node.js者吁、函數(shù)式編程、編程思想等相關(guān)主題饲帅。