はじめに (対象読者・この記事でわかること)
この記事は、Javaのジェネリクスに苦手意識がある初学者〜中級者の方を対象にしています。特に「List<String>とList<?>って何が違うの?」とモヤモヤしている方に最適です。
記事を読み終えると、以下のことが確実にわかるようになります。
- 型パラメータTとワイルドカード?の役割の違い
- どんな場面でどちらを使うべきかの判断基準
- 境界ワイルドカード(? extends / ? super)の使いどころ
普段は何となくコピペで済ませているジェネリクスですが、正しく理解することで保守性の高いコードが書けるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Javaの基本的な文法(クラス・インターフェースの記述)
- ジェネリクスの基礎(ArrayList<String>のような使い方を見たことがある)
- 継承・ポリモーフィズムの基本概念
なぜ「?」が必要なのか:型安全性と柔軟性のジレンマ
Java5で導入されたジェネリクスは、コンパイル時の型検査を強化しながら、汎用的なコードを書けるようにするための仕組みです。しかし、単純に型パラメータTだけでは「どんな型でも受け入れる柔軟さ」と「型安全に値を取り出すこと」が両立できません。
たとえば、異なる型のリストを受け取るメソッドを書きたいとき、List<Object>では不十分です。List<Integer>をList<Object>に代入できない(不変)ため、汎用的な処理が書けません。ここで登場するのがワイルドカード?です。?を使うことで「型は不明だが、特定の型のリスト」として扱えるようになり、柔軟性と型安全性を両立できます。
コードで見る「T」と「?」の違い
ステップ1:型パラメータTで実装してみる
まず、汎用的なBox<T>クラスを定義してみます。
Javapublic class Box<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } }
このTは「宣言時に具体的な型に置き換えられるプレースホルダ」です。次に、このBoxを引数に取るメソッドを書いてみましょう。
Javapublic static <T> void copyBox(Box<T> src, Box<T> dest) { dest.set(src.get()); }
このメソッドは「同じ型のBox同士をコピー」できますが、Box<Integer>からBox<Number>へはコピーできません。型パラメータTが同一でなければならないため、継承関係があっても別物として扱われます。
ステップ2:ワイルドカード?で柔軟性を高める
同じ処理をワイルドカードで書き換えるとこうなります。
Javapublic static void copyBoxWild(Box<? extends Number> src, Box<Number> dest) { dest.set(src.get()); // OK }
? extends Numberとすることで、「Numberのサブタイプなら何でも受け入れる」表現になります。Box<Integer>もBox<Double>も渡せます。重要なのは、ワイルドカードを使った側は型の情報を失うことです。srcから値を取得しても戻り型はNumberになり、Integerにキャストできません。これが「型安全性を保ちながら汎用性を高める」仕組みです。
ハマった点:読み取り専用になってしまう「? extends」
初心者が陥る落とし穴の代表が「コレクションに書き込めない」問題です。
JavaList<? extends Number> list = new ArrayList<Integer>(); list.add(123); // コンパイルエラー
? extends Numberは「何らかのNumberサブタイプのリスト」ですが、実際に入っているのがIntegerかDoubleかは実行時までわかりません。そこでコンパイラは「書き込みは一切禁止」して、読み出し側はNumberとして扱えるようにします。これをPECS原則(Producer extends, Consumer super)と呼び、読み取り専用か書き込み専用かで使い分けます。
解決策:境界ワイルドカードで書き込み可能にする
書き込みたい場合は? superを使います。
Javapublic static <T> void pushAll(Stack<? super T> dst, List<T> src) { for (T t : src) dst.push(t); // OK }
? super Tは「Tのスーパータイプなら何でも受け入れる」ことを示すため、dstに対してT型の値を安全に書き込めます。PECSを意識してextends/superを使い分けることで、柔軟で型安全なAPIが実現できます。
まとめ
本記事では、Javaジェネリクスの「T」と「?」の違いをコード例と共に解説しました。
- 型パラメータ
T:宣言側が型を決め、同一性を保証 - ワイルドカード
?:受け入れ側が型を決め、柔軟性を提供 - 境界ワイルドカード:
extendsで読み取り、superで書き込みを実現
この記事を通して、ジェネリクスを「暗記」から「理解」へと昇華できたでしょうか。正しく使うことで、保守性が高く、再利用しやすいコードが書けるようになります。
次回は、実践的な例として「コレクション操作ユーティリティ」を境界ワイルドカードを使って実装する方法を紹介します。
参考資料
