"Kotlin"系列: 二、Kotlin泛型

island-5783440_1920

前言

很高興遇見你~

在本系列的上一篇中为鳄,我們學(xué)習(xí)了 Kotlin 大部分知識點裳仆,體驗到了 Kotlin 語法的便捷,強大孤钦,以及高效的函數(shù)式編程歧斟。還沒有看過上一篇文章的朋友,建議先去閱讀 "Kotlin"系列: 一偏形、Kotlin入門静袖,接下來我們就進入 Kotlin 泛型的學(xué)習(xí),泛型在我看來是比較復(fù)雜的俊扭,同時也是面試中經(jīng)常問的队橙,很長一段時間,我對泛型的認識比較模糊,那么在使用的時候就更加疑惑了捐康,因此這篇文章希望能帶大家攻克這一知識點

問題

下面我拋出一系列問題仇矾,咱們帶著問題去學(xué)習(xí):

1、什么是泛型解总?

2贮匕、泛型有什么作用?

3倾鲫、怎么去定義和使用泛型粗合?

1、什么是泛型乌昔?

泛型通俗的理解就是:很多的類型隙疚,它通過使用參數(shù)化類型的概念,允許我們在不指定具體類型的情況下進行編程

2磕道、泛型有什么作用供屉?

泛型是 JDK 1.5 引入的安全機制,是一種給編譯器使用的技術(shù):

1溺蕉、提高了代碼的可重用性

2伶丐、將運行期的類型轉(zhuǎn)換異常提前到了編譯期,保證類型的安全疯特,避免類型轉(zhuǎn)換異常

3哗魂、怎么去定義和使用泛型?

我們可以給一個類漓雅,方法录别,或者接口指定泛型,在具體使用的地方指定具體的類型

一邻吞、Java 泛型

要學(xué)習(xí)好 Kotlin 泛型组题,我們先要對 Java 泛型足夠的了解,因為 Kotlin 泛型和 Java 泛型基本上是一樣的抱冷,只不過在 Kotlin 上有些東西換了新的寫法

1崔列、泛型的簡單使用

在 Java 中,我們可以給一個類旺遮,方法赵讯,或者接口指定泛型,在具體使用的地方指定具體的類型

1)耿眉、定義一個泛型類瘦癌,在類名的后面加上 <T> 這種語法結(jié)構(gòu)就是定義一個泛型類,泛型可以有任意多個

//定義一個泛型類
public class JavaGenericClass<T> {

    private T a;

    public JavaGenericClass(T a) {
        this.a = a;
    }

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }

    //泛型類使用
    public static void main(String[] args) {
        //編譯器可推斷泛型類型跷敬,因此 new 對象后面的泛型類型可省略
        JavaGenericClass<String> javaGenericClass1 = new JavaGenericClass<String>("erdai");
        JavaGenericClass<Integer> javaGenericClass2 = new JavaGenericClass<>(666);
        System.out.println(javaGenericClass1.getA());
        System.out.println(javaGenericClass2.getA());
    }
}

//打印結(jié)果
erdai
666

2)讯私、定義一個泛型方法热押,在方法的返回值前面加上 <T> 這種語法結(jié)構(gòu)就是定義一個泛型方法,泛型可以有任意多個斤寇,泛型方法的泛型與它所在的類沒有任何關(guān)系

public class JavaGenericMethod {

    public <T> void getName(T t){
        System.out.println(t.getClass().getSimpleName());
    }

    public static void main(String[] args) {
        JavaGenericMethod javaGenericMethod = new JavaGenericMethod();
        //編譯器可推斷出泛型類型桶癣,因此這里的泛型類型也可省略
        javaGenericMethod.<String>getName("erdai666");
    }
}

//打印結(jié)果
String

3)、定義一個泛型接口

在接口名的后面加上 <T> 這種語法結(jié)構(gòu)就是定義一個泛型接口娘锁,泛型可以有任意多個

public interface JavaGenericInterface<T> {

    T get();
}

class TestClass<T> implements JavaGenericInterface<T>{

    private final T t;

    public TestClass(T t) {
        this.t = t;
    }

    @Override
    public T get() {
        return t;
    }
}

class Client{

    public static void main(String[] args) {
        JavaGenericInterface<String> javaGenericInterface = new TestClass<>("erdai666");
        System.out.println(javaGenericInterface.get());
    }
}
//打印結(jié)果
erdai666

2牙寞、泛型擦除

1、泛型擦除是什么莫秆?

看下面這段代碼:

//使用了不同的泛型類型 結(jié)果得到了相同的數(shù)據(jù)類型
public class JavaGenericWipe {
  
    public static void main(String[] args) {
        Class a = new ArrayList<String>().getClass();
        Class b = new ArrayList<Integer>().getClass();

        System.out.println("a = " + a);
        System.out.println("b = " + b);
        System.out.println("a == b: " + (a == b));
    }
}

//打印結(jié)果
a = class java.util.ArrayList
b = class java.util.ArrayList
a == b: true

為啥會出現(xiàn)這種情況呢间雀?

因為 Java 中的泛型是使用擦除技術(shù)來實現(xiàn)的:泛型擦除是指通過類型參數(shù)合并,將泛型類型實例關(guān)聯(lián)到同一份字節(jié)碼上镊屎。編譯器只為泛型類型生成一份字節(jié)碼惹挟,并將其實例關(guān)聯(lián)到這份字節(jié)碼上

之所以要使用泛型擦除是為了兼容 JDK 1.5 之前運行時的類加載器,避免因為引入泛型而導(dǎo)致運行時創(chuàng)建不必要的類

2缝驳、泛型擦除的具體步驟

1)连锯、擦除所有類型參數(shù)信息,如果類型參數(shù)是有界的用狱,則將每個參數(shù)替換為其第一個邊界运怖;如果類型參數(shù)是無界的,則將其替換為 Object類型擦除的規(guī)則:

<T> 擦除后變?yōu)?Object

<T extends A> 擦除后變?yōu)?A

<? extends A> 擦除后變?yōu)?A

<? super A> 擦除后變?yōu)镺bject

2)夏伊、(必要時)插入類型轉(zhuǎn)換摇展,以保持類型安全

3)、(必要時)生成橋接方法以在子類中保留多態(tài)性

//情況1: 擦除所有類型參數(shù)信息溺忧,如果類型參數(shù)是有界的咏连,則將每個參數(shù)替換為其第一個邊界;如果類型參數(shù)是無界的砸狞,則將其替換為 Object
class Paint {
    void draw() {
        System.out.println("Paint.draw() called");
    }
}

//如果不給 T 設(shè)置邊界捻勉,那么 work 方法里面的 t 就調(diào)用不到 draw 方法
class Painter<T extends Paint> {
    private T t;

    public Painter(T t) {
        this.t = t;
    }
    public void work() {
        t.draw();
    }
}

//情況2:(必要時)插入類型轉(zhuǎn)換镀梭,以保持類型安全
public class JavaGenericWipe {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("erdai");
        stringList.add("666");

        for (String s : stringList) {
            System.out.println(s);
        }
    }
}

//編譯時生成的字節(jié)碼文件翻譯過來大致如下
public class JavaGenericWipe {
    public JavaGenericWipe() {
    }

    public static void main(String[] args) {
        List<String> stringList = new ArrayList();
        stringList.add("erdai");
        stringList.add("666");
        Iterator var2 = stringList.iterator();

        while(var2.hasNext()) {
            //編譯器給我們做了強轉(zhuǎn)的工作
            String s = (String)var2.next();
            System.out.println(s);
        }
    }
}

//情況3 (必要時)生成橋接方法以在子類中保留多態(tài)性
class Node {
    public Object data;

    public Node(Object data) {
        this.data = data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

class MyNode extends Node {

    public MyNode(Integer data) {
        super(data);
    }

    public void setData(Integer data) {
        super.setData(data);
    }
}

//編譯時生成的字節(jié)碼文件翻譯過來大致如下
class MyNode extends Node {

    public MyNode(Integer data) {
        super(data);
    }
    // 編譯器生成的橋接方法
    public void setData(Object data) {
        setData((Integer) data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}

3刀森、偽泛型

Java 中的泛型是一種特殊的語法糖,通過類型擦除實現(xiàn)报账,這種泛型稱為偽泛型研底,我們可以反射繞過編譯器泛型檢查,添加一個不同類型的參數(shù)

//反射繞過編譯器檢查
public static void main(String[] args) {
    
     List<String> stringList = new ArrayList<>();
     stringList.add("erdai");
     stringList.add("666");

     //使用反射增加一個新的元素
     Class<? extends List> aClass = stringList.getClass();
     try {
         Method method = aClass.getMethod("add", Object.class);
         method.invoke(stringList,123);
     } catch (Exception e) {
         e.printStackTrace();
     }

     Iterator iterator = stringList.iterator();
     while (iterator.hasNext()){
         System.out.println(iterator.next());
     }
}
//打印結(jié)果
erdai
666
123

4透罢、泛型擦除進階

下面我拋出一個在工作中經(jīng)常會遇到的問題:

在進行網(wǎng)絡(luò)請求的時候榜晦,傳入一個泛型的實際類型,為啥能夠正確的獲取到該泛型類型羽圃,并利用 Gson 轉(zhuǎn)換為實際的對象乾胶?

答:是因為在運行期我們可以使用反射獲取到具體的泛型類型

What? 泛型不是在編譯的時候被擦除了嗎?為啥在運行時還能夠獲取到具體的泛型類型????

答:泛型中所謂的類型擦除识窿,其實只是擦除 Code 屬性中的泛型信息斩郎,在類常量池屬性(Signature 屬性、LocalVariableTypeTable 屬性)中其實還保留著泛型信息喻频,而類常量池中的屬性可以被 class 文件缩宜,字段表,方法表等攜帶甥温,這就使得我們聲明的泛型信息得以保留锻煌,這也是我們在運行時可以反射獲取泛型信息的根本依據(jù)

//這是反編譯后的 JavaGenericClass.class 文件,可以看到 T
public class JavaGenericClass<T> {

    private T a;

    public JavaGenericClass(T a) {
        this.a = a;
    }

    public T getA() {
        return a;
    }

    public void setA(T a) {
        this.a = a;
    }
  
  //...
}

注意:Java 是在 JDK 1.5 引入的泛型姻蚓,為了彌補泛型擦除的不足宋梧,JVM 的 class 文件也做了相應(yīng)的修改,其中最重要的就是新增了 Signature 屬性表和 LocalVariableTypeTable 屬性表

我們看下下面這段代碼:

class ParentGeneric<T> {

}

class SubClass extends ParentGeneric<String>{

}

class SubClass2<T> extends ParentGeneric<T> {

}

public class GenericGet {

    //獲取實際的泛型類型
    public static <T> Type findGenericType(Class<T> cls) {
        Type genType = cls.getGenericSuperclass();
        Type finalNeedType = null;
        if (genType instanceof ParameterizedType) {
            Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
            finalNeedType = params[0];
        }
        return finalNeedType;
    }
  
    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        SubClass2<Integer> subClass2 = new SubClass2<Integer>();
        //打印 subClass 獲取的泛型
        System.out.println("subClass: " + findNeedClass(subClass.getClass()));
        //打印subClass2獲取的泛型
        System.out.println("subClass2: " + findGenericType(subClass2.getClass()));
    }
}

//運行這段代碼 打印結(jié)果如下
subClass: class java.lang.String
subClass2: T

上面代碼:

1史简、 SubClass 相當(dāng)于對 ParentGeneric<T> 做了賦值操作 T = String乃秀,我們通過反射獲取到了泛型類型為 String

2、SubClass2 對 ParentGeneric<T>沒有做賦值操作 圆兵,我們通過反射獲取到了泛型類型為 T

這里大家肯定會有很多疑問跺讯?

1、為啥 1 中沒有傳入任何泛型的信息卻能獲取到泛型類型呢殉农?

2刀脏、為啥 2 中我創(chuàng)建對象的時候傳入的泛型是 Integer ,獲取的時候變成了 T 呢超凳?

現(xiàn)在我們來仔細分析一波:

上面我講過愈污,類型擦除其實只是擦除 Code 屬性中的泛型信息,在類常量池屬性中還保留著泛型信息轮傍,因此上面的 SubClass 和SubClass2 在編譯的時候其實會保留各自的泛型到字節(jié)碼文件中暂雹,一個是 String,一個是 T 创夜。而 subClass 和 subClass2 是運行時動態(tài)創(chuàng)建的杭跪,這個時候你即使傳入了泛型類型,也會被擦除掉驰吓,因此才會出現(xiàn)上面的結(jié)果涧尿,到這里,大家是否明了了呢檬贰?

如果還有點模糊姑廉,我們再來看一個例子:

class ParentGeneric<T> {

}

public class GenericGet {
    //獲取實際的泛型類型
    public static <T> Type findGenericType(Class<T> cls) {
       Type genType = cls.getGenericSuperclass();
        Type finalNeedType = null;
        if (genType instanceof ParameterizedType) {
            Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
            finalNeedType = params[0];
        }
        return finalNeedType;
    }

    public static void main(String[] args) {
        ParentGeneric<String> parentGeneric1 = new ParentGeneric<String>();
        ParentGeneric<String> parentGeneric2 = new ParentGeneric<String>(){};

        //打印 parentGeneric1 獲取的泛型
        System.out.println("parentGeneric1: " + findGenericType(parentGeneric1.getClass()));
        //打印 parentGeneric2 獲取的泛型
        System.out.println("parentGeneric2: " + findGenericType(parentGeneric2.getClass()));

    }
}
//運行這段代碼 打印結(jié)果如下
parentGeneric1: null
parentGeneric2: class java.lang.String

上述代碼 parentGeneric1 和 parentGeneric2 唯一的區(qū)別就是多了 {},獲取的結(jié)果卻截然不同翁涤,我們在來仔細分析一波:

1桥言、 ParentGeneric 聲明的泛型 T 在編譯的時候其實是保留在了字節(jié)碼文件中萌踱,parentGeneric1 是在運行時創(chuàng)建的,由于泛型擦除号阿,我們無法通過反射獲取其中的類型虫蝶,因此打印了 null

這個地方可能大家又會有個疑問了,你既然保留了泛型類型為 T倦西,那么我獲取的時候應(yīng)該為 T 才是能真,為啥打印的結(jié)果是 null 呢?

如果你心里有這個疑問扰柠,說明你思考的非常細致粉铐,要理解這個問題,我們首先要對 Java 類型(Type)系統(tǒng)有一定的了解卤档,這其實和我上面寫的那個獲取泛型類型的方法有關(guān):

//獲取實際的泛型類型
public static <T> Type findGenericType(Class<T> cls) {
    //獲取當(dāng)前帶有泛型的父類
    Type genType = cls.getGenericSuperclass();
    Type finalNeedType = null;
    //如果當(dāng)前 genType 是參數(shù)化類型則進入到條件體
    if (genType instanceof ParameterizedType) {
        //獲取參數(shù)類型 <> 里面的那些值,例如 Map<K,V> 那么就得到 [K,V]的一個數(shù)組
        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
        //將第一個泛型類型賦值給 finalNeedType
        finalNeedType = params[0];
    }
    return finalNeedType;
}

上述代碼我們需要先獲取這個類的泛型父類蝙泼,如果是參數(shù)化類型則進入到條件體,獲取實際的泛型類型并返回劝枣。如果不是則直接返回 finalNeedType , 那么這個時候就為 null 了

在例1中:

SubClass1 subClass1 = new SubClass1();
SubClass2<Integer> subClass2 = new SubClass2<>();
System.out.println(subClass1.getClass().getGenericSuperclass());
System.out.println(subClass2.getClass().getGenericSuperclass());
//運行程序 打印結(jié)果如下
com.dream.java_generic.share.ParentGeneric<java.lang.String>
com.dream.java_generic.share.ParentGeneric<T>

可以看到獲取到了泛型父類汤踏,因此會走到條件體里面獲取到實際的泛型類型并返回

在例2中:

ParentGeneric<String> parentGeneric1 = new ParentGeneric<String>();
System.out.println(parentGeneric1.getClass().getGenericSuperclass());
//運行程序 打印結(jié)果如下
class java.lang.Object

可以看到獲取到的泛型父類是 Object,因此進不去條件體舔腾,所以就返回 null 了

2溪胶、parentGeneric2 在創(chuàng)建的時候后面加了 {},這就使得 parentGeneric2 成為了一個匿名內(nèi)部類稳诚,且父類就是 ParentGeneric哗脖,因為匿名內(nèi)部類是在編譯時創(chuàng)建的,那么在編譯的時候就會創(chuàng)建并攜帶具體的泛型信息扳还,因此 parentGeneric2 可以獲取其中的泛型類型

通過上面兩個例子我們可以得出結(jié)論:如果在編譯的時候就保存了泛型類型到字節(jié)碼中才避,那么在運行時我們就可以通過反射獲取到,如果在運行時傳入實際的泛型類型氨距,這個時候就會被擦除桑逝,反射獲取不到當(dāng)前傳入的泛型實際類型

例子1中我們指定了泛型的實際類型為 String,編譯的時候就將它存儲到了字節(jié)碼文件中俏让,因此我們獲取到了泛型類型楞遏。例子2中我們創(chuàng)建了一個匿名內(nèi)部類,同樣在編譯的時候會進行創(chuàng)建并保存了實際的泛型到字節(jié)碼中舆驶,因此我們可以獲取到橱健。而 parentGeneric1 是在運行時創(chuàng)建的而钞,雖然 ParentGeneric 聲明的泛型 T 在編譯時也保留在了字節(jié)碼文件中沙廉,但是它傳入的實際類型被擦除了,這種泛型也是無法通過反射獲取的臼节,記住上面這條結(jié)論撬陵,那么對于泛型類型的獲取你就得心應(yīng)手了

5珊皿、泛型獲取經(jīng)驗總結(jié)

其實通過上面兩個例子可以發(fā)現(xiàn),當(dāng)我們定義一個子類繼承一個泛型父類巨税,并給這個泛型一個類型蟋定,我們就可以獲取到這個泛型類型

//定義一個子類繼承泛型父類,并給這個泛型一個實際的類型
class SubClass extends ParentGeneric<String>{

}

//匿名內(nèi)部類草添,其實我們定義的這個匿名內(nèi)部類也是一個子類驶兜,它繼承了泛型父類,并給這個泛型一個實際的類型
ParentGeneric<String> parentGeneric2 = new ParentGeneric<String>(){};

因此如果我們想要獲取某個泛型類型远寸,我們可以通過子類的幫助去取出該泛型類型抄淑,一種良好的編程實踐就是把當(dāng)前需要獲取的泛型類用 abstract 聲明

3、邊界

邊界就是在泛型的參數(shù)上設(shè)置限制條件驰后,這樣可以強制泛型可以使用的類型肆资,更重要的是可以按照自己的邊界類型來調(diào)用方法

1)、Java 中設(shè)置邊界使用 extends 關(guān)鍵字灶芝,完整語法結(jié)構(gòu):<T extends Bound> 郑原,Bound 可以是類和接口,如果不指定邊界夜涕,默認邊界為 Object

2)犯犁、可以設(shè)置多個邊界,中間使用 & 連接女器,多個邊界中只能有一個邊界是類栖秕,且類必須放在最前面,類似這種語法結(jié)構(gòu)

<T extends ClassBound & InterfaceBound1 & InterfaceBound2>

下面我們來演示一下:

abstract class ClassBound{
    public abstract void test1();
}

interface InterfaceBound1{
    void test2();
}

interface InterfaceBound2{
    void test3();
}

class ParentClass <T extends ClassBound & InterfaceBound1 & InterfaceBound2>{
    private final T item;

    public ParentClass(T item) {
        this.item = item;
    }

    public void test1(){
        item.test1();
    }

    public void test2(){
        item.test2();
    }

    public void test3(){
        item.test3();
    }
}

class SubClass extends ClassBound implements InterfaceBound1,InterfaceBound2 {

    @Override
    public void test1() {
        System.out.println("test1");
    }

    @Override
    public void test2() {
        System.out.println("test2");
    }

    @Override
    public void test3() {
        System.out.println("test3");
    }
}

public class Bound {
    public static void main(String[] args) {
        SubClass subClass = new SubClass();
        ParentClass<SubClass> parentClass = new ParentClass<SubClass>(subClass);
        parentClass.test1();
        parentClass.test2();
        parentClass.test3();
    }
}
//打印結(jié)果
test1
test2
test3

4晓避、通配符

1簇捍、泛型的協(xié)變,逆變和不變

思考一個問題俏拱,代碼如下:

Number number = new Integer(666);
ArrayList<Number> numberList = new ArrayList<Integer>();//編譯器報錯 type mismatch

上述代碼暑塑,為啥 Number 的對象可以由 Integer 實例化,而 ArrayList<Number> 的對象卻不能由 ArrayList<Integer> 實例化锅必?

要明白上面這個問題事格,我們首先要明白,什么是泛型的協(xié)變搞隐,逆變和不變

1)驹愚、泛型協(xié)變,假設(shè)我定義了一個 Class<T> 的泛型類劣纲,其中 A 是 B 的子類逢捺,同時 Class<A> 也是 Class<B> 的子類,那么我們說 Class 在 T 這個泛型上是協(xié)變的

2)癞季、泛型逆變劫瞳,假設(shè)我定義了一個 Class<T> 的泛型類倘潜,其中 A 是 B 的子類,同時 Class<B> 也是 Class<A> 的子類志于,那么我們說 Class 在 T 這個泛型上是逆變的

3)涮因、泛型不變,假設(shè)我定義了一個 Class<T> 的泛型類伺绽,其中 A 是 B 的子類养泡,同時 Class<B>Class<A> 沒有繼承關(guān)系,那么我們說 Class 在 T 這個泛型上是不變的

因此我們可以知道 ArrayList<Number> 的對象不能由 ArrayList<Integer> 實例化是因為 ArrayList 當(dāng)前的泛型是不變的奈应,我們要解決上面報錯的問題瓤荔,可以讓 ArrayList 當(dāng)前的泛型支持協(xié)變,如下:

Number number = new Integer(666);
ArrayList<? extends Number> numberList = new ArrayList<Integer>();

2钥组、泛型的上邊界通配符

1)输硝、泛型的上邊界通配符語法結(jié)構(gòu):<? extends Bound>,使得泛型支持協(xié)變程梦,它限定的類型是當(dāng)前上邊界類或者其子類点把,如果是接口的話就是當(dāng)前上邊界接口或者實現(xiàn)類,使用上邊界通配符的變量只讀屿附,不可以寫郎逃,可以添加 null ,但是沒意義

public class WildCard {
    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<Integer>();
        List<Number> numberList = new ArrayList<Number>();
        integerList.add(666);
        numberList.add(123);

        getNumberData(integerList);
        getNumberData(numberList);
    }

    public static void getNumberData(List<? extends Number> data) {
        System.out.println("Number data :" + data.get(0));
    }
}
//打印結(jié)果
Number data: 666
Number data: 123

問題:為啥使用上邊界通配符的變量只讀挺份,而不能寫褒翰?

1、<? extends Bound>,它限定的類型是當(dāng)前上邊界類或者其子類匀泊,它無法確定自己具體的類型优训,因此編譯器無法驗證類型的安全,所以不能寫

2各聘、假設(shè)可以寫揣非,我們向它里面添加若干個子類,然后用一個具體的子類去接收躲因,勢必會造成類型轉(zhuǎn)換異常

3早敬、泛型的下邊界通配符

1)、泛型的下邊界通配符語法結(jié)構(gòu):<? super Bound>大脉,使得泛型支持逆變搞监,它限定的類型是當(dāng)前下邊界類或者其父類,如果是接口的話就是當(dāng)前下邊界接口或者其父接口镰矿,使用下邊界通配符的變量只寫琐驴,不建議讀

public class WildCard {
  
    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<Number>();
        List<Object> objectList = new ArrayList<Object>();
        setNumberData(numberList);
        setNumberData(objectList);
    }
    
    public static void setNumberData(List<? super Number> data) {
        Number number = new Integer(666);
        data.add(number);
    }
}

問題:為啥使用下邊界通配符的變量可以寫,而不建議讀?

1棍矛、<? super Bound>,它限定的類型是當(dāng)前下邊界類或者其父類抛杨,雖然它也無法確定自己具體的類型够委,但根據(jù)多態(tài),它能保證自己添加的元素是安全的怖现,因此可以寫

2茁帽、獲取值的時候,會返回一個 Object 類型的值屈嗤,而不能獲取實際類型參數(shù)代表的類型潘拨,因此建議不要去讀,如果你實在要去讀也行饶号,但是要注意類型轉(zhuǎn)換異常

4铁追、泛型的無邊界通配符

1)、無邊界通配符的語法結(jié)構(gòu):<?>茫船,實際上它等價于 <? extends Object>琅束,也就是說它的上邊界是 Object 或其子類,因此使用無界通配符的變量同樣只讀算谈,不能寫涩禀,可以添加 null ,但是沒意義

public class WildCard {
  
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<String>();
        List<Number> numberList = new ArrayList<Number>();
        List<Integer> integerList = new ArrayList<Integer>();
        stringList.add("erdai");
        numberList.add(666);
        integerList.add(123);
        getData(stringList);
        getData(numberList);
        getData(integerList);
    }
    
     public static void getData(List<?> data) {
        System.out.println("data: " + data.get(0));
    }
}
//打印結(jié)果
data: erdai
data: 666
data: 123

5然眼、PECS 原則

泛型代碼的設(shè)計艾船,應(yīng)遵循PECS原則(Producer extends Consumer super):

1)、如果只需要獲取元素高每,使用 <? extends T>

2)屿岂、如果只需要存儲,使用 <? super T>

//這是 Collections.java 中 copy 方法的源碼
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      //...
}

這是一個很經(jīng)典的例子鲸匿,src 表示原始集合雁社,使用了 <? extends T>,只能從中讀取元素晒骇,dest 表示目標集合霉撵,只能往里面寫元素,充分的體現(xiàn)了 PECS 原則

6洪囤、使用通配符總結(jié)

1)徒坡、當(dāng)你只想讀取值的時候,使用 <? extends T>

2)瘤缩、當(dāng)你只想寫入值的時候喇完,使用 <? super T>

3)、當(dāng)你既想讀取值又想寫入值的時候剥啤,就不要使用通配符

5锦溪、泛型的限制

1)不脯、泛型不能顯式地引用在運行時類型的操作里,如 instanceof 操作和 new 表達式刻诊,運行時類型只適用于原生類型

public class GenericLimitedClass<T> {
    private void test(){
        String str = "";
        //編譯器不允許這種操作
        if(str instanceof T){

        }
        //編譯器不允許這種操作
        T t = new T();
    }
}

2)防楷、不能創(chuàng)建泛型類型的數(shù)組,只可以聲明一個泛型類型的數(shù)組引用

public class GenericLimitedClass<T> {
    private void test(){
       GenericLimitedClass<Test>[] genericLimitedClasses;
       //編譯器不允許
       genericLimitedClasses = new GenericLimitedClass<Test>[10];
    }
}

3)则涯、不能聲明類型為泛型的靜態(tài)字段

public class GenericLimitedClass<T> {
    //編譯器不允許
    private static T t;
}

4)复局、泛型類不可以直接或間接地繼承 Throwable

//編譯器不允許
public class GenericLimitedClass<T> extends Throwable {
   
}

5)、方法中不可以捕獲類型參數(shù)的實例粟判,但是可以在 throws 語句中使用類型參數(shù)

public class GenericLimitedClass<T> {
    private <T extends Throwable> void test1() throws T{
        try {

        //編譯器不允許
        }catch (T exception){

        }
    }
}

6)亿昏、一個類不可以重載在類型擦除后有同樣方法簽名的方法

public class GenericLimitedClass<T> {
    //編譯器不允許
    private void test2(List<String> stringList){

    }

    private void test2(List<Integer> integerList){

    }
}

6、問題

1)档礁、類型邊界和通配符邊界有什么區(qū)別角钩?

類型邊界可以有多個,通配符邊界只能有一個

2)呻澜、List<?>List<Object> 一樣嗎彤断?

不一樣

1、 List<Object> 可讀寫易迹,但是 List<?> 只讀

2宰衙、List<?>可以有很多子類,但是 List<Object> 沒有

二睹欲、Kotlin 泛型

Kotlin 泛型和 Java 泛型基本上是一樣的供炼,只不過在 Kotlin 上有些東西換了新的寫法

1、泛型的基本用法

1)窘疮、在 Kotlin 中我們定義和使用泛型的方式如下:

//1袋哼、定義一個泛型類,在類名后面使用 <T> 這種語法結(jié)構(gòu)就是為這個類定義一個泛型
class MyClass<T>{
    fun method(params: T) {
      
    }
}
//泛型調(diào)用
val myClass = MyClass<Int>()
myClass.method(12)

//2闸衫、定義一個泛型方法涛贯,在方法名的前面加上 <T> 這種語法結(jié)構(gòu)就是為這個方法定義一個泛型
class MyClass{
    fun <T> method(params: T){

    }
}
//泛型調(diào)用
val myClass = MyClass()
myClass.method<Int>(12)
//根據(jù) Kotlin 類型推導(dǎo)機制,我們可以把泛型給省略
myClass.method(12)

//3蔚出、定義一個泛型接口弟翘,在接口名后面加上 <T> 這種語法結(jié)構(gòu)就是為這個接口定義一個泛型
interface MyInterface<T>{
    fun interfaceMethod(params: T)
}

對比 Java 中定義泛型,我們可以發(fā)現(xiàn):在定義類和接口泛型上沒有任何區(qū)別骄酗,在定義方法泛型時稀余,Kotlin 是在方法名前面添加泛型,而 Java 是在返回值前面添加泛型

2趋翻、邊界

1)睛琳、為泛型指定邊界,我們可以使用 <T : Class> 這種語法結(jié)構(gòu),如果不指定泛型的邊界师骗,默認為 Any?

2)历等、如果有多個邊界,可以使用 where 關(guān)鍵字辟癌,中間使用 : 隔開寒屯,多個邊界中只能有一個邊界是類,且類必須放在最前面

//情況1 單個邊界
class MyClass1<T : Number> {

    var data: T? = null

    fun <T : Number> method(params: T) {

    }
}

//情況2 多個邊界使用 where 關(guān)鍵字
open class Animal
interface Food
interface Food2

class MyClass2<T> where T : Animal, T : Food, T : Food2 {

    fun <T> method(params: T) where T : Animal, T : Food, T : Food2 {

    }
}

3愿待、泛型實化

泛型實化在 Java 中是不存在的浩螺,Kotlin 中之所以能實現(xiàn)泛型實化靴患,是因為使用的內(nèi)聯(lián)函數(shù)會對代碼進行替換仍侥,那么在內(nèi)聯(lián)函數(shù)中使用泛型,最終也會使用實際的類型進行替換

1)鸳君、使用內(nèi)聯(lián)函數(shù)配合 reified 關(guān)鍵字對泛型進行實化农渊,語法結(jié)構(gòu)如下:

inline fun <reified T> getGenericType() {
  
}

實操一下:

inline fun <reified T> getGenericType() = T::class.java

fun main() {
    //泛型實化 這種情況在 Java 是會被類型擦除的
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Number>()
    println(result1)
    println(result2)
}
//打印結(jié)果
class java.lang.String
class java.lang.Number

2)、實際應(yīng)用

在我們跳轉(zhuǎn) Activity 的時候通常會這么操作

val intent = Intent(mContext,TestActivity::class.java)
mContext.startActivity(intent)

有沒有感覺寫這種 TestActivity::class.java 的語法很難受或颊,反正我是覺得很難受砸紊,那么這個時候我們就可以使用泛型實化換一種寫法:

//定義一個頂層函數(shù)
inline fun <reified T> startActivity(mContext: Context){
    val intent = Intent(mContext,T::class.java)
    mContext.startActivity(intent)
}

//使用的時候
startActivity<TestActivity>(mContext)

這種寫法是不是清爽了很多,那么在我們跳轉(zhuǎn) Activity 的時候囱挑,可能會攜帶一些參數(shù)醉顽,如下:

val intent = Intent(mContext,TestActivity::class.java)
intent.putExtra("params1","erdai")
intent.putExtra("params2","666")
mContext.startActivity(intent)

這個時候我們可以增加一個函數(shù)類型的參數(shù)炬转,使用 Lambda 表達式去調(diào)用蹬昌,如下:

inline fun <reified T> startActivity(mContext: Context, block: Intent.() -> Unit){
    val intent = Intent(mContext,T::class.java)
    intent.block()
    mContext.startActivity(intent)
}

//使用的時候
startActivity<SecondActivity>(mContext){       
  putExtra("params1","erdai")     
  putExtra("params2","666")
}

4、泛型協(xié)變秉馏,逆變和不變

1)通熄、泛型協(xié)變的語法規(guī)則:<out T> 類似于 Java 的 <? extends Bound>唆涝,它限定的類型是當(dāng)前上邊界類或者其子類,如果是接口的話就是當(dāng)前上邊界接口或者實現(xiàn)類唇辨,協(xié)變的泛型變量只讀廊酣,不可以寫,可以添加 null 赏枚,但是沒意義

open class Person
class Student: Person()
class Teacher: Person()

class SimpleData<out T>{

}

fun main() {
    val person: Person = Student()
    val personGeneric: SimpleData<Person> = SimpleData<Student>()
    val list1: ArrayList<out Person> = ArrayList<Student>()
}

2)亡驰、泛型逆變的語法規(guī)則:<in T> 類似于 Java 的 <? super Bound>,它限定的類型是當(dāng)前下邊界類或者其父類饿幅,如果是接口的話就是當(dāng)前下邊界接口或者其父接口隐解,逆變的泛型變量只能寫,不建議讀

open class Person
class Student: Person()
class Teacher: Person()

class SimpleData<in T>{

}

fun main() {
    val person1: Person = Student()
    val personGeneric1: SimpleData<Student> = SimpleData<Person>()
    val list2: ArrayList<in Person> = ArrayList<Any>()
}

5)诫睬、泛型不變和 Java 語法規(guī)則是一樣的

open class Person
class Student: Person()
class Teacher: Person()

class SimpleData<T>{

}

fun main() {
    val person: Person = Student()
    //編譯器不允許
    val personGeneric: SimpleData<Person> = SimpleData<Student>()
}

6)煞茫、Kotlin 使用 <*> 這種語法結(jié)構(gòu)來表示無界通配符,它等價于 <out Any>,類似于 Java 中的 <?>续徽,在定義一個類的時候你如果使用<out T : Number> 蚓曼,那么 * 就相當(dāng)于 <out Number>

class KotlinGeneric<out T: Number>{

}

//無界通配符 等價于 <out Any>,但是我這個類限制了泛型邊界為 Number钦扭,因此這里相當(dāng)于 <out Number>
fun main() {
    val noBound: KotlinGeneric<*> = KotlinGeneric<Int>()
 
    //根據(jù)協(xié)變規(guī)則 編譯器不允許這樣寫
    val noBound: KotlinGeneric<*> = KotlinGeneric<Any>()
}

三纫版、泛型總結(jié)

要學(xué)好 Kotlin 泛型,就要先學(xué)習(xí)好 Java 泛型客情,最后總結(jié)一下這篇文章講的內(nèi)容:

1其弊、回答了一些關(guān)于泛型的問題

2、講解了 Java 泛型膀斋,其中我覺得泛型擦除和泛型的協(xié)變梭伐,逆變和不變是比較難理解的,因此大家可以多花點時間去理解這一塊

3仰担、講解了 Kotlin 泛型糊识,相對于 Java 泛型,Kotlin泛型就是在語法結(jié)構(gòu)上有些不一樣摔蓝,但功能是完全一樣的赂苗,另外 Kotlin 中的泛型實化是 Java 中所沒有的

好了,到這里贮尉,Kotlin 泛型就講完了拌滋,相信你如果從頭看到這里,收獲一定很多猜谚,如果覺得我寫得還不錯败砂,請給我點個贊吧??,如果有任何問題龄毡,歡迎評論區(qū)一起討論

感謝你閱讀這篇文章

全文到此吠卷,原創(chuàng)不易,歡迎點贊沦零,收藏祭隔,評論和轉(zhuǎn)發(fā),你的認可是我創(chuàng)作的動力

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末路操,一起剝皮案震驚了整個濱河市疾渴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屯仗,老刑警劉巖搞坝,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異魁袜,居然都是意外死亡桩撮,警方通過查閱死者的電腦和手機敦第,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來店量,“玉大人芜果,你說我怎么就攤上這事∪谑Γ” “怎么了右钾?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長旱爆。 經(jīng)常有香客問我舀射,道長,這世上最難降的妖魔是什么怀伦? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任脆烟,我火速辦了婚禮,結(jié)果婚禮上空镜,老公的妹妹穿的比我還像新娘浩淘。我一直安慰自己捌朴,他們只是感情好吴攒,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著砂蔽,像睡著了一般洼怔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上左驾,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天镣隶,我揣著相機與錄音,去河邊找鬼诡右。 笑死安岂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的帆吻。 我是一名探鬼主播域那,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼猜煮!你這毒婦竟也來了次员?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤王带,失蹤者是張志新(化名)和其女友劉穎淑蔚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愕撰,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡刹衫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年醋寝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片带迟。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡甥桂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出邮旷,到底是詐尸還是另有隱情黄选,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布婶肩,位于F島的核電站办陷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏律歼。R本人自食惡果不足惜民镜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望险毁。 院中可真熱鬧制圈,春花似錦、人聲如沸畔况。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跷跪。三九已至馋嗜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吵瞻,已是汗流浹背葛菇。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留橡羞,地道東北人眯停。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像卿泽,于是被迫代替她去往敵國和親莺债。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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