Javaジェネリクスのワイルドカードとは、List<? extends Number> のように「型引数を1つに決めず、範囲で受け取る」ための記号 ? で、extends は「読み出し用」、super は「書き込み用」に使います。 この覚え方を PECS(Producer Extends, Consumer Super)と呼びます。

この記事では、ワイルドカードごとに「何が読めて何が書けるか」を〇×の表にし、その×の部分で実際に出るコンパイルエラー文(Java 21.0.10)を並べました。さらに、PECS をコピペで試せる完成コードと実行結果、Collections.copy などの標準 API のシグネチャの読み方、型消去によってできないこと、そして「ワイルドカードと型パラメータ <T> のどちらにするか」の判断基準までまとめています。

ワイルドカードの早見表

読める・書けるの〇×表

宣言 受け取れる実体の例 要素を読む(get) 要素を書く(add) 主な用途
List<Number> List<Number> だけ 〇 Number として 〇 Number とその子 読み書き両方
List<? extends Number> List<Integer>・List<Double>・List<Number> 〇 Number として ×(null のみ可) 読むだけ(Producer)
List<? super Integer> List<Integer>・List<Number>・List<Object> △ Object としてだけ 〇 Integer とその子 書くだけ(Consumer)
List<?> どんな List でも △ Object としてだけ ×(null のみ可) 型に関係ない処理(件数・削除など)
List<T>(型パラメータ) 呼び出しごとに決まる T 〇 T として 〇 T 読み書き+型の対応関係を表す

×のときに出るエラー文(Java 21.0.10)

やったこと 実際のエラー文(抜粋)
List<? extends Number> に add(1) no suitable method found for add(int) … int cannot be converted to CAP#1
List<? super Integer> の get を Integer で受ける incompatible types: CAP#1 cannot be converted to Integer
List<?> に add("x") no suitable method found for add(String)
List<Integer> を List<Number> に代入 incompatible types: List<Integer> cannot be converted to List<Number>
instanceof List<String> Object cannot be safely cast to List<String>
new T[10] / new List<String>[3] generic array creation
new T() unexpected type / required: class / found: type parameter T
print(List<String>) と print(List<Integer>) を両方定義 name clash: ... have the same erasure
List<int> unexpected type / required: reference / found: int

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ジェネリクスのワイルドカード|extends・super早見表」

動作確認した環境

項目 内容
JDK OpenJDK 21.0.10(LTS)
実行方法 java ファイル名.java(単一ファイル実行)
外部ライブラリ なし(java.util などの標準のみ)

本記事のエラー文・実行結果は、すべてこの環境で実際に出力したものです。

ジェネリクスの基本を3分で

ジェネリクスは、クラスやメソッドを「型を後から決められる部品」にする仕組みです。Java 5 から使えます。ここでは本題のワイルドカードを理解するのに必要な分だけ確認します。

書き方 名前 意味
class Box<T> { ... } ジェネリッククラス 使うときに Box<String> のように型を決める
static <T> T first(List<T> list) ジェネリックメソッド 呼び出しのたびに T が推論される
<T extends Number> 上限境界つき型パラメータ T は Number かその子だけ
<T extends Comparable<? super T>> 再帰的な境界 T 同士で大小比較できる
new Box<>("x") ダイヤモンド演算子 右辺の型引数を省略
// 型パラメータの境界(T extends Number)に合わない型を使うと…
static class NumBox<T extends Number> { T v; }
NumBox<String> b = new NumBox<>();
W8.java:5: error: type argument String is not within bounds of type-variable T
        NumBox<String> b = new NumBox<>();
               ^
  where T is a type-variable:
    T extends Number declared in class NumBox

境界は「この型パラメータに入れてよい型の範囲」をコンパイラに伝えるものです。 型パラメータ名は慣習で T(Type)、E(Element)、K・V(Key・Value)、R(Result)がよく使われます。

なぜワイルドカードが必要なのか

Integer は Number の子ですが、List<Integer> は List<Number> の子ではありません。 これを「ジェネリクスは非変(invariant)」と言います。

static double sum(List<Number> nums) { ... }

List<Integer> ints = List.of(1, 2, 3);
sum(ints);   // 渡せない
W10.java:8: error: method sum in class W10 cannot be applied to given types;
        System.out.println(sum(ints));
                           ^
  required: List<Number>
  found:    List<Integer>
  reason: argument mismatch; List<Integer> cannot be converted to List<Number>

もし渡せてしまうと、sum の中で nums.add(1.5) と書いたときに List<Integer> に Double が入ってしまいます。コンパイラはそれを防ぐために代入を止めています。

そこで、「読むだけだから Integer のリストでも Double のリストでも受けたい」という意図を表すのが ? extends Number です。

static double sum(List<? extends Number> nums) { ... }   // List<Integer> も List<Double> も渡せる

? extends T:読めるが書けない

List<? extends Number> nums = new ArrayList<Integer>();
Number n = nums.get(0);   // 〇 Number として読める
nums.add(1);              // × 書けない
W1.java:5: error: no suitable method found for add(int)
        nums.add(1);
            ^
    method List.add(CAP#1) is not applicable
      (argument mismatch; int cannot be converted to CAP#1)
    method List.add(int,CAP#1) is not applicable
      (actual and formal argument lists differ in length)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Number from capture of ? extends Number
1 error

エラー文の CAP#1 は「? の正体(中身の実際の型)」をコンパイラが仮に名付けたものです(キャプチャ変換)。 実体が List<Double> かもしれないので、int を入れてよいかコンパイラには判断できません。だから書き込みは null 以外すべて止められます。読み出しなら、どの実体でも Number の子なので安全というわけです。

? super T:書けるが、読むと Object

List<? super Integer> box = new ArrayList<Number>();
box.add(1);               // 〇 Integer は書ける
Integer i = box.get(0);   // × Integer としては読めない
Object o = box.get(0);    // 〇 Object としてなら読める
W2.java:6: error: incompatible types: CAP#1 cannot be converted to Integer
        Integer i = box.get(0);
                           ^
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object super: Integer from capture of ? super Integer

実体は List<Integer>・List<Number>・List<Object> のどれかなので、Integer を入れるのはいつでも安全です。 一方で、取り出した要素が Integer だとは限らない(List<Object> に別の型が入っているかもしれない)ので、読むときは Object としてしか受けられません。

? だけ:型に関係ない処理専用

List<?> any = new ArrayList<String>();
any.add("x");   // × 書けない
any.size();     // 〇
any.clear();    // 〇
W3.java:5: error: no suitable method found for add(String)
        any.add("x");
           ^
    method List.add(CAP#1) is not applicable
      (argument mismatch; String cannot be converted to CAP#1)

List<?> は「何かの型のリスト(型は気にしない)」という意味です。 件数を数える、null を数える、全削除するなど、要素の型を使わない処理に向いています。原型(raw type)の List と違い、型安全性のチェックが残ります。

PECS をコピペで試す完成コード

PECS(Producer Extends, Consumer Super)は「値を出してくれる側(Producer)は extends、値を受け取る側(Consumer)は super」という覚え方です。 次のコードを GenericsDemo.java として保存し、そのまま実行できます。

import java.util.*;
import java.util.function.*;

public class GenericsDemo {
    // 1. ジェネリッククラス
    static class Box<T> {
        private final T value;
        Box(T value) { this.value = value; }
        T get() { return value; }
        <R> Box<R> map(Function<? super T, ? extends R> f) { return new Box<>(f.apply(value)); }
        @Override public String toString() { return "Box(" + value + ")"; }
    }

    // 2. ジェネリックメソッド + 境界(T extends Comparable<? super T>)
    static <T extends Comparable<? super T>> T max(List<? extends T> list) {
        T best = list.get(0);
        for (T x : list) if (x.compareTo(best) > 0) best = x;
        return best;
    }

    // 3. ? extends:読むだけ(Producer)
    static double sum(List<? extends Number> nums) {
        double total = 0;
        for (Number n : nums) total += n.doubleValue();
        return total;
    }

    // 4. ? super:書くだけ(Consumer)
    static void fillNumbers(List<? super Integer> out, int count) {
        for (int i = 1; i <= count; i++) out.add(i);
    }

    // 5. PECS:src から読んで dest に書く(Collections.copy と同じ形)
    static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (T item : src) dest.add(item);
    }

    // 6. ? だけ:型に関係ない処理
    static int countNonNull(Collection<?> c) {
        int n = 0;
        for (Object o : c) if (o != null) n++;
        return n;
    }

    // 7. ワイルドカードキャプチャ:List<?> の要素を入れ替える
    static void swapFirstLast(List<?> list) { swapHelper(list); }
    private static <E> void swapHelper(List<E> list) {
        E first = list.get(0);
        list.set(0, list.get(list.size() - 1));
        list.set(list.size() - 1, first);
    }

    record Item(String name, int stock) {}

    public static void main(String[] args) {
        Box<String> b = new Box<>("在庫");
        Box<Integer> len = b.map(String::length);
        System.out.println("1. " + b + " -> " + len);

        System.out.println("2. max(Integer) = " + max(List.of(3, 9, 4)));
        System.out.println("2. max(String)  = " + max(List.of("pen", "tape", "clip")));

        List<Integer> ints = List.of(1, 2, 3);
        List<Double> doubles = List.of(0.5, 1.5);
        System.out.println("3. sum(ints)=" + sum(ints) + " sum(doubles)=" + sum(doubles));

        List<Number> numbers = new ArrayList<>();
        List<Object> objects = new ArrayList<>();
        fillNumbers(numbers, 3);
        fillNumbers(objects, 2);
        System.out.println("4. numbers=" + numbers + " objects=" + objects);

        List<Object> dest = new ArrayList<>(List.of("先頭"));
        copy(dest, ints);
        copy(dest, List.of("ペン", "テープ"));
        System.out.println("5. copy -> " + dest);

        List<Number> all = new ArrayList<>();
        Collections.addAll(all, 1, 2.5, 3L);          // addAll(Collection<? super T>, T...)
        all.addAll(ints);                             // addAll(Collection<? extends E>)
        System.out.println("5. addAll -> " + all);

        List<Item> items = new ArrayList<>(List.of(
            new Item("ペン", 30), new Item("ノート", 5), new Item("クリップ", 120)));
        Comparator<Object> byText = Comparator.comparing(Object::toString);
        items.sort(byText);                            // sort(Comparator<? super E>)
        System.out.println("5. sort(Comparator<Object>) -> " + items.stream().map(Item::name).toList());
        items.sort(Comparator.comparingInt(Item::stock).reversed());
        System.out.println("5. 在庫の多い順 -> " + items.stream().map(Item::name).toList());

        System.out.println("6. countNonNull = " + countNonNull(Arrays.asList("a", null, "b")));

        List<String> names = new ArrayList<>(List.of("A", "B", "C"));
        swapFirstLast(names);
        System.out.println("7. swap -> " + names);

        // 8. 型消去:実行時には型引数が残らない
        List<String> ls = new ArrayList<>();
        List<Integer> li = new ArrayList<>();
        System.out.println("8. 同じクラス? " + (ls.getClass() == li.getClass()) + " " + ls.getClass().getName());
        Object o = ls;
        System.out.println("8. o instanceof List<?> = " + (o instanceof List<?>));
    }
}
java -Dstdout.encoding=UTF-8 GenericsDemo.java

実行結果(OpenJDK 21.0.10):

1. Box(在庫) -> Box(2)
2. max(Integer) = 9
2. max(String)  = tape
3. sum(ints)=6.0 sum(doubles)=2.0
4. numbers=[1, 2, 3] objects=[1, 2]
5. copy -> [先頭, 1, 2, 3, ペン, テープ]
5. addAll -> [1, 2.5, 3, 1, 2, 3]
5. sort(Comparator<Object>) -> [クリップ, ノート, ペン]
5. 在庫の多い順 -> [クリップ, ペン, ノート]
6. countNonNull = 2
7. swap -> [C, B, A]
8. 同じクラス? true java.util.ArrayList
8. o instanceof List<?> = true

結果の読みどころ

番号 何が起きたか ポイント
3 List<Integer> も List<Double> も sum に渡せた ? extends Number は「Number の子のリストなら何でも読む」
4 List<Number> にも List<Object> にも Integer を書けた ? super Integer は「Integer を入れられる箱なら何でも」
5(copy) List<Integer> と List<String> を List<Object> へコピーできた 読む側 extends・書く側 super で、組み合わせの自由度が最大
5(sort) Item のリストを Comparator<Object> で並べ替えられた sort(Comparator<? super E>) なので親の型の比較器も使える
7 List<?> でも入れ替えができた ヘルパーメソッドで ? を E に「キャプチャ」すると書き込める
8 List<String> と List<Integer> のクラスが同じ 型消去で、実行時には型引数が残らない

標準 API のシグネチャを読む

Javadoc や IDE で見る標準 API のシグネチャは、PECS で読むと意味がすぐわかります。以下は javap で Java 21 のクラスファイルから確認したものです。

API シグネチャ(Java 21) 読み方
Collections.copy <T> void copy(List<? super T> dest, List<? extends T> src) src から読み(extends)、dest に書く(super)
Collections.addAll <T> boolean addAll(Collection<? super T> c, T... elements) c は書き込み先なので super
Collections.fill <T> void fill(List<? super T> list, T obj) 書くだけなので super
Collections.max <T extends Comparable<? super T>> T max(Collection<? extends T> coll) 読むだけなので extends、比較は親の Comparable でも可
Collections.sort <T extends Comparable<? super T>> void sort(List<T> list) 並べ替えは読み書き両方なので List<T>
Collections.unmodifiableList <T> List<T> unmodifiableList(List<? extends T> list) 読むだけなので extends
List.addAll boolean addAll(Collection<? extends E> c) 引数から読むので extends
List.sort void sort(Comparator<? super E> c) 比較器は要素を受け取る(消費する)ので super
Stream.map <R> Stream<R> map(Function<? super T, ? extends R> mapper) 引数を受け取る側は super、返す側は extends
Stream.filter Stream<T> filter(Predicate<? super T> predicate) 判定は要素を受け取るので super
Comparator.comparing <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor) Function の引数は super、戻り値は extends

関数型インターフェースの場合は「引数は super、戻り値は extends」と覚えると、Function<? super T, ? extends R> のような形がすっと読めます。Collections.max はソースコード上では <T extends Object & Comparable<? super T>> と書かれています(Object & は以前のバイナリとの互換のための指定です)。

Comparator<? super T> が便利な場面

record Item(String name, int stock) {}
Comparator<Object> byText = Comparator.comparing(Object::toString);
items.sort(byText);   // List<Item> を Comparator<Object> で並べ替えられる

List<Item>.sort が Comparator<? super Item> を受けるので、Object や親クラス用に作った比較器を、子の型のリストで使い回せます。 もし sort(Comparator<E>) だったら、Item 専用の比較器しか渡せません。

型消去でできないこと(実際のエラー)

型消去(type erasure)とは、ジェネリクスの型引数がコンパイル時のチェックにだけ使われ、実行時のクラスファイルからは消えるしくみです。 先ほどの実行結果「8. 同じクラス? true」のとおり、List<String> も List<Integer> も実行時はただの ArrayList です。そのため次のことはコンパイルエラーになります。

できないこと エラー文 代わりの書き方
o instanceof List<String> Object cannot be safely cast to List<String> o instanceof List<?> で判定し、要素を個別に確認
new T[10] generic array creation List<T> を使う/Class<T> と Array.newInstance
new List<String>[3] generic array creation List<List<String>> を使う
new T() unexpected type / required: class Supplier<T> を引数で受け取る
print(List<String>) と print(List<Integer>) name clash: ... have the same erasure メソッド名を変える(printNames など)
static T shared; non-static type variable T cannot be referenced from a static context インスタンスフィールドにする
List<int> unexpected type / required: reference / found: int List<Integer>(ラッパー型)を使う

instanceof List が書けない

Object o = new ArrayList<String>();
if (o instanceof List<String> list) System.out.println(list);
W5.java:5: error: Object cannot be safely cast to List<String>
        if (o instanceof List<String> list) System.out.println(list);
            ^

実行時には「String のリストかどうか」を確かめる情報がないため、コンパイラが止めます。 o instanceof List<?> なら判定できる(実行結果の 8 で true)ので、要素の型が必要なら1つずつ instanceof String で確認します。

ジェネリック型の配列と new T() が作れない

public class W6<T> {
    T[] arr = new T[10];
    List<String>[] lists = new List<String>[3];
    T make() { return new T(); }
}
W6.java:3: error: generic array creation
    T[] arr = new T[10];
              ^
W6.java:4: error: generic array creation
    List<String>[] lists = new List<String>[3];
                           ^
W6.java:5: error: unexpected type
    T make() { return new T(); }
                          ^
  required: class
  found:    type parameter T

実行時に T が何の型か分からないので、配列の要素型やコンストラクタを決められません。 オブジェクトを作りたいときは Supplier<T> を受け取る形にすると型安全に書けます。

static <T> List<T> createN(Supplier<T> factory, int n) {
    List<T> list = new ArrayList<>();
    for (int i = 0; i < n; i++) list.add(factory.get());
    return list;
}
// createN(StringBuilder::new, 3)

型引数だけ違うオーバーロードが衝突する

static void print(List<String> list) {}
static void print(List<Integer> list) {}
W7.java:4: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
    static void print(List<Integer> list) {}
                ^

型消去後はどちらも print(List) になり、区別できなくなるためです。 メソッド名を printNames・printCounts のように分けると解決します。

ワイルドカードと型パラメータ、どちらを使うか

判断表

状況 おすすめ 例
引数から読むだけ ? extends T double sum(List<? extends Number> nums)
引数に書くだけ ? super T void fill(List<? super Integer> out)
読みも書きもする List<T>(ワイルドカードなし) void sortInPlace(List<T> list)
要素の型を使わない ? int count(Collection<?> c)
引数と戻り値の型をつなげたい 型パラメータ <T> <T> T first(List<T> list)
2つの引数の型を関係づけたい 型パラメータ+ワイルドカード <T> void copy(List<? super T> d, List<? extends T> s)
メソッドの戻り値 ワイルドカードを避ける List<? extends Number> を返すと呼び出し側が書きにくい
型パラメータが1回しか出てこない ワイルドカードにできる <T> void print(List<T>) → void print(List<?>)

目安は「型パラメータ T が1か所にしか出てこないなら ? で十分、2か所以上で型をつなげたいなら <T>」です。 たとえば <T> T first(List<T> list) は、引数の要素型と戻り値の型が同じであることを T で表しているので、ワイルドカードにはできません。

戻り値にワイルドカードを使わない理由

List<? extends Number> getValues() { ... }   // 呼び出し側は add できない
List<Number> getValues() { ... }             // 呼び出し側が扱いやすい

ワイルドカードは「受け取る側の柔軟性」を上げるためのものです。 戻り値に付けると、呼び出した人が読み書きの制限を受けるので、具体的な型で返すのが一般的です。

設計のチェックリスト

  • [ ] 引数のコレクションから読むだけなら ? extends を付けた
  • [ ] 引数のコレクションへ書くだけなら ? super を付けた
  • [ ] Comparator・Function・Predicate などの引数は ? super、戻り値側は ? extends
  • [ ] 戻り値の型にはワイルドカードを付けていない
  • [ ] 原型(raw type)の List ではなく List<?> を使った
  • [ ] ジェネリック型の配列ではなく List を使った

よくある質問

Q. List<Object> と List<?> は何が違いますか? A. List<Object> は「Object を入れるリスト」なので何でも add できますが、List<String> を代入できません。List<?> は「何かの型のリスト」なので List<String> も受け取れますが、add はできません。受け取る範囲と書き込みの可否が逆になっています。

Q. PECS を一言で覚えるには? A. 「中身を取り出すなら extends、中身を入れるなら super」です。Collections.copy(List<? super T> dest, List<? extends T> src) を1つ覚えておくと、ほかの API も同じ考え方で読めます。

Q. エラー文に出てくる CAP#1 とは何ですか? A. ワイルドカード ? の「実際の型」を、コンパイラが一時的に付けた名前です(キャプチャ変換)。CAP#1 extends Number とあれば「Number の子のどれか、ただし特定できない型」という意味で、その型に値を書き込めないことを示しています。

Q. 型消去があるのに、なぜジェネリクスを使うのですか? A. 型のチェックはコンパイル時に完了するので、キャストの書き忘れや型の取り違えを実行前に見つけられるからです。実行時の情報が必要な場面だけ Class<T> や Supplier<T> を渡す形で補います。

まとめ

  • ジェネリクスは非変で、List<Integer> は List<Number> に代入できません
  • ? extends T は読むだけ(Producer)、書こうとすると CAP#1 を含むエラーになります
  • ? super T は書くだけ(Consumer)、読むと Object としてしか受けられません
  • PECS は Collections.copy(List<? super T>, List<? extends T>) の形で覚えると標準 API が読めます
  • 関数型インターフェースは「引数は super、戻り値は extends」です
  • 型消去により instanceof List<String>・new T[]・new T()・同じ消去のオーバーロードはエラーになります
  • 型パラメータが1か所だけならワイルドカード、型をつなげたいなら <T> を選びます

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java ジェネリクスのワイルドカード|extends・super早見表」

株式会社RJCでは、Javaのジェネリクスのような型の考え方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?