Thinking In Java Part12(通配符、超類型通配符)

1、通配符
	可以嚮導出類型的數組賦予基類型的數組引用
	class Fruit{}
	class Apple extends Fruit{}
	class Jonathan extends Apple{}
	class Orange extends Fruit{}
	public class CovariantArrays {
	    public static void main(String[] args) {
	        Fruit[] fruit = new Apple[10];
	        fruit[0] = new Apple();
	        fruit[1] = new Jonathan();
	        try{
	            // java.lang.ArrayStoreException
	            fruit[0] = new Fruit();
	        }catch (Exception e){
	            System.out.println(e);
	        }
	        try{
	            // java.lang.ArrayStoreException
	            fruit[0] = new Orange();
	        }catch (Exception e){
	            System.out.println(e);
	        }
	    }
	}
	創建了一個Apple數組,並將其賦值給一個Fruit數組引用,有意義的,因爲Apple也是一種Fruit,因此Apple數組應該也是一個Fruit數組。
	但是,實際的數組類型是Apple[],應該只能放置Apple或apple的子類型,這在編譯器和運行時都可以運行。編譯器運行你將Fruit放在這個數組中,是因爲它有一個Fruit[]引用——因此他運行將Fruit對象或任何從Fruit繼承的對象(Orange)放在這個數組中,所以,編譯期是運行的,但是,運行時數組機制知道它是Apple[],因此會在向數組中放置異構類型 拋出異常。
	實際中向上轉型不合適用在這,我們真正做的是將一個數組賦值給另一個數組。數組的持有其他對象行爲是因爲向上轉型而已,數組本身對自己持有的對象有檢測,因此在編譯期和運行時檢查,我們要小心。
	因爲泛型的主要目標之一是將運行時錯誤移入到編譯器,因此我們接下來嘗試用泛型容器來代替數組。
		// Compile Error
	    ArrayList<Fruit> apples = new ArrayList<Apple>();
	泛型和容器相關正確的說法爲:不能把一個涉及Apple的泛型賦給一個設計Fruit的泛型。如果像在數組的情況中一樣,編譯器對代碼的瞭解足夠多,可以確定所涉及到的容器,那麼它可能運行編譯時通過,但是它不知道任何有關這方面的信息,因此它拒絕向上轉型。實際上着也不是向上轉型——Apple的List不是Fruit的List。Apple的List將持有Apple和Apple的子類型,而Fruit的List將持有任何類型的Fruit,誠然包括Apple,但是他不是一個Apple的List,它仍舊是Fruit的List。Apple的List在類型上不等價與Fruit的List,即使Apple是一種Fruit類型。
	真正的問題是容器的類型,而不是容器持有的類型。與數組不同,泛型沒有內建的協變類型。這是因爲數組在語言中是完全定義的,隱藏可以內建編譯期和運行時的檢查,但是在使用泛型時,編譯器和運行時系統都不知道你想用類型做什麼,以及應該採用什麼規則。
	有時你想要在兩個類型之間建立某種類型的向上轉型關係,可以通過通配符。
	  public static void main(String[] args) {
        ArrayList<? extends Fruit> flist = new ArrayList<>();
        // Compile Error
//        flist.add(new Apple());
        // Compile Error
//        flist.add(new Fruit());
        // Compile Error
//        flist.add(new Orange());
        flist.add(null);
        Fruit fruit = flist.get(0);
    }
    flist類型現在是List<? extends Fruit>,讀作“具有任何從Fruit繼承的類型的列表”,但是,這實際上並不意味着這個List能持有任何類型的Fruit,通配符引用的是明確的類型,因此它意味着“某種flist引用沒有指定的具體類型”。因此這個被賦值的List必須持有諸如Fruit或Apple這樣的類型,但是爲了向上轉型成flist,這個類型是什麼沒人關係。
    因爲我們不知道List持有什麼類型,我們就不能安全向其中添加對象,因此編譯期就會阻止我們添加對象。
    另一方面,如果我們調用返回Fruit的方法,則是安全的,因爲這個List中的任何對象至少具有Fruit類型,因此編譯器允許我們get(0).
2、編譯器
	你可能以爲自己被阻止去調用任何接受參數的方法,其實不然
	public class CompilerIntelligence {
	    public static void main(String[] args) {
	        List<? extends Fruit> flist = Arrays.asList(new Apple());
	        Apple apple = (Apple) flist.get(0);
	        System.out.println(flist.contains(new Apple()));
	        System.out.println( flist.indexOf(new Apple()));
	    }
	}
	contains和indexOf都可以接受Apple對象,並且可以正常執行。是否意味着編譯器實際將檢查代碼,來查看是否有特定的方法修改了他的對象?
	通過ArrayList的文檔,我們可以發現add接受一個泛型參數類型的參數,但是contains和indexOf將接受Object類型的參數,因此,當指定一個  ArrayList<? extends Fruit>時,add的參數就變成了? extends Fruit.因此,編譯器並不能瞭解這裏需要Fruit的哪個具體子類型,因此它不會接受任何類型的Fruit,如果將Apple向上轉型爲Fruit,編譯器將直接拒絕對參數列表中涉及通配符的方法(add)的調用,也就是不能直接add(new Fruit())。
	在contains和indexOf,參數類型爲Object,因此不涉及任何通配符,而編譯器也將允許調用。意味着泛型類的設計者決定哪些調用時安全的,並用Object類作爲參數類型。爲了在類型中使用了通配符的情況下禁止這類調用,我們需要在參數列表中使用類型參數
	public class Holder<T> {
	    private T value;

	    public Holder() {
	    }

	    public Holder(T value) {
	        this.value = value;
	    }

	    public T getValue() {
	        return value;
	    }

	    public void setValue(T value) {
	        this.value = value;
	    }

	    @Override
	    public boolean equals(Object o) {
	        return value.equals(o);
	    }

	    @Override
	    public int hashCode() {
	        return Objects.hash(value);
	    }

	    public static void main(String[] args) {
	        Holder<Apple> appleHolder = new Holder<>(new Apple());
	        Apple value = appleHolder.getValue();
	        appleHolder.setValue(value);
	        // cannot upcast
	//        Holder<Fruit> fruit = appleHolder;
	        Holder<? extends Fruit> fruit = appleHolder;
	        Fruit f = fruit.getValue();
	        value = (Apple)fruit.getValue();
	        try{
	           Orange c =  (Orange)fruit.getValue();
	        } catch (Exception e){
	            System.out.println(e);
	        }
	        // cannot call set
	        fruit.setValue(new Apple());
	        // cannot call set
	        fruit.setValue(new Fruit());
	        System.out.println(fruit.equals(value));

	    }

	}
	如果創建了一個Holder<Apple>不能向上轉型爲Holder<Fruit>,但是可以向上轉型爲Holder<? extends Fruit>如果調用getValue,只會返回一個Fruit——在給定“任何擴展自Fruit的對象”這一邊界後,它所能知道的一切了。如果你能夠了解更多的信息,比如強制轉換成Apple【某種具體的Fruit類型】,這不會導致任何警告,但是存在着ClassCastException【轉成orange】。set方法不能作用於apple或Fruit,是因爲setValue的參數也是“? extends Fruit”,這意味着它可以是任何事物,而編譯器無法驗證“任何事物”的類型安全性。
	但是equals由於它接受的是Object而非T類型,因此,編譯器只關注傳遞進來和要返回的對象類型,它並不會分析代碼,以查看是否執行了任何實際的寫入和讀取操作。
3、逆變
	超類型通配符,可以聲明通配符是由某個特定類的任何基類來界定的,方法用<? super MyClass> 類型參數<? super T>儘管你不能對泛型參數給出一個超類型邊界,即不能聲明<T super MyClass>。super使得我們可以安全地傳遞一個類型對象到泛型類型中。
	static void writeTo(List<? super  Apple> apples){
        apples.add(new Apple());
        apples.add(new Jonathan());
        // Error
//        apples.add(new Fruit());
    }
    參數Apple是Apple的某種基類型的List,這樣我們可以向其中安全添加Apple或Apple的子類型。既然Apple是下界,那麼我們添加Fruit顯然是不安全的,會導致這個List擴大接納範圍,從而可以向其中添加非Apple類型的對象,這是違反靜態類型安全的。
    我們可以根據能夠像一個泛型類型“寫入”(傳遞給一個方法),以及從一個泛型類型中讀取(從一個方法返回),來思考子類型和超類型邊界。
    超類型邊界放鬆了可以向方法傳遞的參數上所做的限制。
    public class GenericWriting {
	    static <T> void writeExact(List<T> list, T item) {
	        list.add(item);
	    }

	    static List<Apple> apples = new ArrayList<Apple>();
	    static List<Fruit> fruit = new ArrayList<Fruit>();
	    static void f1(){
	        writeExact(apples,new Apple());
	        // 未知版本會出現不自動向上轉型而 出現Error
	        writeExact(fruit,new Fruit());
	    }
	    static <T> void writeWithWildcard(List<? super T> list,T item){
	        list.add(item);
	    }
	    static void f2(){
	        writeWithWildcard(apples,new Apple());
	        writeWithWildcard(fruit,new Apple());
	    }

	    public static void main(String[] args) {
	        f1();
	        f2();
	    }
	}
	writeExact沒有使用通配符可能導致不允許將Apple放到List<Fruit>中,即使知道這應該可以的。
	writeWithWildcard中,其參數爲List<? super T>因此這個List將持有從T導出的某種具體類型。這樣就可以安全地將一個T類型的對象或者從T導出的任何對象作爲參數傳遞給List的方法。
	public class GenericReading {
	    static <T> T readExact(List<T> list){
	        return list.get(0);
	    }
	    static List<Apple> apples = Arrays.asList(new Apple());
	    static List<Fruit> fruits = Arrays.asList(new Fruit());
	    static void f1(){
	        Apple apple = readExact(apples);
	        Fruit fruit = readExact(fruits);
	        fruit = readExact(apples);
	    }
	    /**
	     *  如果是一個類,那麼他的類型在這個類初始化完成後就被建立關係
	      */
	    static class Reader<T>{
	        T readExact(List<T> list){return list.get(0);}
	    }
	    static void f2(){
	        Reader<Fruit> fruitReader = new Reader<>();
	        Fruit fruit = fruitReader.readExact(fruits);
	        // compile error readExact(List<Fruit>) cannot be applied to List<Apple>
	//        Fruit a = fruitReader.readExact(apples);
	    }
	    static class CovariantReader<T>{
	        T readCovariant(List<? extends T> list){
	            return list.get(0);
	        }
	    }
	    static void f3(){
	        CovariantReader<Fruit> fruitCovariantReader = new CovariantReader<>();
	        Fruit fruit = fruitCovariantReader.readCovariant(fruits);
	        Fruit fruit2 = fruitCovariantReader.readCovariant(apples);
	    }

	    public static void main(String[] args) {
	        f1();f2();f3();
	    }
	}
	readExact使用了精確類型。因此如果使用這個沒有任何通配符的精確類型,就可以向List寫入和讀取這個精確類型。對於返回值,靜態的泛型方法可以有效地“適應”每個方法調用,並從List<Apple>返回Apple,List<Fruit>返回一個Fruit。因此,如果可以擺脫靜態泛型方法,那麼當只是讀取時,就不需要協變類型了。
	但是,當我們使用泛型類,並創建這個類的實例時,要爲這個類確定參數,從fruitReader中List<Fruit>可以讀取一個Fruit,因爲是它的確切類型,但是List<Apple>還應該產生Fruit對象,而fruitReader不允許這麼做。
	爲了解決上述問題,CovariantReader方法將接受List<? extends T>。因此從這個列表中讀取一個T是安全的(你知道這個列表中的所有對象至少是一個T,並且可能是從T導出的對象)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章