首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 多態下的陣列、泛型與集合:型別协变、逆变與不變的完整規則解析

     編輯:Java 相關知識

Java 型別系統在多態情境下對陣列、泛型、集合有截然不同的規則。本文深入剖析陣列協變、泛型不變、通配符協變/逆變的編譯期與執行期行為,並以程式碼實證說明堆污染風險、ArrayStoreException 機制、PECS 原則,助你徹底掌握這類高頻面試與實戰陷阱。

一、陣列:協變 但有執行期檢查

規則

  • Child[] 可賦值給 Parent[] 變數(協變)
  • 編譯通過,但執行期寫入非 Child 物件拋 ArrayStoreException
  • 讀取安全,寫入有風險

程式碼實證

class Animal { void show() { System.out.println("Animal"); } }
class Dog extends Animal { void show() { System.out.println("Dog"); } }

public class ArrayCovariance {
    public static void main(String[] args) {
        Dog[] dogs = {new Dog(), new Dog()};
        Animal[] animals = dogs;  // 協變:編譯通過

        animals[0].show();  // 輸出 Dog - 讀取安全,動態分派

        // 執行期檢查:嘗試存入非 Dog 物件
        animals[0] = new Animal();  // 編譯通過!執行拋 ArrayStoreException
    }
}

記憶體模型解析

  • dogs 與 animals 指向同一個堆上陣列物件
  • 陣列元素型別資訊保留在執行期(Reified),JVM 執行寫入時檢查實際元素型別

二、泛型:不變 性(Invariant)

規則

  • List<Dog> 不可賦值給 List<Animal>
  • 編譯期直接報錯,無執行期檢查機制(類型擦除)

程式碼實證

import java.util.*;

public class GenericsInvariance {
    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        // List<Animal> animals = dogs;  // 編譯錯誤:incompatible types
    }
}

為什麼?避免堆污染

// 假設允許 List -> List
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;  // 假設合法
animals.add(new Cat());       // 堆污染:Dog 列表塞入 Cat
Dog dog = dogs.get(0);        // 執行期 ClassCastException!

泛型擦除導致執行期無型別資訊,若允許協變,寫入錯誤型別只會在後續讀取時暴露,極難除錯。Java 選擇編譯期禁止。

三、通配符:受限協變/逆變

為彌補泛型不變的剛性,Java 引入 ? extends T(上界)與 ? super T(下界)。

1. 上界通配符 ? extends T:協變(生產者)

  • List<? extends Animal> animals = dogs; 合法
  • 只能讀,不能寫(除 null)
  • 讀出型別為 Animal(或上界)
  • 適用場景:消費/讀取 集合元素(Producer - Extends)

程式碼實證

List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());

List<? extends Animal> animals = dogs;  // 合法

animals.get(0).show();  // 讀取 OK,型別 Animal
// animals.add(new Dog());  // 編譯錯誤:不能寫入
// animals.add(new Animal()); // 編譯錯誤
animals.add(null);      // 唯一合法寫入

2. 下界通配符 ? super T:逆變(消費者)

  • List<? super Dog> animals = new ArrayList<Animal>(); 合法
  • 只能寫,讀出只能是 Object
  • 寫入 Dog 或其子類安全
  • 適用場景:生產/寫入 元素到集合(Consumer - Super)

程式碼實證

List<? super Dog> animals = new ArrayList<Animal>();
animals.add(new Dog());      // 寫入 OK
animals.add(new Puppy());    // 寫入子類 OK
// Animal a = animals.get(0); // 編譯錯誤:讀出只能 Object
Object obj = animals.get(0); // 僅能作 Object 讀取

四、PECS 原則總結

五、集合介面的多態:正常繼承關係

  • List<Object> list = new ArrayList<Object>(); 合法(同型別)
  • Collection<Object> coll = new ArrayList<Object>(); 合法(介面多態)
  • 介面與實現類之間遵循標準多態,不涉及泛型型別參數變化

六、面試高頻陷阱題

  1. List<Object> list = new ArrayList<String>(); → 錯(泛型不變)
  2. List<? extends Object> list = new ArrayList<String>(); → 對(上界協變)
  3. void process(List<Animal> list) 能傳 List<Dog>?→ 不能。改 void process(List<? extends Animal> list)
  4. List<? super Exception> list = new ArrayList<RuntimeException>(); → 錯(RuntimeException 不是 Exception 的父類)
  5. 方法簽名設計:void copy(List<? super T> dest, List<? extends T> src) → 標準 PECS 應用

七、總結

  • 陣列協變:執行期 ArrayStoreException 守護,讀安全寫危險
  • 泛型不變:編譯期禁止,防堆污染,類型擦除無執行期資訊
  • 上界 ? extends T:協變,唯讀,生產者
  • 下界 ? super T:逆變,唯寫,消費者
  • PECS 原則:API 設計黃金法則,靈活兼顧型別安全與複用

掌握這套規則,能讓你設計出型別安全、極具彈性的泛型 API,並輕鬆應對面試中的各種變體題目。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
場景通配符別名能力讀出型別
從集合讀取元素(生產者)? extends TProducer Extends讀 ✓ 寫 ✗T
向集合寫入元素(消費者)? super TConsumer Super讀 ✗ 寫 ✓Object
既要讀又要寫無通配符 <T>Exact Type讀 ✓ 寫 ✓T