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下界ObjectT 或其子類別

無界萬用字元 <?>

代表任何型別:

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 的靈活性
  • 型別參數用於關聯多個型別或回傳值
  • 使用輔助方法捕獲萬用字元