泛型的語法元素其實是很基本的,只不過將這種語法遞廻擴展之後,可以撰寫出相當複雜的泛型定義,然而無論再怎麼複雜的寫法,基本語法元素大致不離:限制泛型可用類型、使用型態通配字元(Wildcard)、以及泛型的擴充與繼承這幾個語法。
在定義泛型類別時,預設您可以使用任何的型態來實例化泛型類別中的型態持有者,但假設您想要限制使用泛型類別時,只能用某個特定型態或其子類別來實例化型態持有者的話呢?
您可以在定義型態持有者時,一併使用 "extends" 指定這個型態持有者實例化時,實例化的對象必須是擴充自某個類型或實作某介面,舉範例 12.9 來說。
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.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.10先寫一個父類別。
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 的父類別。
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 定義一個介面。
public interface IFoo<T1, T2> {
public void setFoo1(T1 foo1);
public void setFoo2(T2 foo2);
public T1 getFoo1();
public T2 getFoo2();
}
您可以如範例 12.13 的方式實作 IFoo 介面,實作時保留所有的型態持有者。
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 層角括號的程式碼,看來真的很令人頭痛,新的泛型功能有其好處,但撰寫程式時也要同時考慮可讀性,因為可讀性有時反而是開發程式時比較注重的。