Java 泛型萬用字元
萬用字元 ? 代表未知型別,用於增加泛型的靈活性。
為什麼需要萬用字元
泛型不是協變的,這會造成限制:
// 陣列是協變的
Object[] objArray = new Integer[10]; // OK
// 泛型不是協變的
List<Object> objList = new ArrayList<Integer>(); // 編譯錯誤!
萬用字元解決這個問題:
List<?> anyList = new ArrayList<Integer>(); // OK
三種萬用字元
| 萬用字元 | 說明 | 讀取 | 寫入 |
|---|---|---|---|
? | 無界 | Object | 不能(除了 null) |
? extends T | 上界 | T | 不能(除了 null) |
? super T | 下界 | Object | T 或其子類別 |
無界萬用字元 <?>
代表任何型別:
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// 使用
printList(Arrays.asList(1, 2, 3));
printList(Arrays.asList("a", "b", "c"));
printList(Arrays.asList(1.0, 2.0, 3.0));
限制
List<?> list = new ArrayList<String>();
// ✓ 可以讀取(作為 Object)
Object obj = list.get(0);
// ✗ 不能寫入(因為不知道實際型別)
// list.add("hello"); // 編譯錯誤
// ✓ 可以加入 null
list.add(null); // OK
上界萬用字元 <? extends T>
代表 T 或 T 的子類別(Producer Extends):
// 可以接受 List<Integer>、List<Double>、List<Number>
public static double sum(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
List<Number> numbers = Arrays.asList(1, 2.5, 3);
sum(ints); // 6.0
sum(doubles); // 7.5
sum(numbers); // 6.5
限制
List<? extends Number> numbers = new ArrayList<Integer>();
// ✓ 可以讀取(作為 Number)
Number n = numbers.get(0);
// ✗ 不能寫入
// numbers.add(1); // 編譯錯誤
// numbers.add(1.0); // 編譯錯誤
// 為什麼?因為不知道 list 的實際型別
// 如果是 List<Double>,加入 Integer 就會出錯
下界萬用字元 <? super T>
代表 T 或 T 的父類別(Consumer Super):
// 可以接受 List<Integer>、List<Number>、List<Object>
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// 使用
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(ints); // OK
addNumbers(numbers); // OK
addNumbers(objects); // OK
限制
List<? super Integer> list = new ArrayList<Number>();
// ✓ 可以寫入 Integer 或其子類別
list.add(1);
// ✗ 讀取只能得到 Object
Object obj = list.get(0); // 只能當作 Object
// Integer n = list.get(0); // 編譯錯誤
PECS 原則
Producer Extends, Consumer Super
- 讀取資料(Producer):使用
extends - 寫入資料(Consumer):使用
super
// 複製元素:從 source 讀取,寫入 dest
public static <T> void copy(List<? extends T> source,
List<? super T> dest) {
for (T item : source) {
dest.add(item);
}
}
// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(ints, numbers); // 從 Integer 複製到 Number
實際範例
// Collections.addAll 的簡化版
public static <T> void addAll(Collection<? super T> c, T... elements) {
for (T e : elements) {
c.add(e);
}
}
// Collections.max 的簡化版
public static <T extends Comparable<? super T>> T max(Collection<? extends T> c) {
Iterator<? extends T> it = c.iterator();
T max = it.next();
while (it.hasNext()) {
T next = it.next();
if (next.compareTo(max) > 0) {
max = next;
}
}
return max;
}
萬用字元 vs 型別參數
什麼時候用萬用字元
// ✓ 只需要讀取
public void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
// ✓ 參數之間無關聯
public boolean containsAll(Collection<?> c1, Collection<?> c2);
什麼時候用型別參數
// ✓ 需要在多處使用相同型別
public <T> void copy(List<T> source, List<T> dest) {
dest.addAll(source);
}
// ✓ 需要回傳參數的型別
public <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
萬用字元捕獲
有時需要「捕獲」萬用字元:
// 這個無法編譯
public static void swap(List<?> list, int i, int j) {
list.set(i, list.get(j)); // 編譯錯誤
list.set(j, list.get(i));
}
// 使用輔助方法捕獲萬用字元
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
常見使用場景
1. 讀取資料
// extends:生產者
public double average(List<? extends Number> numbers) {
return numbers.stream()
.mapToDouble(Number::doubleValue)
.average()
.orElse(0);
}
2. 寫入資料
// super:消費者
public void fill(List<? super Integer> list, int count) {
for (int i = 0; i < count; i++) {
list.add(i);
}
}
3. 轉換資料
public <T, R> List<R> transform(
List<? extends T> input,
Function<? super T, ? extends R> mapper) {
List<R> result = new ArrayList<>();
for (T item : input) {
result.add(mapper.apply(item));
}
return result;
}
4. 比較器
// Comparator<? super T> 允許使用父類別的比較器
public <T> T max(Collection<? extends T> coll,
Comparator<? super T> comp) {
T max = null;
for (T item : coll) {
if (max == null || comp.compare(item, max) > 0) {
max = item;
}
}
return max;
}
// 使用
Comparator<Number> byValue = Comparator.comparing(Number::doubleValue);
List<Integer> ints = Arrays.asList(3, 1, 4, 1, 5);
Integer maxInt = max(ints, byValue); // 可以用 Number 的比較器比較 Integer
重點整理
| 萬用字元 | 用途 | 記憶方式 |
|---|---|---|
<?> | 只讀取,不寫入 | 任何型別 |
<? extends T> | 讀取資料(Producer) | 「取出來」的東西至少是 T |
<? super T> | 寫入資料(Consumer) | 「放進去」的 T 一定可以 |
- PECS:Producer Extends, Consumer Super
- 萬用字元增加 API 的靈活性
- 型別參數用於關聯多個型別或回傳值
- 使用輔助方法捕獲萬用字元