はじめに (対象読者・この記事でわかること)

この記事は、Javaのジェネリクスに苦手意識がある初学者〜中級者の方を対象にしています。特に「List<String>List<?>って何が違うの?」とモヤモヤしている方に最適です。

記事を読み終えると、以下のことが確実にわかるようになります。 - 型パラメータTとワイルドカード?の役割の違い - どんな場面でどちらを使うべきかの判断基準 - 境界ワイルドカード(? extends / ? super)の使いどころ

普段は何となくコピペで済ませているジェネリクスですが、正しく理解することで保守性の高いコードが書けるようになります。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法(クラス・インターフェースの記述) - ジェネリクスの基礎(ArrayList<String>のような使い方を見たことがある) - 継承・ポリモーフィズムの基本概念

なぜ「?」が必要なのか:型安全性と柔軟性のジレンマ

Java5で導入されたジェネリクスは、コンパイル時の型検査を強化しながら、汎用的なコードを書けるようにするための仕組みです。しかし、単純に型パラメータTだけでは「どんな型でも受け入れる柔軟さ」と「型安全に値を取り出すこと」が両立できません。

たとえば、異なる型のリストを受け取るメソッドを書きたいとき、List<Object>では不十分です。List<Integer>List<Object>に代入できない(不変)ため、汎用的な処理が書けません。ここで登場するのがワイルドカード?です。?を使うことで「型は不明だが、特定の型のリスト」として扱えるようになり、柔軟性と型安全性を両立できます。

コードで見る「T」と「?」の違い

ステップ1:型パラメータTで実装してみる

まず、汎用的なBox<T>クラスを定義してみます。

Java
public class Box<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } }

このTは「宣言時に具体的な型に置き換えられるプレースホルダ」です。次に、このBoxを引数に取るメソッドを書いてみましょう。

Java
public static <T> void copyBox(Box<T> src, Box<T> dest) { dest.set(src.get()); }

このメソッドは「同じ型のBox同士をコピー」できますが、Box<Integer>からBox<Number>へはコピーできません。型パラメータTが同一でなければならないため、継承関係があっても別物として扱われます。

ステップ2:ワイルドカード?で柔軟性を高める

同じ処理をワイルドカードで書き換えるとこうなります。

Java
public 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」

初心者が陥る落とし穴の代表が「コレクションに書き込めない」問題です。

Java
List<? extends Number> list = new ArrayList<Integer>(); list.add(123); // コンパイルエラー

? extends Numberは「何らかのNumberサブタイプのリスト」ですが、実際に入っているのがIntegerDoubleかは実行時までわかりません。そこでコンパイラは「書き込みは一切禁止」して、読み出し側はNumberとして扱えるようにします。これをPECS原則(Producer extends, Consumer super)と呼び、読み取り専用か書き込み専用かで使い分けます。

解決策:境界ワイルドカードで書き込み可能にする

書き込みたい場合は? superを使います。

Java
public 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で書き込みを実現

この記事を通して、ジェネリクスを「暗記」から「理解」へと昇華できたでしょうか。正しく使うことで、保守性が高く、再利用しやすいコードが書けるようになります。

次回は、実践的な例として「コレクション操作ユーティリティ」を境界ワイルドカードを使って実装する方法を紹介します。

参考資料