第8章 泛型

第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è):

  1. 向集合中添加對(duì)象元素的時(shí)候愁溜,沒(méi)有對(duì)元素的類(lèi)型進(jìn)行檢查,也就是說(shuō)媒役,我們往集合中添加任意對(duì)象,編譯器都不會(huì)報(bào)錯(cuò)宪迟。

  2. 當(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é)變和逆協(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 數(shù)組協(xié)變

Java中泛型是非協(xié)變的。如下圖所示

泛型不是協(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ò)提示如下:

編譯錯(cuò)誤:類(lèi)型不兼容

Java中泛型和數(shù)組的不同行為菠剩,的確引起了許多混亂易猫。就算我們使用通配符,這樣寫(xiě):

List<? extends Number> list = new ArrayList<Number>();  
list.add(new Integer(1)); //error  

仍然是報(bào)錯(cuò)的:

add元素錯(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)型狮暑。如下圖所示

協(xié)變

代碼示例

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蚤氏。

禁止向List<? extends Number>添加任意對(duì)象

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.Collectionscopy方法中(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)主題饲帅。

開(kāi)發(fā)者社區(qū) QRCode.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末复凳,一起剝皮案震驚了整個(gè)濱河市瘤泪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌育八,老刑警劉巖对途,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異髓棋,居然都是意外死亡实檀,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)按声,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)膳犹,“玉大人,你說(shuō)我怎么就攤上這事签则⌒氪玻” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵渐裂,是天一觀的道長(zhǎng)豺旬。 經(jīng)常有香客問(wèn)我,道長(zhǎng)柒凉,這世上最難降的妖魔是什么哈垢? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮扛拨,結(jié)果婚禮上耘分,老公的妹妹穿的比我還像新娘。我一直安慰自己绑警,他們只是感情好求泰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著计盒,像睡著了一般渴频。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上北启,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天卜朗,我揣著相機(jī)與錄音,去河邊找鬼咕村。 笑死场钉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懈涛。 我是一名探鬼主播逛万,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼批钠!你這毒婦竟也來(lái)了宇植?” 一聲冷哼從身側(cè)響起得封,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎指郁,沒(méi)想到半個(gè)月后忙上,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡闲坎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年晨横,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箫柳。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡手形,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出悯恍,到底是詐尸還是另有隱情库糠,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布涮毫,位于F島的核電站瞬欧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏罢防。R本人自食惡果不足惜艘虎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望咒吐。 院中可真熱鬧野建,春花似錦、人聲如沸恬叹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绽昼。三九已至唯鸭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間硅确,已是汗流浹背目溉。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留菱农,地道東北人缭付。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像大莫,于是被迫代替她去往敵國(guó)和親蛉腌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 泛型 泛型(Generic Type)簡(jiǎn)介 通常情況的類(lèi)和函數(shù),我們只需要使用具體的類(lèi)型即可:要么是基本類(lèi)型羔味,要么...
    Tenderness4閱讀 1,412評(píng)論 4 2
  • 寫(xiě)在開(kāi)頭:本人打算開(kāi)始寫(xiě)一個(gè)Kotlin系列的教程河咽,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,421評(píng)論 1 3
  • 前言 人生苦多赋元,快來(lái) Kotlin 忘蟹,快速學(xué)習(xí)Kotlin! 什么是Kotlin搁凸? Kotlin 是種靜態(tài)類(lèi)型編程...
    任半生囂狂閱讀 26,145評(píng)論 9 118
  • 都說(shuō)朋友是互相謙讓?zhuān)ハ鄮椭闹担钦乙粋€(gè)真正的朋友真的非常難,而且是太多覺(jué)得很好的朋友护糖,因?yàn)橐稽c(diǎn)事情就翻...
    4點(diǎn)半的恩賜閱讀 478評(píng)論 0 0
  • 3月的天氣,春天的氣息褥芒,雨珠不斷的,不斷的洗刷著玻璃,萬(wàn)物在雨的沖刷下顯得楚楚動(dòng)人嫡良,郁郁蔥蔥锰扶,枯木在雨后也得到了重...
    6ad5b7598ebc閱讀 504評(píng)論 4 10