【Java】java 中的泛型通配符——從「偷偷地」地改變集合元素提及

一直沒注意這方面的內容,想來這也算是基礎了,就寫了這個筆記。java

首先java的通配符共有三種————先別緊張,如今只是粗略的過一下,看不看其實無所謂程序員

類型 介紹
<?> 無限定通配符,等價於 <? extends Object>
<? extends Number> 上限通配符,表示參數類型只能是 Number 或是 Number 的子類。
<? super Number> 下限通配符,表示參數類型只能是 Number 或是 Number 的父類。

而後再讓咱們定義四個類,下面會用到數組

class A {
    public String getName() {
        return "A";
    }
}

class B extends A{
    @Override
    public String getName() {
        return "B";
    }
}

class BAge extends B{
    @Override
    public String getName() {
        return "C";
    }
    
    public int getAge() {
        return 100;
    }
}

class BSize extends B{
    @Override
    public String getName() {
        return "D";
    }
    
    public int getSize() {
        return -1;
    }
}

從一個奇怪的現象提及

  1. 首先,咱們再引入一個類 PrintAges ,用於打印 BAge 的 getAge()
class PrintAges{
    public static void print(BAge[] ages){
        if (ages == null)
            return;

        for (BAge bage : ages){
            if (bage != null)
                System.out.println(bage.getAge());
        } 
    }
}

仔細看看上面這個類,你以爲我寫的 PrintAges 怎樣?夠完美嗎,不會引起異常吧?我以爲也很完美了,確定不會有異常出如今個人代碼裏了。ide

  1. 咱們測試下
BAge[] temps = new BAge[]{new BAge(), new BAge()};
PrintAges.print(temps);

輸出:函數

100
100

完美運行。測試

  1. 咱們再增長兩行
BAge[] temps = new BAge[]{new BAge(), new BAge()};
B[] barray = temps;  // 新增長的第一行
barray[0] = new BSize(); // 新增長的第二行
PrintAges.print(temps);

你猜怎麼着?我偷偷地改變了數組中的元素!我在 BAge 類型的數組中的元素賦了一個 BSize 的對象!
並且,編譯經過了。可是確定會有異常出現,你猜是在哪一行?code

輸出:對象

Exception in thread "main" java.lang.ArrayStoreException: JavaApp.BSize at JavaApp.JavaApplicationStudyGen.main(JavaApplicationStudyGen.java:33)

原本我覺得會在 PrintAges 的 print 方法中發生異常,可是實際上新增長的第二行發生了運行時錯誤,賦值錯誤。遊戲

而在C#中,這種問題出現的可能性就更小了。C#中,新增的第一行是沒法經過編譯的。

那麼,這種問題在集合……準確地說是在泛型裏會不會出現呢?get

  1. 上述問題在泛型中的討論。

咱們先對 PrintAges 添加一個 print 函數的重載

class PrintAges{
    public static void print(ArrayList<BAge> list) {
        if (list == null)
            return;
        
        for(BAge age : list) System.out.println(age.getAge());
    }
    public static void print(BAge[] ages){
        if (ages == null)
            return;

        for (BAge bage : ages){
            if (bage != null)
                System.out.println(bage.getAge());
        } 
    }
}

而後咱們對用再次運行以下代碼:

ArrayList<BAge> list = new ArrayList<BAge>();
list.add(new BAge());
ArrayList<B> yourList = list; // 編譯錯誤
yourList.set(0, new BSize()); // star 1
BAge age = list.get(0); // star 2
PrintAges.print(list);

此次,Java 處理的比較嚴格,在把 ArrayList<BAge> 賦值給 ArrayList<B> 類型的對象時產生了編譯錯誤。

在 C# 裏,也是同樣的,在把 ArrayList<BAge> 賦值給 ArrayList<B> 類型的對象時會產生編譯錯誤。

一開始,我不理解這樣作對 list 引用的對象 ArrayList 會產生什麼負面影響。

可是,不能賦值的緣由,把一個 BSize 類型的對象放在了一個其實是 ArrayList 的集合裏。而ArrayList 又假設集合中的元素類型都是 BAge 。倒不是運行時的虛擬機會假設,由於泛型最後都會類型擦除(type erasure)。其實倒不是類型擦除自己引發了這個錯誤,而是原本就存在這樣一種現象。我給出類型擦除以後的樣子是爲了便於理解。

通過類型擦除以後, star 2 所在行的代碼就會變成

BAge age = (BSize)list.get(0); // star 2

這樣就是徹底不正確的了。

也就是說,咱們應該禁止相似 ArrayList<B> yourList = new ArrayList<BAge>() 這樣的賦值,不然,就會出現這樣的錯誤和意外。

說實話,B[] barray = new BAge[]{new BAge(), new BAge()} 這樣的賦值操做也該被禁止的,可是 Java 就能夠。看看人家 C# 就不容許這樣作(笑)

記住這樣的錯誤。接下來,咱們就能夠討論 Java 的泛型通配符了。

通配符出現的緣由

因此所,通配符的出現就是爲了在錯誤避免上述錯誤的同時,給程序員提供一點便利

而通配符是怎麼樣發生做用的呢?是經過編譯器給定的三條「遊戲規則」(也便是上面給的表格裏的規則)發生做用的。

在一開始理解的時候是須要一點邏輯能力的:

  1. 上限通配符 <? extends B> 確保了可讀性, <? extends B> 表示參數類型只能是 B 或是 B 的子類 能夠被編譯經過的語句:
ArrayList<? extends B> list = new ArrayList<A>(); // 編譯錯誤
ArrayList<? extends B> list = new ArrayList<B>(); // ok
ArrayList<? extends B> list = new ArrayList<BAge>(); // ok
ArrayList<? extends B> list = new ArrayList<BSize>(); // ok

基於以上的編譯規則,咱們能夠得出如下事實:

  • 你必定能從 list 中讀取到一個 B 元素,由於 list 要麼指向 ArrayList<B> ,要麼指向包含 B 子類對象的 ArrayList<B>
  • 你不能不能插入一個 B 元素 ,由於 list 可能指向的是 ArrayList<BSize> 或者指向 ArrayList<BAge>
  • 你不能不能插入一個 BAge 元素 ,由於 list 可能指向的是 ArrayList<BSize>
  • 你不能不能插入一個 BSize 元素 ,由於 list 可能指向的是 ArrayList<BAge>

注意,上述代碼中, list 中的 T 被替換成了 ? extends B

也就是說,讀取操做能夠被確保,你必定能從 list 中讀取到一個 B 元素 這樣, list.get 方法就能夠被正常使用了。

list.set(int, T) 就被替換成了 list.set(int, ? extends B),這個方法就被編譯器「禁止」了。也就是說,若是你寫出 list.set(0, new B())list.set(0, new BSize()) 是不行的。

在這裏你確定要提出疑問了,你不是說符合「遊戲規則」 <? extends B> 表示參數類型只能是 B 或是 B 的子類 就行的嗎? 我只能說,文字所能傳達的信息是有限的,這個表述也只適用於 ArrayList<? extends B> list = new ArrayList<A>(); 這樣的賦值時刻。仍是得看上述推導的「事實」

  1. 下限通配符 <? super B> 確保了寫入性
ArrayList<? super B> list = new ArrayList<Object>(); // ok
ArrayList<? super B> list = new ArrayList<A>(); // ok
ArrayList<? super B> list = new ArrayList<B>(); // ok
ArrayList<? super B> list = new ArrayList<BAge>(); // 編譯錯誤
ArrayList<? super B> list = new ArrayList<BSize>(); // 編譯錯誤

基於以上的編譯規則,咱們能夠得出如下事實:

  • 你必定能插入一個 B 類型的對象或者 B 子類型的對象。由於, list 要麼指向包含 B 類型的 ArrayList,要麼指向包含 B 超類型的 ArrayList 對象,好比: list 多是 ArrayList<Object>ArrayList<A>
  • 你必定你不能保證讀取到 B ,由於 list 可能指向 ArrayList<Object> 或者是 ArrayList<B>

這樣, list.set 方法就能夠被正常使用了。假設 list 指向 ArrayList<Object> ,咱們把一個 B 類型的對象添加到 ArrayList<Object> 中也沒錯啊。

  • 或者,咱們把一個 BAge 對象添加到 ArrayList<Object>ArrayList<A> 中也沒錯啊。
  • 或者,咱們把一個 BSize 對象添加到 ArrayList<Object>ArrayList<A> 中也沒錯啊。

總結

  1. 通配符的出現是爲了讓程序員在避免上述錯誤的狀況下能放寬一點要求,即所謂的「符合我編譯器的規則,就讓你舒服」
  2. ? extends B 確保了可讀性,? super B 確保了寫入性。
  3. ? extends B? super B 給人的感受是逆操做。
相關文章
相關標籤/搜索