前一篇集合框架到處看到 <T> / <K, V>,這篇展開講泛型本身。Java 泛型有個別語言沒有的歷史包袱——「型別擦除」,所以才會出現 ?、extends / super 這套設計。理解了擦除,泛型裡大半的「為什麼要這樣寫」就說得通了。
為什麼要泛型
沒泛型時:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 要強制轉型,runtime 才會炸
list.add(123); // 沒人擋
有泛型後,編譯期就把型別錯誤擋掉:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 不用 cast
list.add(123); // ❌ 編譯錯
泛型類別
class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Box<String> b1 = new Box<>("hi");
Box<Integer> b2 = new Box<>(42);
<>(diamond operator,Java 7+)讓右側型別可省略。
多個型別參數:
class Pair<K, V> {
private final K key;
private final V value;
public Pair(K k, V v) { this.key = k; this.value = v; }
public K key() { return key; }
public V value() { return value; }
}
Pair<String, Integer> p = new Pair<>("age", 30);
慣例命名:T(type)、E(element)、K/V(key/value)、R(return)、N(number)。
泛型方法
方法自帶型別參數,獨立於 class:
class Util {
public static <T> T firstOrNull(List<T> xs) {
return xs.isEmpty() ? null : xs.get(0);
}
public static <K, V> Map<V, K> invert(Map<K, V> m) {
Map<V, K> r = new HashMap<>();
m.forEach((k, v) -> r.put(v, k));
return r;
}
}
String s = Util.firstOrNull(List.of("a", "b"));
<T> 寫在回傳型別前。
上界 bounded type parameter
T extends X:T 必須是 X 或其子型別:
class NumberBox<T extends Number> {
private T value;
public NumberBox(T v) { this.value = v; }
public double doubled() {
return value.doubleValue() * 2; // 能呼叫 Number 的方法
}
}
new NumberBox<>(3); // ✅ Integer extends Number
new NumberBox<>(3.14); // ✅ Double extends Number
// new NumberBox<>("a"); // ❌
多重界(最多一個 class,可多個 interface):
<T extends Comparable<T> & Serializable> void f(T t) { ... }
萬用字元 wildcard
? 代表「任意未知型別」。常見三種用法:
無界 <?>
只在意「是個泛型容器」、不在乎內容型別:
void printSize(Collection<?> c) {
System.out.println(c.size());
}
上界 ? extends T
「T 或 T 的子型別」——生產者 producer,可讀不可寫:
double sum(List<? extends Number> nums) {
double s = 0;
for (Number n : nums) s += n.doubleValue(); // 讀 OK
return s;
}
sum(List.of(1, 2, 3)); // List\<Integer\> ✅
sum(List.of(1.0, 2.0)); // List\<Double\> ✅
不能寫入(除了 null):
List<? extends Number> xs = ...;
// xs.add(1); ❌ 編譯期不知道實際型別是 Integer 還是 Double
下界 ? super T
「T 或 T 的父型別」——消費者 consumer,可寫,但讀不回具體型別:
void addInts(List<? super Integer> xs) {
xs.add(1);
xs.add(2);
}
addInts(new ArrayList<Integer>()); // ✅
addInts(new ArrayList<Number>()); // ✅
addInts(new ArrayList<Object>()); // ✅
讀出來只能當 Object:
Object o = xs.get(0); // 只知道是某個 Integer 的父類
PECS 口訣
Producer Extends, Consumer Super
集合「拿出來給人用」(producer)→ extends;集合「給人塞東西進來」(consumer)→ super。
Collections.copy(List<? super T> dest, List<? extends T> src) 是經典範例。
type erasure:泛型只活在編譯期
JVM 看不到泛型,runtime 全被抹成原始型別(T → Object、T extends Number → Number)。
「擦除」發生在編譯期末段:.java 編譯到 .class 時,List<String> 會被替換為 raw List,所有 T 變成 Object(或 bound 上界)。runtime 看到的是沒有泛型資訊的 class 檔——這就是為什麼 list instanceof List<String> 不能寫(語法禁止)。
後果是一連串限制:
1. 不能 new T()
class Box<T> {
T value = new T(); // ❌
}
繞法:傳 factory 或 Class<T>:
class Box<T> {
T value;
Box(Supplier<T> factory) { this.value = factory.get(); }
}
new Box<>(ArrayList::new);
2. 不能 instanceof T
if (x instanceof T) ... // ❌
3. 不能建泛型陣列
T[] arr = new T[10]; // ❌
List<String>[] arrays = new List<String>[10]; // ❌
4. static 欄位不能用 class 的型別參數
class Box<T> {
static T shared; // ❌
}
5. 兩個泛型 overload 在 erasure 後一樣 → 編譯錯
void f(List<String> a) {}
void f(List<Integer> a) {} // ❌ 兩者 erasure 都是 List
泛型與繼承的反直覺處
即使 Dog extends Animal,List<Dog> 也不是 List<Animal> 的子型別——泛型是 invariant(不變)的。
為什麼要這樣設計?想個反例就懂:如果 List<Dog> 能傳給吃 List<Animal> 的 method,那個 method 就能往裡面 add(new Cat())——型別系統破功。
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // ❌ 編譯錯
要相容就用 wildcard:
void feed(List<? extends Animal> animals) { ... }
feed(dogs); // ✅
陣列卻是「協變」(covariant):
Object[] arr = new String[3]; // 編譯過
arr[0] = 1; // ❌ runtime ArrayStoreException
陣列協變 + erasure 是 Java 早期的歷史包袱,新程式碼盡量避免混用陣列與泛型。
泛型在 record 上
record Pair<A, B>(A first, B second) {
public <C> Pair<A, C> withSecond(C newSecond) {
return new Pair<>(first, newSecond);
}
}
Pair<String, Integer> p = new Pair<>("age", 30);
Pair<String, Double> p2 = p.withSecond(3.14);
下一篇換口味——enum 與字串。enum 在 Java 是「真正的型別」(不是 int 別名),可以帶方法、帶欄位;字串則展開 String immutability、StringBuilder、Java 15+ 的 text block。
Latest Updates
- 2026.06.11 Content updated
