Skip to content

Latest commit

 

History

History
517 lines (364 loc) · 23.4 KB

CH12.md

File metadata and controls

517 lines (364 loc) · 23.4 KB

第 12 章 泛型

在 J2SE 5.0 中新增了「泛型」(Generics)功能,而且許多 API 都根據這個新功能重新改寫了,例如 List、Map、Set 等相關類別,雖然即使不瞭解泛型的新功能,也可以照 J2SE 1.4 或舊版本的語法來使用這些類別,但編譯時會出現一些惱人的警訊(Warnings)。

泛型解決的不只是讓您少寫幾個類別的程式碼,還在於讓您定義「安全的」泛型類別(Generics class),泛型提供編譯時期檢查,您不會因為將物件置入某個容器(Container)而失去其型態,瞭解這一個章節的內容後,對於使用其它 API 有關泛型的功能而出現的警訊,您就知道其原因何在了。


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 的真正型態:

GenericFoo2<Integer, Boolean> foo = 
              new GenericFoo2<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);

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 層角括號的程式碼,看來真的很令人頭痛,新的泛型功能有其好處,但撰寫程式時也要同時考慮可讀性,因為可讀性有時反而是開發程式時比較注重的。

12.3 接下來的主題

每一個章節的內容由淺至深,初學者該掌握的深度要到哪呢?在這個章節中,對於初學者我建議至少掌握以下幾點內容:

  • 瞭解沒有泛型功能前,如何使用 Object 解決問題
  • 會定義基本的泛型類別及宣告其實例(即 12.1.2 的內容)
  • 會擴充泛型類別與實作泛型介面

到這個章節為止,Java 的語法大部份已經說明完畢了,接下來要進行的,是讓您開始熟悉 J2SE 中一些常用的 API 類別,熟悉這些 API 類別是學習 Java 的必要過程,首先要先瞭解的是相關的物件容器(Container),像是 List、Map、Set 等,幾乎在各種應用領域中,都會很常使用到這些物件容器。