Java SE 6 技術手冊

12.1 泛型入門

J2SE 5.0 提供的泛型,目的在讓您定義「安全的」泛型類別(Generics class),事實上 J2SE 5.0 前 Object 解決泛型類別的部份需求,J2SE 5.0 之後再解決的是型態安全問題,這個小節會先介紹沒有泛型功能前的設計方法,再來看看幾個 J2SE 5.0 泛型的定義方式,從中瞭解使用泛型的好處。

12.1.1 沒有泛型之前

考慮您要設計下面的 BooleanFoo 與 IntegerFoo 兩個類別,這是兩個很無聊的類別,但足以說明需求。

範例 12.1 BooleanFoo.java

public class BooleanFoo {
    private Boolean foo;

    public void setFoo(Boolean foo) {
        this.foo = foo;
    }

    public Boolean getFoo() {
        return foo;
    }
}

範例 12.2 IntegerFoo.java

public class IntegerFoo {
    private Integer foo;

    public void setFoo(Integer foo) {
        this.foo = foo;
    }

    public Integer getFoo() {
        return foo;
    }
}

觀察範例 12.1 與 12.2 兩個類別,其中除了宣告成員的型態、參數列的型態與方法返回值的型態不同之外,剩下的程式碼完全相同,或許有點小聰明的程式設計人員會將第一個類的內容複製至另一個檔案中,然後用編輯器「取代」功能一次取代所有的型態名稱(即將 Boolean 取代為 Integer)。

雖然是有些小聰明,但如果類別中的邏輯要修改,您就需要修改兩個檔案,泛型(Generics)的需求就在此產生,當您定義類別時,發現到好幾個類別的邏輯其實都相同,就只是當中所涉及的型態不一樣時,使用複製、貼上、取代的功能來撰寫程式,只是讓您增加不必要的檔案管理困擾。

由於 Java 中所有的類別最上層都繼承自 Object 類別,您可以定義如範例 12.3 的類別來取代範例 12.1 與 12.2 的類別。

範例 12.3 ObjectFoo.java

public class ObjectFoo {
    private Object foo;

    public void setFoo(Object foo) {
        this.foo = foo;
    }

    public Object getFoo() {
        return foo;
    }
}

由於 Java 中所有定義的類別,都以 Object 為最上層的父類別,所以用它來實現泛型(Generics)功能是一個不錯的考量,在 J2SE 1.4 或之前版本上,大部份的開發人員會這麼作,您只要撰寫如範例 12.3 的類別,然後可以如下的使用它:

ObjectFoo foo1 = new ObjectFoo();
ObjectFoo foo2 = new ObjectFoo();

foo1.setFoo(new Boolean(true));
// 記得轉換操作型態
Boolean b = (Boolean) foo1.getFoo();

foo2.setFoo(new Integer(10));
// 記得轉換操作型態
Integer i = (Integer) foo2.getFoo();

看來還不錯,但是設定至 foo1 或 foo2 的 Integer 或 Boolean 實例會失去其型態資訊,從 getFoo() 傳回的是 Object 型態的實例,您必須轉換它的操作型態,問題出在這邊,粗心的程式設計人員往往會忘了要作這個動作,或者是轉換型態時用錯了型態 (像是該用Boolean卻用了Integer),例如:

ObjectFoo foo1 = new ObjectFoo();
foo1.setFoo(new Boolean(true));
String s = (String) foo1.getFoo();

由於語法上並沒有錯誤,所以編譯器檢查不出上面的程式有錯誤,真正的錯誤要在執行時期才會發生,這時惱人的 ClassCastException 就會出來搞怪,在使用 Object 設計泛型程式時,程式人員要再細心一些,例如在 J2SE 1.4 或舊版本上,所有存入 List、Map、Set 容器中的實例都會失去其型態資訊,要從這些容器中取回物件並加以操作的話,就得記住取回的物件是什麼型態。

12.1.2 定義泛型類別

當您定義類別時,發現到好幾個類別的邏輯其實都相同,就只是當中所涉及的型態不一樣時,使用複製、貼上、取代的功能來撰寫程式,只會讓您增加不必要的檔案管理困擾。

由於 Java 中所有定義的類別,都以Object為最上層的父類別,所以在 J2SE 5.0之前,Java 程式設計人員可以使用 Object 定義類別以解決以上的需求,為了讓定義出來的類別可以更加通用(Generic),傳入的值或傳回的實例都是以 Object 型態為主,當您要取出這些實例來使用時,必須記得將之轉換為原來的類型或適當的介面,如此才可以操作物件上的方法。

然而使用 Object 來撰寫泛型類別(Generic Class)留下了一些問題,因為您必須要轉換型態或介面,粗心的程式設計人員往往會忘了要作這個動作,或者是轉換型態或介面時用錯了型態或介面(像是該用 Boolean 卻用了 Integer),但由於語法上是可以的,所以編譯器檢查不出錯誤,因而執行時期就會發生 ClassCastException。

在 J2SE 5.0 之後,提出了針對泛型(Generics)設計的解決方案,要定義一個簡單的泛型類別是簡單的,直接來看範例12.4如何取代範例12.3的類別定義。

範例 12.4 GenericFoo.java

public class GenericFoo<T> {
    private T foo;

    public void setFoo(T foo) {
        this.foo = foo;
    }

    public T getFoo() {
        return foo;
    }
}

在範例 12.4中,使用 <T> 用來宣告一個型態持有者(Holder)名稱 T,之後您可以用 T 這個名稱作為型態代表來宣告成員、參數或返回值型態,然後您可以如範例 12.5 來使用這個類別。

範例 12.5 GenericFooDemo.java

public class GenericFooDemo {
    public static void main(String[] args) {
        GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
        GenericFoo<Integer> foo2 = new GenericFoo<Integer>();

        foo1.setFoo(new Boolean(true));
        Boolean b = foo1.getFoo(); // 不需要再轉換型態
        System.out.println(b);

        foo2.setFoo(new Integer(10));
        Integer i = foo2.getFoo(); // 不需要再轉換型態
        System.out.println(i);
    }
}

與單純使用 Object 宣告型態所不同的地方在於,使用泛型所定義的類別在宣告及配置物件時,您可以使用角括號一併指定泛型類別型態持有者 T 真正的型態,而型態或介面轉換就不再需要了,getFoo() 所設定的引數或傳回的型態,就是您在宣告及配置物件時在 <> 之間所指定的型態,您所定義出來的泛型類別在使用時多了一層安全性,可以省去惱人的 ClassCastException 發生,編譯器可以幫您作第一層防線,例如下面的程式會被檢查出錯誤:

GenericFoo<Boolean> foo1 = new GenericFoo<Boolean>();
foo1.setFoo(new Boolean(true));
Integer i = foo1.getFoo(); // 傳回的是Boolean型態

foo1 使用 getFoo() 方法傳回的是 Boolean 型態的實例,若您要將這個實例指定給 Integer 型態的變數,顯然在語法上不合,編譯器這時檢查出錯誤:

GenericFooDemo.java:7: incompatible types
found : java.lang.Boolean
required: java.lang.Integer
Integer i = foo1.getFoo();

如果使用泛型類別,但宣告及配置物件時不一併指定型態呢?那麼預設會使用 Object 型態,不過您就要自己轉換物件的介面型態了,例如 GenericFoo 可以這麼宣告與使用:

GenericFoo foo3 = new GenericFoo();
foo3.setFoo(new Boolean(false));

但編譯時編譯器會提出警訊,告訴您這可能是不安全的操作:

Note: GenericFooDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

回過頭來看看下面的宣告:

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

GenericFoo<Boolean> 宣告的 foo1 與 GenericFoo<Integer> 宣告的 foo2 是相同的類型嗎?答案是否定的!基本上 foo1 與 foo2 是兩個不同的類型,foo1 是 GenericFoo<Boolean> 類型,而 foo2 是 GenericFoo<Integer> 類型,所以您不可以將 foo1 所參考的實例指定給 foo2,或是將 foo2 所參考的實例指定給 foo1,要不然編譯器會回報以下錯誤:

incompatible types
found : GenericFoo<java.lang.Integer>
required: GenericFoo<java.lang.Boolean>
foo1 = foo2;

良葛格的話匣子 自訂義泛型類別時,型態持有者名稱可以使用 T(Type),如果是容器的元素可以使用 E(Element),鍵值匹配的話使用 K(Key)與 V(Value),Annotation 的話可以用 A,可以參考 J2SE 5.0 API 文件說明上的命名方式。

12.1.3 幾個定義泛型的例子

您可以在定義泛型類別時,宣告多個類型持有者,像範例 12.6 的類別上宣告了兩個型態持有者 T1 與 T2。

範例 12.6 GenericFoo2.java

public class GenericFoo2<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;
    }
}

您可以如下使用 GenericFoo2 類別,分別以 Integer 與 Boolean 設定 T1 與 T2 的真正型態:

GenericFoo<Integer, Boolean> foo = 
              new GenericFoo<Integer, Boolean>();

泛型可以用於宣告陣列型態,範例 12.7 是個簡單示範。

範例 12.7 GenericFoo3.java

public class GenericFoo3<T> {
    private T[] fooArray;

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

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

您可以像下面的方式來使用範例 12.7 所定義的類別。

String[] strs = {"caterpillar", "momor", "bush"};
GenericFoo3<String> foo = new GenericFoo3<String>();
foo.setFooArray(strs);
strs = foo.getFooArray();

注意您可以使用泛型機制來宣告一個陣列,例如下面這樣是可行的:

public class GenericFoo<T> {
    private T[] fooArray;
    // ...
}

但是您不可以使用泛型來建立陣列的實例,例如以下是不可行的:

public class GenericFoo<T> {
    private T[] fooArray = new T[10]; // 不可以使用泛型建立陣列實例
    // ...
}

如果您已經定義了一個泛型類別,想要用這個類別在另一個泛型類別中宣告成員的話要如何作?舉個實例,假設您已經定義了範例 12.4 的類別,現在想要設計一個新的類別,當中包括了範例12.4的類別實例作為其成員,您可以如範例 12.8 的方式設計。

範例 12.8 WrapperFoo.java

public class WrapperFoo<T> {
    private GenericFoo<T> foo;

    public void setFoo(GenericFoo<T> foo) {
        this.foo = foo;
    }

    public GenericFoo<T> getFoo() {
        return foo;
    }
}

這麼一來,您就可以保留型態持有者T的功能,一個使用的例子如下:

GenericFoo<Integer> foo = new GenericFoo<Integer>();
foo.setFoo(new Integer(10));
WrapperFoo<Integer> wrapper = new WrapperFoo<Integer>();
wrapper.setFoo(foo);