Java 泛型詳解

本文部分摘自 On Java 8

概述

在 Java5 以前,普通的類和方法只能使用特定的類型:基本數據類型或類類型,如果編寫的代碼需要應用於多種類型,這種嚴苛的限制對代碼的束縛就會很大

Java5 的一個重大變化就是引入泛型,泛型實現了參數化類型,使得你編寫的組件(通常是集合)可以適用於多種類型。泛型的初衷是通過解耦類或方法與所使用的類型之間的約束,使得類或方法具備最寬泛的表達力。然而很快你就會發現,Java 中的泛型並沒有你想的那麼完美,甚至存在一些令人迷惑的實現

泛型類

促成泛型出現的最主要動機之一就是為了創建集合類,集合用於存放要使用到的對象。現有一個只能持有單個對象的類:

class Automobile {}

public class Holder1 {
    private Automobile a;
    public Holder1(Automobile a) { this.a = a; }
    Automobile get() { return a; }
}

如果沒有泛型,那麼就必須明確指定其持有的對象的類型,會導致該復用性不高,它無法持有其他類型的對象,我們當然不希望為每個類型都編寫一個新類

在 Java5 以前,為了解決這個問題,我們可以讓這個類直接持有 Object 類型的對象,這樣就可以持有多種不同類型的對象了。但通常而言,我們只會用集合存儲同一類型的對象。泛型的主要目的之一就是用來約定集合要存儲什麼類型的對象,並且通過編譯器確保規約得以滿足

所以,與其使用 Object,我們更希望先指定一個類型佔位符,稍後再決定具體使用什麼類型。由此我們需要使用類型參數,用尖括號括住,放在類名後面。然後在使用這個類時,再用實際的類型替換此類型參數

public class GenericHolder<T> {
    private T a;
    public GenericHolder() {}
    public void set(T a) { this.a = a; }
    public T get() { return a; }

    public static void main(String[] args) {
        // 在 Java7 中右邊的尖括號可以為空
        GenericHolder<Automobile> h2 = new GenericHolder<Automobile>();
        GenericHolder<Automobile> h3 = new GenericHolder<>();
        h3.set(new Automobile()); // 此處有類型校驗
        Automobile a = h3.get();  // 無需類型轉換
        //- h3.set("Not an Automobile"); // 報錯
    }
}

元組類庫

有時一個方法需要能返回多個對象,而 return語句只能返回單個對象,解決的方法就是創建一個對象,用它來打包想要返回的多個對象。元組的概念正是基於此,元組將一組對象直接打包存儲於單一對象中,可以從該對象讀取其中元素,卻不允許向其中存儲新對象(這個概念也稱數據傳輸對象或信使)

元組可以具有任意長度,元組中的對象可以是不同類型的,我們希望能為每個對象指明類型,這時泛型就派上用場了。例如下面是一個可以存儲兩個對象的元組:

public class Tuple<A, B> {
    public final A a1;
    public final B a2;
    public Tuple(A a, B b) { a1 = a; a2 = b; }
    public String rep() { return a1 + ", " + a2; }

    @Override
    public String toString() {
        return "(" + rep() + ")";
    }
}

使用 final 修飾成員變量可以保證其不被修改,如果用戶想存儲不同的元素,那麼就必須創建新的 Tuple 對象。當然也可以允許用戶重新對 a1、a2 賦值,但無疑前一種形式會更加安全

利用繼承機制可以實現長度更長的元組:

public class Tuple3<A, B, C> extends Tuple2<A, B> {
    public final C a3;
    public Tuple3(A a, B b, C c) {
        super(a, b);
        a3 = c;
    }

    @Override
    public String rep() {
        return super.rep() + ", " + a3;
    }
}

泛型方法

到目前為止,我們已經研究了參數化整個類,其實還可以參數化類中的方法。類本身是否是泛型,與它的方法是否是泛型並沒有什麼直接關係。我們應該儘可能使用泛型方法,通常將單個方法泛型化要比將整個類泛型化要更加清晰易懂

要定義泛型方法,請將泛型參數列表放置在返回值之前:

public class GenericMethods {
    public <T> void f(T x) {
        System.out.println(x.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods gm = new GenericMethods();
        gm.f("");
        gm.f(1);
        gm.f(1.0);
        gm.f(1.0F);
        gm.f('c');
        gm.f(gm);
    }
}

使用泛型方法時,通常不需要指定參數類型,因為編譯器會找出這些類型,這稱為類型參數推斷,因此,對 f() 的調用看起來像普通的方法調用,而且像是被重載了無數次一樣

泛型擦除

當你開始深入研究泛型時,你會發現一個殘酷的現實:在泛型代碼內部,無法獲取任何有關泛型參數類型的信息

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]
*/

正如上例中輸出所示,你只能看到用作參數佔位符的標識符,這並非有用的信息。Java 泛型是使用擦除實現的,這意味着當你在使用泛型時,任何具體的類型信息都被擦除了,你唯一知道的就是你在使用一個對象。因此 List<String> 和 List 在運行時實際上是相同的類型,它們都被擦除成原生類型 List

再來看一個例子:

class Manipulator<T> {
    private T obj;

    Manipulator(T x) {
        obj = x;
    }

    // Error: cannot find symbol: method f():
    public void manipulate() {
        obj.f();
    }
}

public class Manipulation {
    public static void main(String[] args) {
        HasF hf = new HasF();
        Manipulator<HasF> manipulator = new Manipulator<>(hf);
        manipulator.manipulate();
    }
}

因為擦除,Java 編譯器無法將 manipulate() 方法能調用 obj 的 f() 方法這一需求映射到 HasF 具有 f() 方法這個事實上。為了調用 f(),我們必須協助泛型類,為泛型類給定一個邊界,以此告訴編譯器只能接受遵循這個邊界的類型。這裡重用了 extends 關鍵字。由於有了邊界,下面的代碼就能通過編譯:

public class Manipulator2<T extends HasF> {
    private T obj;

    Manipulator2(T x) {
        obj = x;
    }

    public void manipulate() {
        obj.f();
    }
}

邊界 <T extends HasF> 聲明 T 必須是 HasF 類型或其子類。如果情況確實如此,就可以安全地在 obj 上調用 f() 方法。泛型類型參數會擦除到它的第一個邊界(可能有多個邊界,稍後你將看到)。我們還提到了類型參數的擦除。編譯器實際上會把類型參數替換為它的擦除,就像上面的示例,T 擦除到了 HasF,就像在類的聲明中用 HasF 替換了 T 一樣。如果我們願意,完全可以把上例的 T 替換成 HashF,效果也是一樣的,那麼泛型的意義又何在呢?

這提出了很重要的一點:泛型只有在類型參數比某個具體類型(以及其子類)更加「泛化」,代碼能跨多個類工作時才有用。因此,使用類型參數通常比簡單的聲明類更加複雜。但是,不能因此認為使用 <T extends HasF> 形式就是有缺陷的。你必須查看所有的代碼,從而確定代碼是否複雜到必須使用泛型的程度

有關泛型擦除的困惑,其實是 Java 為實現泛型的一種妥協,因為泛型並不是 Java 語言出現時就有的。擦除減少了泛型的泛化性,泛型類型只有在靜態類型檢測期間才出現,在此之後,程序中的所有泛型類型都將被擦除,替換為它們的非泛型上界。例如, List<T> 這樣的類型註解會被擦除為 List,普通的類型變量在未指定邊界的情況下會被擦除為 Object

在 Java5 以前編寫的類庫是沒有使用泛型的,而作者可能打算重新用泛型編寫,或者根本不打算這樣做。Java 設計者們既要保證舊代碼和類文件依然合法,還得考慮當某個類庫變為泛型時,不會破壞依賴於它的代碼和應用。Java 設計者們最終認為泛型是唯一可行的解決方案,擦除使得向泛型的遷移成為可能,為了實現非泛型的代碼和泛型代碼共存,必須將某個類庫使用了泛型這樣的「證據」擦除

基於上述觀點,當你在編寫泛型代碼時,必須時刻提醒自己,你只是看起來擁有有關參數的類型信息而言。因為擦除,我們無法在運行時知道確切的類型,為了補償擦除帶來的弊端,我們可以為所需的類型顯示傳遞一個 Class 對象,以在類型表達式中使用它

class Building {
}

class House extends Building {
}

public class ClassTypeCapture<T> {
    Class<T> kind;

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

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

    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 =
                new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building()));
        System.out.println(ctt1.f(new House()));
        ClassTypeCapture<House> ctt2 =
                new ClassTypeCapture<>(House.class);
        System.out.println(ctt2.f(new Building()));
        System.out.println(ctt2.f(new House()));
    }
}

邊界和通配符

由於擦除會刪除類型信息,因此唯一可用於無限制泛型參數的方法是那些 Object 可用的方法。邊界允許我們對泛型使用的參數類型施以類型,將參數限制為某類型的子集,那麼就可以調用該子集中的方法。為了應用約束,Java 泛型使用了 extends 關鍵字

class Coord {
    public int x, y, z;
}

interface Weight {
    int weight();
}

class Solid<T extends Coord & Weight> {
    T item;

    Solid(T item) {
        this.item = item;
    }

    T getItem() {
        return item;
    }

    int getX() {
        return item.x;
    }

    int getY() {
        return item.y;
    }

    int getZ() {
        return item.z;
    }

    int weight() {
        return item.weight();
    }
}

class Bounded
        extends Coord implements Weight {

    @Override
    public int weight() {
        return 0;
    }
}

public class BasicBounds {
    public static void main(String[] args) {
        Solid<Bounded> solid =
                new Solid<>(new Bounded());
        solid.getY();
        solid.weight();
    }
}

引入通配符可以在泛型實例化時更加靈活地控制,也可以在方法中控制方法的參數,具體語法如下:

  • ? extends T:表示 T 或 T 的子類
  • ? super T:表示 T 或 T 的父類
  • ?:表示可以是任意類型

值得注意的問題

在這裡主要闡述在使用 Java 泛型時會出現的各類問題

1. 任何基本數據類型不能作為類型參數

Java 泛型的限制之一是不能將基本類型用作類型參數。因此,不能創建 ArrayList<int> 之類的東西。 解決方法是使用基本類型的包裝器類以及自動裝箱機制。如果創建一個 ArrayList<Integer>,並將基本類型 int 應用於這個集合,那麼你將發現自動裝箱機制將自動地實現 int 到 Integer 的雙向轉換,這幾乎就像是有一個 ArrayList<int> 一樣

2. 實現參數化接口

一個類不能實現同一個泛型接口的兩種變體,由於擦除的原因,這兩個變體會成為相同的接口。下面是產生這種衝突的情況:

interface Payable<T> {}

class Employee implements Payable<Employee> {}

class Hourly extends Employee implements Payable<Hourly> {}

Hourly 不能編譯,因為擦除會將 Payable<Employe> 和 Payable<Hourly> 簡化為相同的類 Payable,這樣,上面的代碼就意味着在重複兩次地實現相同的接口。十分有趣的是,如果從 Payable 的兩種用法中都移除掉泛型參數(就像編譯器在擦除階段所做的那樣)這段代碼就可以編譯

3. 轉型和警告

使用帶有泛型類型參數的轉型不會有任何效果,例如:

class Storage<T> {
    
    private Object obj;

    Storage() {
        obj = new Object();
    }

    @SuppressWarnings("unchecked")
    public T pop() {
        return (T)obj;
    }
}

public class GenericCast {

    public static void main(String[] args) {
        Storage<String> storage = new Storage<>();
        System.out.println(storage.pop());
    }
}

如果沒有 @SuppressWarnings 註解,編譯器將對 pop() 產生 「unchecked cast」 警告。由於擦除的原因,編譯器無法知道這個轉型是否是安全的,並且 pop() 方法實際上並沒有執行任何轉型。 這是因為,T 被擦除到它的第一個邊界,默認情況下是 Object,因此 pop() 實際上只是將 Object 轉型為 Object

4. 重載

下面的程序是不能編譯的,因為擦除,所以重載方法產生了相同的類型簽名

public class UseList<W, T> {
    void f(List<T> v) {}
    void f(List<W> v) {}
}

Tags: