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

程式設計師詩人發表於2017-06-16

第6章 泛型


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

點選這裡 > 去京東商城購買閱讀

點選這裡 > 去天貓商城購買閱讀

非常感謝您親愛的讀者,大家請多支援!!!有任何問題,歡迎隨時與我交流~


6.1 泛型(Generic Type)簡介

通常情況的類和函式,我們只需要使用具體的型別即可:要麼是基本型別,要麼是自定義的類。

但是尤其在集合類的場景下,我們需要編寫可以應用於多種型別的程式碼,我們最簡單原始的做法是,針對每一種型別,寫一套刻板的程式碼。

這樣做,程式碼複用率會很低,抽象也沒有做好。

在SE 5種,Java引用了泛型。泛型,即“引數化型別”(Parameterized Type)。顧名思義,就是將型別由原來的具體的型別引數化,類似於方法中的變數引數,此時型別也定義成引數形式,我們稱之為型別引數,然後在使用時傳入具體的型別(型別實參)。

我們知道,在數學中泛函是以函式為自變數的函式。類比的來理解,程式設計中的泛型就是以型別為變數的型別,即引數化型別。這樣的變數引數就叫型別引數(Type Parameters)。

本章我們來一起學習一下Kotlin泛型的相關知識。

6.1.1 為什麼要有型別引數

我們先來看下沒有泛型之前,我們的集合類是怎樣持有物件的。

在Java中,Object類是所有類的根類。為了集合類的通用性。我們把元素的型別定義為Object,當放入具體的型別的時候,再作強制型別轉換。

這是一個示例程式碼:

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

但是這裡不能向list1、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 Tin 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/chapter6_generics


相關文章