1. Generics 的不變性:List<Number> ≠ List<Integer>
在 Java 中,泛型的設計是嚴格的:它們是不變的。這代表即使一個型別是另一個型別的子型別(例如,Integer 是 Number 的子型別),帶有這些型別參數的集合也彼此無關。
想像一下:你有一個裝蘋果的盒子(List<Integer>),有人說這其實是「裝水果的盒子」(List<Number>)。看起來好像有道理,畢竟蘋果是水果。但這樣一來就能把香蕉(Double)放進去,一切就壞掉了——因為這盒其實是「蘋果盒」。
程式碼範例:
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // 編譯錯誤!
編譯器在此處特別嚴格:它不允許把「蘋果清單」變成「水果清單」。否則我們就能往裡面加任何東西,程式會在執行期間才崩潰。
因此,List<Number> 和 List<Integer> 是兩個完全不同的型別,儘管 Integer 本身是 Number 的子型別。
與陣列相反:
Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // 允許(陣列具共變性)
numArr[0] = 3.14; // 執行階段錯誤(ArrayStoreException)
結論:為了在編譯期確保型別安全,泛型是不變的。
2. 型別參數的界限
有時需要限制泛型類別或方法可使用的型別。為此會使用界限(bounds)。
上界(extends)
class Stats<T extends Number> {
private T[] nums;
// ...
}
現在 Stats 只能用於是 Number 子型別的型別(Integer、Double、Float 等)。嘗試建立 Stats<String> 會導致編譯錯誤。
以介面作為限制
class Sorter<T extends Comparable<T>> {
void sort(List<T> list) { /* ... */ }
}
現在 Sorter 只能用於實作 Comparable 介面的型別。
多重界限
可以透過 & 一次指定多個限制:
class MyClass<T extends Number & Comparable<T>> { /* ... */ }
T 必須是 Number 的子型別,且要實作 Comparable<T>。
順序很重要:先類別,再介面。
3. 認識 wildcard:?
Wildcard 是一種「佔位」型別,表示:「這裡可以是某個型別,但我不知道確切是哪一個」。
範例:
- List<?> —— 任意型別的清單。
- List<? extends Number> —— 任何 Number 子型別的清單(例如 Integer、Double)。
- List<? super Integer> —— 任何 Integer 超型別的清單(例如 Integer、Number、Object)。
為什麼需要 wildcards?
它們能讓方法處理彼此相關但不同的型別集合。
範例:唯讀(producer)
void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(123); // 錯誤!不可新增元素
}
- 可以把元素當作 Number 讀取。
- 不可新增元素(除了 null)。
範例:只寫(consumer)
void addIntegers(List<? super Integer> list) {
list.add(42); // OK
// Integer x = list.get(0); // 錯誤!無法得知會回傳哪種型別
}
- 可以加入 Integer(或其子型別)的元素。
- 無法安全地把元素以 Integer 讀出(只能當作 Object)。
PECS 規則
PECS —— 「Producer Extends, Consumer Super」:
- Producer — Extends:若集合只「提供」資料(producer),使用 ? extends T。
- Consumer — Super:若集合只「接收」資料(consumer),使用 ? super T。
好記口訣:extends——用於讀取(producer),super——用於寫入(consumer)。
比較:陣列具共變性,泛型具不變性
- 陣列共變:可以把 Integer[] 指派給 Number[] 變數。
- 泛型不變:不可把 List<Integer> 指派給 List<Number> 變數。
Wildcards 可在一定程度上「緩解」泛型的不變性。
5. 泛型方法與型別推論;限制(type erasure)
泛型方法
可以宣告能處理任意型別的泛型方法:
public static <T> void printList(List<T> list) {
for (T elem : list) {
System.out.println(elem);
}
}
型別推論(type inference)
Java 可以「推斷」型別參數:
List<String> strings = List.of("a", "b");
printList(strings); // T = String
泛型的限制:型別擦除
- 在 Java 中,泛型透過型別擦除實作:編譯後會移除型別參數資訊。
- 在位元組碼中,List<String> 與 List<Integer> 沒有差別。
- 不能建立泛型型別的陣列:new List<String>[10] —— 錯誤。
- 不能對型別參數使用 instanceof:obj instanceof List<String> —— 錯誤。
6. 集合與 Stream API 的實作練習
在清單之間複製元素
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
- src —— producer(? extends T)
- dest —— consumer(? super T)
使用方式:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK:Number 是 Integer 的超型別
Stream API 與 wildcards
List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;
numbers.stream()
.map(Number::doubleValue)
.forEach(System.out::println);
搭配 wildcard 的篩選
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
7. 常見錯誤
錯誤一:Raw types(原始型別)。
使用原始型別會關閉型別檢查,並導致執行階段錯誤。
List list = new ArrayList(); // raw type——不好!
list.add("字串");
list.add(123); // 可以加入任何東西
String s = (String) list.get(1); // ClassCastException!
切勿使用 raw types。請一律指定型別參數:List<String>、List<Integer>。
錯誤二:搭配 extends 的不安全操作。
嘗試向帶有 ? extends ... 的集合新增元素會導致編譯錯誤。
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // 編譯錯誤!
錯誤三:因型別擦除導致的多載衝突。
不能只依泛型參數來多載方法——型別擦除後簽章會相同。
public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // 編譯錯誤!
錯誤四:泛型型別的陣列。
由於型別擦除,無法建立參數化型別的陣列。
List<String>[] arr = new List<String>[10]; // 編譯錯誤!
GO TO FULL VERSION