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

Java 泛型通配符深度解析:extends 與 super 的 PECS 原則實戰

     編輯:Java 程式設計入門

前言

Java 5 引入泛型後,<? extends T> 與 <? super T> 成為 API 設計的核心工具。然而許多開發者對「為何 extends 不能 add、super 不能 get」僅知其一不知其二。本文從**類型理論、編譯器推導、實際 API 設計**三個維度,徹底講透這對關鍵字的本質差異與最佳實踐。

一、核心結論:PECS 原則

Producer Extends, Consumer Super (PECS)

  • 只讀(Producer):用 <? extends T> —— 安全取出 T 或其子類。
  • 只寫(Consumer):用 <? super T> —— 安全放入 T 或其子類。
  • 又讀又寫:不要用通配符,直接用 <T>。

二、類型推導視角:為什麼編譯器這麼「嚴格」?

1. List<? extends Fruit> —— 協變

List<? extends Fruit> list = new ArrayList<Apple>(); // 合法
// list.add(new Apple()); // ❌ 編譯錯誤
Fruit f = list.get(0); // ✅ 安全:無論實際是 Apple/Orange,都是 Fruit

編譯器視角:實際類型可能是 Apple、Orange、Fruit 任一子類。**寫入任何具體類型都可能破壞類型安全**(例如實際是 Orange 卻塞入 Apple),故**禁止所有 add(E) 操作**;但**讀出時統一向上造型為 Fruit 一定安全**。

2. List<? super Apple> —— 逆變

List<? super Apple> list = new ArrayList<Fruit>(); // 合法
list.add(new Apple()); // ✅ 安全:Fruit 能裝 Apple
list.add(new GreenApple()); // ✅ 子類也行
// Fruit f = list.get(0); // ❌ 編譯錯誤
Object o = list.get(0); // ✅ 唯一安全:只能當 Object

編譯器視角:實際類型可能是 Apple、Fruit、Object 任一父類。**讀出時無法確定具體類型**(可能是 Fruit 也可能是 Object),故**只能賦值給 Object**;但**寫入 Apple 或其子類一定安全**(父類引用指向子類物件合法)。

三、實戰 API 設計範例

1. 複製元素:copy(List<? super T> dest, List<? extends T> src)

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) {      // src 只讀 → extends
        dest.add(t);       // dest 只寫 → super
    }
}

這是 JDK Collections.copy 的簽名精髓:來源用 extends,目標用 super,實現最大化複用。

2. 比較器:Comparator<? super T>

public static <T> T max(List<? extends T> list, Comparator<? super T> cmp) {
    T max = list.get(0);
    for (T t : list) {
        if (cmp.compare(t, max) > 0) max = t;
    }
    return max;
}
  • 列表只讀 → ? extends T
  • 比較器只消費 T(比較兩個 T)→ ? super T,支援傳入 Comparator<Object> 比較 String 列表。

四、常見誤區與避坑指南

| 誤區 | 正確理解 | |------|----------| | List<Object> 是 List<String> 父類 | **錯**!泛型**不變**,兩者無繼承關係。 | | extends 只能用於返回值 | 可用於**參數**(只讀場景),如 void process(List<? extends Dto> list)。 | | super 只能用於參數 | 可用於**返回值**(工廠模式),如 <T> List<? super T> factory()。 | | 又要加又要取就用 List<?> | List<?> **只能取 Object、只能放 null**;雙向操作請用 **具體類型參數 <T>**。 |

五、進階:類型擦除與橋接方法

泛型在編譯期經過**類型擦除**,List<? extends Fruit> 擦除為 List(raw type)。編譯器插入**橋接方法**與**檢查轉型**確保運行期安全:

// 源碼
list.add(new Apple()); // 編譯錯誤

// 擦除後偽代碼(若允許通過)
List raw = list; // 實際是 ArrayList
raw.add(new Apple()); // 運行期拋 ClassCastException

編譯器在編譯期攔截,避免污染堆記憶體,**這就是泛型「擦除但不失安全」的核心機制**。

六、總結速查表

通配符可讀出類型可寫入類型典型場景
<? extends T>T 及父類❌ (僅 null)來源、生產者、只讀遍歷
<? super T>Object 唯一T 及子類目標、消費者、收集器、比較器
<T>TT又讀又寫、內部實現、遞歸泛型

💡 記憶口訣:
「取用 extends,存用 super」
「Producer Extends, Consumer Super」
下次看到通配符,先問自己:這個集合在當前作用域是「生產者」還是「消費者」?

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved