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。さらに、ワイルドカードをメソッドパラメータに保ち、戻り値の型に配置しないでください。

javagenericswildcardpecscollections