細說 Java 泛型及其應用

aoho發表於2019-04-01

引出泛型

我們通過如下的示例,引出為什麼泛型的概念。

public class Test {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("abc");
        list.add(2);

        for (int i = 0; i < list.size(); i++) {
            String name = (String) list.get(i); // error
            System.out.println("name:" + name);
        }
    }
}
複製程式碼

當獲取列表中的第二個元素時,會報錯,java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。這是常見的型別轉換錯誤。

當我們將元素放入到列表中,並沒有使用指定的型別,在取出元素時使用的是預設的 Object 型別。因此很容易出現型別轉換的異常。

我們想要實現的結果是,集合能夠記住集合內元素各型別,且能夠達到只要編譯時不出現問題,執行時就不會出現 java.lang.ClassCastException 異常。泛型剛好能滿足我們的需求。

什麼是泛型?

泛型,即引數化型別。一提到引數,最熟悉的就是定義方法時有形參,然後呼叫此方法時傳遞實參。那麼引數化型別怎麼理解呢?顧名思義,就是將型別由原來的具體的型別引數化,類似於方法中的變數引數,此時型別也定義成引數形式(可以稱之為型別形參),然後在使用/呼叫時傳入具體的型別(型別實參)。

泛型的本質是為了引數化型別,即在不建立新的型別的情況下,通過泛型指定的不同型別來控制形參具體限制的型別。在泛型使用過程中,操作的資料型別被指定為一個引數,這種引數型別可以用在類、介面和方法中,分別被稱為泛型類、泛型介面和泛型方法。

泛型的特點

Java 語言中引入泛型是一個較大的功能增強。不僅語言、型別系統和編譯器有了較大的變化,已支援泛型,而且類庫也進行了大翻修,所以許多重要的類,比如集合框架,都已經成為泛型化的了。這帶來了很多好處:

  1. 型別安全。 泛型的主要目標是提高 Java 程式的型別安全。通過知道使用泛型定義的變數的型別限制,編譯器可以在一個高得多的程度上驗證型別假設。
  2. 消除強制型別轉換。 泛型的一個附帶好處是,消除原始碼中的許多強制型別轉換。這使得程式碼更加可讀,並且減少了出錯機會。
  3. 潛在的效能收益。 泛型為較大的優化帶來可能。在泛型的初始實現中,編譯器將強制型別轉換(沒有泛型的話,程式設計師會指定這些強制型別轉換)插入生成的位元組碼中。

命名型別引數

推薦的命名約定是使用大寫的單個字母名稱作為型別引數。對於常見的泛型模式,推薦的名稱是:

  • K:鍵,比如對映的鍵
  • V:值,比如 List 和 Set 的內容,或者 Map 中的值
  • E:元素
  • T:泛型
public class Generic<T> { 
    //key的型別為T  
    private T key;

    public Generic(T key) { 
    	//泛型構造方法形參key的型別也為T
        this.key = key;
    }

    public T getKey() { 
    	//泛型方法getKey的返回值型別為T
       return key;
    }
}
複製程式碼

如上定義了一個普通的泛型類,成員變數的型別為 T,T的型別由外部指定。泛型方法和泛型建構函式同樣如此。


Generic<Integer> genericInteger = new Generic<Integer>(123456); //1

Generic<String> genericString = new Generic<String>("key_vlaue"); // 2

System.out.println("key is " + genericInteger.getKey());
System.out.println("key is " + genericString.getKey());
複製程式碼

泛型的型別引數只能是類型別(包括自定義類),不能是簡單型別。傳入的實參型別需與泛型的型別引數型別相同,即為Integer/String。

如上所述,定義的泛型類,就一定要傳入泛型型別實參麼?

並不是這樣,在使用泛型的時候如果傳入泛型實參,則會根據傳入的泛型實參做相應的限制,此時泛型才會起到本應起到的限制作用。如果不傳入泛型型別實參的話,在泛型類中使用泛型的方法或成員變數定義的型別可以為任何的型別。

Generic genericString = new Generic("111111");
Generic genericInteger = new Generic(4444);

System.out.println("key is " + genericString.getKey());
System.out.println("key is " + genericInteger.getKey());
複製程式碼

如上的程式碼片段,將會輸出如下的結果:

key is 111111
key is 4444
複製程式碼

在不傳入泛型型別實參的情況下,泛型類中使用的泛型防範或成員變數可以為 Integer 或 String 等等其他任意型別。不過需要注意的是,泛型的型別引數只能是類型別,不能是簡單型別。且不能對確切的泛型型別使用 instanceof 操作。對於不同傳入的型別實參,生成的相應物件例項的型別是不是一樣的呢?具體看如下的示例:

public class GenericTest {

    public static void main(String[] args) {

        Generic<Integer> name = new Box<String>("111111");
        Generic<String> age = new Box<Integer>(712);

        System.out.println("name class:" + name.getClass());  
        System.out.println("age class:" + age.getClass()); 
        System.out.println(name.getClass() == age.getClass());    // true
    }

}
複製程式碼

由輸出結構可知,在使用泛型類時,雖然傳入了不同的泛型實參,但並沒有真正意義上生成不同的型別,傳入不同泛型實參的泛型類在記憶體上只有一個,即還是原來的最基本的型別(本例中為 Generic),當然在邏輯上我們可以理解成多個不同的泛型型別。

究其原因,在於 Java 中的泛型這一概念提出的目的,其只是作用於程式碼編譯階段。在編譯過程中,對於正確檢驗泛型結果後,會將泛型的相關資訊擦除。也就是說,成功編譯過後的 class 檔案中是不包含任何泛型資訊的。泛型資訊不會進入到執行時階段。

泛型型別在邏輯上看以看成是多個不同的型別,實際上都是相同的基本型別。

萬用字元

Ingeter 是 Number 的一個子類,同時 Generic<Ingeter>Generic<Number> 實際上是相同的一種基本型別。那麼問題來了,在使用 Generic<Number> 作為形參的方法中,能否使用Generic<Ingeter> 的例項傳入呢?在邏輯上類似於 Generic<Number>Generic<Ingeter> 是否可以看成具有父子關係的泛型型別呢?下面我們通過定義一個方法來驗證。

public void show(Generic<Number> obj) {
    System.out.println("key value is " + obj.getKey());
}
複製程式碼

進行如下的呼叫:

Generic<Integer> genericInteger = new Generic<Integer>(123);

show(genericInteger);  //error Generic<java.lang.Integer>  cannot be applied to Generic<java.lang.Number>
複製程式碼

通過提示資訊我們可以看到 Generic<Integer> 不能被看作為 Generic<Number> 的子類。由此可以看出:同一種泛型可以對應多個版本(因為引數型別是不確定的),不同版本的泛型類例項是不相容的。

我們不能因此定義一個 show(Generic<Integer> obj)來處理,因此我們需要一個在邏輯上可以表示同時是Generic和Generic父類的引用型別。由此型別萬用字元應運而生。

T、K、V、E 等泛型字母為有型別,型別引數賦予具體的值。除了有型別,還可以用萬用字元來表述型別, 未知型別,型別引數賦予不確定值,任意型別只能用在宣告型別、方法引數上,不能用在定義泛型類上。將方法改寫成如下:

public void show(Generic<?> obj) {
    System.out.println("key value is " + obj.getKey());
}
複製程式碼

此處 ? 是型別實參,而不是型別形參。即和 Number、String、Integer 一樣都是實際的型別,可以把 看成所有型別的父類,是一種真實的型別。可以解決當具體型別不確定的時候,這個萬用字元就是 ?;當操作型別時,不需要使用型別的具體功能時,只使用 Object 類中的功能。那麼可以用 ? 萬用字元來表未知型別。

泛型上下邊界

在使用泛型的時候,我們還可以為傳入的泛型型別實參進行上下邊界的限制,如:型別實參只准傳入某種型別的父類或某種型別的子類。為泛型新增上邊界,即傳入的型別實參必須是指定型別的子型別。

public void show(Generic<? extends Number> obj) {
    System.out.println("key value is " + obj.getKey());
}
複製程式碼

我們在泛型方法的入參限定引數型別為 Number 的子類。

Generic<String> genericString = new Generic<String>("11111");
Generic<Integer> genericInteger = new Generic<Integer>(2222);


showKeyValue1(genericString); // error
showKeyValue1(genericInteger);

複製程式碼

當我們的入參為 String 型別時,編譯報錯,因為 String 型別並不是 Number 型別的子類。

型別萬用字元上限通過形如 Generic<? extends Number> 形式定義;相對應的,型別萬用字元下限為Generic<? super Number>形式,其含義與型別萬用字元上限正好相反,在此不作過多闡述。

泛型陣列

在 java 中是不能建立一個確切的泛型型別的陣列的,即:

List<String>[] ls = new ArrayList<String>[10];  
複製程式碼

如上會編譯報錯,而使用萬用字元建立泛型陣列是可以的:

List<?>[] ls = new ArrayList<?>[10]; 

//List<String>[] ls = new ArrayList[10];
複製程式碼

JDK1.7 對泛型的簡化,所以另一種宣告也是可以的。

由於JVM泛型的擦除機制,在執行時 JVM 是不知道泛型資訊的。泛型陣列實際的執行時物件陣列只能是原始型別( T[]為Object[],Pair[]為Pair[] ),而實際的執行時陣列物件可能是T型別( 雖然執行時會擦除成原始型別 )。成功建立泛型陣列的唯一方式就是建立一個被擦出型別的新陣列,然後對其轉型。

public class GenericArray<T> {
    private Object[] array;  //維護Object[]型別陣列
    @SupperessWarning("unchecked")
    public GenericArray(int v) {
        array = new Object[v];
    }
    public void put(int index, T item) {
        array[index] = item;
    }
    public T get(int index) { 
    	return (T)array[index]; 
    } //陣列物件出口強轉
    public T[] rep() { return (T[])array; } //執行時無論怎樣都是Object[]型別 
    public static void main (String[] args){
        GenericArray<Integer> ga = new GenericArray<Integer>(10);
        // Integer[] ia = ga.rep(); //依舊ClassCastException
        Object[] oa = ga.rep(); //只能返回物件陣列型別為Object[]
        ga.put(0, 11);
        System.out.println(ga.get(0)); // 11
    }
}
複製程式碼

在執行時,陣列物件的出口做轉型輸出,入口方法在編譯期已實現型別安全,所以出口方法可以放心強制型別轉換,保證成功。

小結

本文主要講了 Java 泛型的相關概念和應用。泛型使編譯器可以在編譯期間對型別進行檢查以提高型別安全,減少執行時由於物件型別不匹配引發的異常。由泛型的誕生介紹相關的概念,在保證程式碼質量的情況下,如何使用泛型去簡化開發。

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

java 泛型詳解

相關文章