skip to content
BlogZzz

[Learn] Java Generics:型別參數、wildcard 與 PECS

/ 8 min read

Updated:
Table of Contents

在 Collections 文章中,List<String>、Set<Integer> 與 Map<String, Student> 都在尖括號內指定了型別

這個機制就是 Generics

Generics 的目的不是讓語法變複雜,而是讓 compiler 知道容器與 method 接受什麼型別,把錯誤提早到 compile time

這篇會從 generic collection 開始,逐步寫出 generic class、generic method、bounded wildcard 與 PECS

Generics 提供 compile-time 型別安全

尖括號中的型別就是 generic type argument

List<String> names = new ArrayList<>();
Map<String, Integer> scoresByName = new HashMap<>();
  • List<String> 只接受 String
  • Map<String, Integer> 使用 String key 與 Integer value
  • 右側的 <> 稱為 diamond operator,compiler 會從左側推斷型別

沒有指定 generic type 的寫法稱為 raw type

List values = new ArrayList();
values.add("Amy");
values.add(100);
String name = (String) values.get(1); // runtime 發生 ClassCastException

使用 Generics 後,錯誤會提早到 compile time

List<String> values = new ArrayList<>();
values.add("Amy");
// values.add(100); // 無法編譯

generic type 不能直接使用 primitive type

// List<int> scores = new ArrayList<>();
List<Integer> scores = new ArrayList<>();

Integer 是 int 對應的 wrapper class,Java 會在許多情況下自動進行 boxing 與 unboxing

寫一個 generic class

class 名稱後面的 <T> 表示這個 class 接受一個 type parameter

public final class Box<T> {
private final T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}

建立 instance 時再決定 T 的實際型別

Box<String> nameBox = new Box<>("Amy");
Box<Integer> scoreBox = new Box<>(90);
String name = nameBox.getValue();
Integer score = scoreBox.getValue();

同一份 Box 實作可以保存不同型別,又不需要把回傳值手動 cast

// nameBox 只接受 String
// Box<String> invalid = new Box<>(100); // 無法編譯

type parameter 只在宣告它的範圍內有效

instance field 可以使用 class 的 T,但 static member 屬於整個 class,不能直接依賴某一個 instance 的 T

寫一個 generic method

method 也可以宣告自己的 type parameter

public static <T> T first(List<T> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("items 不可為空");
}
return items.get(0);
}

<T> 放在回傳型別之前,表示這個 method 宣告了一個 type parameter T

呼叫時,compiler 會根據 argument 推斷實際型別

String firstName = first(List.of("Amy", "Ben"));
Integer firstScore = first(List.of(80, 90));

同一個 method 不需要為 String 與 Integer 各寫一份

class 自己不是 generic class,也可以擁有 generic method

public final class CollectionUtils {
private CollectionUtils() {
}
public static <T> T first(List<T> items) {
return items.get(0);
}
}

常見的 type parameter 名稱如下

  • T 代表一般 type
  • E 代表 element
  • K 代表 key
  • V 代表 value
  • R 代表 result

這些只是命名慣例,不是特殊關鍵字

bounded type parameter

需要限制 T 必須屬於某一類型時,可以加上 upper bound

public static <T extends Number> double doubleValue(T value) {
return value.doubleValue();
}

這裡的 extends 同時涵蓋 class 與 interface bound

double intValue = doubleValue(10);
double decimalValue = doubleValue(3.14);
// doubleValue("10"); // String 不是 Number,無法編譯

bound 讓 method body 可以安全使用 Number 提供的 method

extends、super 與 wildcard

即使 Integer extends Number,List<Integer> 也不是 List<Number> 的 subtype

List<Integer> integers = List.of(1, 2, 3);
// List<Number> numbers = integers; // 無法編譯

如果 method 只需要讀取不同的 Number subtype,可以使用 upper-bounded wildcard

public static double total(List<? extends Number> values) {
double result = 0;
for (Number value : values) {
result += value.doubleValue();
}
return result;
}

這個 method 可以接收 List<Integer>、List<Long> 或 List<Double>

double result = total(List.of(10, 20, 30));

? extends Number 適合讀取 Number,但不能安全地新增特定 Number subtype

如果目的主要是放入 Integer,可以使用 lower-bounded wildcard

public static void addDefaults(List<? super Integer> target) {
target.add(0);
target.add(100);
}

可以用 PECS 幫助記憶

Producer Extends
Consumer Super

資料來源負責產生值時考慮 extends,接收資料寫入時考慮 super

如果 method 需要同時精確地讀寫同一型別,通常宣告 <T> 會比無限制 wildcard 更清楚

完整範例:CollectionUtils

建立以下檔案

src/
└── com/
└── example/
├── app/
│ └── Main.java
└── util/
└── CollectionUtils.java

CollectionUtils.java 同時示範 type parameter 與 wildcard

package com.example.util;
import java.util.List;
public final class CollectionUtils {
private CollectionUtils() {
}
public static <T> T first(List<T> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("items 不可為空");
}
return items.get(0);
}
public static <T> void copy(
List<? extends T> source,
List<? super T> target) {
target.addAll(source);
}
public static double total(List<? extends Number> values) {
double result = 0;
for (Number value : values) {
result += value.doubleValue();
}
return result;
}
}

Main.java 使用同一組 method 處理不同型別

package com.example.app;
import com.example.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
String firstName = CollectionUtils.first(
List.of("Amy", "Ben"));
List<Integer> scores = List.of(80, 90);
List<Number> numbers = new ArrayList<>();
CollectionUtils.copy(scores, numbers);
System.out.println("第一位:" + firstName);
System.out.println("複製結果:" + numbers);
System.out.println("總分:" + CollectionUtils.total(scores));
}
}

編譯並執行

Terminal window
javac -d out \
src/com/example/util/CollectionUtils.java \
src/com/example/app/Main.java
java -cp out com.example.app.Main

執行結果如下

第一位:Amy
複製結果:[80, 90]
總分:170.0

copy 的 source 是 producer,所以使用 ? extends T

target 是 consumer,所以使用 ? super T

常見錯誤:使用 raw type

List values = new ArrayList();

raw type 會失去 compile-time 型別檢查,也會讓程式出現 unchecked warning

應該指定 type argument

List<String> values = new ArrayList<>();

常見錯誤:以為 List 是 List

如果允許這種指派,呼叫端就能把 Double 放進原本只接受 Integer 的 list

因此 generic type 預設是 invariant

需要彈性時,根據讀寫方向使用 wildcard

常見錯誤:到處使用 wildcard

List<?> 表示元素型別未知,因此不能安全地新增一般值

如果 method 需要精確地讀取與寫入同一型別,宣告 <T> 通常更清楚

常見錯誤:把 PECS 當成固定語法

PECS 是選擇 API 型別的判斷方式,不是每個 List 都必須加 extends 或 super

沒有跨 subtype 的需求時,直接使用 List<T> 即可

結論

  • Generics 把許多型別錯誤提前到 compile time
  • generic class 讓同一個資料結構保存不同型別
  • generic method 可以在非 generic class 中宣告自己的 type parameter
  • bounded type parameter 限制 T 必須提供哪些能力
  • generic type 預設 invariant,subtype 關係不會自動延伸到 List
  • producer 使用 extends,consumer 使用 super
  • 不需要 subtype 彈性時,直接使用清楚的 List<T>

參考資料