ワイルドカード ?

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • ワイルドカード ? は「型は分からないけど、何か 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) に対する扱いが大きく違います。

diagram (will load when visible)

本章ではまず素の ? の使い方を押さえ、そのあとに「読み取り専用なら ? 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)」というルールを覚えておいてください。StringObject のサブクラスでも、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 という有名な合言葉でまとめています。読み方は「ペックス」です。

  • P roducer E xtends ─ 値を取り出す (生産する) 側は <? extends T>
  • C onsumer S uper ─ 値を受け取る (消費する) 側は <? super T>

つまり「リストから読み取って使う立場なら extends、リストに書き込む立場なら super」と覚えれば OK です。コピー処理を例にすると、src (コピー元 = 生産者) は <? extends T>dst (コピー先 = 消費者) は <? super T> になります。標準ライブラリの Collections.copy のシグネチャはまさにそうなっていて、PECS の見本のような API です。

diagram (will load when visible)

図の上半分が「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 (生産者 = 出す側) は extendsConsumer (消費者 = 受け取る側) は 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> で柔軟に
  • extendssuper の取り違え ─ 読むなら extends、書くなら super (PECS)
  • <? extends ?> ─ 二重ワイルドカードはコンパイルエラー、設計を見直す
  • ワイルドカードを必要以上に多用 ─ ジェネリックメソッドで <T> を使ったほうがすっきり書ける場面もある

パフォーマンス考慮事項

  • 型消去で実行時オーバーヘッドゼロ ─ コンパイル時のチェックのみ
  • コンパイル速度に若干影響 ─ ワイルドカードが深いと推論が遅くなる
  • Collections の実装 ─ ワイルドカードによる API 柔軟性が性能に直接寄与するわけではない
  • IDE 推論 ─ ネストしたワイルドカードは IntelliJ が遅くなる傾向、簡潔さを保つ
この章のポイント

ここまでの要点 ? は不明な 1 型、<? extends T> は読み取り (Producer)、<? super T> は書き込み (Consumer)。PECS で覚える。

やってみよう

それでは課題に挑戦しましょう。今回の課題はシンプルに「配列の要素数を返す」だけです。本文では int countItems(List<?> list) というワイルドカードを使ったシグネチャを紹介しましたが、テスト環境の都合で実装は配列版を使います。やることは次のとおりです。

  1. Solution.countItems(int[] arr) メソッドを完成させる
  2. 引数 arr の要素数を int で返す
  3. 配列の長さは 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 つができることを確認しましょう。

  1. ワイルドカード の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. ワイルドカード とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. Solution.countItems(int[] arr) メソッドを実装し、配列の要素数を int で返すこと
  2. arr.length を使って要素数を取得すること (ループでカウントする必要はない)
  3. 戻り値の型は int、引数の型は int[] のシグネチャを変えないこと

入出力例

test-cases.txt

countItems([1,2,3])3 countItems([])0 countItems([5])1 countItems([10,20,30,40,50])5

ヒント

main.java
main.java
学習モード

メモ

ワイルドカード ?

⌘S で保存