泛型通配符

通配符

首先,要展示數(shù)組的一種特殊行為,可以向?qū)С鲱愋偷臄?shù)組賦予基類型的數(shù)組引用。

class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new Jonathan();
        try {
            fruits[0] = new Fruit();
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            fruits[0] = new Orange();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}
// Outputs
java.lang.ArrayStoreException: com.daidaijie.generices.holder.Fruit
java.lang.ArrayStoreException: com.daidaijie.generices.holder.Orange

main()的第一行創(chuàng)建了一個Apple數(shù)組,并將其賦值給一個Fruit數(shù)組引用。這是有意義的,因為Apple也是一種Fruit,所以Apple數(shù)組應該也是一個Fruit數(shù)組。
但是,如果實際的數(shù)組類型是Apple[],你應該只能在其中放置Apple或者Apple的子類型,這在編譯期和運行時都可以工作。但是要注意的是,編譯器允許Fruit放置到這個數(shù)組中,這對于編譯器來說是有意義的,因為它有一個Fruit引用——它有什么理由不允許將這個Fruit對象或者任意從Fruit繼承出來的對象(例如Orange)放置到這個數(shù)組中呢?因此在編譯期,這是允許的。但是,運行時數(shù)組機制知道它是Apple,因此會在向數(shù)組中放置異構類型時拋出異常。
實際上,向上轉(zhuǎn)型并不合適用在這里。這里真正做的是將一個數(shù)組賦值為另一個數(shù)組。數(shù)組的行為應該是它可以持有其他對象,這里只是因為我們能夠向上轉(zhuǎn)型而已,所以很明顯,數(shù)組對象可以保留有關它們包含的對象類型的規(guī)則。就好像數(shù)組對它們持有的對象是有意識的,因為在編譯期檢查和運行時檢查之間,你不能濫用它們。
對數(shù)組的這種賦值并不是那么可怕,因為在運行時可以發(fā)現(xiàn)你已經(jīng)插入不正確的類型,但是泛型的主要目的是將這種錯誤檢測能夠移入到編譯期。因此當試圖使用泛型容器來代替數(shù)組的時候,會發(fā)生什么呢。

public class NonCovariantGenerics {
    // compile error
    List<Fruit> flist = new ArrayList<Apple>;
}

第一次看這段代碼的時候會認為,"不能講一個Apple容器賦值給一個Fruit容器"。但是,泛型不僅和容器相關正確的說法是,"不能把一個涉及Apple的泛型賦值給一個涉及Fruit的泛型"。如果就像在數(shù)組的情況一樣,編譯器對代碼的了解足夠多,就可以確定所涉及到的容器,,那么它可能會留下一些余地。但是它不知道任何有關這方面的信息,因此她拒絕向上轉(zhuǎn)型。然而這根本不是向上轉(zhuǎn)型——AppleList不是FruitList。AppleList將持有Apple的子類型,而Fruit將持有任何類型的Fruit,誠然,這包括Apple在內(nèi),但是它不是一個AppleList,它仍舊是FruitListAppleList在類型上不等價于FruitList,即使Apple是一種Fruit類型。
而真正的問題是在談論容器的類型,而不是容器持有的類型。與數(shù)組不同,泛型沒有內(nèi)建的協(xié)變類型。這是因為數(shù)組在語言中是完全定義的,因此內(nèi)建了編譯期和運行期的檢查,但是在使用泛型時,編譯器和運行時系統(tǒng)都不知道你想用類型干什么,以及應該采用什么樣的規(guī)則。
但是有時候,想在兩個類型之間建立某種向上轉(zhuǎn)型的關系,這正是通配符允許的。

public class GenericsAndCovariance {
    public static void main(String[] args) {
        List<? extends Fruit> flist = new ArrayList<>();
        // flist.add(new Apple());
        // flist.add(new Fruit());
        // flist.add(new Object());
        flist.add(null); //合法但是沒有意義
        // 我們至少知道這會返回Fruit類型
        Fruit f = flist.get(0);
    }
}

flist類型現(xiàn)在是List<? extends Fruit>,可以將其讀作"具有任何從Fruit繼承的類型的類型的列表",但是這實際上并不意味著這個List將持有任何類型的Fruit。通配符引用的是明確的類型,一次它意味著"某種flist引用沒有指定具體的類型"。因此這個被賦值的List必須持有諸如Fruit或者Apple這樣的某種指定的類型,但是為了向上轉(zhuǎn)型為flist,這個類型是什么沒人關心。
如果唯一的限制是這個List要持有某種具體的Fruit或者Fruit的子類型,但是實際上并不關心它是什么,那么可以用這樣的List做什么呢?如果不知道List要持有什么類型,那么怎么樣才能向其中安全地添加對象呢,就像在CovariantArrays.java中向上轉(zhuǎn)型數(shù)組一樣,所以答案是不能,除非編譯器而不是運行時系統(tǒng)可以阻止這種操作的發(fā)生,但是很快會發(fā)現(xiàn)這一問題。
現(xiàn)在事情有點極端了,因為不能向剛剛聲明過將持有Apple對象的List放置一個Apple對象了。但是編譯器并不知道這一點。List<?extends Fruit>可以合法地指向一個List<Orange>。因此一旦執(zhí)行了這種類型的向上轉(zhuǎn)型,就會丟失掉向其中傳遞任何對象的能力,就算是傳遞Object也不行。
但另一方面,如果調(diào)用的是一個返回Fruit的方法,則是安全的,因為這個List中任何的對象至少具有Fruit類型,因此編譯器允許這樣做。

“智能”的編譯器

編譯器不一定會阻止通配符修飾的泛型類中,調(diào)用任何接受參數(shù)的方法。

public class CompilerIntelligence {

    public static void main(String[] args) {
        List<? extends Fruit> flist = Arrays.asList(new Apple());
        Apple a = (Apple) flist.get(0); // no warning
        flist.contains(new Apple()); // args is 'Object'
        flist.indexOf(new Apple()); // args is 'Object'
    }
}

上面代碼中,對contains()indexOf()的調(diào)用,這兩個方法都接受Apple對象作為參數(shù),而這些調(diào)用都可以正常執(zhí)行。這不是因為編譯器會去檢查代碼,以查看特定的方法是否修改了某個對象,而是因為contains()indexOf()將接受Object類型的參數(shù)。而add()卻是接收了一個具有泛型參數(shù)類型的參數(shù),因為當指定一個ArrayList<? extends Fruits>時,add()的參數(shù)就變成了?Extends Fruits,從這個描述中編譯器并不能了解到這里需要Fruits的哪個具體子類型,因此它也不會接收任何類型的Fruit。即使是將Apple向上轉(zhuǎn)型為Fruit,也無關緊要——編譯器將直接拒絕對參數(shù)列表設計通配符的方法(例如add()的調(diào)用)。
在調(diào)用contains()indexOf()時,參數(shù)類型是Object,因此不涉及任何通配符,而編譯器也將允許這個調(diào)用。
所以這意味著,這將由泛型類的設計者來決定哪種調(diào)用是“安全的”,并使用Object作為其參數(shù)類型。而為了在類型中使用通配符的情況下禁止這類調(diào)用,我們需要在參數(shù)中使用類型參數(shù)。
可以在一個簡單的Holder類中看到這一點。

public class Holder<T> {
    private T value;

    public Holder() {
    }

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

    public T get() {
        return value;
    }

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

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

    public static void main(String[] args) {
        Holder<Apple> apple = new Holder<>(new Apple());
        Apple d = apple.get();

        // can't not upcast
        // Holder<Fruit> fruit = apple;
        Holder<? extends Fruit> fruit = apple; // Ok
        Fruit p = fruit.get();
        try {
            Orange c = (Orange) fruit.get();
        } catch (Exception e) {
            System.out.println(e);
            // fruit.set(new Apple()); Can't call set()
            // fruit.set(new Fruit); Can't call set()
            System.out.println(fruit.equals(d)); // Ok
        }
    }
}
// Outputs
java.lang.ClassCastException: com.daidaijie.generices.holder.Apple cannot be cast to com.daidaijie.generices.holder.Orange
true

Holder有一個接受T類型對象的set()方法,和一個get()方法,以及一個接受Object對象的equals()方法。
可以看到代碼中,Holder<Apple>不能向上轉(zhuǎn)型為Holder<Fruits>,但是可以向上轉(zhuǎn)型為Holder<? extends Fruits>。如果調(diào)用get(),它只會返回一個Fruit——這就是在給定“任何擴展自Fruit的對象”這一邊界之后,它所能知道的一切。如果能夠了解更多的信息,那么可以轉(zhuǎn)型到某種具體的Fruit類型,而這不會調(diào)至任何的警告,但是存在得到ClassCastException的風險。set()方法不能工作于AppleFruit,因為set的參數(shù)也是“? extends Fruit”,這意味它可以是任何事物,而編譯器無法驗證“任何事物”的類型安全性。
但是,equals()方法工作良好,因為它將接受Object類型而并非T類型的參數(shù)。因此,編譯器只關注傳遞進來和要返回的對象類型,它并不會分析代碼,以查看是否執(zhí)行了任何實際的寫入和讀取操作。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容