Java 5 引入泛型,核心目標是將類型檢查從運行時前移到編譯期,消除強制轉型,構建類型安全的集合與 API。但 Java 採用類型擦除實現,導致泛型信息在運行期大多丟失,這帶來了獨特的約束與陷阱。本文系統梳理泛型核心機制、通配符體系、PECS 原則、擦除副作用與實戰模式。
// 定義泛型接口
public interface Map {
V put(K key, V value);
V get(K key);
}
// 實例化:提供具體類型參數
Map wordCount = new HashMap<>();
wordCount.put("apple", 1);
Integer count = wordCount.get("apple"); // 無需轉型,編譯器保證類型安全
命名慣例:單個大寫字母 —— Element、Key、Value、Type、Source、Result。
List strings = new ArrayList<>();
// List
原因:若允許上述賦值,可向 objects 添加 Integer,導致 strings 讀取時拋出 ClassCastException,破壞類型安全。數組是協變的 (String[] 是 Object[] 子類型),但泛型不變。
| 語法 | 名稱 | 含義 | 典型場景 |
|---|---|---|---|
? | 無界通配符 | 任意類型,等價 ? extends Object | 只讀操作、List>.size() |
? extends T | 上界通配符 | T 或其子類型 | 生產者:讀取元素 |
? super T | 下界通配符 | T 或其父類型 | 消費者:寫入元素 |
// 生產者:從集合讀取元素 → 用 extends
public void printAll(List extends Number> nums) {
for (Number n : nums) System.out.println(n); // 讀出即為 Number
// nums.add(1); // 編譯錯誤!無法確定實際類型,禁止寫入
}
// 消費者:向集合寫入元素 → 用 super
public void addIntegers(List super Integer> nums) {
nums.add(1); // 寫入 Integer 安全:實際類型至少是 Integer 父類
nums.add(2);
// Number n = nums.get(0); // 編譯錯誤!讀出只能是 Object
}
// 無界通配符:既不讀具體類型,也不寫具體類型
public void printSize(List> list) {
System.out.println(list.size());
}
// Collection.addAll(Collection extends E>) —— 生產者
List ints = Arrays.asList(1, 2, 3);
List nums = new ArrayList<>();
nums.addAll(ints); // 安全:Integer 是 Number 子類
// Collections.copy(List super T> dest, List extends T> src)
// dest 是消費者 (super),src 是生產者 (extends)
List
Object,有界用界限類型)。List → List (原始類型);T extends Number → Number。
// ❌ 不能 new T() / new T[] / T.class
// ❌ 不能 instanceof T / catch (T e) / T 為基本類型
// ❌ 靜態字段/方法不能引用類型參數
// ❌ 重載方法擦除後簽名相同會衝突
List list = new ArrayList<>();
Method add = list.getClass().getMethod("add", Object.class);
add.invoke(list, 123); // 成功!運行期 List 只見 Object
System.out.println(list); // [123]
// 方法級類型參數 ,不依賴類級參數
public static T max(Collection extends T> coll, Comparator super T> cmp) {
Iterator extends T> it = coll.iterator();
T max = it.next();
while (it.hasNext()) {
T next = it.next();
if (cmp.compare(next, max) > 0) max = next;
}
return max;
}
// 調用時編譯器自動推導 T
String s = max(Arrays.asList("a", "b", "c"), String::compareTo);
Integer i = max(Arrays.asList(1, 2, 3), Integer::compareTo);
// 甚至可省略顯式類型參數,靠目標類型推導
// 要求 T 實現 Comparable,保證可比較
public static > T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
// 多重界限:T 既是 Serializable 又是 Comparable
public static > void saveAndCompare(T a, T b) { ... }
因擦除,new T[] 不可能。解決方案:顯式傳遞 Class
// 集合轉數組:toArray(Class) Java 11+
String[] arr = list.toArray(String[]::new);
// 反射實例化
public static T newInstance(Class clazz) throws ReflectiveOperationException {
return clazz.getDeclaredConstructor().newInstance();
}
// 類型安全的數據庫查詢映射
public List query(String sql, Class rowType, Object... params) {
// 使用 rowType 進行反射映射,編譯器保證返回 List
}
@SuppressWarnings("unchecked") 僅在證明安全時最小範圍使用。List 類型安全;String[] 協變導致運行期 ArrayStoreException。? extends T (生產者),輸出/寫入用 ? super T (消費者)。void foo(T... args) 可能導致堆污染,Java 7+ 用 @SafeVarargs 抑制警告。@param <T> 說明約束與語義。List>、Map。可用於 instanceof、數組創建。MethodHandle、VarHandle 通過 Class 對象在運行期實現類型安全的動態調用,繞過擦除限制。Java 泛型以「編譯期類型安全、運行期類型擦除」為核心權衡。精通通配符體系與 PECS 原則,理解擦除邊界與 Class