《Kotlin 極簡教程 》第6章 泛型

第6章 泛型


《Kotlin極簡教程》正式上架:

點擊這裏 > 去京東商城購買閱讀

點擊這裏 > 去天貓商城購買閱讀

很是感謝您親愛的讀者,你們請多支持!!!有任何問題,歡迎隨時與我交流~


6.1 泛型(Generic Type)簡介

一般狀況的類和函數,咱們只須要使用具體的類型便可:要麼是基本類型,要麼是自定義的類。html

可是尤爲在集合類的場景下,咱們須要編寫能夠應用於多種類型的代碼,咱們最簡單原始的作法是,針對每一種類型,寫一套刻板的代碼。java

這樣作,代碼複用率會很低,抽象也沒有作好。git

在SE 5種,Java引用了泛型。泛型,即「參數化類型」(Parameterized Type)。顧名思義,就是將類型由原來的具體的類型參數化,相似於方法中的變量參數,此時類型也定義成參數形式,咱們稱之爲類型參數,而後在使用時傳入具體的類型(類型實參)。github

咱們知道,在數學中泛函是以函數爲自變量的函數。類比的來理解,編程中的泛型就是以類型爲變量的類型,即參數化類型。這樣的變量參數就叫類型參數(Type Parameters)。編程

本章咱們來一塊兒學習一下Kotlin泛型的相關知識。數組

6.1.1 爲何要有類型參數

咱們先來看下沒有泛型以前,咱們的集合類是怎樣持有對象的。安全

在Java中,Object類是全部類的根類。爲了集合類的通用性。咱們把元素的類型定義爲Object,當放入具體的類型的時候,再做強制類型轉換。dom

這是一個示例代碼:ide

class RawArrayList {
    public int length = 0;
    private Object[] elements;

    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;
    }

    @Override
    public String toString() {
        return "RawArrayList{" +
            "length=" + length +
            ", elements=" + Arrays.toString(elements) +
            '}';
    }
}

一個簡單的測試代碼以下:函數式編程

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);

        //Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
        String x = (String)rawArrayList.get(2);
        System.out.println(x);

    }

}

咱們能夠看出,在使用原生態類型(raw type)實現的集合類中,咱們使用的是Object[]數組。這種實現方式,存在的問題有兩個:

  1. 向集合中添加對象元素的時候,沒有對元素的類型進行檢查,也就是說,咱們往集合中添加任意對象,編譯器都不會報錯。
  2. 當咱們從集合中獲取一個值的時候,咱們不能都使用Object類型,須要進行強制類型轉換。而這個轉換過程因爲在添加元素的時候沒有做任何的類型的限制跟檢查,因此容易出錯。例如上面代碼中的:
String a = (String)rawArrayList.get(0);

對於這行代碼,編譯時不會報錯,可是運行時會拋出類型轉換錯誤。

因爲咱們不能籠統地把集合類中全部的對象是視做Object,而後在使用的時候各自做強制類型轉換。所以,咱們引入了類型參數來解決這個類型安全使用的問題。

Java 中的泛型是在1.5 以後加入的,咱們能夠爲類和方法分別定義泛型參數,好比說Java中的Map接口的定義:

public interface Map<K,V> {
    ...
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    V put(K key, V value);
    V remove(Object key);
    void putAll(Map<? extends K, ? extends V> m);
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }
}

咱們在Kotlin 中的寫法基本同樣:

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?
    @SinceKotlin("1.1")
    @PlatformDependent
    public fun getOrDefault(key: K, defaultValue: @UnsafeVariance V): V {
        // See default implementation in JDK sources
        return null as 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
    ...

}

好比,在實例化一個Map時,咱們使用這個函數:

fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V>

類型參數K,V是一個佔位符,當泛型類型被實例化和使用時,它將被一個實際的類型參數所替代。

代碼示例

>>> val map = mutableMapOf<Int,String>(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}
>>> map.put(4,"c")
null
>>> map
{1=a, 2=b, 3=c, 4=c}

mutableMapOf<Int,String>表示參數化類型<K , V>分別是Int 和 String,這是泛型類型集合的實例化,在這裏,放置K, V 的位置被具體的Int 和 String 類型所替代。

泛型主要是用來限制集合類持有的對象類型,這樣使得類型更加安全。當咱們在一個集合類裏面放入了錯誤類型的對象,編譯器就會報錯:

>>> map.put("5","e")
error: type mismatch: inferred type is String but Int was expected
map.put("5","e")
        ^

Kotlin中有類型推斷的功能,有些類型參數能夠直接省略不寫。上面的mapOf後面的類型參數能夠省掉不寫:

>>> val map = mutableMapOf(1 to "a", 2 to "b", 3 to "c")
>>> map
{1=a, 2=b, 3=c}

Java和Kotlin 的泛型實現,都是採用了運行時類型擦除的方式。也就是說,在運行時,這些類型參數的信息將會被擦除。Java 和Kotlin 的泛型對於語法的約束是在編譯期。

6.2 型變(Variance)

6.2.1 Java的類型通配符

Java 泛型的通配符有兩種形式。咱們使用

  • 子類型上界限定符 ? extends T 指定類型參數的上限(該類型必須是類型T或者它的子類型)
  • 超類型下界限定符 ? super T 指定類型參數的下限(該類型必須是類型T或者它的父類型)

咱們稱之爲類型通配符(Type Wildcard)。默認的上界(若是沒有聲明)是 Any?,下界是Nothing。

代碼示例:

class Animal {

    public void act(List<? extends Animal> list) {
        for (Animal animal : list) {
            animal.eat();
        }
    }

    public void aboutShepherdDog(List<? super ShepherdDog> list) {
        System.out.println("About ShepherdDog");
    }

    public void eat() {
        System.out.println("Eating");
    }

}

class Dog extends Animal {}

class Cat extends Animal {}

class ShepherdDog extends Dog {}

咱們在方法act(List<? extends Animal> list)中, 這個list能夠傳入如下類型的參數:

List<Animal>
List<Dog>
List<ShepherdDog>
List<Cat>

測試代碼:

List<Animal> list3 = new ArrayList<>();
        list3.add(new Dog());
        list3.add(new Cat());
        animal.act(list3);

        List<Dog> list4 = new ArrayList<>();
        list4.add(new Dog());
        list4.add(new Dog());
        animal.act(list4);

        List<Cat> list5 = new ArrayList<>();
        list5.add(new Cat());
        list5.add(new Cat());
        animal.act(list5);

爲了更加簡單明瞭說明這些類型的層次關係,咱們圖示以下:

對象層次類圖:

集合類泛型層次類圖:

也就是說,List<Dog>並非List<Animal>的子類型,而是兩種不存在父子關係的類型。

List<? extends Animal>List<Animal>List<Dog>等的父類型,對於任何的 List<X>這裏的 X 只要是Animal的子類型,那麼List<? extends Animal>就是 List<X>的父類型。

使用通配符List<? extends Animal>的引用, 咱們不能夠往這個List中添加Animal類型以及其子類型的元素:

List<? extends Animal> list1 = new ArrayList<>();

        list1.add(new Dog());
        list1.add(new Animal());

這樣的寫法,Java編譯器是不容許的。

螢幕快照 2017-06-30 23.46.12.png

由於對於set方法,編譯器沒法知道具體的類型,因此會拒絕這個調用。可是,若是是get方法形式的調用,則是容許的:

List<? extends Animal> list1 = new ArrayList<>();
List<Dog> list4 = new ArrayList<>();
list4.add(new Dog());
list4.add(new Dog());
animal.act(list4);
list1 = list4;
animal.act(list1);

咱們這裏把引用變量List<? extends Animal> list1直接賦值List<Dog> list4, 由於編譯器知道能夠把返回對象轉換爲一個Animal類型。

相應的, ? super T超類型限定符的變量類型List<? super ShepherdDog>的層次結構以下:

螢幕快照 2017-06-30 23.56.35.png

在Java中,還有一個無界通配符,即單獨一個?。如List<?>?能夠表明任意類型,「任意」是未知類型。例如:

Pair<?>

參數替換後的Pair類有以下方法:

? getFirst()
void setFirst(?)

咱們能夠調用getFirst方法,由於編譯器能夠把返回值轉換爲Object。
可是不能調用setFirst方法,由於編譯器沒法肯定參數類型。

通配符在類型系統中具備重要的意義,它們爲一個泛型類所指定的類型集合提供了一個有用的類型範圍。泛型參數代表的是在類、接口、方法的建立中,要使用一個數據類型參數來表明未來可能會用到的一種具體的數據類型。它能夠是Integer類型,也能夠是String類型。咱們一般把它的類型定義成 E、T 、K 、V等等。

當咱們在實例化對象的時候,必須聲明T具體是一個什麼類型。因此當咱們把T定義成一個肯定的泛型數據類型,參數就只能是這種數據類型。此時,咱們就用到了通配符代替指定的泛型數據類型。

若是把一個對象分爲聲明、使用兩部分的話。泛型主要是側重於類型的聲明的代碼複用,通配符則側重於使用上的代碼複用。泛型用於定義內部數據類型的參數化,通配符則用於定義使用的對象類型的參數化。

使用泛型、通配符提升了代碼的複用性。同時對象的類型獲得了類型安全的檢查,減小了類型轉換過程當中的錯誤。

6.2.2 協變(covariant)與逆變(contravariant)

在Java中數組是協變的,下面的代碼是能夠正確編譯運行的:

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中,由於 Integer 是 Number 的子類型,數組類型 Integer[] 也是 Number[] 的子類型,所以在任何須要 Number[] 值的地方均可以提供一個 Integer[] 值。

而另外一方面,泛型不是協變的。也就是說, List<Integer> 不是 List<Number> 的子類型,試圖在要求 List<Number> 的位置提供 List<Integer> 是一個類型錯誤。下面的代碼,編譯器是會直接報錯的:

List<Integer> integerList = new ArrayList<>();
        integerList.add(0);
        integerList.add(1);
        integerList.add(2);
        List<Number> numberList = new ArrayList<>();
        numberList = integerList;

編譯器報錯提示以下:

螢幕快照 2017-07-01 00.59.16.png

Java中泛型和數組的不一樣行爲,的確引發了許多混亂。

就算咱們使用通配符,這樣寫:

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

仍然是報錯的:

螢幕快照 2017-07-01 01.03.54.png

爲何Number的對象能夠由Integer實例化,而ArrayList<Number>的對象卻不能由ArrayList<Integer>實例化?list中的<? extends Number>聲明其元素是Number或Number的派生類,爲何不能add Integer?爲了解決這些問題,須要瞭解Java中的逆變和協變以及泛型中通配符用法。

逆變與協變

Animal類型(簡記爲F, Father)是Dog類型(簡記爲C, Child)的父類型,咱們把這種父子類型關係簡記爲F <| C。

而List<Animal>, List<Dog>的類型,咱們分別簡記爲f(F), f(C)。

那麼咱們能夠這麼來描述協變和逆變:

當F <| C 時, 若是有f(F) <| f(C),那麼f叫作協變(Convariant);

當F <| C 時, 若是有f(C) <| f(F),那麼f叫作逆變(Contravariance)。
若是上面兩種關係都不成立則叫作不可變。

協變和逆協變都是類型安全的。

Java中泛型是不變的,可有時須要實現逆變與協變,怎麼辦呢?這時就須要使用咱們上面講的通配符?

<? extends T>實現了泛型的協變

List<? extends Number> list = new ArrayList<>();

這裏的? extends Number表示的是Number類或其子類,咱們簡記爲C。

這裏C <| Number,這個關係成立:List<C> <| List< Number >。即有:

List<? extends Number> list1 = new ArrayList<Integer>();  
List<? extends Number> list2 = new ArrayList<Float>();

可是這裏不能向list一、list2添加除null之外的任意對象。

list1.add(null);
        list2.add(null);

        list1.add(new Integer(1)); // error
        list2.add(new Float(1.1f)); // error

由於,List<Integer>能夠添加Interger及其子類,List<Float>能夠添加Float及其子類,List<Integer>、List<Float>都是List<? extends Number>的子類型,若是能將Float的子類添加到List<? extends Number>中,那麼也能將Integer的子類添加到List<? extends Number>中, 那麼這時候List<? extends Number>裏面將會持有各類Number子類型的對象(Byte,Integer,Float,Double等等)。Java爲了保護其類型一致,禁止向List<? extends Number>添加任意對象,不過能夠添加null。

螢幕快照 2017-07-01 01.25.30.png

<? super T>實現了泛型的逆變

List<? super Number> list = new ArrayList<>();

? super Number 通配符則表示的類型下界爲Number。即這裏的父類型F是? super Number, 子類型C是Number。即當F <| C , 有f(C) <| f(F) , 這就是逆變。代碼示例:

List<? super Number> list3 = new ArrayList<Number>();  
List<? super Number> list4 = new ArrayList<Object>();  
list3.add(new Integer(3));  
list4.add(new Integer(4));

也就是說,咱們不能往List<? super Number >中添加Number的任意父類對象。可是能夠向List<? super Number >添加Number及其子類對象。

PECS

如今問題來了:咱們何時用extends何時用super呢?《Effective Java》給出了答案:

PECS: producer-extends, consumer-super

好比,一個簡單的Stack API:

public class Stack<E>{  
    public Stack();  
    public void push(E e):  
    public E pop();  
    public boolean isEmpty();  
}

要實現pushAll(Iterable<E> src)方法,將src的元素逐一入棧:

public void pushAll(Iterable<E> src){  
    for(E e : src)  
        push(e)  
}

假設有一個實例化Stack<Number>的對象stack,src有Iterable<Integer>與 Iterable<Float>;

在調用pushAll方法時會發生type mismatch錯誤,由於Java中泛型是不可變的,Iterable<Integer>與 Iterable<Float>都不是Iterable<Number>的子類型。

所以,應改成

// Wildcard type for parameter that serves as an E producer  
public void pushAll(Iterable<? extends E> src) {  
    for (E e : src)   // out T, 從src中讀取數據,producer-extends
        push(e);  
}

要實現popAll(Collection<E> dst)方法,將Stack中的元素依次取出add到dst中,若是不用通配符實現:

// popAll method without wildcard type - deficient!  
public void popAll(Collection<E> dst) {  
    while (!isEmpty())  
        dst.add(pop());    
}

一樣地,假設有一個實例化Stack<Number>的對象stack,dst爲Collection<Object>;

調用popAll方法是會發生type mismatch錯誤,由於Collection<Object>不是Collection<Number>的子類型。

於是,應改成:

// Wildcard type for parameter that serves as an E consumer  
public void popAll(Collection<? super E> dst) {  
    while (!isEmpty())  
        dst.add(pop());   // in T, 向dst中寫入數據, consumer-super
}

Naftalin與Wadler將PECS稱爲 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, 寫入dest數據
        ListIterator<? extends T> si=src.listIterator();   // out T, 讀取src數據
        for (int i=0; i<srcSize; i++) {  
            di.next();  
            di.set(si.next());  
        }  
    }  
}

6.3 Kotlin的泛型特點

正如上文所講的,在 Java 泛型裏,有通配符這種東西,咱們要用 ? extends T 指定類型參數的上限,用 ? super T 指定類型參數的下限。

而Kotlin 拋棄了這個東西,引用了生產者和消費者的概念。也就是咱們前面講到的PECS。生產者就是咱們去讀取數據的對象,消費者則是咱們要寫入數據的對象。這兩個概念理解起來有點繞。

咱們用代碼示例簡單講解一下:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {  
        ...
        ListIterator<? super T> di=dest.listIterator();   // in T, 寫入dest數據
        ListIterator<? extends T> si=src.listIterator();   // out T, 讀取src數據
         ...
}

List<? super T> dest是消費數據的對象,這些數據會寫入到該對象中,這些數據該對象被「吃掉」了(Kotlin中叫in T)。

List<? extends T> src是生產提供數據的對象。這些數據哪裏來的呢?就是經過src讀取得到的(Kotlin中叫out T)。

6.3.1 out T in T

在Kotlin中,咱們把那些只能保證讀取數據時類型安全的對象叫作生產者,用 out T 標記;把那些只能保證寫入數據安全時類型安全的對象叫作消費者,用 in T 標記。

若是你以爲太晦澀難懂,就這麼記吧:

out T 等價於 ? extends T

in T 等價於 ? super T
此外, 還有 * 等價於 ?

6.3.2 聲明處型變

Kotlin 泛型中添加了聲明處型變。看下面的例子:

interface Source<out T> {
    fun <T> nextT();
}

咱們在接口的聲明處用 out T 作了生產者聲明以實現安全的類型協變:

fun demo(str: Source<String>) {
    val obj: Source<Any> = str // 合法的類型協變
}

Kotlin 中有大量的聲明處協變,好比 Iterable 接口的聲明:

public interface Iterable<out T> {
    public operator fun iterator(): Iterator<T>
}

由於 Collection 接口和 Map 接口都繼承了 Iterable 接口,而 Iterable 接口被聲明爲生產者接口,因此全部的 Collection 和 Map 對象均可以實現安全的類型協變:

val c: List<Number> = listOf(1, 2, 3)

這裏的 listOf() 函數返回 List<Int> 類型,由於 List 接口實現了安全的類型協變,因此能夠安全地把 List<Int> 類型賦給 List<Number> 類型變量。

6.3.3 類型投影

將類型參數 T 聲明爲 out 很是方便,而且能避免使用處子類型化的麻煩,可是有些類實際上不能限制爲只返回 T

一個很好的例子是 Array:

class Array<T>(val size: Int) {
    fun get(index: Int): T {  }
    fun set(index: Int, value: T) {  }
}

該類在 T 上既不能是協變的也不能是逆變的。這形成了一些不靈活性。考慮下述函數:

fun copy(from: Array<Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

這個函數應該將項目從一個數組複製到另外一個數組。若是咱們採用以下方式使用這個函數:

val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" } 
copy(ints, any) // 錯誤:指望 (Array<Any>, Array<Any>)

這裏咱們將遇到一樣的問題:Array <T>T 上是不型變的,所以 Array <Int>Array <Any> 都不是另外一個的子類型。

那麼,咱們惟一要確保的是 copy() 不會作任何壞事。咱們阻止它寫到 from,咱們能夠:

fun copy(from: Array<out Any>, to: Array<Any>) {}

如今這個from是一個受Array<out Any>限制的(投影的)數組。在Kotlin中,稱爲類型投影(type projection)。其主要做用是參數做限定,避免不安全操做。

相似的,咱們也可使用 in 投影一個類型:

fun fill(dest: Array<in String>, value: String) {}

Array<in String> 對應於 Java 的 Array<? super String>,也就是說,咱們能夠傳遞一個 CharSequence 數組或一個 Object 數組給 fill() 函數。

相似Java中的無界類型通配符?, Kotlin 也有對應的星投影語法*

例如,若是類型被聲明爲 interface Function <in T, out U>,咱們有如下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

*投影跟 Java 的原始類型相似,不過是安全的。

6.6 泛型類

聲明一個泛型類

class Box<T>(t: T) {
    var value = t
}

一般, 要建立這樣一個類的實例, 咱們須要指定類型參數:

val box: Box<Int> = Box<Int>(1)

可是, 若是類型參數能夠經過推斷獲得, 好比, 經過構造器參數類型, 或經過其餘手段推斷獲得, 此時容許省略類型參數:

val box = Box(1) // 1 的類型爲 Int, 所以編譯器知道咱們建立的實例是 Box<Int> 類型

6.5 泛型函數

類能夠有類型參數。函數也有。類型參數要放在函數名稱以前:

fun <T> singletonList(item: T): List<T> {}
fun <T> T.basicToString() : String {  // 擴展函數
}

要調用泛型函數,在函數名後指定類型參數便可:

val l = singletonList<Int>(1)

泛型函數與其所在的類是不是泛型沒有關係。泛型函數獨立於其所在的類。咱們應該儘可能使用泛型方法,也就是說若是使用泛型方法能夠取代將整個類泛型化,那麼就應該只使用泛型方法,由於它可使事情更明白。

本章小結

泛型是一個很是有用的東西。尤爲在集合類中。咱們能夠發現大量的泛型代碼。

本章咱們經過對Java泛型的回顧,對比介紹了Kotlin泛型的特點功能,尤爲是協變、逆變、inout等概念,須要咱們深刻去理解。只有深刻理解了這些概念,咱們才能更好理解並用好Kotlin的集合類,進而寫出高質量的泛型代碼。

泛型實現是依賴OOP中的類型多態機制的。Kotlin是一門支持面向對象編程(OOP)跟函數式編程(FP)強大的語言。咱們已經學習了Kotlin的語言基礎知識、類型系統、集合類、泛型等相關知識了,相信您已經對Kotlin有了一個初步的瞭解。

在下一章節中,咱們將一塊兒來學習Kotlin的面向對象編程相關的知識。

本章示例代碼工程:

https://github.com/EasyKotlin...

相關文章
相關標籤/搜索