Java 5之後加入了「泛型(generic)」,允許將物件的型態以參數的形式來定義。「?」是泛型的萬用字元,表示任意的物件型態,「?」還可以與「extends」和「super」兩個關鍵字合用,至於它們的用法和差別在哪,將是本篇文章要探討的部份。



泛型的萬用字元「?」

先來說明一下泛型的萬用字元「?」是要來做什麼用的。先看一下以下的程式:

class A {

}

class B extends A {

}

class C extends B {

}

class Q {

}

ArrayList<B> list1 = new ArrayList<B>();
ArrayList<B> list2 = new ArrayList();
ArrayList<B> list3 = new ArrayList<>();

ArrayList<Object> list4 = new ArrayList<Object>();
ArrayList<Object> list5 = new ArrayList();
ArrayList<Object> list6 = new ArrayList<>();

ArrayList list7 = new ArrayList<B>();
ArrayList list8 = new ArrayList<Object>();
ArrayList list9 = new ArrayList();
ArrayList list10 = new ArrayList<>();

ArrayList<?> list11 = new ArrayList<B>();
ArrayList<?> list12 = new ArrayList<Object>();
ArrayList<?> list13 = new ArrayList();
ArrayList<?> list14 = new ArrayList<>();

試問,以上的14個list物件,各自可否進行以下幾樣的操作呢?

  • 儲存A類別的物件。
  • 儲存B類別的物件。
  • 儲存C類別的物件。
  • 儲存Q類別的物件。
  • 取得A類別的物件。
  • 取得B類別的物件。
  • 取得C類別的物件。
  • 取得Q類別的物件。

先來看看list1:

//list1.add(new A()); // 編譯錯誤
((ArrayList) list1).add(new A());
list1.add(new B());
list1.add(new C());
//list1.add(new Q()); // 編譯錯誤
((ArrayList) list1).add(new Q()); // 要對list1變數強制轉型
A a = list1.get(0);
B b = list1.get(1);
C c = (C) list1.get(2);
//Q q = (Q) list1.get(3); // 會編譯錯誤,無法直接對取得的元素強制轉型
Q q = (Q) ((ArrayList) list1).get(3); // 要對list1變數強制轉型

list1不可直接但可以間接儲存A類別和Q類別的物件,也不可以直接但可以間接取得Q類別的物件。

再來看看list2:

//list2.add(new A()); // 編譯錯誤
((ArrayList) list2).add(new A());
list2.add(new B());
list2.add(new C());
//list2.add(new Q()); // 編譯錯誤
((ArrayList) list2).add(new Q()); // 要對list2變數強制轉型
A a = list2.get(0);
B b = list2.get(1);
C c = (C) list2.get(2);
//Q q = (Q) list2.get(3); // 會編譯錯誤,無法直接對取得的元素強制轉型
Q q = (Q) ((ArrayList) list2).get(3); // 要對list2變數強制轉型

list2和list1一樣,皆不可直接但可以間接儲存A類別和Q類別的物件,也不可以直接但可以間接取得Q類別的物件。

再來看看list3:

//list3.add(new A()); // 編譯錯誤
((ArrayList) list3).add(new A());
list3.add(new B());
list3.add(new C());
//list3.add(new Q()); // 編譯錯誤
((ArrayList) list3).add(new Q()); // 要對list3變數強制轉型
A a = list3.get(0);
B b = list3.get(1);
C c = (C) list3.get(2);
//Q q = (Q) list3.get(3); // 會編譯錯誤,無法直接對取得的元素強制轉型
Q q = (Q) ((ArrayList) list3).get(3); // 要對list3變數強制轉型

list3和list2與list1一樣,皆不可直接但可以間接儲存A類別和Q類別的物件,也不可以直接但可以間接取得Q類別的物件。至此我們可以知道,使用new運算子實體化物件時所傳入的泛型型態,實際上並不能真的對傳入的物件型態有所限制,主要還是看變數的型態。

同理,list4、list5、list6、list7、list8、list9、list10想必可以直接儲存和取得所有的物件型態。程式如下:

list4.add(new A());
list4.add(new B());
list4.add(new C());
list4.add(new Q());
A a4 = (A) list4.get(0);
B b4 = (B) list4.get(1);
C c4 = (C) list4.get(2);
Q q4 = (Q) list4.get(3);

list5.add(new A());
list5.add(new B());
list5.add(new C());
list5.add(new Q());
A a5 = (A) list5.get(0);
B b5 = (B) list5.get(1);
C c5 = (C) list5.get(2);
Q q5 = (Q) list5.get(3);

list6.add(new A());
list6.add(new B());
list6.add(new C());
list6.add(new Q());
A a6 = (A) list6.get(0);
B b6 = (B) list6.get(1);
C c6 = (C) list6.get(2);
Q q6 = (Q) list6.get(3);

list7.add(new A());
list7.add(new B());
list7.add(new C());
list7.add(new Q());
A a7 = (A) list7.get(0);
B b7 = (B) list7.get(1);
C c7 = (C) list7.get(2);
Q q7 = (Q) list7.get(3);

list8.add(new A());
list8.add(new B());
list8.add(new C());
list8.add(new Q());
A a8 = (A) list8.get(0);
B b8 = (B) list8.get(1);
C c8 = (C) list8.get(2);
Q q8 = (Q) list8.get(3);

list9.add(new A());
list9.add(new B());
list9.add(new C());
list9.add(new Q());
A a9 = (A) list9.get(0);
B b9 = (B) list9.get(1);
C c9 = (C) list9.get(2);
Q q9 = (Q) list9.get(3);

list10.add(new A());
list10.add(new B());
list10.add(new C());
list10.add(new Q());
A a10 = (A) list10.get(0);
B b10 = (B) list10.get(1);
C c10 = (C) list10.get(2);
Q q10 = (Q) list10.get(3);

接著來看看使用萬用字元「?」的泛型型態,list11、list12、list13、list14變數都是一樣的型態,只看list11即可,其他的和list11都是一樣的。測試程式如下:

//list11.add(new A()); // 編譯錯誤
((ArrayList) list11).add(new A()); // 要對list11變數強制轉型
//list11.add(new B()); // 編譯錯誤
((ArrayList) list11).add(new B()); // 要對list11變數強制轉型
//list11.add(new C()); // 編譯錯誤
((ArrayList) list11).add(new C()); // 要對list11變數強制轉型
//list11.add(new Q()); // 編譯錯誤
((ArrayList) list11).add(new Q()); // 要對list11變數強制轉型
A a4 = (A) list11.get(0);
B b4 = (B) list11.get(1);
C c4 = (C) list11.get(2);
Q q4 = (Q) list11.get(3);

list11不可直接但可以間接儲存A類別和Q類別的物件,卻可以直接取得所有類型的物件,這究竟是為什麼呢?「?」所代表的到底是什麼?是「Object」嗎?撰寫以下程式來判斷「?」到底是不是「Object」:

Object o = new Object();
list11.add(o); // 編譯失敗
o = list11.get(0);

從以上程式可知,「?」並不是「Object」。「?」代表著未知,不知道會傳入怎樣的物件,所以無法保證傳入物件的型態(與之後提到的泛型轉換有關)。但是回傳的狀況就不同了,未知的物件,一定是一個「Object」物件,所以回傳的物件元素參考,可以使用Object型態的變數來儲存。

那如果同樣傳入「?」型態的物件而不是「Object」型態的物件,可行嗎?程式如下:

list11.add(list11.get(0)); // 編譯失敗

即便是從自己本身取出的「?」型態之元素,也無法直接作為參數傳回自己本身。

萬用字元「?」與extends和super關鍵字

extends和super皆與繼承關係有關,如果寫成「? extends B」,表示為B類別或是B的任意子類別;如果寫成「? super B」,表示為B類別或是B的任意父類別。在上一節所介紹的萬用字元「?」,其實就等同於「? extends Object」,表示為Object或是Object的子類別,即為所有任意的類別。

承上一節的程式,若再加上以下這段程式碼:

ArrayList<? extends B> list15 = new ArrayList();
ArrayList<? super B> list16 = new ArrayList();

在來看看list15和list16,能不能存取A、B、C、Q的類別所實體化出來的物件。測試程式如下:

//list15.add(new A()); // 編譯錯誤
((ArrayList) list15).add(new A()); // 要對list15變數強制轉型
//list15.add(new B()); // 編譯錯誤
((ArrayList) list15).add(new B()); // 要對list15變數強制轉型
//list15.add(new C()); // 編譯錯誤
((ArrayList) list15).add(new C()); // 要對list15變數強制轉型
//list15.add(new Q()); // 編譯錯誤
((ArrayList) list15).add(new Q()); // 要對list15變數強制轉型
A a15 = list15.get(0);
B b15 = list15.get(1);
C c15 = (C) list15.get(2);
// Q q15 = (Q) list15.get(3); // 會編譯錯誤,無法直接對取得的元素強制轉型
Q q = (Q) ((ArrayList) list15).get(3); // 要對list15變數強制轉型

//list16.add(new A()); // 編譯錯誤
((ArrayList) list16).add(new A()); // 要對list16變數強制轉型
list16.add(new B());
list16.add(new C());
//list16.add(new Q()); // 編譯錯誤
((ArrayList) list16).add(new Q()); // 要對list16變數強制轉型
A a16 = (A) list16.get(0);
B b16 = (B) list16.get(1);
C c16 = (C) list16.get(2);
Q q16 = (Q) list16.get(3);

list15不可直接但可以間接儲存所有類別的物件,也可以直接取得所有有繼承關係類別的物件,但沒有繼承關係類別的物件需要間接存取。這是因為list15以物件變數型態的方式確保了其元素只能有B類別的物件實體或是B之子類別的物件實體,所以回傳的物件元素可以確定一定是B類別型態,但是傳入的物件型態並無法保證(與之後提到的泛型轉換有關)。

list16不可直接但可以間接儲存A類別和Q類別的物件(與之後提到的泛型轉換有關),卻可以直接取得所有類型的物件。可以直接取得所有類型的物件是因為Object類別為其他所有類別的父類別,所以list16回傳的物件型態可當作是Object型態。

泛型的轉換

擁有泛型的類別型態也可以轉型,以ArrayList為例,任何形式的泛型類別型態,都可以轉換成以下三種型態:

  • ArrayList
  • ArrayList<?>
  • ArrayList<? extends Object>

若已知B類別繼承自A類別,C類別繼承自B類別,則ArrayList<B>可以轉換成:

  • ArrayList
  • ArrayList<?>
  • ArrayList<? extends Object>
  • ArrayList<? extends B>
  • ArrayList<? extends A>
  • ArrayList<? super B>
  • ArrayList<? super C>

注意,ArrayList<B>不能轉換為ArrayList<A>!

當ArrayList<B>轉換成ArrayList<? extends A>時,我們預期原先的ArrayList<B>只會擁有B類別或是B類別的子類別所實體化的物件元素,因此我們可以保證這個ArrayList物件轉成ArrayList<? extends A>之型態後,所取得的元素必定是A類別或是其子類別物件,因為B類別繼承自A類別。但是我們卻無法保證傳入ArrayList<? extends A>的物件之型態為B類別或是B類別的子類別,也有可能會有個D類別繼承自A類別,若將D類別實體化的物件存進去了,就無法保證這個ArrayList內的元素一定是B類別或是B類別的子類別,因為編譯器並不知道這個ArrayList<? extends A>型態所指到的ArrayList物件,原先只能夠接受B類別或是B類別的子類別。為了保護這個ArrayList物件型態的一致性,在泛型使用到extends時,會避免允許泛型參數的傳入,只允許傳出,傳出的物件參考型態為A類別。

當ArrayList<B>轉換成ArrayList<? super C>時,我們預期原先的ArrayList<B>只會擁有B類別或是B類別的子類別所實體化的物件元素,我們可以保證這個ArrayList物件轉成ArrayList<? super C>之型態後,一定可以繼續儲存C類別或是其子類別所實體化出來的物件,因為C類別繼承自B類別,是B類別的子類別。但是我們卻無法保證從ArrayList<? super C>物件型態所參考到的實際物件,所存放的元素都是C類別或是其子類別實體化出來的物件,也許有個E類別繼承自B類別,而E類別實體化出來的物件就存在這個ArrayList物件中。為了保護這個ArrayList物件型態的正確性,在泛型使用到super時,回傳的物件型態一律為Object,且只能傳入C類別或是其子類別所實體化出來的物件。

若已知B類別繼承自A類別,C類別繼承自B類別,則ArrayList<? extends B>可以轉換成:

  • ArrayList
  • ArrayList<?>
  • ArrayList<? extends Object>
  • ArrayList<? extends A>

使用extends關鍵字的泛型只出不進,確保ArrayList物件內的取出的元素為B類別或是B類別的子類別所實體化的物件,而此種物件當然也算是B類別的父類別(A類別)或其子類別所實體化的物件。

若已知B類別繼承自A類別,C類別繼承自B類別,則ArrayList<? super B>可以轉換成:

  • ArrayList
  • ArrayList<?>
  • ArrayList<? extends Object>
  • ArrayList<? super C>

原先的ArrayList<? super B>允許儲存B類別或是B類別的子類別所實體化的物件元素,而此種物件當然也算是B類別的子類別(C類別或其C類別的子類別)所實體化的物件。

結語

泛型是Java程式語言非常方便的語法,可以讓程式在編譯階段的時候變得更為嚴謹,能避免許多型態上的錯誤,也可以減少型態轉換的次數,增加一些程式的效能。但泛型的觀念有點複雜,又很容易混淆,是學習Java必須要跨越的障礙之一,通常也是各類Java考試的必考項目。