深入理解 Java 泛型

靜默虛空發表於2019-03-21

:notebook: 本文已歸檔到:「blog

:keyboard: 本文中的示例程式碼已歸檔到:「javacore

為什麼需要泛型

JDK5 引入了泛型機制

為什麼需要泛型呢?回答這個問題前,先讓我們來看一個示例。

public class NoGenericsDemo {
    public static void main(String[] args) {
        List list = new ArrayList<>();
        list.add("abc");
        list.add(18);
        list.add(new double[] {1.0, 2.0});
        Object obj1 = list.get(0);
        Object obj2 = list.get(1);
        Object obj3 = list.get(2);
        System.out.println("obj1 = [" + obj1 + "]");
        System.out.println("obj2 = [" + obj2 + "]");
        System.out.println("obj3 = [" + obj3 + "]");

        int num1 = (int)list.get(0);
        int num2 = (int)list.get(1);
        int num3 = (int)list.get(2);
        System.out.println("num1 = [" + num1 + "]");
        System.out.println("num2 = [" + num2 + "]");
        System.out.println("num3 = [" + num3 + "]");
    }
}
// Output:
// obj1 = [abc]
// obj2 = [18]
// obj3 = [[D@47089e5f]
// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
// at io.github.dunwu.javacore.generics.NoGenericsDemo.main(NoGenericsDemo.java:23)
複製程式碼

示例說明:

在上面的示例中,List 容器沒有指定儲存資料型別,這種情況下,可以向 List 新增任意型別資料,編譯器不會做型別檢查,而是默默的將所有資料都轉為 Object

假設,最初我們希望向 List 儲存的是整形資料,假設,某個傢伙不小心存入了其他資料型別。當你試圖從容器中取整形資料時,由於 List 當成 Object 型別來儲存,你不得不使用型別強制轉換。在執行時,才會發現 List 中資料不儲存一致的問題,這就為程式執行帶來了很大的風險(無形傷害最為致命)。

而泛型的出現,解決了型別安全問題。

泛型具有以下優點:

  • 編譯時的強型別檢查

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

  • 避免了型別轉換

未使用泛型:

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
複製程式碼

使用泛型:

List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast
複製程式碼
  • 泛型程式設計可以實現通用演算法

通過使用泛型,程式設計師可以實現通用演算法,這些演算法可以處理不同型別的集合,可以自定義,並且型別安全且易於閱讀。

泛型型別

泛型型別是被引數化的類或介面。

泛型類

泛型類的語法形式:

class name<T1, T2, ..., Tn> { /* ... */ }
複製程式碼

泛型類的宣告和非泛型類的宣告類似,除了在類名後面新增了型別引數宣告部分。由尖括號(<>)分隔的型別引數部分跟在類名後面。它指定型別引數(也稱為型別變數)T1,T2,...和 Tn。

一般將泛型中的類名稱為原型,而將 <> 指定的引數稱為型別引數

  • 未應用泛型的類

在泛型出現之前,如果一個類想持有一個可以為任意型別的資料,只能使用 Object 做型別轉換。示例如下:

public class Info {
	private Object value;

	public Object getValue() {
		return value;
	}

	public void setValue(Object value) {
		this.value = value;
	}
}
複製程式碼
  • 單型別引數的泛型類
public class Info<T> {
    private T value;

    public Info() { }

    public Info(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Info{" + "value=" + value + '}';
    }
}

public class GenericsClassDemo01 {
    public static void main(String[] args) {
        Info<Integer> info = new Info<>();
        info.setValue(10);
        System.out.println(info.getValue());

        Info<String> info2 = new Info<>();
        info2.setValue("xyz");
        System.out.println(info2.getValue());
    }
}
// Output:
// 10
// xyz
複製程式碼

在上面的例子中,在初始化一個泛型類時,使用 <> 指定了內部具體型別,在編譯時就會根據這個型別做強型別檢查。

實際上,不使用 <> 指定內部具體型別,語法上也是支援的(不推薦這麼做),如下所示:

public static void main(String[] args) {
    Info info = new Info();
    info.setValue(10);
    System.out.println(info.getValue());
    info.setValue("abc");
    System.out.println(info.getValue());
}
複製程式碼

示例說明:

上面的例子,不會產生編譯錯誤,也能正常執行。但這樣的呼叫就失去泛型型別的優勢。

  • 多個型別引數的泛型類
public class MyMap<K,V> {
    private K key;
    private V value;

    public MyMap(K key, V value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public String toString() {
        return "MyMap{" + "key=" + key + ", value=" + value + '}';
    }
}

public class GenericsClassDemo02 {
    public static void main(String[] args) {
        MyMap<Integer, String> map = new MyMap<>(1, "one");
        System.out.println(map);
    }
}
// Output:
// MyMap{key=1, value=one}
複製程式碼
  • 泛型類的型別巢狀
public class GenericsClassDemo03 {
    public static void main(String[] args) {
        Info<String> info = new Info("Hello");
        MyMap<Integer, Info<String>> map = new MyMap<>(1, info);
        System.out.println(map);
    }
}
// Output:
// MyMap{key=1, value=Info{value=Hello}}
複製程式碼

泛型介面

介面也可以宣告泛型。

泛型介面語法形式:

public interface Content<T> {
    T text();
}
複製程式碼

泛型介面有兩種實現方式:

  • 實現介面的子類明確宣告泛型型別


public class GenericsInterfaceDemo01 implements Content<Integer> {
    private int text;

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

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

    public static void main(String[] args) {
        GenericsInterfaceDemo01 demo = new GenericsInterfaceDemo01(10);
        System.out.print(demo.text());
    }
}
// Output:
// 10
複製程式碼
  • 實現介面的子類不明確宣告泛型型別
public class GenericsInterfaceDemo02<T> implements Content<T> {
    private T text;

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

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

    public static void main(String[] args) {
        GenericsInterfaceDemo02<String> gen = new GenericsInterfaceDemo02<>("ABC");
        System.out.print(gen.text());
    }
}
// Output:
// ABC
複製程式碼

泛型方法

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

泛型方法語法形式如下:

public <T> T func(T obj) {}
複製程式碼

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

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

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

public class GenericsMethodDemo01 {
    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 GenericVarargsMethodDemo {
    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 語言引入泛型是為了在編譯時提供更嚴格的型別檢查,並支援泛型程式設計。不同於 C++ 的模板機制,Java 泛型是使用型別擦除來實現的,使用泛型時,任何具體的型別資訊都被擦除了

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

  • 把泛型中的所有型別引數替換為 Object,如果指定型別邊界,則使用型別邊界來替換。因此,生成的位元組碼僅包含普通的類,介面和方法。
  • 擦除出現的型別宣告,即去掉 <> 的內容。比如 T get() 方法宣告就變成了 Object get()List<String> 就變成了 List。如有必要,插入型別轉換以保持型別安全。
  • 生成橋接方法以保留擴充套件泛型型別中的多型性。型別擦除確保不為引數化型別建立新類;因此,泛型不會產生執行時開銷。

讓我們來看一個示例:

public class GenericsErasureTypeDemo {
    public static void main(String[] args) {
        List<Object> list1 = new ArrayList<Object>();
        List<String> list2 = new ArrayList<String>();
        System.out.println(list1.getClass());
        System.out.println(list2.getClass());
    }
}
// Output:
// class java.util.ArrayList
// class java.util.ArrayList
複製程式碼

示例說明:

上面的例子中,雖然指定了不同的型別引數,但是 list1 和 list2 的類資訊卻是一樣的。

這是因為:使用泛型時,任何具體的型別資訊都被擦除了。這意味著:ArrayList<Object>ArrayList<String> 在執行時,JVM 將它們視為同一型別。

Java 泛型的實現方式不太優雅,但這是因為泛型是在 JDK5 時引入的,為了相容老程式碼,必須在設計上做一定的折中。

泛型和繼承

泛型不能用於顯式地引用執行時型別的操作之中,例如:轉型、instanceof 操作和 new 表示式。因為所有關於引數的型別資訊都丟失了。當你在編寫泛型程式碼時,必須時刻提醒自己,你只是看起來好像擁有有關引數的型別資訊而已。

正是由於泛型時基於型別擦除實現的,所以,泛型型別無法向上轉型

向上轉型是指用子類例項去初始化父類,這是物件導向中多型的重要表現。

深入理解 Java 泛型

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

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

List<Integer> list = new ArrayList<>();
List<Object> list2 = list; // Erorr
複製程式碼

型別邊界

有時您可能希望限制可在引數化型別中用作型別引數的型別。型別邊界可以對泛型的型別引數設定限制條件。例如,對數字進行操作的方法可能只想接受 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 關鍵字後面的第一個型別引數可以是類或介面,其他型別引數只能是介面。

示例:

public class GenericsExtendsDemo02 {
    static class A { /* ... */ }
    interface B { /* ... */ }
    interface C { /* ... */ }
    static class D1 <T extends A & B & C> { /* ... */ }
    static class D2 <T extends B & A & C> { /* ... */ } // 編譯報錯
    static class E extends A implements B, C { /* ... */ }

    public static void main(String[] args) {
        D1<E> demo1 = new D1<>();
        System.out.println(demo1.getClass().toString());
        D1<String> demo2 = new D1<>(); // 編譯報錯
    }
}
複製程式碼

型別萬用字元

型別萬用字元一般是使用 ? 代替具體的型別引數。例如 List<?> 在邏輯上是 List<String>List<Integer> 等所有 List<具體型別實參> 的父類。

上界萬用字元

可以使用**上界萬用字元**來縮小型別引數的型別範圍。

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

public class GenericsUpperBoundedWildcardDemo {
    public static double sumOfList(List<? extends Number> list) {
        double s = 0.0;
        for (Number n : list) {
            s += n.doubleValue();
        }
        return s;
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        System.out.println("sum = " + sumOfList(li));
    }
}
// Output:
// sum = 6.0
複製程式碼

下界萬用字元

**下界萬用字元**將未知型別限制為該型別的特定型別或超類型別。

注意:上界萬用字元和下界萬用字元不能同時使用

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

public class GenericsLowerBoundedWildcardDemo {
    public static void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        addNumbers(list);
        System.out.println(Arrays.deepToString(list.toArray()));
    }
}
// Output:
// [1, 2, 3, 4, 5]
複製程式碼

無界萬用字元

無界萬用字元有兩種應用場景:

  • 可以使用 Object 類中提供的功能來實現的方法。
  • 使用不依賴於型別引數的泛型類中的方法。

語法形式:<?>

public class GenericsUnboundedWildcardDemo {
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        List<String> ls = Arrays.asList("one", "two", "three");
        printList(li);
        printList(ls);
    }
}
// Output:
// 1 2 3
// one two three
複製程式碼

萬用字元和向上轉型

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

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
    }
}
複製程式碼

擴充套件閱讀:Oracle 泛型文件

泛型的約束

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 泛型

參考資料

相關文章