為什麼需要泛型?
泛型程式設計消除了到處可見的 Object 與強制轉型,提升代碼類型安全性與可讀性,將運行時錯誤前移至編譯期。
泛型類定義
class Pair<T> {
private T first;
private T second;
public Pair() {}
public Pair(T first, T second) { this.first = first; this.second = second; }
public T getFirst() { return first; }
public T getSecond() { return second; }
public void setFirst(T v) { first = v; }
public void setSecond(T v) { second = v; }
}
// 使用
Pair<String> p = new Pair<>("min", "max");
<T> 為類型參數
Pair<String> 為參數化類型
- 原始類型:
Pair(不帶參數,不推薦使用)
泛型方法定義
class ArrayAlg {
// 1. 最簡單泛型方法(無限定)
public static <T> T getMiddle(T[] a) {
return a[a.length / 2];
}
// 2. 類型限定:T 必須實作 Comparable
public static <T extends Comparable> T min(T[] a) {
if (a == null || a.length == 0) return null;
T min = a[0];
for (T e : a) if (min.compareTo(e) > 0) min = e;
return min;
}
// 3. 多重限定:<T extends Comparable & Serializable>
// 4. Comparable 本身是泛型,完整寫法下講介紹
}
類型推斷
String[] names = {"John", "Q.", "Public"};
String mid = ArrayAlg.<String>getMiddle(names); // 顯式指定
String mid2 = ArrayAlg.getMiddle(names); // 編譯器自動推斷
基本型別限制
基本型別無法作為類型參數:ArrayAlg.getMiddle(new double[]{1.2}) 編譯錯誤。必須使用包裝類型 Double。
核心原理:類型擦除
JVM 沒有泛型類型物件——所有物件都是普通類物件。泛型純粹是編譯器行為。 這是 Java 與 C++ 模板的本質區別,也是 Java 泛型表達能力受限的根源。
擦除規則
| 情況 | 擦除目標 | 範例 |
無限定 <T> | Object | Pair<T> → Pair (欄位變 Object) |
有限定 <T extends Comparable> | 第一個限定類型 | 擦除為 Comparable |
擦除後的 Pair 類
class Pair {
private Object first;
private Object second;
public Pair() {}
public Pair(Object first, Object second) { ... }
public Object getFirst() { return first; }
public Object getSecond() { return second; }
public void setFirst(Object v) { first = v; }
public void setSecond(Object v) { second = v; }
}
限定切換與效能
多重限定 <T extends A & B> 會產生限定切換:擦除為第一個類型(A),代碼中必要時插入強轉切換到 B。建議:常用介面放前面,標記介面放後面。
使用端:泛型代碼塊翻譯
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
// 編譯器自動翻譯為:
Object obj = buddies.getFirst();
Employee buddy = (Employee) obj; // 插入強制轉型
泛型與多態的衝突與橋接方法
問題 1:擦除導致方法簽名衝突
public class DateInterval extends Pair<Date> {
@Override
public void setSecond(Date second) { // 子類重寫
if (second.compareTo(getFirst()) >= 0) super.setSecond(second);
}
}
擦除後:DateInterval 繼承 Pair(擦除後 setSecond(Object)),同時宣告 setSecond(Date)。兩個方法共存,多態似乎失效?
解決:橋接方法
編譯器自動生成橋接方法保護多態語義:
// 反編譯看到:
public void setSecond(Object second) { // 橋接方法
setSecond((Date) second); // 轉發到真正重寫方法
}
public void setSecond(Date second) { ... } // 真正的重寫方法
@Override 實際作用於 setSecond(Object)
- 橋接方法確保父類引用調用
setSecond(Object) 時正確分派到子類邏輯
問題 2:協變返回類型衝突
public class DateInterval extends Pair<Date> {
@Override
public Date getSecond() { // 協變返回類型
return (Date) super.getSecond().clone();
}
}
反編譯發現兩個同名同參數方法共存:
public Date getSecond(); // 協變方法
public Object getSecond(); // 橋接方法
JVM 允許同簽名不同返回類型的方法共存(位元碼層面以返回類型區分),調用時正確分派。
小結
- 泛型 = 編譯期類型檢查 + 擦除 + 自動插入強轉
- 無運行時泛型資訊(除簽名保留的簽名註解)
- 橋接方法解決擦除與多態/協變的衝突
- 基本型別無法泛型化,需裝箱,有效能成本
下篇將介紹:通配符(? extends T/? super T)、PECS 原則、泛型與陣列的衝突、類型令牌模式等進階主題。
- 上一篇文章: JavaBean 為何必須實作 Serializable?序列化機制深度解析
- 下一篇文章: Java 線程安全單例模式全集:七種實現方式深度對比
Copyright ©
程式碼之家 All Rights Reserved