Java泛型的轉換,「T」、「?」、「? extends T」和「? super T」究竟有什麼差別?


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

泛型的萬用字元「?」

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

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

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

先來看看list1:

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

再來看看list2:

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

再來看看list3:

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

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

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

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

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

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

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

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

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

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

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

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<? extends A>之型態後,一定可以繼續儲存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考試的必考項目。

關於作者

Magic Len

各位好,我是Magic Len,是這網站的管理員。我是台灣台中大肚山上人,畢業於台中高工資訊科和台灣科技大學資訊工程系,曾在桃機航警局服役。我熱愛自然也熱愛科學,喜歡和別人分享自己的知識與經驗。如果你有興趣認識我,可以加我的Facebook,並且請註明是從MagicLen來的。

相關文章