Java SE 6 技術手冊

12.2 泛型進階語法

泛型的語法元素其實是很基本的,只不過將這種語法遞廻擴展之後,可以撰寫出相當複雜的泛型定義,然而無論再怎麼複雜的寫法,基本語法元素大致不離:限制泛型可用類型、使用型態通配字元(Wildcard)、以及泛型的擴充與繼承這幾個語法。

12.2.1 限制泛型可用類型

在定義泛型類別時,預設您可以使用任何的型態來實例化泛型類別中的型態持有者,但假設您想要限制使用泛型類別時,只能用某個特定型態或其子類別來實例化型態持有者的話呢?

您可以在定義型態持有者時,一併使用 "extends" 指定這個型態持有者實例化時,實例化的對象必須是擴充自某個類型或實作某介面,舉範例 12.9 來說。

範例 12.9 ListGenericFoo.java

import java.util.List;

public class ListGenericFoo<T extends List> {
    private T[] fooArray;

    public void setFooArray(T[] fooArray) {
        this.fooArray = fooArray;
    }

    public T[] getFooArray() {
        return fooArray;
    }
}

ListGenericFoo 在宣告類型持有者時,一併指定這個持有者實例化的對象,必須是實作 java.util.List 介面(interface)的類別,在限定持有者時,無論是要限定的對象是介面或類別,都是使用 "extends" 關鍵字,範例中您使用 "extends" 限定型態持有者實例化的對象,必須是實作 List 介面的類別,像 java.util.LinkedList 與 java.util.ArrayList 就實作了 List 介面(第 13 章就會介紹),例如下面的程式片段是合法的使用方式:

ListGenericFoo<LinkedList> foo1 = 
                  new ListGenericFoo<LinkedList>();
ListGenericFoo<ArrayList> foo2 = 
                  new ListGenericFoo<ArrayList>();

但如果不是實作 List 的類別,編譯時就會發生錯誤,例如下面的程式片段通不過編譯:

ListGenericFoo<HashMap> foo3 = 
                  new ListGenericFoo<HashMap>();

因為 java.util.HashMap 並沒有實作 List 介面(事實上 HashMap 實作了 Map 介面),編譯器會在編譯時期就檢查出這個錯誤:

type parameter java.util.HashMap is not within its bound
ListGenericFoo<HashMap> foo3 = new ListGenericFoo<HashMap>();

HashMap 並沒有實作 List 介面,所以無法作為實例化型態持有者的對象,事實上,當您沒有使用 "extends" 關鍵字限定型態持有者時,預設是 Object 下的所有子類別都可以實例化型態持有者,也就是說在您定義泛型類別時如果只寫以下的話:

public class GenericFoo<T> {
    //....
}

其實就相當於以下的定義方式:

public class GenericFoo<T extends Object> {
    //....
}

由於 Java 中所有的實例都繼承自 Object 類別,所以定義時若只寫 <T> 就表示,所有類型的物件都可以實例化您所定義的泛型類別。

良葛格的話匣子 實際上由於 List、Map、Set 與實作這些介面的相關類別,都已經用新的泛型功能重新改寫過了,實際撰寫時會更複雜一些,例如實際上您還可以再細部定義範例 12.9 的 ListGenericFoo:

import java.util.List;
public class ListGenericFoo<T extends List<String>> {
    private T[] fooArray;
    public void setFooArray(T[] fooArray) {
        this.fooArray = fooArray;
    }
    public T[] getFooArray() {
        return fooArray;
    }
}

這麼定義之後,您就只能使用 ArrayList<String> 來實例化 ListGenericFoo 了,例如:

ListGenericFoo<ArrayList<String>> foo = 
    new ListGenericFoo<ArrayList<String>>();

下一個章節會說明 List、Map、Set 等的使用,雖然展開後的程式似乎很複雜,但實際上還是這個章節所介紹泛型語法的延伸,為了說明方便,在這個章節中,請先忽略 List、Map、Set 上的泛型定義,先當它是個介面就好了。

12.2.2 型態通配字元(Wildcard)

仍然以範例 12.4 所定義的 GenericFoo 來進行說明,假設您使用 GenericFoo 類別來如下宣告名稱:

GenericFoo<Integer> foo1 = null;
GenericFoo<Boolean> foo2 = null;

那麼名稱 foo1 就只能參考 GenericFoo<Integer> 類型的實例,而名稱 foo2 只能參考 GenericFoo<Boolean> 類型的實例,也就是說下面的方式是可行的:

foo1 = new GenericFoo<Integer>();
foo2 = new GenericFoo<Boolean>();

現在您有這麼一個需求,您希望有一個參考名稱 foo 可以如下接受所指定的實例:

foo = new GenericFoo<ArrayList>(); 
foo = new GenericFoo<LinkedList>();

簡單的說,您想要有一個 foo 名稱可以參考的對象,其型態持有者實例化的對象是實作 List 介面的類別或其子類別,要宣告這麼一個參考名稱,您可以使用 '?'「通配字元」(Wildcard),'?' 代表未知型態,並使用 "extends" 關鍵字來作限定,例如:

GenericFoo<? extends List> foo = null;
foo = new GenericFoo<ArrayList>();
.....
foo = new GenericFoo<LinkedList>();
....

<? extends List> 表示型態未知,只知會是實作 List 介面的類別,所以如果型態持有者實例化的對象不是實作 List 介面的類別,則編譯器會回報錯誤,例如以下這行無法通過編譯:

GenericFoo<? extends List> foo = new GenericFoo<HashMap>();

因為 HashMap 沒有實作 List 介面,所以建立的 GenericFoo<HashMap> 實例不能指定給 foo 名稱來參考,編譯器會回報以下的錯誤:

incompatible types
found : GenericFoo<java.util.HashMap>
required: GenericFoo<? extends java.util.List>
GenericFoo<? extends List> foo = new GenericFoo<HashMap>();

使用 '?' 來作限定有時是很有用的,例如若您想要自訂一個 showFoo() 方法,方法的內容實作是針對 String 或其子類的實例而制定的,例如:

public void showFoo(GenericFoo foo) {
     // 針對String或其子類而制定的內容
}

如果只作以上的宣告,那麼像 GenericFoo<Integer>GenericFoo<Boolean> 等型態都可以傳入至方法中,如果您不希望任何的型態都可以傳 入showFoo() 方法中,您可以使用以下的方式來限定:

public void showFoo(GenericFoo<? extends String> foo) {
    // 針對String或其子類而制定的內容,例如下面這行
    System.out.println(foo.getFoo());
}

這麼一來,如果有粗心的程式設計人員傳入了您不想要的型態,例如 GenericFoo<Boolean> 型態的實例,則編譯器都會告訴它這是不可行的,在宣告名稱時如果指定了 <?> 而不使用 "extends",則預設是允許 Object 及其下的子類,也就是所有的 Java 物件了,那為什麼不直接使用 GenericFoo 宣告就好了,何必要用 GenericFoo<?> 來宣告?使用通配字元有點要注意的是,透過使用通配字元宣告的名稱所參考的物件,您沒辦法再對它加入新的資訊,您只能取得它當中的資訊或是移除當中的資訊,例如:

GenericFoo<String> foo = new GenericFoo<String>();
foo.setFoo("caterpillar");

GenericFoo<?> immutableFoo = foo;
// 可以取得資訊
System.out.println(immutableFoo.getFoo());

// 可透過immutableFoo來移去foo所參考實例內的資訊
immutableFoo.setFoo(null);

// 不可透過immutableFoo來設定新的資訊給foo所參考的實例
// 所以下面這行無法通過編譯
//  immutableFoo.setFoo("良葛格");

所以使用 <?> 或是 <? extends SomeClass> 的宣告方式,意味著您只能透過該名稱來取得所參考實例的資訊,或者是移除某些資訊,但不能增加它的資訊,理由很簡單,因為您不知道 <?> 或是 <? extends SomeClass> 宣告的參考名稱,實際上參考的物件,當中確實儲存的是什麼類型的資訊,基於泛型的設計理念,當然也就沒有理由能加入新的資訊了,因為若能加入,被加入的物件同樣也會有失去型態資訊的問題。

除了可以向下限制,您也可以向上限制,只要使用 "super" 關鍵字,例如:

GenericFoo<? super StringBuilder> foo = null;

如此, foo 就只接受 StringBuilder 及其上層的父類型態,也就是只能接受 GenericFoo<StringBuilder>GenericFoo<Object> 的實例。

12.2.3 擴充泛型類別、實作泛型介面

您可以擴充一個泛型類別,保留其型態持有者,並新增自己的型態持有者,例如範例12.10先寫一個父類別。

範例 12.10 GenericFoo4.java

public class GenericFoo4<T1, T2> {
    private T1 foo1;
    private T2 foo2;

    public void setFoo1(T1 foo1) {
        this.foo1 = foo1;
    }

    public T1 getFoo1() {
        return foo1;
    }

    public void setFoo2(T2 foo2) {
        this.foo2 = foo2;
    }

    public T2 getFoo2() {
        return foo2;
    }
}

再如範例 12.11 寫一個子類別擴充範例 12.10 的父類別。

範例 12.11 SubGenericFoo4.java

public class SubGenericFoo4<T1, T2, T3> 
                extends GenericFoo4<T1, T2> {
    private T3 foo3;

    public void setFoo3(T3 foo3) {
        this.foo3 = foo3;
    }

    public T3 getFoo3() {
        return foo3;
    }
}

如果決定要保留型態持有者,則父類別上宣告的型態持有者數目在繼承下來時必須寫齊全,也就是說在範例 12.11 中,父類上 GenericFoo4 上出現的 T1 與 T2 在 SubGenericFoo4 中都要出現,如果不保留型態持有者,則繼承下來的 T1 與 T2 自動變為 Object,建議是父類別的型態持有者都要保留。 介面實作也是類似,例如先如範例 12.12 定義一個介面。

範例 12.12 IFoo.java

public interface IFoo<T1, T2> {
    public void setFoo1(T1 foo1);
    public void setFoo2(T2 foo2);
    public T1 getFoo1();
    public T2 getFoo2();
}

您可以如範例 12.13 的方式實作 IFoo 介面,實作時保留所有的型態持有者。

範例 12.13 ConcreteFoo.java

public class ConcreteFoo<T1, T2> implements IFoo<T1, T2> {
    private T1 foo1;
    private T2 foo2;

    public void setFoo1(T1 foo1) {
        this.foo1 = foo1;
    }

    public T1 getFoo1() {
        return foo1;
    }

    public void setFoo2(T2 foo2) {
        this.foo2 = foo2;
    }

    public T2 getFoo2() {
        return foo2;
    }
}

良葛格的話匣子 有的人一看到角括號就開始頭痛,老實說我也是這些人當中的一個,Java 新增的泛型語法雖基本,但根據語法展開來的寫法卻可以寫的很複雜,我不建議使用泛型時將程式碼寫的太複雜,像是遞迴了 n 層角括號的程式碼,看來真的很令人頭痛,新的泛型功能有其好處,但撰寫程式時也要同時考慮可讀性,因為可讀性有時反而是開發程式時比較注重的。