Kotlin中的泛型

ScottSong發表於2018-11-08

部落格地址:sguotao.top/Kotlin-2018…

一個生產環境問題引發的思考。

在JDK1.5之前,生產環境中總是會出現這樣類似的問題:

List list = new ArrayList ();
list.add ("foo");
list.add (new Integer (42));  // added by "mistake"

for (Iterator i = list.iterator (); i.hasNext (); ) {
    String s = (String) i.next (); 
       // ClassCastException for Integer -> String
    // work on `s'
    System.out.println (s);
}
複製程式碼

由於add()接受Object型別,可能在開發除錯階段測試資料使用的都是String型別,直到上線到生產環境中,某次傳入了一個Integer型別資料,於是系統崩潰了……

為了解決此類的資料型別安全問題,Java在JDK1.5中提出了泛型,於是問題提早的在開發階段暴露出來:

// constructed generic type
List<String> list = new ArrayList<String> ();
list.add ("foo");
list.add (42); // error: cannot find symbol: method add(int)
for (String s : list)
    System.out.println (s);
複製程式碼

除了解決資料型別安全問題,泛型的引入也更多的使用到設計模式當中。泛型的本質就是讓型別也變成引數。比如定義函式時宣告形參,在呼叫函式時傳入實參。型別的引數化也同樣,定義函式或類時宣告成泛型(泛型形參),在呼叫或例項化時傳入具體的型別(泛型實參)。

Java中的泛型

Java中泛型可以用在類、介面和方法中,分別稱為泛型類、泛型介面和泛型方法。下面分別來看一下。

泛型類

泛型應用在類的宣告中,稱為泛型類,其格式如下:

[訪問許可權] class 類名 <泛型,泛型……>{
    ……
}
複製程式碼

比如定義如下泛型類:

class CustomGenerics<V> { //泛型形參,常見的泛型形參標識如T、E、K、V等
    private V value; //成員變數的型別為V,V是在例項化是外部傳入的。

    CustomGenerics(V value) {
        this.value = value;
    }

    public V getValue() {
        return value;
    }
}
複製程式碼

型別建立的格式如下:

類名<具體型別> 物件名稱 = new 類名<具體型別>()
複製程式碼

比如例項化上面定義的泛型類。

public class TestGenerics {
    //在例項化泛型類時,指定泛型實參
    CustomGenerics<String> cStr = new CustomGenerics<String>("sguotao");
    CustomGenerics<Integer> cInt = new CustomGenerics<Integer>(9456);
}
複製程式碼

總結一下泛型類中的一些注意事項:

  1. 在例項化泛型類時,要指定泛型實參。(即需要指定具體型別)
  2. 指定的泛型實參型別只能是類型別,不能是基本資料型別。(不能是int,long,可以是Integer,Long等)

泛型介面

宣告泛型介面的格式與宣告泛型類相似:

interface 介面名<泛型,泛型……>{
 ……
}
複製程式碼

比如定義如下泛型介面。

interface CustomGenericsInterface<T> {
    public T generate();
}
複製程式碼

在實現泛型介面的類中,指定泛型實參。

class CustomImpl implements CustomGenericsInterface<String> {
    public String generate() {
        return "hello";
    }
}
複製程式碼

泛型方法

宣告泛型方法的格式:

[訪問許可權] <泛型> 返回值型別 方法名( 泛型 引數名)
複製程式碼

比如上面的泛型類中定義如下的泛型方法:

class CustomGenerics<V> { //泛型形參,常見的泛型形參標識如T、E、K、V等
    private V value; //成員變數的型別為V,V是在例項化是外部傳入的。

    CustomGenerics(V value) {
        this.value = value;
    }

    public V getValue() {
        return value;
    }

    //泛型方法
    public <T> V genericMethod(T t1, V v1) { //泛型方法需要在返回值型別前有<泛型>的標記
        //泛型方法中可以使用泛型方法宣告的泛型,也可以使用泛型類宣告的泛型
        return value;
    }

    //泛型方法
    public <V> void genericMethod(V v1) {//泛型方法中宣告的泛型引數V與泛型類中宣告的泛型T不是同一個
    }
    
    //泛型方法
    public static <T> void genericStaticMethod(T t1) {//靜態的泛型方法無法使用泛型類中宣告的泛型

    }
}
複製程式碼

總結一下泛型方法中的一些注意事項:

  1. 判斷一個方法是否為泛型方法最直接的方式,看方法返回值前是否有<泛型>的標記;
  2. 在泛型類中宣告的泛型方法,即可以使用泛型類中宣告的泛型,也可以使用泛型方法中宣告的泛型;比如上面示例中的泛型方法genericMethod(T t1, V v1) 。
  3. 如果泛型方法中宣告的泛型引數與泛型類中的泛型引數相同,那麼可以認為泛型方法中的泛型引數覆蓋了泛型類中的泛型引數,比如上面示例中的genericMethod(V v1)。
  4. 還有一點需要指出,泛型類中的使用了泛型引數,但是在返回值前沒有<泛型>標記的方法,不是泛型方法,比如上面示例中的getValue(),該方法只是使用了泛型引數,並不是泛型方法。
  5. 如果泛型方法是靜態方法,那麼此時泛型方法是無法使用泛型類中宣告的泛型,比如上面示例中的泛型方法genericStaticMethod(T t1),該方法只能使用泛型方法中的泛型T,無法使用泛型類中的泛型V。

泛型擦除

Java中的泛型是偽泛型,要了解偽泛型,先來了解什麼是真泛型?在C#中使用的泛型,就是真泛型。如在C#中定義泛型:

 //泛型類
public class GenericClass<T>
 {
    T _t;
     public GenericClass(T t)
    {
        _t = t;
    }
    public override string ToString()
    {
        return _t.ToString();
    }
}
    
public class Program
{
    static void Main(string[] args)
    {
        GenericClass<int> gInt = new GenericClass<int>(123456); 
        Console.WriteLine(gInt.GetType());
        Console.WriteLine(gInt.ToString());

        GenericClass<string> gStr = new GenericClass<string>("Test");
        Console.WriteLine(gStr.GetType());
        Console.WriteLine(gStr.ToString());

        Console.Read(); 
     }
}
複製程式碼

檢視輸出結果:

20180903153594145422523.png
檢視IL發現:
20180903153594152962628.png
而Java中的泛型只存在於編譯期,在生成的位元組碼檔案中是不包含任何泛型資訊的。比如下面的兩個方法,在位元組碼中具有相同的函式簽名。
20180903153594325756741.png
使用泛型的時候加上的型別引數,會在編譯器在編譯的時候去掉,這個過程就稱為型別擦除。

在C#裡面泛型無論在程式原始碼中、編譯後的IL中或是執行期的CLR中都是切實存在的,List與List就是兩個不同的型別,它們在系統執行期生成,有自己的虛方法表和型別資料。

在Java語言中的泛型則不一樣,它只在程式原始碼中存在,在編譯後的位元組碼檔案中,就已經被替換為原來的原始型別(Raw Type)了,並且在相應的地方插入了強制轉型程式碼,因此對於執行期的Java語言來說,ArrayList與ArrayList就是同一個型別。

萬用字元?及型變

是否有這樣的疑問,為什麼Number的物件可以由Integer例項化,而ArrayList的物件卻不能由ArrayList例項化?先來看下面的程式碼:

Number num = new Integer(1);  
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch
複製程式碼

Integer是Number的子類,所以Number物件可以由Integer例項化,這是Java多型的特性,那麼Integer是Number的子類,List是不是List 的父類呢?答案是否定的。List和List沒有繼承關係,看似需要宣告多個方法來接收這些不同的泛型實參了,這顯然與Java的多型性是相背離的。

這時,就需要一個在邏輯上可以用來表示同時是List和List 的父類的一個引用型別,由此,型別萬用字元應運而生。Java中的萬用字元用?來表示,萬用字元?可以認為是任意型別的父類,它是一個具體的型別,是泛型實參,這裡需要注意與泛型形參T、V的區別。

萬用字元的引入不只是解決了泛型實參之間的邏輯關係,更重要的一點,對泛型引入了邊界的概念。

萬用字元?的上界

萬用字元的上界使用<? extends T>的格式,表示類或者方法接收T或者T的子型別,比如:

List<? extends Number> list = new ArrayList<Number>();
複製程式碼

萬用字元?的上界,又可以稱為協變。

萬用字元?的下界

萬用字元的下界使用<? super T>的格式,表示類或者方法接收T或者T的父型別,比如:

 List<? super Integer> list = new ArrayList<Number>();
複製程式碼

萬用字元?的下界,又稱為逆變。關於逆變和協變,下面詳細介紹:

協變和逆變

Java中的泛型是既不支援協變,也不支援逆變。那什麼是逆變,什麼是協變?簡單的說,協變就是定義了型別的上邊界,而逆變則定義了型別的下邊界。看一個協變的例子:

ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch
List<? extends Number> list = new ArrayList<Number>();
複製程式碼

? extends Number的含義是:接收Number的子類,也包括Number,作為泛型實參。再來看逆變。

在Java中是不能將父類的例項賦值給子類的變數,但是在泛型中可以通過萬用字元?來模擬逆變,比如:

List<? super Integer> list = new ArrayList<Number>();
複製程式碼

? super Integer的含義是:接收Integer的基類,也包括Integer本身作為泛型實參。

協變與逆變的數學定義:

逆變與協變用來描述型別轉換(type transformation)後的繼承關係,其定義:如果A、B表示型別,f(⋅)表示型別轉換,≤表示繼承關係(比如,A≤B表示A是由B派生出來的子類);

  • f(⋅)是逆變(contravariant)的,當A≤B時有f(B)≤f(A)成立;
  • f(⋅)是協變(covariant)的,當A≤B時有f(A)≤f(B)成立;
  • f(⋅)是不變(invariant)的,當A≤B時上述兩個式子均不成立,即f(A)與f(B)相互之間沒有繼承關係。

什麼時候使用協變和逆變

什麼時候使用協變?extends,什麼時候使用逆變 ?super,在《Effective Java》中給出了一個PECS原則:

PECS:Producer extends,Customer super

當使用泛型類作為生產者,需要從泛型類中取資料時,使用extends,此時泛型類是協變的; 當使用泛型類作為消費者,需要往泛型類中寫資料時,使用suepr,此時泛型類是逆變的。 一個經典的案例就是Collections中的copy方法。

    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();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }
複製程式碼

copy方法實現從源src到目的dest的複製,源src可以看作是生產者,使用協變,目的dest可以看作是消費者,使用逆變。

Kotlin中的泛型

前面用了大量的篇幅來介紹Java中的泛型,其實瞭解了Java中的泛型,就會使用Kotlin中的泛型,區別僅僅是寫法和關鍵字上的區別。

Kotlin中的泛型方法,比如:

class GenericKotlin<T>(var value: T) {//宣告泛型類

    //宣告泛型方法
    public fun <V> genericMethod(t: T, v: V): Unit {
    }
}

//宣告泛型介面
interface GenericKotlinInterface<T> {
    public fun generate(): T
}
複製程式碼

Kotlin中的型變

先來看一下Kotlin中的型變:

fun main(args: Array<String>) {
    //協變
    val list: List<Number> = listOf(1, 2, 3, 4)
    //逆變
    val comparable: Comparable<Int> = object : Comparable<Any> {
        override fun compareTo(other: Any): Int {
            TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
        }
    }
}
複製程式碼

我們看一下List的宣告原始碼:

public interface List<out E> : Collection<E> {
   ……
    public operator fun get(index: Int): E

    // Search Operations
    /**
     * Returns the index of the first occurrence of the specified element in the list, or -1 if the specified
     * element is not contained in the list.
     */
    public fun indexOf(element: @UnsafeVariance E): Int
複製程式碼

Kotlin中的協變不再是? extends,而是使用out關鍵字,語義更加貼切,生產者生產產品使用out。泛型既可以作為函式的引數,也可以作為函式的返回值。當泛型作為函式的返回值時,稱為協變點,當泛型作為函式引數時,稱為逆變點。

再來看List的原始碼,這裡的List是隻讀的List,使用out關鍵字修飾泛型,這裡將泛型E作為協變來使用,也就是當做函式的返回值。但是原始碼中也將E作為函式的引數使用,即當做逆變來使用,由於函式(比如indexOf)並不會修改List,所以加註解@UnsafeVariance來修飾。

再來看Comparable的原始碼:

public interface Comparable<in T> {
    /**
     * Compares this object with the specified object for order. Returns zero if this object is equal
     * to the specified [other] object, a negative number if it's less than [other], or a positive number
     * if it's greater than [other].
     */
    public operator fun compareTo(other: T): Int
}
複製程式碼

Kotlin中的逆變也不再是? super,而是使用關鍵字in,消費者消費產品使用in。Comparable中的泛型被宣告為逆變,也就說Comparable中泛型T被當做函式的引數。

最後總結一下,泛型既可以作為函式的返回值,也可以作為函式的引數。當作為函式的返回值時,泛型是協變的,使用out修飾;當作為函式的引數時,泛型是逆變的,使用in修飾。

  1. 在泛型形參前面加上out關鍵字,表示泛型的協變,作為返回值,為只讀型別,泛型引數的繼承關係與類的繼承關係保持一致,比如List和List;
  2. 在泛型引數前面加上in表示逆變,表示泛型的逆變,作為函式的引數,為只寫型別,泛型引數的繼承關係與類的繼承關係相反,比如Comparable和Comparable。

星投影

Kotlin中的星投影,用符號來表示,作用類似於Java中的萬用字元?,比如當不確認泛型型別時,可以使用來代替。需要注意的時,*只能出現在泛型形參的位置,不能作為在泛型實參。

 //星投影
val list: MutableList<*> = ArrayList<Number>()
複製程式碼

參考連結

  1. www.jprl.com/Blog/archiv…
  2. Kotlin Bootcamp for Programmers
  3. Kotlin Koans

相關文章