Kotlin中的泛型

博客地址:sguotao.top/Kotlin-2018…html

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

在JDK1.5以前,生產環境中老是會出現這樣相似的問題:c#

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);
複製代碼

除了解決數據類型安全問題,泛型的引入也更多的使用到設計模式當中。泛型的本質就是讓類型也變成參數。好比定義函數時聲明形參,在調用函數時傳入實參。類型的參數化也一樣,定義函數或類時聲明成泛型(泛型形參),在調用或實例化時傳入具體的類型(泛型實參)。less

Java中的泛型

Java中泛型能夠用在類、接口和方法中,分別稱爲泛型類、泛型接口和泛型方法。下面分別來看一下。dom

泛型類

泛型應用在類的聲明中,稱爲泛型類,其格式以下:ide

[訪問權限] 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
相關文章
相關標籤/搜索