Java 泛型 T,E,K,V,?,傻傻分不清?

ZNineSun發表於2021-01-01

1.前言

Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時型別安全檢測機制,該機制允許開發者在編譯時檢測到非法的型別。
泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。

2.泛型的好處

  • 在不使用泛型時,通過對型別 Object 的引用來實現引數的“任意化”,“任意化”帶來的缺點是要做顯式的強制型別轉換,但是這種轉換是要求開發者對實際引數型別可以預知的情況下進行的。
  • 對於強制型別轉換錯誤的情況,編譯器可能不提示錯誤,在執行的時候才出現異常,這是本身就是一個安全隱患。
  • 那麼泛型的好處就是在編譯的時候能夠檢查型別安全,並且所有的強制轉換都是自動和隱式的。
import java.util.Date;
import java.util.List;
public class Generic<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }

    public static void main(String[] args) {
        //這種宣告方式不需要強制轉換
        Generic<TbInfo> generic1 = new Generic();
        generic1.set(new TbInfo(1, new Date()));
        TbInfo tb = generic1.get();
        System.out.println(tb.toString());

        //這種宣告方式需要強制轉換
        Generic generic2 = new Generic();
        generic2.set(new TbInfo(1, new Date()));
        TbInfo tb2 = (TbInfo) generic2.get();
        System.out.println(tb.toString());
    }
}

下面附上TbInfo程式碼:

public class TbInfo{
    private Integer agencyId;
    private Date sendTime;
    public TbInfo(Integer agencyId, Date sendTime) {
        this.agencyId = agencyId;
        this.sendTime = sendTime;
    }
    public String getAgencyContent() {
        return agencyContent;
    }

    public void setAgencyContent(String agencyContent) {
        this.agencyContent = agencyContent;
    }
    public Integer getAgencyId() {
        return this.agencyId;
    }
    public void setAgencyId(Integer agencyId) {
        this.agencyId = agencyId;
    }
    public Date getSendTime() {
        return this.sendTime;
    }
    public void setSendTime(Date sendTime) {
        this.sendTime = sendTime;
    }

    /* This code was generated by TableGo tools, mark 2 end. */

    @Override
    public String toString() {
        return "TbInfo{" +
                ", agencyId=" + agencyId +
                ", sendTime=" + sendTime +
                '}';
    }
}

執行結果如下:
在這裡插入圖片描述

3.泛型中萬用字元

我們在定義泛型類,泛型方法,泛型介面的時候經常會碰見很多不同的萬用字元,比如 T,E,K,V 等等,這些萬用字元又都是什麼意思呢?
常用的 T,E,K,V,?

本質上這些個都是萬用字元,沒啥區別,只不過是編碼時的一種約定俗成的東西。比如上述程式碼中的 T ,我們可以換成 A-Z 之間的任何一個 字母都可以,並不會影響程式的正常執行,但是如果換成其他的字母代替 T ,在可讀性上可能會弱一些。通常情況下,T,E,K,V,?是這樣約定的:

  • ?表示不確定的 java 型別
  • T (type) 表示具體的一個java型別
  • K V (key value) 分別代表java鍵值中的Key Value
  • E (element) 代表Element

3.1 ?無界萬用字元

我們先看一個小的demo
我有一個父類 Animal 和幾個子類,如狗、貓等,現在我需要一個動物的列表,我的第一個想法是像這樣的:

List<Animal> listAnimals

但是老闆的想法確實這樣的:

List<? extends Animal> listAnimals

老闆為什麼想要使用萬用字元而不是簡單的泛型呢?
萬用字元其實在宣告區域性變數時是沒有什麼意義的,但是當你為一個方法宣告一個引數時,它是非常重要的。比如以下程式碼:

public class Generic01 {
    static int countLegs (List<? extends Animal > animals ) {
        int retVal = 0;
        for ( Animal animal : animals )
        {
            retVal += animal.countLegs();
        }
        return retVal;
    }
    static int countLegs1 (List< Animal > animals ){
        int retVal = 0;
        for ( Animal animal : animals )
        {
            retVal += animal.countLegs();
        }
        return retVal;
    }
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        // 不會報錯
        countLegs( dogs );
        // 報錯
        countLegs1(dogs);
    }
}

在這裡插入圖片描述

其中Animal,Dog程式碼如下

  • Animal
public class Animal {
    private Integer legs;
    public Integer countLegs() {
        return legs;
    }
    public Integer getLegs() {
        return legs;
    }
    public void setLegs(Integer legs) {
        this.legs = legs;
    }
}
  • Dog
public class Dog extends Animal {
    @Override
    public Integer countLegs() {
        return super.countLegs();
    }
}

但是在我們實際上使用時根本不知道我們要傳入的引數到底是Dog還是Cat,所以對於不確定或者不關心實際要操作的型別,可以使用無限制萬用字元( 尖括號裡一個問號,即 <?> ) 表示可以持有任何型別。
像 countLegs 方法中,限定了上界,但是不關心具體型別是什麼,所以對於傳入的 Animal 的所有子類都可以支援,並且不會報錯。而 countLegs1 就不行。於是我們引出了上界萬用字元 < ? extends E>

3.2 上界萬用字元<? extends E>

上界:用 extends 關鍵字宣告,表示引數化的型別可能是所指定的型別,或者是此型別的子類。

在型別引數中使用 extends 表示這個泛型中的引數必須是 E 或者 E 的子類,這樣有兩個好處:

  • 如果傳入的型別不是 E 或者 E 的子類,編譯不成功
  • 泛型中可以使用 E 的方法,要不然還得強轉成 E 才能使用

型別引數列表中如果有多個型別引數上限,用逗號分開,比如:

private <K extends A, E extends B> E test(K arg1, E arg2){
    E result = arg2;
    arg2.compareTo(arg1);
    //.....
    return result;
}

3.3 下界萬用字元<? super E>

下界: 用 super 進行宣告,表示引數化的型別可能是所指定的型別,或者是此型別的父型別,直至 Object

在型別引數中使用 super 表示這個泛型中的引數必須是 E 或者 E 的父類。

package com.ownplus.consumerown.test.generic;

import java.util.ArrayList;
import java.util.List;

public class Generic02 {
    private <T> void test(List<? super T> dst, List<T> src) {
        for (T t : src) {
            dst.add(t);
        }
    }
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        for (int i=0;i<10;i++){
            Dog dog=new Dog();
            dog.setLegs(i);
            dogs.add(dog);
        }
        List<Animal> animals = new ArrayList<>();
        new Generic02().test(animals, dogs);
    }
}

我大致解釋一下這串程式碼:

首先我們傳入的引數是animals,dogs
List<? super T> dst 表示泛型中的引數必須是T的父類,那麼這個T又是什麼呢
我們接下來看第二個引數List src ,說明我們在使用test方法時,傳入的第二個引數的泛型型別就是T對應的型別
現在我們回到main方法中,new Generic02().test(animals, dogs) 說明傳入的T的型別是dog型別,第一個引數對應的萬用字元? 應該是T 對應的父類,而我們傳入的第一個引數是animals,那就是說明? 對應的型別是 animal 型別,符合 ?為T的父類 這一規則

4.?和 T 的區別

        // 指定元素的集合只能是T型別
        List<T> list1=new LinkedList<>();
        // 指定元素的集合可以是任意型別,不過這種沒有任何意義,一般是方法中,只是為了說明用法
        List<?> list2=new LinkedList<>();

?和 T 都表示不確定的型別,區別在於我們可以對 T 進行操作,但是對 ?不行,比如如下這種 :

// 可以
T t = operate();
// 不可以
?car = operate();
  • T 是一個 確定的 型別,通常用於泛型類和泛型方法的定義
  • ?是一個 不確定 的型別,通常用於泛型方法的呼叫程式碼和形參,不能用於定義類和泛型方法。

4.1 區別1:通過 T 來 確保 泛型引數的一致性

// 通過 T 來 確保 泛型引數的一致性
public <T extends Number> void
test(List<T> dest, List<T> src)

//萬用字元是 不確定的,所以這個方法不能保證兩個 List 具有相同的元素型別
public void test(List<? extends Number> dest, List<? extends Number> src)

比如以下程式碼:

public class Generic03 {
    //可以保證兩個list元素的一致性
    public <T> void test0(List<T> dest, List<T> src) {
    }

    public static void main(String[] args) {
        List<String> a1 = new LinkedList<>();
        List<Number> a2 = new LinkedList<>();
        new Generic03().test0(a1, a2);
    }
}

則會出現以下結果:在這裡插入圖片描述

但是如果我們使用?萬用字元:

public class Generic03 {
    //不可以保證兩個list元素的一致性
    public <T> void test1(List<? extends T> dest, List<? extends T> src) {
    }
    public static void main(String[] args) {
        List<String> a1 = new LinkedList<>();
        List<Number> a2 = new LinkedList<>();
        new Generic03().test1(a1, a2);
    }
}

我們會發現並沒有報錯在這裡插入圖片描述

4.2 區別2:型別引數可以多重限定而萬用字元不行

在這裡插入圖片描述

使用 & 符號設定多重邊界(Multi Bounds),指定泛型型別 T 必須是 MultiLimitInterfaceA 和 MultiLimitInterfaceB 的共有子型別,此時變數 t 就具有了所有限定的方法和屬性。
對於萬用字元(?)來說,因為它不是一個確定的型別,所以不能進行多重限定。

4.3 區別3:萬用字元可以使用超類限定而型別引數不行

型別引數 T 只具有 一種 型別限定方式: T extends A
但是萬用字元 ? 可以進行 兩種限定:

? extends A
? super A

5.Class< T> 和 Class<?> 區別

前面介紹了 ?和 T 的區別,那麼對於,Class 和 Class<?> 又有什麼區別呢?
Class 和 Class<?>最常見的是在反射場景下的使用,這裡以用一段反射的程式碼來說明下:

// 通過反射的方式生成  multiLimit物件,這裡比較明顯的是,我們需要使用強制型別轉換
MultiLimit multiLimit = (MultiLimit)
Class.forName("com.glmapper.bridge.boot.generic.MultiLimit").newInstance();

對於上述程式碼,在執行期,如果反射的型別不是 MultiLimit 類,那麼一定會報 java.lang.ClassCastException 錯誤。
對於這種情況,則可以使用下面的程式碼來代替,使得在在編譯期就能直接 檢查到型別的問題:
在這裡插入圖片描述

Class 在例項化的時候,T 要替換成具體類。Class<?> 它是個通配泛型,? 可以代表任何型別,所以主要用於宣告時的限制情況。比如,我們可以這樣做申明:

// 可以
public Class<?> clazz;
// 不可以,因為 T 需要指定型別
public Class<T> clazzT;

所以當不知道定宣告什麼型別的 Class 的時候可以定義一 個Class<?>。
那如果也想 public Class clazzT 這樣的話,就必須讓當前的類也指定 T ,即:

public class Test3<T> {
    public Class<?> clazz;
    // 不會報錯
    public Class<T> clazzT;}

相關文章