ジェネリクスが存在する理由
ジェネリックな型を使うと、型安全性を捨てることなく、コードを一度だけ書いて多くの型に再利用できます。IntBox、StringBox、UserBox を別々に書く代わりに、T が呼び出し側の埋める置き場所となる単一の Box<T> を書きます。
ArrayList<String> や HashMap<String, Integer> と書くたびに、あなたはすでにジェネリクスを使ってきました。<...> の部分が型引数です。このページでは、自分でジェネリックを書く方法を示します。
その代替手段 - すべてを Object として格納する方法 - は型情報を捨て去り、醜くエラーを起こしやすいキャストを強います。
最後の行のそのキャストは実行時に ClassCastException を投げます。これはまさに、ジェネリクスが不可能にしようと設計されたたぐいのバグです。
ジェネリッククラス
クラス名の後ろに、山かっこで型パラメータを宣言します。慣例として 1 文字の大文字を使います。「type(型)」の T、「element(要素)」の E、キー/値の K/V です。
Box の内部では、各 T は呼び出し側が指定したものに置き換わります。Box<String> は String のみを保持して返す箱です。コンパイラはプログラムが実行される前に name.set(99) を拒否します。
右側の空の <>(ダイヤモンド演算子)を使うと、コンパイラが左側から型引数を推論するので、<String> を 2 回繰り返さずに済みます。
ジェネリックメソッド
1 つのメソッドが、クラスとは独立した独自の型パラメータを持てます。パラメータ <T> を戻り値の型の 前 に置きます。
T を明示的に渡すことは決してありません。コンパイラが引数から推論します。Collections.sort や List.of のようなユーティリティが、どんな要素型でも型安全であり続けられるのは、ジェネリックメソッドのおかげです。
境界型パラメータ
ジェネリックな型が 一部 の型に対してしか意味をなさない場合があります。extends はパラメータを制約し、その境界のメソッドを呼べるようにします。ここで T extends Number は T が Number またはその任意のサブクラス(Integer、Double など)であることを意味し、doubleValue() が利用可能になります。
ここでの extends は「〜のサブタイプである」という意味で、クラスにもインターフェースにも使えることに注意してください。要素を比較する必要があるときには <T extends Comparable<T>> が非常によく使われます。
ワイルドカード: ? extends と ? super
微妙な落とし穴: Integer は Number であるにもかかわらず、List<Integer> は List<Number> では ありません。ジェネリクスは 不変(invariant) です。読むだけ、または書くだけの場合に、ワイルドカードがこの制約を緩めます。
読み出す側のプロデューサーには ? extends T を、書き込む側のコンシューマーには ? super T を使います(「PECS」の規則 - Producer Extends, Consumer Super)。
? extends Number のリストは、要素を Number として読み出すことはできますが、そこに追加することはできません(コンパイラには正確な要素型が分からないため)。? super Integer のリストは Integer を追加できますが、読み出しは Object として返ってきます。データの流れに合ったワイルドカードを選びましょう。
型消去とその限界
ジェネリクスはコンパイル時の機能です。コンパイル後は型パラメータが 消去 され、実行時には Box<String> も Box<Integer> もどちらも単なる Box です。これによりジェネリクスは古いコードとの後方互換性を保ちますが、実際の制限も課されます。
// どれもコンパイルできない - 型パラメータは実行時に存在しない:
T value = new T(); // 型パラメータはインスタンス化できない
T[] array = new T[10]; // ジェネリック配列は作成できない
if (list instanceof List<String>) { } // 型引数はテストできない
実行時には型が消えているため、リフレクションで「T は何だったのか?」と尋ねることはできず、ジェネリック引数だけが異なるメソッドをオーバーロードすることもできません(foo(List<String>) と foo(List<Integer>) は同じシグネチャに消去されます)。実行時に本当に型が必要なときは、Class<T> トークンをコンストラクタやメソッドのパラメータとして渡します。
次へ: ラムダ式
ジェネリックメソッドが型をパラメータとして受け取ることを見てきました。次のステップは 振る舞い をパラメータとして扱うことです。ラムダ式を使うと、コードの断片 - 関数 - をメソッドに渡せます。これはまさに、たった今型安全に書けるようになったジェネリックなコレクションを、ソートしたりフィルタしたり変換したりする方法そのものです。
よくある質問
Java のジェネリクスとは何ですか?
ジェネリクスを使うと、クラスやメソッドを 1 つの具体的な型に固定するのではなく、後で指定する型に対して動作するように書けます。山かっこで型パラメータを宣言し - class Box<T> - 呼び出し側が実際の型を埋めます - Box<String>。するとコンパイラがその型をあらゆる箇所で強制するので、不一致をコンパイル時に検出でき、手動のキャストを省けます。
なぜ Object ではなくジェネリクスを使うのですか?
Object を使うとすべての型情報が失われます。誤ったものを入れてもコンパイラは止められず、取り出すたびにキャストが必要になります(実行時に ClassCastException が出るおそれもあります)。ジェネリクスはそのチェックをコンパイル時へ前倒しします。List<String> は Integer を受け付けませんし、get() はすでに String を返すので、キャストも不要で実行時の不意打ちもありません。
Java ジェネリクスにおける型消去とは何ですか?
型消去とは、ジェネリックな型情報がコンパイル時にしか存在しないことを意味します。コンパイル後は、実行時には List<String> も List<Integer> もどちらも単なる List であり、型パラメータは消去されます。これが、new T[10] と書けず、list instanceof List<String> を呼べず、リフレクションで型パラメータを読めない理由です。ジェネリクスはコンパイル時の安全性を与えるものであり、実行時の型データではありません。