JavaのGenerics: ワイルドカード ? extends と ? super
1. 概要
Genericsはコンパイル時にJavaコードを型安全にします。しかし、List<Product>を受け取るメソッドを書くと、BookがProductのサブクラスであっても、List<Book>を受け入れていないことに気付くでしょう。この記事では、その理由を説明します。その後、より柔軟なAPIを書くためにワイルドカード? extendsと? superの使い方を紹介します。読み終わったら、PECSの原則に従ってどのワイルドカードを選ぶか、そして理解しにくいコンパイルエラーを避ける方法がわかるでしょう。
2. 問題: Genericsは不変
この記事全体を通じて使う単純なクラス階層を定義します:
public class Product {
private final String name;
private final double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
public String name() { return name; }
public double price() { return price; }
}
public class Book extends Product {
public Book(String name, double price) {
super(name, price);
}
}
BookはProductです。では、List<Book>はList<Product>でしょうか?
List<Book> books = new ArrayList<>();
List<Product> products = books; // コンパイルエラー: 互換性のない型
答えはいいえです。JavaのGenericsは不変です。AがBのサブクラスでも、List<A>とList<B>は無関係な2つの型のままです。
理由は型安全性にあります。上の行がコンパイルされたと仮定すると、以下のように書くことができます:
products.add(new Product("ボールペン", 5_000)); // Bookだけを含むはずのリストにProductを追加
Book first = books.get(0); // ClassCastException!
配列は異なります。Javaの配列は共変です。そのため、類似のエラーは実行時にのみ表面化します:
Object[] items = new String[2];
items[0] = 42; // コンパイルOK、実行時にArrayStoreExceptionをスロー
Genericsはコンパイル時にエラーをブロックする方法を選択します。代わりに柔軟性が損なわれます。ワイルドカードはその柔軽性を取り戻すために作られました。
3. 上限付きワイルドカード: ? extends T
3.1. コレクションからデータを読み出す
製品のリストの価格合計を計算する必要があるとしましょう。最初のバージョンは通常このようになります:
static double totalPrice(List<Product> products) { ... }
このメソッドはList<Book>を受け入れません。? extendsで修正しましょう:
public final class CatalogUtils {
public static double totalPrice(List<? extends Product> products) {
double total = 0;
for (Product p : products) {
total += p.price();
}
return total;
}
}
List<? extends Product>は「Productまたはそのサブクラスである何らかの型の要素を含むリスト」という意味です。おかげで:
List<Product>、List<Book>、またはList<Ebook>(存在する場合)を渡すことができます。- 読み出された各要素は確実に
Productです。したがって、for (Product p : products)ループは有効です。
List<Book> books = List.of(
new Book("Clean Code", 300_000),
new Book("Effective Java", 450_000));
System.out.println(CatalogUtils.totalPrice(books));
750000.0
3.2. なぜ要素を追加できないのか?
? extendsを持つリストに要素を追加してみましょう:
static void addBook(List<? extends Product> products) {
products.add(new Book("Refactoring", 400_000)); // コンパイルエラー
products.add(null); // OK、しかし意味がありません
}
コンパイラは?が実際に何であるかを知りません。渡されたリストはList<Ebook>である可能性があります。その場合、通常のBookを追加すると型安全性が破られます。したがって、コンパイラはnullを除くadd操作を禁止します。
覚えやすいルール:? extends TはTを読み出すだけです。この場合、コレクションはプロデューサーの役割を果たします。つまり、データをあなたに提供する場所です。
4. 下限付きワイルドカード: ? super T
今度は反対のことをします。Bookを含むことができるリストにデフォルトの本をロードするメソッドが必要です:
public static void addDefaultBooks(List<? super Book> target) {
target.add(new Book("Clean Code", 300_000));
target.add(new Book("Effective Java", 450_000));
}
List<? super Book>は「要素型がBookまたはそのスーパークラスであるリスト」という意味です。そのような型はすべてBookを含むことができるため、add(new Book(...))は常に安全です。
List<Book> books = new ArrayList<>();
List<Product> products = new ArrayList<>();
List<Object> anything = new ArrayList<>();
CatalogUtils.addDefaultBooks(books); // OK
CatalogUtils.addDefaultBooks(products); // OK
CatalogUtils.addDefaultBooks(anything); // OK
System.out.println(products.size());
2
一方、読み出すときは、要素がObjectであることだけが確実です:
static void printFirst(List<? super Book> target) {
Object first = target.get(0); // OK
Book book = target.get(0); // コンパイルエラー
}
リストはList<Object>である可能性があり、あらゆるものを含むことができるため、コンパイラはObject型のみを保証できます。
ルール:? super TはTを書き込むためのものです。この場合、コレクションはコンシューマーです。つまり、あなたが提供するデータを消費する場所です。
5. PECS原則
上記の2つのルールはPECS:Producer Extends, Consumer Superに合わせられます。
Tをあなたに提供するパラメータには? extends Tを使用します。- あなたから
Tを受け取るパラメータには? super Tを使用します。 - 読み書きの両方を行うパラメータの場合は、ワイルドカードを使わず、正確な型
Tを宣言します。
5.1. 例: 2つのリスト間のコピー
public static <T> void copy(List<? extends T> source, List<? super T> target) {
for (T item : source) {
target.add(item);
}
}
sourceはプロデューサーなのでextendsを使用します。targetはコンシューマーなのでsuperを使用します。結果として、メソッドは非常に柔軟です:
List<Book> books = List.of(new Book("DDD", 500_000));
List<Product> cart = new ArrayList<>();
CatalogUtils.<Book>copy(books, cart); // T = Book:Bookを読み出し、親リストに書き込む
CatalogUtils.<Product>copy(books, cart); // T = Product も有効です
JDKはこの原則を正しく適用しています。Collections.copyのシグネチャは:
public static <T> void copy(List<? super T> dest, List<? extends T> src)
5.2. 例: スーパークラスに対して使用するComparator
List.sortはシグネチャsort(Comparator<? super E> c)を持ちます。
Comparatorはコンシューマーです。要素を受け入れて比較するからです。? superのおかげで、Product用に書かれたコンパレータを使用してList<Book>をソートできます:
Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);
List<Book> books = new ArrayList<>(List.of(
new Book("Effective Java", 450_000),
new Book("Clean Code", 300_000)));
books.sort(byPrice);
books.forEach(b -> System.out.println(b.name()));
Clean Code
Effective Java
このパターンはStream APIで非常によく見られます。例えばmap(Function<? super T, ? extends R> mapper):functionの入力はコンシューマー(super)、出力はプロデューサー(extends)です。
6. 非境界ワイルドカード: ?
メソッドが要素型を気にしない場合は?を使用します:
public static int countNonNull(List<?> items) {
int count = 0;
for (Object item : items) {
if (item != null) count++;
}
return count;
}
List<?>はすべてのリストを受け入れます:List<Book>、List<String>、List<Integer>。読み出された要素はObjectであり、null以外は何も追加できません。
List<?>とList<Object>を混同しないでください。List<Object>を受け入れるメソッドは、セクション2の不変性のため、List<String>を受け入れません。
7. 動作をテストする
JUnit 5とAssertJをpom.xmlに追加します:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
</dependencies>
プロジェクトがSpring Boot 3.xを使用している場合、spring-boot-starter-testは両方のライブラリをすでに含んでいます。
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CatalogUtilsTest {
@Test
void totalPrice_acceptsListOfSubtype() {
List<Book> books = List.of(
new Book("Clean Code", 300_000),
new Book("Effective Java", 450_000));
assertThat(CatalogUtils.totalPrice(books)).isEqualTo(750_000.0);
}
@Test
void addDefaultBooks_worksWithSupertypeList() {
List<Object> anything = new ArrayList<>();
anything.add("本ではありません");
CatalogUtils.addDefaultBooks(anything);
assertThat(anything).hasSize(3);
assertThat(anything.get(1)).isInstanceOf(Book.class);
}
@Test
void copy_movesBooksIntoProductList() {
List<Book> books = List.of(new Book("DDD", 500_000));
List<Product> cart = new ArrayList<>();
CatalogUtils.copy(books, cart);
assertThat(cart).extracting(Product::name).containsExactly("DDD");
}
@Test
void addDefaultBooks_failsAtRuntimeOnImmutableList() {
List<Product> immutable = List.of();
assertThatThrownBy(() -> CatalogUtils.addDefaultBooks(immutable))
.isInstanceOf(UnsupportedOperationException.class);
}
}
最初の3つのテストは、メソッドが予想通りサブタイプとスーパータイプを受け入れることを確認します。2番目のテストは、List<Object>が古い要素を保持し、追加のBookを受け入れることを示しています。最後のテストは重要なポイントを思い出させてくれます:ワイルドカードはコンパイル時に型のみをチェックします。リストが変更を許可するかどうかは知りません。
8. よくあるエラーと使わない場合
戻り値の型にワイルドカードを使用する。
public static List<? extends Product> findAll() { ... } // 推奨されません
呼び出し元はほぼ読み取り専用のリストを取得し、?を至る所に持ち運ぶ必要があります。ワイルドカードはメソッドのパラメータに配置する必要があります。戻り値の型ではなく。
? extendsを使用してからaddを呼び出そうとする。 メソッドがコレクションに書き込む必要がある場合、extendsは誤った選択です。PECSをもう一度確認してください。
読み書きの両方で正確な型が必要な場合はワイルドカードを使用する。 例えば、2つの要素をスワップします:
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 tmp = list.get(i);
list.set(i, list.get(j));
list.set(j, tmp);
}
List<?>で直接list.set(i, list.get(j))を呼び出すとコンパイルエラーになります。ジェネリックヘルパー<T>は?の実際の型を「キャプチャ」して操作できるようにします。このテクニックはワイルドカードキャプチャと呼ばれます。パブリックAPIがList<?>である必要がない場合は、直接<T> void swap(List<T> list, ...)と宣言する方が簡単です。
内部クラスのワイルドカードの過度な使用。 ワイルドカードは、ライブラリやユーティリティなどの共有APIで最も有用です。1か所からしか呼び出されないプライベートメソッドでは、具体的な型の方が読みやすいことが多いです。
注意:
? extendsはリストを不変にしません。メソッドはclear()またはremove(...)を呼び出すことができます。これらの操作は要素型を知る必要がないためです。データを保護する必要がある場合は、List.copyOf(...)またはCollections.unmodifiableList(...)を使用してください。
注意: 同じワイルドカードに
extendsとsuperの両方を使用することはできません。またクラスやメソッドで宣言された型パラメータに? superを書くこともできません(<T super Book>は無効です)。
9. 結論
JavaのGenericsは不変です。したがってList<Book>はList<Product>ではなく、これは実行時の型エラーを回避するのに役立ちます。ワイルドカードはAPIに柔軟性をもたらします:
? extends Tはデータを提供するパラメータに使用されます。Tを読み出すことはできますが、追加することはできません。? super Tはデータを受け取るパラメータに使用されます。Tを追加できますが、読み出すのはObjectだけです。?は要素型を気にしない場合に使用されます。
迷ったときは、PECSを思い出すだけです:Producer Extends, Consumer Super。さらに、ワイルドカードをメソッドパラメータに保ち、戻り値の型に配置しないでください。