1. 程式人生 > >java泛型之泛型邊界

java泛型之泛型邊界

在網上發現這篇文章寫得不錯,地址:http://build.cthuwork.com:8081/wordpress/category/java教程/java再談泛型/


首先本文假定讀者對Java的泛型有基礎的瞭解,若需要請參考其他資料配合閱讀。

泛型的泛參(type argument)可以使用實際型別或者萬用字元(wildcard)。其中萬用字元可以通過邊界(bound)來限制其接受的實際引數的型別。根據其種類,可以分為無界(unbounded)、上界(upper bound)和下界(lower bound)。其泛型邊界決定了輸入(input)和輸出(output)分別能接受什麼型別。

輸入為其函式的引數、屬效能夠賦值的值的型別,輸出為函式的返回值、獲取到的屬性的值的型別。

一、實際型別

泛型的泛參可以使用實際型別。也就是類似於List<String>,直接指定泛型的型別。這時候泛型的表現最容易理解,輸入和輸出都為實際型別。需要注意的一點是,泛型不支援協變(Covariant),協變需使用萬用字元。為什麼泛型不支援協變呢。我們先從支援協變的陣列開始考慮。考慮以下程式碼:

Object[] array = new String[1];
array[0] = 12.450F;


這段程式碼是可以通過編譯的,然而會讓靜態型別的Java語言在沒有任何強制型別轉換的情況下出現型別異常。我們嘗試往一個String型別的陣列索引為0的位置賦值一個Float型別的值,這當然是行不通和完全錯誤的。Java陣列能夠協變是一個設計上的根本錯誤,它能導致你的程式碼在你完全不知情的情況下崩潰和異常,但現在改已經為時已晚。幸好我們有和經常使用集合API,否則最常見的情況可能如下:

public Number evil;
public void setAll(Number[] array) {
    for (int i = 0;i < array.length;i++) {
        array[i] = evil;
    }
}
public void looksGood() {
    atSomewhereWeDontKnown(); //We summoned evil to our kawaii and poor code
    Float[] ourKawaiiArray = getOurKawaiiArray(); //Oops
}
public void atSomewhereWeDontKnown() {
    evil = 12450;
}
public Float[] getOurKawaiiArray() {
    Float[] weWantFloatFilled = new Float[0xFF];
    setAll(weWantFloatFilled); //Buts... we got (1)E(2)V(4)I(5)L(0)...
    return weWantFloatFilled;
}

注:我試了一下,以上程式碼執行looksGood()時會出錯,第4行報java.lang.ArrayStoreException。所以,不知道作者所說的we got (1)E(2)V(4)I(5)L(0)...是什麼意思。可能是java某個老版本執行的結果。

我們可不想讓(1)E(2)V(4)I(5)L(0)充滿我們的程式碼。所以,泛型吸取了這個教訓,本身就是為了提高型別安全性而設計的泛型不能犯這樣的低階錯誤。所以你不能寫以下程式碼:

List<Object> array = new ArrayList<String>;
array.set(0, 12.450F);

這段程式碼在第一行就無法通過編譯,因為你嘗試協變一個泛型。其解決辦法和其他的說明將在後續討論。

二、萬用字元

1.無界萬用字元

無界萬用字元為”?”,可以接受任何的實際型別作為泛參。其能接受的輸入和輸出型別十分有限。

①可用輸入型別

嚴格意義上不能接受任何的型別作為輸入,考慮以下程式碼:

<span style="font-family:Source Sans Pro, Helvetica, sans-serif;color:#141412;"><span style="line-height: 24px;">List<?> list = new ArrayList<String>();
list.add("123");//報異常,</span></span><span style="font-family:Source Sans Pro, Helvetica, sans-serif;color:#141412;"><span style="line-height: 24px;">list.add(null)正確</span></span>

你可能覺得這段程式碼看起來沒有問題。通常會這樣考慮,我們可以簡單的把無界萬用字元”?”看成Object,往一個Object型別的列表加一個String有什麼問題?況且其實際就是String型別。其實並不能通過編譯,這並不是編譯器出現了錯誤。這裡有個邏輯漏洞,我們仔細考慮無界萬用字元的意義。無界萬用字元代表其接受任何的實際型別,但這並不意味著任何的實際型別都可以作為其輸入和輸出。其語義上有微妙的但巨大的區別。其含義是不確定到底是哪個實際型別。可能是String,可能是UUID,可能是任何可能的型別。如果這是個UUID列表,那麼往裡面加String等就會出事。如果是String列表,往裡面加UUID等也會出事。或者我們不管其是什麼型別的列表,往裡面加Object,然而Object裡有你的實際型別的屬性和方法麼。即使實際是Object列表,我們也無法確定。那麼,無界萬用字元就不能接受任何輸入了麼,看起來是這樣。其實有個例外,null作為一個十分特殊的值,表示不引用任何物件。我們可以說String型別的值可以為null、UUID型別的值可以為null,甚至Object型別的值可以為null。無論是什麼型別,都可以接受null作為其值,表示不引用任何物件。所以無界萬用字元的輸入唯一可接受的是可為所有型別的null。

②可用輸出型別

無界萬用字元的輸出型別始終為Object,因為其意義為接受任何的實際型別作為泛參,而任何的實際型別都可以被協變為Object型別,所以其輸出型別自然就為Object了。沒有什麼需要注意的地方。

2.上界萬用字元

上界萬用字元為”extends”,可以接受其指定型別或其子類作為泛參。其還有一種特殊的形式,可以指定其不僅要是指定型別的子類,而且還要實現某些介面。這種用法非常少用,我在很多開源專案中基本沒看到這種用法。由於這和本章內容無關,不影響輸入和輸出的型別,所以暫不描述。

①可用輸入型別

嚴格意義上同樣不能接受任何的型別作為輸入,出於嚴謹目的,我們再從頭分析一遍,這次以Minecraft的原始碼為例,考慮以下程式碼:

List<? extends EntityLiving> list = new ArrayList<EntityPlayer>();
list.add(player);

你可能覺得這段程式碼又沒問題了,EntityPlayer確實繼承了EntityLiving。往一個EntityLiving的列表里加EntityPlayer有什麼問題?放肆!12450!好不鬧/w\。這裡的問題在於如果實際上是EntityPig的列表呢。這麼想你就應該懂了,和無界萬用字元差不多,其只是限定了列表必須是EntityLiving的子類而已,我們並不知道實際是什麼。所以在這裡我們只能新增EntityLiving型別的物件。是不是覺得有什麼不對?對了,我就是超威藍貓!好不鬧/w\,我們能在EntityLiving上呼叫EntityPlayer的getGameProfile麼,明顯不能,況且我們到底能不能例項化EntityLiving也是個問題。這裡真的很容易混淆概念,一定要牢記,只能使用null作為上界萬用字元的輸入值。

②可用輸出型別

好了,這次終於能玩了,上界萬用字元的輸出型別為其指定的型別,實際上如果萬用字元位於泛型類的宣告中例如:

public class Foo<T extends EntityLiving> {
    public T entity;
}

這個類中entity欄位的實際型別不是所有型別的父類Object了,而是EntityLiving,這可以用檢視位元組碼的方式證實。當然其型別是Object也不會有太大的差別,可以想到的問題是當我們以某種方式往其內部傳入了Object型別或其他不是EntityLiving型別或其子類的物件時,可能會出現型別轉換異常或者更嚴重的留下隨時程式碼會崩潰的隱患。而直接使用EntityLiving型別作為其實際型別就會在嘗試這麼做的同時丟擲型別轉換異常,從而避免這種問題。

3.下界萬用字元

下界萬用字元為”super”,可以接受其指定型別或其父類作為泛參。可能很多人都沒有用過下界萬用字元,因為其真的很少用。其主要用處之一是在使用Java或第三方的API的泛型類時,對泛參型別不同,但泛參具有繼承關係,且主要關注其輸入的泛型物件進行歸納。以Minecraft的原始碼為例,考慮以下程式碼:

private EntityMob ourKawaiiMob;
private EntityMob otherKawaiiMob;
public int compareMobEntity(Comparator<? super EntityMob> comparator) {
    return comparator.compare(ourKawaiiMob, otherKawaiiMob);
}

此方法可以接受一個比較器,用於比較兩EntityMob。這裡的含義是,我們希望接受一個EntityMob或其父類的比較器。例如Comparator<Entity>只會把EntityMob看成一個Entity進行比較,這樣我們就可以對EntityMob的某一部分進行比較。我們不能將一個完全不是EntityMob的父類的比較器,例如Comparator<EntityMinecart>作為引數傳入。也不能將一個EntityMob的子類的比較器,例如Comparator<EntityZombie>作為引數傳入。因為實際我們比較的是EntityMob或其子類的物件,即使我們傳入的是其子類的比較器,我們也不能保證不會發生用Comparator<EntityCreeper>比較一個EntityEnderman的情況。又或者即使我們利用Java的型別擦除這麼做了,java的動態型別檢查會強制丟擲ClassCastException。所以在這種情況下應該使用下界萬用字元。

①可用輸入型別

下界萬用字元的輸入型別為其指定的型別或子類。因為其意義為接受其指定型別或其父類作為泛參。那麼無論我們提供的物件是什麼型別,只要是其指定的型別或子類的物件,那麼毫無例外一定是其指定的型別的物件。我們不能提供其指定的型別的父類作為物件,考慮以下程式碼:

private EntityLiving our;
private EntityLiving other;
Comparator<? super EntityMob> comparator = new EntityMobComparator();
comparator.compare(our, other);

這段程式碼不能通過編譯,我們嘗試用一個EntityMob的比較器來比較EntityLiving。不仔細考慮可能以為這並沒有什麼問題,EntityMob的比較器完全有能力來比較EntityLiving啊?但是實際情況是如果這段程式碼成功編譯,而且沒有動態型別檢查的話EntityMob的比較器就可能會嘗試其獲取EntityLiving並沒有的,屬於EntityMob的屬性,然後就會獲取到非法的資料,或導致Java執行時崩潰,這當然是不行的。好在我們即使這麼做了,Java也會強制丟擲ClassCastException。

②可用輸出型別

下界萬用字元的輸出型別始終為Object,因為其意義為接受其指定型別或其父類作為泛參,我們並不知道具體是哪一個父類。而任何的實際型別都可以被協變為Object型別,所以其輸出型別自然就為Object了。

三、回顧泛型邊界和輸入輸出型別的區別

泛型邊界並不直接代表著能接受的輸入輸出的型別,其含義為能接受什麼樣的實際型別。而輸入輸出型別能是什麼則是根據泛型邊界的含義得出的,其中的限制是由於我們只能通過泛型邊界對實際型別進行猜測而產生的,希望大家能仔細理解其中的含義。


四、編譯前後比較

泛型系統是作為Java 5的一套增強型別安全及減少顯式型別轉換的系統出現的。泛型也叫引數化型別,顧名思義,通過給型別賦予一定的泛型引數,來達到提高程式碼複用度和減少複雜性的目的。

在Java中,泛型是作為語法糖出現的。在虛擬機器層面,並不存在泛型這種型別,也不會對泛型進行膨脹,生成出類似於List<String>、List<Entry>之類的型別。在虛擬機器看來,List<E>這個泛型型別只是普通的型別List而已,這種行為叫泛型擦除(Type Erasure)。

那麼在Java中泛型是如何如何實現其目的的呢?Java的泛型充分利用了多型性。將無界(unbounded)的萬用字元(wildcard)理解為Object型別,因為Object型別是所有除標量(Scalar)以外,包括普通的陣列和標量陣列的型別的父類。將所有有上界(upper bound)的萬用字元理解為其上界型別例如<T extends CharSequence>將被理解為CharSequence型別。並在相應的地方自動生成checkcast位元組碼進行型別檢查和轉換,這樣就既可以實現泛型,又不需要在位元組碼層面的進行改動來支援泛型。這樣的泛型叫做偽泛型。

編譯前

publicclass Foo<T extendsCharSequence> {
    privateT value;
    publicvoid set(T value) {
        this.value = value;
    }
    publicT get() {
        returnthis.value;
    }
    publicstatic void main(String[] args) {
        Foo<String> foo = newFoo<String>();
        foo.set("foo");
        String value = foo.get();
    }
}

編譯後:

publicclass Foo {
    privateCharSequence value;
    publicvoid set(CharSequence value) {
        this.value = value;
    }
    publicCharSequence get() {
        returnthis.value;
    }
    publicstatic void main(String[] args) {
        Foo foo = newFoo();
        foo.set("foo");
        String value = (String) foo.get();
    }
}