【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 怎樣?夠完美嗎,不會引發異常吧?我覺得也很完美了,肯定不會有異常出現在我的代碼里了。

  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 的對象!
而且,編譯通過了。但是肯定會有異常出現,你猜是在哪一行?

輸出:

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

本來我以為會在 PrintAges 的 print 方法中發生異常,但是實際上新增加的第二行發生了運行時錯誤,賦值錯誤。

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

那么,這種問題在集合……准確地說是在泛型里會不會出現呢?

  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 給人的感覺是逆操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM