Java 型別系統在多態情境下對陣列、泛型、集合有截然不同的規則。本文深入剖析陣列協變、泛型不變、通配符協變/逆變的編譯期與執行期行為,並以程式碼實證說明堆污染風險、ArrayStoreException 機制、PECS 原則,助你徹底掌握這類高頻面試與實戰陷阱。
Child[] 可賦值給 Parent[] 變數(協變)Child 物件拋 ArrayStoreExceptionclass 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 指向同一個堆上陣列物件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(下界)。
? extends T:協變(生產者)List<? extends Animal> animals = dogs; 合法null)Animal(或上界)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); // 唯一合法寫入
? super T:逆變(消費者)List<? super Dog> animals = new ArrayList<Animal>(); 合法Dog 或其子類安全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 讀取
| 場景 | 通配符 | 別名 | 能力 | 讀出型別 |
|---|---|---|---|---|
| 從集合讀取元素(生產者) | ? extends T | Producer Extends | 讀 ✓ 寫 ✗ | T |
| 向集合寫入元素(消費者) | ? super T | Consumer Super | 讀 ✗ 寫 ✓ | Object |
| 既要讀又要寫 | 無通配符 <T> | Exact Type | 讀 ✓ 寫 ✓ | T |