從一知半解到揭曉Java高級語法—泛型


前言

泛型是Java基礎知識的重點,雖然我們在初學Java的時候,都學過泛型,覺得自己掌握對於Java泛型的使用(全是錯覺),往后的日子,當我們深入去閱讀一些框架源碼,你就發現了,自己會的只是簡單的使用,卻看不懂別人的泛型代碼是怎么寫的,還可以這樣,沒錯,別人寫出來的代碼那叫藝術,而我......

探討

Java語言為什么存在着泛型,而像一些動態語言Python,JavaScipt卻沒有泛型的概念?

原因是,像JavaC#這樣的靜態編譯型的語言,它們在傳遞參數的時候,參數的類型,必須是明確的,看一個例子,簡單編寫一個存放int類型的棧—StackInt,代碼如下:

public class StackInt {

    private int maxSize;
    private int[] items;
    private int top;

    public StackInt(int maxSize){
        this.maxSize = maxSize;
        this.items = new int[maxSize];
        this.top = -1;
    }

    public boolean isFull(){
        return this.top == this.maxSize-1;
    }

    public boolean isNull(){
        return this.top <= -1;
    }

    public boolean push(int value){
        if(this.isFull()){
            return false;
        }
        this.items[++this.top] = value;
        return true;
    }

    public int pop(){
        if(this.isNull()){
            throw new RuntimeException("當前棧中無數據");
        }
        int value = this.items[top];
        --top;
        return value;
    }
}

在這里使用構造函數初始化一個StackInt對象時,可以傳入String字符串嗎?很明顯是不行的,我們要求的是int類型,傳入字符串String類型,這樣在語法檢查階段時會報錯的,像Java這樣的靜態編譯型的語言,參數的類型要求是明確的

在這里插入圖片描述

泛型解決了什么問題?

參數不安全:引入泛型,能夠在編譯階段找出代碼的問題,而不是在運行階段

泛型要求在聲明時指定實際數據類型,Java 編譯器在編譯時會對泛型代碼做強類型檢查,並在代碼違反類型安全時發出告警。早發現,早治理,把隱患扼殺於搖籃,在編譯時發現並修復錯誤所付出的代價遠比在運行時小。

避免類型轉換:

未使用泛型:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);    //需要在取出Value的時候進行強制轉換

使用泛型:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   //不需要強制轉換

重復編碼::通過使用泛型,可以實現通用編碼,可以處理不同類型的集合,並且類型安全且易於閱讀。像上面的StackInt類,我們不能針對每個類型去編寫對應類型的棧,那樣太麻煩了,而泛型的出現就很好的解決了這點

擴展

在上面的StackInt類有一些不好的地方,那就是太具體了,不夠抽象,不夠抽象,那么它的復用性也是不高的,例如,在另外的場景下,我需要的是往棧里存String類型的字符串,或者是其他類型,那么StackInt類就做不到了,那么有什么方法能夠做到呢?再寫一個StackString類,不可能,那樣不得累死。那就只有引入基類Object了,我們改進一下代碼:

public class StackObject {

    private int maxSize;
    private Object[] items;
    private int top;

    public StackObject(int maxSize){
        this.maxSize = maxSize;
        this.items = new Object[maxSize];
        this.top = -1;
    }

    public boolean isFull(){
        return this.top == this.maxSize-1;
    }

    public boolean isNull(){
        return this.top <= -1;
    }

    public boolean push(Object value){
        if(this.isFull()){
            return false;
        }
        this.items[++this.top] = value;
        return true;
    }

    public Object pop(){
        if(this.isNull()){
            throw new RuntimeException("當前棧中無數據");
        }
        Object value = this.items[top];
        --top;
        return value;
    }
}

使用StackObject可以存儲任意類型的數據,那么這樣做,又有什么優點和缺點呢?

優點:StackObject類變得相對抽象了,我們可以往里面存儲任何類型的數據,這樣就避免了寫一些重復代碼
在這里插入圖片描述

缺點:

1、用Object表示的對象是比較抽象的,它失去了類型的特點,那么我們在做一些運算的時候,可能會頻繁的拆箱裝箱的過程

在這里插入圖片描述

看上面的例圖,我們理解的認為存放了兩個數值,1234554321,將兩個進行相加,這是很常見的操作,但是報錯了,編譯器給我們的提示是,+操作運算不能用於兩個Object類型,那么只能對其進行類型轉換,這也是我們上面說到的泛型能解決的問題,我們需要這樣做,int sum = (int)val1 + (int)val2;,同時在涉及拆箱裝箱時,是有一定性能的損耗的,關於拆箱裝箱在這里不作描述,可以參考我寫過的隨筆—— 深入理解Java之裝箱與拆箱

2、對於我們push進去的值,我們在取出的時候,容易忘記類型轉換,或者不記得它的類型,類型轉換錯誤,這在后面的一些業務可能埋下禍根,例如下面這個場景:直到運行時錯誤才暴露出來,這是不安全的,也是違反軟件開發原則的,應該盡早的在編譯階段就發現問題,解決問題

在這里插入圖片描述

3、使用Object太過於模糊了,沒有具體類型的意義

最好不要用到Object,因為Object是一切類型的基類,也就是說他把一些類型的特點給抹除了,比如上面存的數字,對於數字來說,加法運算就是它的一個特點,但是用了Object,它就失去了這一特點,失去類型特有的行為

引入泛型

什么是泛型?

泛型:是被參數化的類或接口,是對類型的約定

泛型類

class name<T1, T2, ..., Tn> { /* ... */ }

一般將泛型中的類名稱為原型,而將 <> 指定的參數稱為類型參數<> 相當於類型的約定,T就是類型,相當於一個占位符,由我們在調用時指定

使用泛型改進一下上面StackObject類,但是,數組和泛型不能很好地結合。你不能實例化具有參數化類型的數組,例如下面的代碼是不合格的:

public StackT(int maxSize){
    this.maxSize = maxSize;
    this.items = new T[maxSize];
    this.top = -1;
}

在這里插入圖片描述

Java 中不允許直接創建泛型數組,這是因為相比於C++,C#的語法,Java泛型其實是偽泛型,這點在后面會說到,但是,可以通過創建一個類型擦除的數組,然后轉型的方式來創建泛型數組。

private int maxSize;
private T[] items;     
private int top;

public StackT(int maxSize){
    this.maxSize = maxSize;
    this.items = (T[]) new Object[maxSize];
    this.top = -1;
}

實際上,真的需要存儲泛型,還是使用容器更合適,回到原來的代碼上,需要知道的是,泛型類型不能是基本類型的,需要是包裝類

在這里插入圖片描述

上面說到了Java 中不允許直接創建泛型數組,事實上,Java中的泛型我們是很難通new的方式去實例化對象,不僅僅是實例化對象,甚至是獲取T的真實類型也是很難的,當然通過反射的機制還是可以獲取到的,Java獲取真實類型的方式有 3 種,分別是:

1、類名.class

2、對象.getClass

3、class.forName("全限定類名")

但是,在這里,12的方式都是做不到的,雖然我們在外邊明確的傳入了Integer類型,new StackT<Integer>(3);但是在StackT

類,使用T.class還是獲取不到真實類型的,第 2 種方式的話,並沒有傳入對象,前面也說到是沒有辦法new方式實例化的,而通過反射機制是可以做到的,這里不作演示,需要了解的話可以參考 —— Java如何獲得泛型類的真實類型Java通過反射獲取泛型的類型

在這里插入圖片描述

但是在C#中的泛型以及C++的模板,這是很容易做到的,所以說Java的泛型是偽泛型,Java並不是做不到像C#一樣,而是為了遷就老的JDK語法所作出的妥協,至於上面為什么做不到這樣,這就要說到泛型的類型擦除了。

再說類型擦除之前,先說一下泛型接口,和泛型方法吧

泛型接口

接口也可以聲明泛型,泛型接口語法形式:

public interface Content<T> {
    T text();
}

泛型接口有兩種實現方式:

  • 實現接口的子類明確聲明泛型類型
public class ContentImpl implements Content<Integer> {
    private int text;

    public ContentImpl(int text) {
        this.text = text;
    }

    public static void main(String[] args) {
        ContentImpl one = new ContentImpl(10);
        System.out.print(one.text());
    }
}
// Output:
// 10
  • 實現接口的子類不明確聲明泛型類型
public class ContentImpl<T> implements Content<T> {
    private T text;

    public ContentImpl(T text) {
        this.text = text;
    }

    @Override
    public T text() { return text; }

    public static void main(String[] args) {
        ContentImpl<String> two = new ContentImpl<>("ABC");
        System.out.print(two.text());
    }
}
// Output:
// ABC

泛型方法

泛型方法是引入其自己的類型參數的方法。泛型方法可以是普通方法、靜態方法以及構造方法。

泛型方法語法形式如下:

public <T> T func(T obj) {}

是否擁有泛型方法,與其所在的類是否是泛型沒有關系。

泛型方法的語法包括一個類型參數列表,在尖括號內,它出現在方法的返回類型之前。對於靜態泛型方法,類型參數部分必須出現在方法的返回類型之前。類型參數能被用來聲明返回值類型,並且能作為泛型方法得到的實際類型參數的占位符。

使用泛型方法的時候,通常不必指明類型參數,因為編譯器會為我們找出具體的類型。這稱為類型參數推斷(type argument inference)。類型推斷只對賦值操作有效,其他時候並不起作用。如果將一個泛型方法調用的結果作為參數,傳遞給另一個方法,這時編譯器並不會執行推斷。編譯器會認為:調用泛型方法后,其返回值被賦給一個 Object 類型的變量。

public class GenericsMethod {
    public static <T> void printClass(T obj) {
        System.out.println(obj.getClass().toString());
    }

    public static void main(String[] args) {
        printClass("abc");
        printClass(10);
    }
}
// Output:
// class java.lang.String
// class java.lang.Integer

泛型方法中也可以使用可變參數列表

public class GenericVarargsMethod {
    public static <T> List<T> makeList(T... args) {
        List<T> result = new ArrayList<T>();
        Collections.addAll(result, args);
        return result;
    }

    public static void main(String[] args) {
        List<String> ls = makeList("A");
        System.out.println(ls);
        ls = makeList("A", "B", "C");
        System.out.println(ls);
    }
}
// Output:
// [A]
// [A, B, C]

類型擦除

事實上,Java的運行大致可以分為兩個階段,編譯階段運行階段

那么對於Java泛型來說,當編譯階段過后,泛型 T 是已經被擦除了,所以在運行階段,它已經丟失了 T 的具體信息,而我們去實例化一個對象的時候,比如T c = new T();,它的發生時機是在運行階段,而在運行階段,你要new T(),就需要知道 T 的具體類型,實際上這時候 T是被替換成Integer了,而JVM是不知道T的類型的,所以是沒有辦法實例化的。

那么,類型擦除做了什么呢?它做了以下工作:

  • 把泛型中的所有類型參數替換為 Object,如果指定類型邊界,則使用類型邊界來替換。因此,生成的字節碼僅包含普通的類,接口和方法。
  • 擦除出現的類型聲明,即去掉 <> 的內容。比如 T get() 方法聲明就變成了 Object get()List<String> 就變成了 List。如有必要,插入類型轉換以保持類型安全。
  • 生成橋接方法以保留擴展泛型類型中的多態性。類型擦除確保不為參數化類型創建新類;因此,泛型不會產生運行時開銷。

讓我們來看一個示例:

import java.util.*;

public class ErasedTypeEquivalence {

    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }

}
/* Output:
true
*/

ArrayList<String>ArrayList<Integer> 應該是不同的類型。不同的類型會有不同的行為。例如,如果嘗試向 ArrayList<String> 中放入一個 Integer,所得到的行為(失敗)和 向 ArrayList<Integer> 中放入一個 Integer 所得到的行為(成功)完全不同。但是結果輸出的是true,這意味着使用泛型時,任何具體的類型信息都被擦除了,ArrayList<Object>ArrayList<Integer> 在運行時,JVM 將它們視為同一類型class java.util.ArrayList

再用一個例子來對於該謎題的補充:

import java.util.*;

class Frob {}
class Fnorkle {}
class Quark<Q> {}

class Particle<POSITION, MOMENTUM> {}

public class LostInformation {

    public static void main(String[] args) {
        
        List<Frob> list = new ArrayList<>();
        Map<Frob, Fnorkle> map = new HashMap<>();
        Quark<Fnorkle> quark = new Quark<>();
        Particle<Long, Double> p = new Particle<>();
        
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
    }

}
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/

根據 JDK 文檔,Class.getTypeParameters() “返回一個 TypeVariable 對象數組,表示泛型聲明中聲明的類型參數...” 這暗示你可以發現這些參數類型。但是正如上例中輸出所示,你只能看到用作參數占位符的標識符,這並非有用的信息。

殘酷的現實是:在泛型代碼內部,無法獲取任何有關泛型參數類型的信息。

以上兩個例子皆出《Java 編程思想》第五版 —— On Java 8中的例子,本文借助該例子,試圖講清楚Java泛型是使用類型擦除這里機制實現的,能力不足,有錯誤的地方,還請指正。關於On Java 8一書,已在github上開源,並有熱心的伙伴將之翻譯成中文,現在給出閱讀地址,On Java 8

擦除的問題

擦除的代價是顯著的。泛型不能用於顯式地引用運行時類型的操作中,例如轉型、instanceof 操作和 new 表達式。因為所有關於參數的類型信息都丟失了,當你在編寫泛型代碼時,必須時刻提醒自己,你只是看起來擁有有關參數的類型信息而已。

考慮如下的代碼段:

class Foo<T> {
    T var;
}

看上去當你創建一個 Foo 實例時:

Foo<Cat> f = new Foo<>();

class Foo 中的代碼應該知道現在工作於 Cat 之上。泛型語法也在強烈暗示整個類中所有 T 出現的地方都被替換,就像在 C++ 中一樣。但是事實並非如此,當你在編寫這個類的代碼時,必須提醒自己:“不,這只是一個 Object“。

繼承問題

泛型時基於類型擦除實現的,所以,泛型類型無法向上轉型

向上轉型是指用子類實例去初始化父類,這是面向對象中多態的重要表現。

在這里插入圖片描述

Integer 繼承了 ObjectArrayList 繼承了 List;但是 List<Interger> 卻並非繼承了 List<Object>

這是因為,泛型類並沒有自己獨有的 Class 類對象。比如:並不存在 List<Object>.class 或是 List<Interger>.class,Java 編譯器會將二者都視為 List.class

如何解決上面所產生的問題:

其實並不一定要通過new的方式去實例化,我們可以通過顯式的傳入源類,一個Class<T> clazz的對象來補償擦除,例如instanceof 操作,在程序中嘗試使用 instanceof 將會失敗。類型標簽可以使用動態 isInstance() ,這樣改進代碼:

public class Improve<T> {
	
    //錯誤方法
    public boolean  f(Object arg) {
        // error: illegal generic type for instanceof
        if (arg instanceof T) {
            return true;
        }
        return false;
    }
    //改進方法
    Class<T> clazz;
    
	public Improve(Class<T> clazz) {
        this.clazz = clazz;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }
}

實例化:

試圖在 new T() 是行不通的,部分原因是由於擦除,部分原因是編譯器無法驗證 T 是否具有默認(無參)構造函數。

Java 中的解決方案是傳入一個工廠對象,並使用該對象創建新實例。方便的工廠對象只是 Class 對象,因此,如果使用類型標記,則可以使用 newInstance() 創建該類型的新對象:

class Improve<T> {
    Class<T> kind;

    Improve(Class<T> kind) {
        this.kind = kind;
    }
    
    public T get(){
        try {
            return kind.newInstance();
        } catch (InstantiationException |
                IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

class Employee {
    @Override
    public String toString() {
        return "Employee";
    }
}

public class InstantiateGenericType {
    public static void main(String[] args) {
        Improve<Employee> fe = new Improve<>(Employee.class);
        System.out.println(fe.get());
    }
}
/* Output:
Employee
*/

通過這樣改進代碼,可以實現創建對象的實例,但是要注意的是,newInstance();方法調用無參構造函數的,如果傳入的類型,沒有無參構造的話,是會拋出InstantiationException異常的。

泛型數組

泛型數組這部分,我們在上面說到可以通過創建一個類型擦除的數組,然后轉型的方式來創建泛型數組,這次我們可以通過顯式的傳入源類的方式來編寫StackT類,解決創建泛型數組的問題,代碼如下:

public class StackT<T> {

    private int maxSize;
    private T[] items;
    private int top;

    public StackT(int maxSize, Class<T> clazz){
        this.maxSize = maxSize;
        this.items = this.createArray(clazz);
        this.top = -1;
    }

    public boolean isFull(){
        return this.top == this.maxSize-1;
    }

    public boolean isNull(){
        return this.top <= -1;
    }

    public boolean push(T value){
        if(this.isFull()){
            return false;
        }
        this.items[++this.top] = value;
        return true;
    }

    public T pop(){
        if(this.isNull()){
            throw new RuntimeException("當前棧中無數據");
        }
        T value = this.items[top];
        --top;
        return value;
    }

    private T[] createArray(Class<T> clazz){
        T[] array =(T[])Array.newInstance(clazz, this.maxSize);
        return array;
    }

}

邊界

有時您可能希望限制可在參數化類型中用作類型參數的類型。類型邊界可以對泛型的類型參數設置限制條件。例如,對數字進行操作的方法可能只想接受 Number 或其子類的實例。

要聲明有界類型參數,請列出類型參數的名稱,然后是 extends 關鍵字,后跟其限制類或接口。

類型邊界的語法形式如下:

<T extends XXX>

示例:

public class GenericsExtendsDemo01 {
    static <T extends Comparable<T>> T max(T x, T y, T z) {
        T max = x; // 假設x是初始最大值
        if (y.compareTo(max) > 0) {
            max = y; //y 更大
        }
        if (z.compareTo(max) > 0) {
            max = z; // 現在 z 更大
        }
        return max; // 返回最大對象
    }

    public static void main(String[] args) {
        System.out.println(max(3, 4, 5));
        System.out.println(max(6.6, 8.8, 7.7));
        System.out.println(max("pear", "apple", "orange"));
    }
}
// Output:
// 5
// 8.8
// pear

示例說明:

上面的示例聲明了一個泛型方法,類型參數 T extends Comparable<T> 表明傳入方法中的類型必須實現了 Comparable 接口。

類型邊界可以設置多個,語法形式如下:

<T extends B1 & B2 & B3>

注意:extends 關鍵字后面的第一個類型參數可以是類或接口,其他類型參數只能是接口。

通配符

通配符是Java泛型中的一個非常重要的知識點。很多時候,我們其實不是很理解通配符和泛型類型T區別,容易混淆在一起,其實還是很好理解的,T 都表示不確定的類型,區別在於我們可以對 T 進行操作,但是對 不行,比如如下這種 :

// 可以
T t = operate();
// 不可以
? car = operate();

但是這個並不是我們混淆的原因,雖然T 都表示不確定的類型,T 通常用於泛型類和泛型方法的定義,通常用於泛型方法的調用代碼和形參,不能用於定義類和泛型方法。用代碼解釋一下,回到文章最初說的棧類StackT,我們以這個為基礎來解釋,上面的觀點:

public class Why {
    public static void main(String[] args) {

        StackT<Integer> stackT = new StackT<>(3, Integer.class);
        stackT.push(8);
        StackT<String> stackT1 = new StackT<>(3, String.class);
        stackT1.push("7");
        test(stackT1);

    }
    public static void test(StackT stackT){
        System.out.println(stackT.pop());
    }
}
// Output: 8

以我們編寫的StackT類,進行測試,編寫一個test方法,傳入參數類型StackT,上面的程序正常輸出字符串"7" ,這沒有什么問題,問題在這里失去了泛型的限定,傳進去的實參StackT1,是被我們限定為StackT<String> ,但是我們通過編譯器可以看到stackT.pop()出來的對象,並沒有String類型的特有方法,也就是說,它其實是Object

在這里插入圖片描述

那么我們就需要修改test方法的形參,改為:

public static void test(StackT<String> stackT){
    System.out.println(stackT.pop());
}

這樣子就回到了我們問題的本質來了,將形參修改為StackT<String>,這起到了泛型的限定作用,但是會出現這樣的問題,如果我們需要向該方法傳入StackT<Integer>類型的對象 stackT是,因為方法形參限定了StackT<String>,,這時候就報錯了

在這里插入圖片描述

這個時候就是通配符?起作用了,將方法形參改為StackT<?>就可以了,這也就確定了我們剛剛的結論,通配符通常是用於泛型傳參,而不是泛型類的定義。

public static void test(StackT<?> stackT){
    System.out.println(stackT.pop());
}

但是這種用法我們通常也不會去用,因為它還是失去了類型的特點,即當無界泛型通配符作為形參時,作為調用方,並不限定傳遞的實際參數類型。但是,在方法內部,泛型類的參數和返回值為泛型的方法,不能使用!

在這里插入圖片描述

這里,StackT.push就不能用了,因為我並不知道?傳的是Integer還是String ,還是其他類型,所以是會報錯的。

但是我們有時候是有這樣的需求的,我們在接收泛型棧StackT作為形參的時候,我想表達一種約束的關系,但是又不像StackT<String>一樣,約束的比較死板,而Java是面向對象的語言,那么就會有繼承的機制,我想要的約束關系是我能接收的泛型棧的類型都是Number類的派生類,即不會像?無界通配符一樣失去類的特征,又不會像StackT<String>約束的很死,這就引出了上界通配符的概念。

上界通配符

可以使用上界通配符來縮小類型參數的類型范圍。

它的語法形式為:<? extends Number>

public class Why {
    public static void main(String[] args) {

        StackT<Integer> stackT = new StackT<>(3, Integer.class);
        stackT.push(8);
        StackT<String> stackT1 = new StackT<>(3, String.class);
        stackT1.push("7");
        StackT<Double> stackT2 = new StackT<>(3, Double.class);
        
        //通過
        test(stackT);
        test(stackT2);
        //error
        test(stackT1);

    }
    
    public static void test(StackT<? extends Number> stackT){

        System.out.println(stackT.pop());
    }
}

這樣就實現了一類類型的限定,但是需求變更了,我現在希望的約束關系是我能接收的泛型棧的類型都是Number類的父類,或者父類的父類,那么有上界,自然就有下界

下界通配符

下界通配符將未知類型限制為該類型的特定類型或超類類型。

注意:上界通配符和下界通配符不能同時使用

它的語法形式為:<? super Number>

public class Why {
    public static void main(String[] args) {

        StackT<Number> stackT1 = new StackT<>(3, Number.class);
        stackT1.push(8);
        StackT<Double> stackT2 = new StackT<>(3, Double.class);
        StackT<Object> stackT3 = new StackT<>(3, Object.class);
        //通過
        test(stackT1);
        test(stackT3);
        //error
        test(stackT2);

    }

    public static void test(StackT<? super Number> stackT){

        System.out.println(stackT.pop());
    }
}

這樣子的話,就確保了我們的test方法只接收Number類型以上的方法。泛型的各種高級語法可能在寫業務代碼的時候可以規避,但是如果你要去寫一些框架的時候,由於你不知道框架的使用者的使用場景,那么掌握泛型的高級語法就很有用了。

通配符和向上轉型

前面,我們提到:泛型不能向上轉型。但是,我們可以通過使用通配符來向上轉型

public class GenericsWildcardDemo {
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        List<Number> numList = intList;  // Error

        List<? extends Integer> intList2 = new ArrayList<>();
        List<? extends Number> numList2 = intList2;  // OK
    }
}

通配符邊界問題,關於一些更加深入的解惑可以參考整理的轉載的文章——Java泛型解惑之上下通配符

泛型約束

Pair<int, char> p = new Pair<>(8, 'a');  // 編譯錯誤
public static <E> void append(List<E> list) {
    E elem = new E();  // 編譯錯誤
    list.add(elem);
}
public class MobileDevice<T> {
    private static T os; // error

    // ...
}
public static <E> void rtti(List<E> list) {
    if (list instanceof ArrayList<Integer>) {  // 編譯錯誤
        // ...
    }
}
List<Integer> li = new ArrayList<>();
List<Number>  ln = (List<Number>) li;  // 編譯錯誤
List<Integer>[] arrayOfLists = new List<Integer>[2];  // 編譯錯誤
// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ }    // 編譯錯誤

// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // 編譯錯誤
public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   // compile-time error
        // ...
    }
}
public class Example {
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { } // 編譯錯誤
}

實踐總結

泛型命名

泛型一些約定俗成的命名:

  • E - Element
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

使用泛型的建議

  • 消除類型檢查告警
  • List 優先於數組
  • 優先考慮使用泛型來提高代碼通用性
  • 優先考慮泛型方法來限定泛型的范圍
  • 利用有限制通配符來提升 API 的靈活性
  • 優先考慮類型安全的異構容器

參考資料:

深入理解 Java 泛型

On Java 8

Java泛型解惑之 extends T>和 super T>上下界限

7月的直播課——Java 高級語法—泛型


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM