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

《高質量代碼》第7章精讀:泛型擦除、通配符協變與實戰避坑指南

     編輯:Java 綜合教學

《高質量代碼》第7章精讀:泛型擦除、通配符協變與實戰避坑指南

泛型與反射是 Java 類型系統的雙子星。泛型通過編譯期類型檢查提升安全性,反射則打破編譯期限制實現動態性。本文深度解析《改善 Java 程序的 151 個建議》第 7 章(建議 93-97),揭示泛型擦除本質、通配符 PECS 原則、協變逆變陷阱,助你寫出更健壯的泛型代碼。

建議 93:Java 泛型是可以擦除的 —— 理解類型擦除本質

Java 泛型僅存在於編譯期,運行期所有泛型信息被擦除為原始類型。這導致三大核心限制:

1. 重載失效

public void listMethod(List<String> list) {}
public void listMethod(List<Integer> list) {} // 編譯錯誤:擦除後簽名相同

2. 擦除轉換規則

泛型類型擦除後類型
List<String>、List<Integer>、List<T>List
List<String>[]List[]
List<? extends E>、List<? super E>List<E>
List<T extends Serializable & Cloneable>List<Serializable>(取第一個邊界)

3. 經典陷阱驗證

List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true,同一 Class 對象

// 泛型數組初始化非法
// List<String>[] arr = new List<String>[10]; // 編譯錯誤

// instanceof 不能帶泛型參數
// list1 instanceof List<String> // 編譯錯誤

4. 設計原因

  • 避免 JVM 大規模重構(C++ 模板運行期展開導致代碼膨脹)
  • 保證與舊版本兼容(Raw Type 互操作)

建議 94:不能初始化泛型參數和數組

因擦除導致編譯期無法獲取 T 具體類型:

class Test<T> {
    // private T t = new T();           // 編譯錯誤
    // private T[] arr = new T[5];      // 編譯錯誤
    private List<T> list = new ArrayList<>(); // 合法:ArrayList 內部用 Object[]
}

解決方案:通過反射在運行期傳入 Class 對象初始化:

class Test<T> {
    private T t;
    private T[] arr;
    
    public Test(Class<T> clazz) {
        try {
            t = clazz.newInstance();
            arr = (T[]) Array.newInstance(clazz, 5);
        } catch (Exception e) { throw new RuntimeException(e); }
    }
}
// 調用:new Test<String>(String.class)

建議 95:強制聲明泛型實際類型

當編譯器無法推斷類型時(如空參數、混合類型),必須显式指定:

// 空參數:默認推斷為 Object
List<Object> list2 = ArrayUtils.asList(); 

// 強制指定為 Integer
List<Integer> intList = ArrayUtils.<Integer>asList(); 

// 混合類型:編譯器只推斷 Object,但我們期望 Number
List<Number> numList = ArrayUtils.<Number>asList(1, 2, 3.1);

語法:類名.<類型參數>方法名(),統一輸入輸出類型預期。

建議 96:不同場景使用不同泛型通配符 —— PECS 原則

核心口訣:Producer Extends, Consumer Super (PECS)

1. 只讀場景 → 上界 <? extends E>

// 從集合讀取元素進行業務處理
public static <E> void read(List<? extends E> list) {
    for (E e : list) { // 安全:取出的是 E 或其子類
        process(e);
    }
}
// 調用:read(Arrays.asList("A", "B")); // E 推斷為 String

此時集合作為生產者,只能取不能存(除 null)。

2. 只寫場景 → 下界 <? super E>

// 向集合寫入元素
public static <E> void write(List<? super Number> list) {
    list.add(123);      // Integer 是 Number 子類
    list.add(3.14);     // Double 是 Number 子類
    // list.add(new Object()); // 錯誤:Object 不是 Number 父類
}

此時集合作為消費者,只能存不能安全讀(只能當 Object 讀)。

3. 經典案例:Collections.copy

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // src 讀取 → extends
    // dest 寫入 → super
    for (int i = 0; i < src.size(); i++) {
        dest.set(i, src.get(i));
    }
}

建議 97:警惕泛型不支持協變與逆變

Java 數組支持協變,但泛型不支持:

Number[] nums = new Integer[10];     // 合法:數組協變
// List<Number> list = new ArrayList<Integer>(); // 錯誤:泛型不協變

原因:若允許,可導致類型污染:

List<Number> list = new ArrayList<Integer>();
list.add(3.14); // 運行時放入 Double,但實際是 Integer 列表 → ClassCastException

通配符模擬協變/逆變

  • 模擬協變:List<? extends Number> list = new ArrayList<Integer>();(只讀安全)
  • 模擬逆變:List<? super Integer> list = new ArrayList<Number>();(只寫安全)

類型關係速查表

表達式結果
Integer 是 Number 子類型✓
ArrayList<Integer> 是 List<Integer> 子類型✓
Integer[] 是 Number[] 子類型✓
List<Integer> 是 List<Number> 子類型✗
List<Integer> 是 List<? extends Integer> 子類型✗
List<Integer> 是 List<? super Integer> 子類型✗

總結

  • 泛型擦除是 Java 為兼容性妥協的產物,運行期無泛型信息
  • 無法 new T()、new T[]、泛型數組、instanceof 泛型
  • PECS 原則指導通配符選擇:讀用 extends,寫用 super
  • 泛型不協變/逆變,通過通配符在類型安全前提下模擬
  • 無法推斷類型時,显式聲明 <Type>method()
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved