Java | Java 詳細・モダン文法:ジェネリクス – ワイルドカード(?)

Java Java
スポンサーリンク

ワイルドカード(?)を一言でいうと

ジェネリクスのワイルドカード ? は、

「ここには“何らかの型”が入るけど、具体的な型は気にしない・決めたくない」というときの “あいまいな型パラメータ”

です。

List<?>
List<? extends Number>
List<? super Integer>

のように使います。

自分でクラスを書くときは <T> など「名前付きの型パラメータ」を使い、
どんな型が来るか分からないコレクションを「引数として受け取る」ときに ? が活きてきます。


まずは一番素朴な List<?> から

List<?> は「要素の型は何でもいいけど、読み取り専用っぽく扱うリスト」

次の 3 つを比べながらイメージをつかみましょう。

List<String> strings = new ArrayList<>();
List<Object> objects = new ArrayList<>();
List<?> unknown = new ArrayList<String>();
Java

List<String>
これは「要素は必ず String」のリスト。

List<Object>
これは「要素は Object。つまり、何でも入れられる」リスト。
StringIntegerUser も入ってしまう、かなり「何でもあり」な状態です。

List<?>
これは「要素の“具体的な型”は分からないし決めないが、“どれか 1 種類の型”であることは保証される」リストです。

ここがとても重要です。

List<?> は「何でも混ぜて入っている」わけではなく、
例えば実体が List<String> なら「中身は全部 String」だし、
List<Integer> なら「中身は全部 Integer」です。

しかし、? と書いた側からは「それが何かは知らない・決めない」という宣言になります。

List<?> に対してできること・できないこと

典型的な例を見てください。

public static void printAll(List<?> list) {
    for (Object e : list) {
        System.out.println(e);
    }
}
Java

この printAll メソッドは、

List<String>
List<Integer>
List<User>

など、あらゆる「要素型 1 種」のリストを受け取れます。

中身を読む(get する)のは OK です。
ただし要素型が何か分からないので、Object として受け取るしかありません。

一方、list.add(...) はどうでしょうか。

public static void addSomething(List<?> list) {
    // list.add("hello");   // コンパイルエラー
    // list.add(123);       // これもコンパイルエラー
    list.add(null);         // これは唯一 OK
}
Java

List<?> に具体的な値を add することはできません(null だけ例外的に OK)。

理由はこうです。

例えば、実際に渡ってきたリストが List<String> だった場合、
list.add(123) は「本来あってはならない状態」を作ってしまいますが、
List<?> からは「それが List<String> なのか List<Integer> なのか分からない」。

だから、コンパイラは「どんな非 null も add させない」という安全側の判断をします。

ここで一つ、大事な感覚を持ってください。

List<?> で受け取ったものは「読めるけど書けない(null 以外)」
つまり「ほぼ読み取り専用のリスト」として扱う、ということです。


上限境界ワイルドカード ? extends T(「T のサブタイプ」)

List<? extends Number> の意味

? extends T は、

「T のサブクラス(あるいは T 自身)である“何かの型”のリスト」

という意味です。

例えば、

List<? extends Number> numbers;
Java

この変数には、

List<Integer>
List<Double>
List<BigDecimal>

などが代入できます。

「Number の一族が入ったリスト」という宣言だと思ってください。

? extends で受け取ると「読むときだけ Number として扱える」

典型的な使い方は、「数字のリストから合計を出したい」ときです。

public static double sum(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {
        total += n.doubleValue();
    }
    return total;
}
Java

このメソッドは、

List<Integer>
List<Double>
List<Long>

など、あらゆる「Number のサブタイプのリスト」に対して使えます。

ここでのポイントは、

「中身の要素を取り出すときには、最低でも Number であることが保証されている」

ということです。

だから doubleValue() を安心して呼べます。

しかし ? extends に対しても add はほぼできない

List<? extends Number> に対して add できるか試してみます。

public static void addSomething(List<? extends Number> list) {
    // list.add(1);        // コンパイルエラー
    // list.add(1.0);      // これもコンパイルエラー
    list.add(null);        // これは OK
}
Java

なぜ 1(Integer)も 1.0(Double)もダメなのか。

List<? extends Number> の実体が何か分からないからです。

もしかしたら List<Integer> かもしれないし、
List<Double> かもしれないし、
List<BigDecimal> かもしれない。

そんなときに、コンパイラは「どの具体型にも“絶対に安全”とは言えないから add を禁止する」判断をします。

ここから見えてくる重要なルールがあります。

? extends T で受け取ったリストは、

「T として“読み出す”ことはできるが、(null 以外)“書き込む”ことはできない」

という性質を持ちます。

List<? extends Number> は、
「Number たちを “読み取る” ための引数」に向いている、ということです。


下限境界ワイルドカード ? super T(「T のスーパータイプ」)

List<? super Integer> の意味

今度は逆向きです。? super T は、

「T か T のスーパークラスである“何かの型”のリスト」

を表します。

例えば、

List<? super Integer> list;
Java

この変数には、

List<Integer>
List<Number>
List<Object>

などが代入できます。

「Integer も安全に突っ込める“箱”」というイメージを持ってください。

? super は「T を安全に add できる」ための型

次のメソッドを見てください。

public static void addSomeIntegers(List<? super Integer> list) {
    list.add(1);      // OK
    list.add(2);      // OK
    list.add(3);      // OK
}
Java

このメソッドは、

List<Integer>
List<Number>
List<Object>

どれに対しても安全です。

なぜなら、

List<Integer> に Integer を入れるのは当然 OK
List<Number> に Integer を入れるのも、Integer は Number のサブクラスだから OK
List<Object> に Integer を入れるのも、Integer は Object のサブクラスだから OK

だからです。

つまり ? super Integer は、「Integer を突っ込んでも絶対に問題が起きない箱」という宣言になっています。

ただし get するときはほぼ Object としてしか扱えない

List<? super Integer> から要素を取り出すとどうなるか。

public static void useList(List<? super Integer> list) {
    Object o = list.get(0);     // これは OK
    // Integer i = list.get(0); // コンパイルエラー
}
Java

なぜ Integer i で受け取れないのか。

? super Integer の実体が、例えば List<Object> かもしれないからです。

List<Object> から取り出したものを「必ず Integer だ」とは言えません。
StringDouble かもしれない。

だから、コンパイラは
「get したものを Integer として扱うのは保証できない」と判断してエラーにします。

結局、? super T で get できる安全な型は Object だけになります。

ここから、もう一つの重要なルールが見えてきます。

? super T で受け取ったリストは、

「T(やそのサブクラス)を add することには向いているが、get したものは Object としてしか扱えない」

という性質になります。


extends と super の違いを一言で整理する(PECS)

ここまでの内容をギュッと格言っぽく言うと、
有名な「PECS」という覚え方になります。

Producer Extends, Consumer Super(PECS)です。

Producer(生産者)は extends
Consumer(消費者)は super

という意味です。

もっと噛み砕くとこうなります。

? extends T
「T を“生産する側(取り出す側)”として使う」
→ 読み取り用。T として get できるが、add は null 以外基本禁止。

? super T
「T を“消費する側(突っ込まれる側)”として使う」
→ 書き込み用。T を add できるが、get は Object としてしか扱えない。

例えば、

「いろんな Number サブクラスのリストから値を読む」なら List<? extends Number>
「Integer を詰め込みたい箱として使いたい」なら List<? super Integer>

という選び方になります。


具体例で「ワイルドカードのありがたみ」を感じる

例1:どんな要素型のリストでも中身を表示したい

ワイルドカードなしだと、こう書かざるを得ません。

public static void printStrings(List<String> list) { ... }
public static void printIntegers(List<Integer> list) { ... }
public static void printUsers(List<User> list) { ... }
Java

明らかに重複が多いですよね。

ワイルドカードを使うと、これを 1 つにまとめられます。

public static void printAll(List<?> list) {
    for (Object e : list) {
        System.out.println(e);
    }
}
Java

呼ぶ側は、

List<String> strings = List.of("a", "b");
List<Integer> ints = List.of(1, 2, 3);

printAll(strings);
printAll(ints);
Java

のように、どんな型のリストでも渡せます。

「引数として、いろいろな型付きリストを受け取りたいが、中身は読むだけ」という場面で、
List<?> はとても役に立ちます。

例2:Number サブタイプのリストの合計を出す

List<Integer>, List<Double> などから合計を取りたい関数。

ワイルドカードなしだと、型ごとにオーバーロードすることになります。

sumInt(List<Integer>)
sumDouble(List<Double>)

など。

? extends Number を使えば、これを 1 本にできます。

public static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}
Java

どんな Number サブタイプのリストでも渡せますが、
内部では「Number として読むだけ」に徹しています。

例3:Integer を突っ込みたい、より汎用的な箱に add する

例えば、「Integer をどこかのコレクションに足し込みたい」のだけど、
相手が List<Integer> なのか List<Number> なのか List<Object> なのか分からない、という状況。

? super Integer を使えば一つのメソッドで対応できます。

public static void addInts(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}
Java

これなら、

List<Integer>
List<Number>
List<Object>

全部に対して安全に呼べます。


ワイルドカードと型パラメータの使い分けのざっくり指針

初心者が迷いやすいポイントがこれです。

List<?> で受けるべきか、<T> を使ったジェネリックメソッドにするべきか?」

おおまかな目安はこうです。

メソッドの中で「同じ型パラメータどうしの関係」を表したいとき
(例:引数 1 と引数 2 は同じ型、戻り値も同じ型)
<T> を使った方がよい(ジェネリックメソッド)

引数として「どんな型のコレクションでもいいが、中身の具体的な型は意識しない」
→ ワイルドカード(? / ? extends / ? super)を使う

例えば、

「リストをそのまま返すだけ」のときは、明らかに <T> が合います。

public static <T> List<T> identity(List<T> list) {
    return list;
}
Java

一方、「単に表示するだけ」のときは List<?> の方が自然です。

public static void printAll(List<?> list) { ... }
Java

ワイルドカードは「関係性を持たない、“その場限りのあいまいさ” を表現するもの」
型パラメータ <T> は「メソッド全体・クラス全体に跨る、“同じ T であること” の関係を表現するもの」

と捉えると、使い分けの感覚がつかみやすくなります。


まとめ:ワイルドカード(?)を自分の中でこう位置づける

ワイルドカード ? を初心者向けに整理すると、こうなります。

?
「型は何か分からない・決めないけど、“1 種類の型” であることだけは保証される」

? extends T
「T か T のサブクラスの“どれか”のリスト。T として読み取るには向いているが、書き込みはほぼ不可」

? super T
「T か T のスーパークラスのリスト。T を add するには向いているが、get したものは Object として扱う」

そして、PECS(Producer Extends, Consumer Super)という格言。

値を“生産する(取り出す)”側として使うなら ? extends
値を“消費する(突っ込む)”側として使うなら ? super

タイトルとURLをコピーしました