Java基礎-泛型

Groot發表於2022-02-10

概述

什麼是泛型?泛型能解決什麼問題?
泛型即引數化型別,會讓你的程式更易讀,也更安全。
在Java增加泛型特性前【JDK5前】,泛型的程式的設計是用繼承來實現的。如下程式可以正常編譯和執行,但將get的結果強制型別轉換會產生一個錯誤。為了解決這個問題,泛型應運而生。

 /**
     * 不同型別的元素add到ArrayList中
     */
    @Test
    public void mixedValueTest(){
        ArrayList mixedValue = new ArrayList();
        mixedValue.add(1L);
        mixedValue.add("string");
        mixedValue.forEach(item->{
            System.out.println((String)item);
        });
    }
  // java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String

在泛型使用過程中,操作的資料型別被指定為一個引數,這種引數型別可以用在類、介面和方法中,分別被稱為泛型類、泛型介面、泛型方法。

特性

泛型只在編譯階段有效,在編譯時會採取去泛型化的措施:將泛型相關的資訊擦除,同時在物件進入和離開方法的邊界處新增型別檢查和型別轉換的方法。也就是說,泛型資訊不會進入到執行階段。

 /**
     * 使用型別引數約束元素的型別
     */
    @Test
    public void traitTest(){
        ArrayList<String> stringList = new ArrayList<String>();
        stringList.add("string a");
        stringList.add("string b");

        ArrayList<Integer> IntegerList = new ArrayList<Integer>();
        IntegerList.add(100);
        IntegerList.add(200);
        if(stringList.getClass()==IntegerList.getClass()){
            System.out.println("相同型別"); // 輸出相同型別
        }
        // 泛型型別在邏輯上可以看成是多個不同的型別,實際上都是相同的型別
    }

泛型的定義和使用

泛型有三種使用方式:泛型類、泛型介面、泛型方法。

泛型類

泛型型別用於類的定義中,被稱為泛型類,泛型類例項化時需要傳入泛型型別實參【不傳預設為Object】。最典型的是各種容器類ListArrayListSetMap

1. 泛型類的定義
class 類名稱 <泛型標識:可以隨便寫任意標識號,標識指定的泛型的型別>{
  private 泛型標識 變數名;
  .....
  }
}
常用的泛型標識【一般使用大寫字母標識】: K/V/T/E/U/S/R......

2. 泛型類的使用
// 泛型相當於普通類的工廠
類目<具體的資料型別> 物件名 = new 類名<具體的資料型別[JDK7後可省略]>();

3. 從泛型類派生子類
// 子類也是泛型類,子類和父類的泛型型別要一致,只能在父類的基礎上進行擴充套件【子類必須包含父類的泛型標識】
class 子類名<泛型標識:T> extends 父類名<泛型標識:T>

// 子類不是泛型類,父類要明確泛型的資料型別
class 子類名 extends 父類名<明確的資料型別>

注意事項:

  • 泛型的型別引數只能是Object型別,不能是基本資料型別
  • 如果沒有指定具體的資料型別。預設操作的型別是Object
  • 泛型型別在邏輯上可以看成是多個不同的型別,但實際上都是相同的型別

泛型介面

泛型介面與泛型類的定義及使用基本相同。

1. 泛型介面的定義
interface 介面名稱<泛型標識:可以隨便寫任意標識號,標識指定的泛型的型別>{
  private 泛型標識 方法名();
  .....
  }

2. 泛型介面的使用
// 實現類是泛型類,實現類和介面的泛型型別保持一致
class 實現類<T> implements 泛型介面<T>
// 實現類不是泛型類,介面要明確資料型別
class 實現類 implements 泛型介面<String>

泛型方法

泛型方法:是在呼叫方法的時候指明泛型的具體型別,泛型方法中的泛型標識跟泛型類中的泛型標識無關,可以看成一個獨立的體系。

1. 泛型方法定義
// 關鍵在於修飾符後的菱形語法<E>
修飾符 <E> 返回值型別 方法名(E ele){
    return Objects.isNull(ele);
}
// 修飾符後的菱形語法<E>,可以理解宣告此方法為泛型方法,泛型類的使用了泛型的成員方法並不是泛型方法。

2. 泛型方法和可變引數
public <E> void printElement(E... elements){
    for(E element : elements){
        System.out.println(element);
    }
}

3. 泛型方法的使用
printElement("string",100,true);

小結:

  • 泛型方法能是方法獨立於類而產生變化
  • 無論何時,如果能做到,就該儘量使用泛型方法,而不是將類泛型化
  • 如果static方法要使用泛型能力,就必須將其定義為泛型方法,原因是靜態方法無法訪問類上定義的泛型。

萬用字元

泛型標識本質上也是萬用字元,沒啥區別,是編碼時約定俗成的東西。
通常情況下,T,E,K,V,? 是這樣約定的:

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

? 和 T 的區別

T是一個確定的型別,通常用於泛型類和泛型方法的定義,?是一個不確定的型別,通常用於泛型方法的呼叫程式碼和形參,不能用於定義類和泛型方法。

  • 通過T可以確定泛型引數的一致性

    // ?本身就是不確定的元素,不能保證List的元素是一致的
    void t1(List<? extends Number> list);
    // 確保泛型引數是一致的,List中儲存相同的元素
    void t1(List<T> list);
  • 型別引數T可以多重限定,而萬用字元?不行

  • 萬用字元可以使用超類限定而型別引數不行

    T extends Number  // 允許
    T susper Number  // 不允許
    
    ? extends Number  // 允許
    ? susper Number  // 允許

萬用字元

<?>無限定的萬用字元

無限定萬用字元經常與容器類配合使用,它其中的 ? 其實代表的是未知型別,所以涉及到 ? 時的操作,一定與具體型別無關,只能操作與型別無關的方法:

ArrayList<? super Number> arrayList3 = new ArrayList<>();
arrayList3.add(1);
arrayList3.add(1.2);

ArrayList<?> arrayList4 = new ArrayList<>();
// 將任意ArrayList加入ArrayList<?>不會出錯
arrayList4 = arrayList3;
// 操作arrayList4.add不能正常編譯
// arrayList4.add(1);  
for (Object o : arrayList4) {
   // do something
}

上限萬用字元 < ? extends E>

<? extends E>限定引數型別的上限:引數型別必須是EE的子型別:

// 限定型別實參實現了Comparable介面,其中T表示限定型別的子型別,Comparable為限定型別的上界
public static <T extends Comparable> T min(T... comparbleElements)
// 一個型別變數或萬用字元可以有多個限定
public static <T extends Comparable & Serializable> T min(T... comparableElements)

小結:

  • 如果傳入的型別不是限定型別及其子類,編譯不成功
  • 只能取,且只能取EE的父型別

Java基礎-泛型

下限萬用字元 <? super E>

<? super E>限定引數型別的下界:引數型別必須是EE 的超型別。
susper關鍵字和extends關鍵字功能相反,使用<? super Integer>萬用字元表示:

  • 允許呼叫set(? super Integer)方法傳入Integer的引用;
  • 不允許呼叫get()方法獲得Integer的引用。

Java基礎-泛型

對比extends和super萬用字元區別:

  • <? extends T>允許呼叫讀方法T get()獲取T的引用,但不允許呼叫寫方法set(T)傳入T的引用(傳入null除外)
  • <? super T>允許呼叫寫方法set(T)傳入T的引用,但不允許呼叫讀方法T get()獲取T的引用(獲取Object除外)。

一個是允許讀不允許寫,另一個是允許寫不允許讀。

型別擦除

泛型資訊只存在於程式碼編譯階段,在進入 JVM 之前,與泛型相關的資訊會被擦除掉,專業術語叫做型別擦除。
在泛型類被型別擦除的時候,之前泛型類中的型別引數部分如果沒有指定上限,如 <T>則會被轉譯成普通的 Object 型別,如果指定了上限如 <T extends String>則型別引數就被替換成型別上限。

無限制型別擦除

泛型標識未限定型別,編譯器將未繫結的型別T替換為實際型別的Object

 public class Erasure<T>{
        private T key;

        public T getKey() {
            return key;
        }

        public void setKey(T key) {
            this.key = key;
        }
    }
 // 型別擦除後
 public class Erasure{
        private Object key;

        public Object getKey() {
            return key;
        }

        public void setKey(Object key) {
            this.key = key;
        }
    }

有限制型別擦除

泛型標識有限定型別,編譯器會將繫結型別引數T替換為第一個繫結類【存在多重限定的情況】:

 public class Erasure<T extends Number & Comparable>{
        private T key;

        public T getKey() {
            return key;
        }

        public void setKey(T key) {
            this.key = key;
        }
    }
// 型別擦除後
 public class Erasure{
        private Number key;

        public Number getKey() {
            return key;
        }

        public void setKey(Number key) {
            this.key = key;
        }
    }

橋接方法

一種允許擴充套件泛型類或實現泛型介面(帶有具體型別引數)的類仍用作原始型別的方法。是編譯器行為,在編譯時自動在子類新增橋接方法以維持多型性。
編譯器保護對橋接方法的訪問,強制直接對其進行顯式呼叫會導致編譯時錯誤。

 // 介面
 interface IBridgeMethod<T> {
        public T info(T key);
    }

    // 泛型資訊擦除後編譯的位元組碼
    interface IBridgeMethod {
        public Object info(Object key);
    }

 // 實現類
 // 型別擦除後,方法簽名不匹配,父類的info(Object key)方法不會被重寫,為了解決這個問題並在型別擦除之後保留泛型型別的多型性,Java 編譯器生成一個橋接方法來確保子型別按預期工作
 public class BridgeMethodImpl implements IBridgeMethod<Integer>{
        @Override
        public Integer info(Integer key) {
            return null;
        }
    }
    // 泛型資訊擦除後編譯的位元組碼:子類自動生成一個與父類的方法簽名一致的橋接方法,可以通過反射或反編譯看到
    public class BridgeMethodImpl implements IBridgeMethod<Integer>{
        public Integer info(Integer key) {
            return null;
        }
        // 編譯器為了讓子類有一個與父類的方法簽名一致的方法,就在子類自動生成一個與父類的方法簽名一致的橋接方法
        @Override
        public Object info(Object key) {
            return info((Integer)key);
        }
    }

型別擦除帶來的侷限性

型別擦除,是泛型能夠與之前的 java 版本程式碼相容共存的原因。但也因為型別擦除,它會抹掉很多繼承相關的特性,這是它帶來的侷限性。理解型別擦除有利於我們繞過開發當中可能遇到的雷區,同樣理解型別擦除也能讓我們繞過泛型本身的一些限制。比如:
Java基礎-泛型
正常情況下,因為泛型的限制,編譯器不讓最後一行程式碼編譯通過,因為類似不匹配,但是,基於以上對型別擦除的瞭解,利用反射,我們可以繞過這個限制:

    @Test
    public void t4() throws Exception {
        ArrayList<Integer> arrayList2 = new ArrayList<>();
        arrayList2.add(123);
        Method method = arrayList2.getClass().getDeclaredMethod("add",Object.class);
        method.invoke(arrayList2,"test");
        System.out.println(arrayList2);  //[123, test]
    }
    // 可以看到,利用型別擦除的原理,用反射的手段就繞過了正常開發中編譯器不允許的操作限制

泛型的PECS原則

  • Producer extends原則:當只想從容器中獲取元素,請把這個容器看成生產者,使用<? extends T>
  • Consumer super 原則:當只想操作容器中的元素,請把這個容器看成消費者,使用<? super T>
本作品採用《CC 協議》,轉載必須註明作者和本文連結
死磕,不要放棄,終將會有所收穫。

相關文章