ワイルドカード ?
このレッスンで分かること
- ワイルドカード
?は「型は分からないけど、何か 1 つ」のプレースホルダList<? extends Number>は 上限境界 ─Numberかそのサブクラス、読み取り専用List<? super Integer>は 下限境界 ─Integerかその親、書き込み専用- PECS = Producer Extends, Consumer Super のニーモニック
ワイルドカード ? とは
ジェネリクスのワイルドカード
?と上限境界? extends T下限境界? super Tを学び、PECS の原則で使い分けられるようになろう。
ワイルドカード ? ─ 「何でも OK」の型
前のレッスンまでで、ArrayList<Integer> ArrayList<String> のように ジェネリクス で型を縛る書き方に慣れてきたと思います。型をきっちり指定するのは安全で読みやすい反面、「型は何でもいいから、とにかくリストを受け取って件数だけ数えたい」というシーンでは少しやりすぎになります。たとえば「リストの長さを返すメソッド」を作るのに、Integer 版と String 版と Double 版を別々に書くなんてバカバカしいですよね。
そこで登場するのが ワイルドカード という記号 ? です。? は「型は気にしない、何でも OK」を表す特別な型パラメータで、List<?> と書けば「中身の型は何だかわからないが、とにかく List である」という意味になります。Java のジェネリクスを使いこなすうえで避けて通れない概念で、Android 開発でも業務システムでも頻繁に登場します。
?は「未知の型 (unknown type)」と呼ばれます。List<Object>とは似て非なるもので、List<Object>は「中身がObject型のリスト」、List<?>は「中身の型が何かは呼び出し側だけが知っているリスト」です。後で見るように、書き込み (add) に対する扱いが大きく違います。
本章ではまず素の ? の使い方を押さえ、そのあとに「読み取り専用なら ? extends T、書き込み専用なら ? super T」という PECS のルールを学んでいきましょう。
素の ? で「件数だけ数える」メソッドを書く
まずは一番シンプルなパターンから見ていきます。次のコードは、どんな要素型の List でも受け取って件数を返すメソッドです。
Java
import java.util.List;
public class Counter {
public static int countItems(List<?> list) {
return list.size();
}
}引数の型が List<?> になっているので、呼び出し側は List<Integer> でも List<String> でも自由に渡せます。list.size() は中身の型に依らず常に int を返すので、ワイルドカードと相性抜群です。これを List<Object> にしてしまうと、List<String> を渡したときに incompatible types と言われてコンパイルが通りません。List<String> は List<Object> の サブタイプ ではないからです (ジェネリクスは継承関係を引き継がない、これも初学者が必ず引っかかるポイントです)。
ジェネリクスの「不変 (invariant)」というルールを覚えておいてください。
StringはObjectのサブクラスでも、List<String>はList<Object>のサブクラスにはなりません。だから「とにかくリストならなんでも」を表したいときにList<?>が必要になります。
<? extends T> 上限境界 ─ PECS の Producer
素の ? は便利ですが、要素を取り出して使いたいときには情報が足りなさすぎます。たとえば「数値のリストを受け取って合計を求める」関数を書きたいとき、List<?> だと取り出した値が Object 扱いになって + 演算ができません。そこで使うのが <? extends T> という 上限境界 (upper bound) です。
Java
import java.util.List;
public class Sum {
public static double sumAll(List<? extends Number> list) {
double total = 0.0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
}List<? extends Number> は「Number または Number のサブクラス (Integer Double Long など) を要素に持つリスト」を意味します。中身は確実に Number として扱えるので、doubleValue() のような Number のメソッドが安心して呼べます。一方で、このリストに add で要素を入れることは原則できません (null 以外)。中身の本当の型が Integer なのか Double なのかは呼び出し側しか知らないので、コンパイラは「安全に追加できる型」を決められないからです。
<? extends T>は「データの生産者 (Producer)」だと覚えてください。値を取り出して読む方向に向いていて、書き込みは封じられています。getterばかり使いたい場面で活躍します。
<? super T> 下限境界 ─ PECS の Consumer
逆に、「リストに要素を追加したい (書き込みたい)」場合に使うのが <? super T> という 下限境界 (lower bound) です。
Java
import java.util.List;
public class Adder {
public static void addNumbers(List<? super Integer> list) {
list.add(10);
list.add(20);
list.add(30);
}
}List<? super Integer> は「Integer または Integer の スーパークラス (Number Object など) を要素に持つリスト」を意味します。Integer を入れる先としては List<Integer> List<Number> List<Object> のどれでも問題ないので、add(10) のような書き込みが安全に行えます。一方で、取り出した値の型は Object までしか保証されないので、Integer x = list.get(0); のような書き方はできません (Object 経由ならできますが、それは何の型情報も得られていません)。
<? super T>は「データの消費者 (Consumer)」です。値を受け取って書き込む方向に向いていて、読み出した値はObject扱いになります。setter的な使い方をしたい場面で活躍します。
PECS の覚え方
ここまで出てきた 2 つの境界を、Joshua Bloch の『Effective Java』が PECS という有名な合言葉でまとめています。読み方は「ペックス」です。
ProducerExtends ─ 値を取り出す (生産する) 側は<? extends T>ConsumerSuper ─ 値を受け取る (消費する) 側は<? super T>
つまり「リストから読み取って使う立場なら extends、リストに書き込む立場なら super」と覚えれば OK です。コピー処理を例にすると、src (コピー元 = 生産者) は <? extends T>、dst (コピー先 = 消費者) は <? super T> になります。標準ライブラリの Collections.copy のシグネチャはまさにそうなっていて、PECS の見本のような API です。
図の上半分が「Number 以下を読み取る」上限境界、下半分が「Integer 以上に書き込む」下限境界のイメージです。継承ツリー 上で、extends は下から上に絞り込み、super は上に向かって広がる、と捉えると感覚がつかめてきます。
よくある間違い
ワイルドカードはルールが細かく、初学者が必ずハマるポイントが 3 つあります。事前に押さえておきましょう。
?とTを同じものだと思ってしまう — メソッドの型パラメータ<T>は「呼び出し時に決まる具体的な型」を表すので、引数と戻り値で同じTを使うとリンクされます。一方?は「何か未知の型」というだけで、複数の?は別々の未知です。「引数を読み取って同じ型のまま返したい」なら<T> T pick(List<T> list)のようにTを使い、「中身は何でもいい、件数だけ知りたい」なら?を使う、と使い分けましょうList<?>にaddしようとする —List<?>は「中身の本当の型が分からないリスト」なので、安全に追加できる値はnullだけです。list.add(10)と書くとcapture of ?というエラーメッセージで止まります。addしたいときは<? super T>を使うか、具体的な型<Integer>を使ってくださいPECSの方向を逆に覚えてしまう — 「extendsで書き込めそう」「superで読み取れそう」と直感で考えると逆向きに覚えてしまいがちです。Producer(生産者 = 出す側) はextends、Consumer(消費者 = 受け取る側) はsuperという言葉ごと暗記してしまうのが早道です
もう 1 つ補足すると、戻り値の型に ? を使うのは避けるのが定石です。List<?> getList() のようなメソッドは、呼び出し側で要素を取り出すたびに Object 扱いになって扱いづらくなります。戻り値で型を返したいときは <T> List<T> のように型パラメータを使うのが現代の Java の流儀です。
標準ライブラリのソースを読むと、
PECSがいかに徹底されているかがよく分かります。Collections.addAll(Collection<? super T> c, T... elements)やStream.map(Function<? super T, ? extends R> mapper)など、入出力の方向に応じて境界の向きが切り替わっています。プロのコードを読むときの良いトレーニングになるので、暇なときに眺めてみてください。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
Collections.copy(dst, src)─<T> void copy(List<? super T> dst, List<? extends T> src)の典型- 汎用ユーティリティ ─
<T> T max(List<? extends T> list, Comparator<? super T> c) - Stream の
collect─ 内部で広範に PECS が使われている - API 設計 ─ 「読み取りしかしない引数」「書き込みしかしない引数」を明示するときに使う
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
List<Object>で受けている ─List<String>を渡せない。List<? extends Object>で柔軟にextendsとsuperの取り違え ─ 読むならextends、書くならsuper(PECS)<? extends ?>─ 二重ワイルドカードはコンパイルエラー、設計を見直す- ワイルドカードを必要以上に多用 ─ ジェネリックメソッドで
<T>を使ったほうがすっきり書ける場面もある
パフォーマンス考慮事項
- 型消去で実行時オーバーヘッドゼロ ─ コンパイル時のチェックのみ
- コンパイル速度に若干影響 ─ ワイルドカードが深いと推論が遅くなる
Collectionsの実装 ─ ワイルドカードによる API 柔軟性が性能に直接寄与するわけではない- IDE 推論 ─ ネストしたワイルドカードは IntelliJ が遅くなる傾向、簡潔さを保つ
ここまでの要点
? は不明な 1 型、<? extends T> は読み取り (Producer)、<? super T> は書き込み (Consumer)。PECS で覚える。
やってみよう
それでは課題に挑戦しましょう。今回の課題はシンプルに「配列の要素数を返す」だけです。本文では int countItems(List<?> list) というワイルドカードを使ったシグネチャを紹介しましたが、テスト環境の都合で実装は配列版を使います。やることは次のとおりです。
Solution.countItems(int[] arr)メソッドを完成させる- 引数
arrの要素数をintで返す - 配列の長さは
arr.lengthで取得できる
配列の要素数を返すだけなので、return arr.length; の 1 行で済みます。本文で学んだ List<?> 版を頭の中で書き換えるなら return list.size(); 相当の操作です。size() と length は名前が違うだけで、やっていることは「件数を返す」で同じです。
慣れてきたら、自分でメモ用紙に List<? extends Number> 版と List<? super Integer> 版のメソッドを書き分けてみてください。たとえば「Number のリストを受け取って合計を返す」「Integer を 3 つ追加する」のような小さな関数を考えると、PECS の使い分けが体に染み込みます。
次のレッスンでは、Comparable Comparator のような実世界のジェネリクス API を見ながら、<T extends Comparable<? super T>> のような複雑なシグネチャの読み方に進んでいきます。まずはここで「? <? extends T> <? super T> の 3 つを PECS で使い分ける」という感覚を掴んでおきましょう。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は Pair<K,V> を作る で、Pair<K,V> を作る を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- ワイルドカード の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. ワイルドカード とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
Solution.countItems(int[] arr)メソッドを実装し、配列の要素数をintで返すことarr.lengthを使って要素数を取得すること (ループでカウントする必要はない)- 戻り値の型は
int、引数の型はint[]のシグネチャを変えないこと
入出力例
test-cases.txt
countItems([1,2,3]) → 3
countItems([]) → 0
countItems([5]) → 1
countItems([10,20,30,40,50]) → 5