Java 泛型進階:類型邊界、通配符、PECS 原則與泛型方法實戰
一、限制泛型可用類型:有界類型參數
1.1 語法與機制
<T extends BoundType> // 上界:T 必須是 BoundType 或其子類
<T super BoundType> // 下界:僅用於通配符,T 必須是 BoundType 或其父類
extends 關鍵字同時用於繼承類別與實作介面。
- 未指定邊界時預設
<T extends Object>。
- 可指定多重邊界:
<T extends Comparable<T> & Serializable>(類別最多一個,介面多個)。
1.2 程式碼範例:繼承抽象類別
// 限制 T 必須為 Animal 或其子類
class GenericClass<T extends Animal> {
private T obj;
public void setObj(T obj) { this.obj = obj; }
public T getObj() { return obj; }
}
abstract class Animal {
public abstract void eat();
}
class Dog extends Animal {
public void eat() { System.out.println("啃骨頭"); }
}
class Cat extends Animal {
public void eat() { System.out.println("吃魚肉"); }
}
// 使用
GenericClass<Dog> dogBox = new GenericClass<>();
dogBox.setObj(new Dog());
dogBox.getObj().eat(); // 編譯期保證有 eat() 方法
// GenericClass<String> errorBox = new GenericClass<>(); // 編譯錯誤:String 非 Animal 子類
1.3 程式碼範例:實作介面
interface Animal2 { void eat2(); }
class Dog2 implements Animal2 { public void eat2() { System.out.println("啃骨頭2"); } }
class Cat2 implements Animal2 { public void eat2() { System.out.println("吃魚肉2"); } }
class GenericClass2<T extends Animal2> { ... } // 同樣運作
二、類型通配符:解決泛型不變性問題
2.1 泛型不變性痛點
Generic<Boolean> g1 = new Generic<>();
Generic<Integer> g2 = new Generic<>();
// g1 = g2; // 編譯錯誤:Generic<Boolean> 非 Generic<Integer> 子類
// Generic<Object> g3 = g1; // 編譯錯誤:泛型不變性
2.2 三種通配符形式
| 語法 | 名稱 | 可讀取型別 | 可寫入型別 | 適用場景 |
<?>無限定通配符 | Object | null | 僅需 Object 方法(toString、equals) |
<? extends T>上界通配符 | T 及其子類 | null | Producer(讀取/消費資料) |
<? super T>下界通配符 | Object | T 及其子類 | Consumer(寫入/生產資料) |
2.3 PECS 原則(Producer Extends, Consumer Super)
// Producer:從結構讀取資料 → 用 extends
public void printAnimals(List<? extends Animal> animals) {
for (Animal a : animals) { // 讀取時視為 Animal
a.eat();
}
// animals.add(new Dog()); // 錯誤:不可寫入具體類型
}
// Consumer:向結構寫入資料 → 用 super
public void addDogs(List<? super Dog> animals) {
animals.add(new Dog()); // 寫入 Dog 及其子類安全
animals.add(new Puppy()); // Puppy 繼承 Dog
// Animal a = animals.get(0); // 錯誤:讀取只能視為 Object
}
2.4 完整範例程式碼
public class WildcardDemo {
public static void main(String[] args) {
GenericClass<Dog> dogBox = new GenericClass<>();
dogBox.setObj(new Dog());
GenericClass<Cat> catBox = new GenericClass<>();
catBox.setObj(new Cat());
GenericClass<String> strBox = new GenericClass<>();
// 1. 無限定通配符:只能讀取 Object 方法
GenericClass<?> wildcard = dogBox;
Object obj = wildcard.getObj(); // 僅能當 Object
// wildcard.setObj(new Dog()); // 錯誤:不可寫入
// 2. 上界通配符:可讀取 Animal 方法
GenericClass<? extends Animal> upper = dogBox;
Animal a = upper.getObj(); // 安全讀取為 Animal
a.eat(); // 呼叫 Animal 方法
// upper.setObj(new Dog()); // 錯誤
// 3. 下界通配符:可寫入 Dog 及子類
GenericClass<? super Dog> lower = new GenericClass<Animal>();
lower.setObj(new Dog()); // 寫入 Dog
lower.setObj(new Puppy()); // 寫入 Puppy (Dog 子類)
// lower.setObj(new Cat()); // 錯誤:Cat 非 Dog 子類
Object o = lower.getObj(); // 讀取只能是 Object
}
}
interface Animal { void eat(); }
class Dog implements Animal { public void eat() { System.out.println("啃骨頭"); } }
class Puppy extends Dog { public void eat() { System.out.println("喝奶"); } }
class Cat implements Animal { public void eat() { System.out.println("吃魚肉"); } }
class GenericClass<T> {
private T obj;
public void setObj(T obj) { this.obj = obj; }
public T getObj() { return obj; }
}
三、泛型方法:方法級別的類型參數化
3.1 語法定義
[修飾符] <T> 回傳型別 方法名(參數列表) { ... }
- 類型參數
<T> 置於修飾符與回傳型別之間。
- 可用於回傳型別、參數型別、區域變數型別。
- 靜態方法必須使用泛型方法,無法使用類別的型別參數。
- 可重載:不同邊界的泛型方法視為不同簽名。
3.2 何時使用泛型方法而非泛型類?
- 型別約束僅作用於單一方法的多個參數之間(如比較兩個同型別物件)。
- 工具類靜態方法(
Collections.sort、Arrays.asList)。
- 不希望整個類別參數化,僅特定操作需要型別安全。
3.3 程式碼範例
public class GenericMethodDemo {
public static void main(String[] args) {
GenericUtil util = new GenericUtil();
// 1. 一般泛型方法:任意型別列印
util.println("Hello");
util.println(123);
util.println(true);
util.println(3.14);
util.println(new Dog());
// 2. 有界泛型方法:僅接受 Animal 子類
util.printlnAnimal(new Dog()); // 呼叫 eat()
util.printlnAnimal(new Cat());
// util.printlnAnimal("String"); // 編譯錯誤
// 3. 靜態泛型方法
GenericUtil.staticPrint("靜態泛型方法");
}
}
class GenericUtil {
// 一般泛型方法
public <T> void println(T content) {
System.out.println(content);
}
// 有界泛型方法(重載)
public <T extends Animal> void println(T animal) {
animal.eat(); // 安全呼叫 Animal 方法
}
// 靜態泛型方法
public static <T> void staticPrint(T content) {
System.out.println("Static: " + content);
}
}
interface Animal { void eat(); }
class Dog implements Animal { public void eat() { System.out.println("啃骨頭"); } }
class Cat implements Animal { public void eat() { System.out.println("吃魚肉"); } }
四、進階主題:型別擦除與可變參數陷阱
- 型別擦除:泛型資訊僅存在編譯期,執行期被擦除為邊界類型(或 Object)。
- 不可建立泛型陣列:
new T[10]、new List<String>[10] 皆非法,改用 List<List<String>>。
- 可變參數堆污染:
<T> void foo(T... args) 會建立 Object[],若 T 為具體型別可能導致 ClassCastException,需加 @SafeVarargs 註解(Java 7+)。
- 橋接方法:編譯器為多態產生橋接方法,可能導致效能微損與堆疊追蹤混淆。