Javaのジェネリクスは、Java 5で導入された、クラスやメソッドが扱う型をパラメータ化する仕組みです。コレクションから任意の型を安全に取り出せるようにし、キャストと実行時エラーを減らします。この記事ではJava固有の実装に絞り、ジェネリッククラス・ジェネリックメソッドの書き方、境界ワイルドカード、型消去による制限、プリミティブ型が使えない理由、命名慣習までをコード付きで解説します。ジェネリクスそのものの概念や他言語との違いはジェネリクスとは|型パラメータの仕組みと主要言語での違いで整理しています。
まとめ:Javaジェネリクスの要点
- Java 5以降、
List<String>のように型を指定してキャストなしで安全に扱える。 - 実装はジェネリッククラス(
class Box<T>)とジェネリックメソッド(<T> T method(...))の2系統。 - 柔軟性の鍵は境界ワイルドカード。読み取りは
? extends、書き込みは? super(PECSの原則)。 - Java固有の制約は型消去(実行時に型引数が消える)とプリミティブ型が使えないこと。ラッパークラスで扱う。
Javaがジェネリクスを導入した理由
Java 5より前のコレクションは、要素をすべてObjectとして保持していました。そのため取り出すたびにキャストが必要で、誤った型を入れてもコンパイルは通り、実行時にClassCastExceptionで初めて発覚しました。ジェネリクスは型引数を明示することで、この誤りをコンパイル時に検出し、同時に取り出し時のキャストを不要にします。型安全性とコードの簡潔さを両立させたのが導入の目的です。
ジェネリッククラスの宣言と使い方
クラス名の後に<T>と型パラメータを宣言すると、そのクラス内でTを型として使えます。利用側は<String>のように具体型を指定します(右辺はダイヤモンド演算子<>で省略可)。
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> box = new Box<>();
box.set("hello");
String s = box.get(); // キャスト不要
ジェネリックメソッドの宣言と使い方
メソッド単位で型パラメータを持たせるには、戻り値の型の前に<T>を置きます。クラスがジェネリックでなくても、静的メソッドなどに個別に適用できます。
public static <T> T firstOf(List<T> list) {
return list.get(0);
}
Integer n = firstOf(List.of(10, 20, 30));
String s = firstOf(List.of("a", "b"));
境界型と境界ワイルドカード(PECS)
型パラメータに上限を設けるには<T extends Number>と書き、Numberのサブタイプに限定してそのメソッド(doubleValue()など)を安全に呼べます。さらに柔軟性を高めるのがワイルドカード?です。データを読み取る側は? extends(上限境界)、書き込む側は? super(下限境界)を使います。「Producer Extends, Consumer Super」の頭文字を取ってPECSと呼ばれる原則です。逆に、同じコレクションで読み取りと書き込みの両方を行う場面でワイルドカードを使うのは誤りで、その場合はワイルドカードではなく具体的な型パラメータ<T>を宣言すべきです。
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total; // 読み取り専用:? extends
}
public static void fill(List<? super Integer> list) {
list.add(1);
list.add(2); // 書き込み:? super
}
型消去とその制限
Javaのジェネリクスは型消去で実装されており、コンパイル後は型引数の情報が取り除かれます。そのためList<String>とList<Integer>は実行時には同じListクラスになります。
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
boolean same = (a.getClass() == b.getClass());
このsameはtrueになります。型消去の結果、実行時には型引数で区別できません。この制約から、new T()によるインスタンス生成、obj instanceof List<String>のような型引数を含むinstanceof判定、Tの配列生成などは行えません。実行時に型で分岐したい場合は、Class<T>を引数で受け取るなどの回避策を使います。
プリミティブ型が使えない理由
ジェネリクスの型引数にはintやdoubleといったプリミティブ型を直接指定できません。List<int>は書けず、List<Integer>のようにラッパークラスを使います。これは型消去でTがObjectに置き換わる設計上、Objectを継承しないプリミティブ型を保持できないためです。実際にはオートボクシング/アンボクシングによりlist.add(1)のように書けますが、大量要素ではintとIntegerの変換コストが無視できない点に注意します。
型パラメータの命名慣習
型パラメータ名は自由ですが、可読性のため1文字の慣習的な名前が広く使われます。役割が名前から伝わるため、独自の長い名前より慣習に従うほうが読み手に親切です。
| 記号 | 意味 | 主な使用箇所 |
|---|---|---|
| E | Element(要素) | コレクション(List<E>) |
| T | Type(型) | 汎用的な型 |
| K / V | Key / Value | マップ(Map<K,V>) |
| N | Number(数値) | 数値系の型 |
| S, U | 2番目・3番目の型 | 複数型パラメータ |
Java SE標準ライブラリでの総称型
Java標準ライブラリはジェネリクスを全面的に採用しています。コレクションフレームワークのList<E>やMap<K,V>はもちろん、Stream APIのStream<T>、値の有無を表すOptional<T>もすべて総称型です。日常的に使うこれらのAPIがジェネリクスで設計されているため、自作クラスでも同じ流儀に合わせると一貫性のあるコードになります。
よくある質問
ジェネリクスの型引数にintは使えますか?
直接は使えません。List<int>は不可で、ラッパークラスのList<Integer>を使います。オートボクシングにより値の出し入れはintのように書けますが、内部ではInteger型として扱われます。
「? extends」と「? super」はどう使い分けますか?
データを取り出す(読む)側では? extends、データを入れる(書く)側では? superを使います。頭文字を取ったPECS(Producer Extends, Consumer Super)が判断の目安です。読み書き両方を行うなら、ワイルドカードではなく具体的な型パラメータ<T>を使います。
型消去があるとリフレクションで型引数を取得できませんか?
ローカル変数の型引数は実行時に取得できません。ただし、フィールドやメソッドのシグネチャに書かれた型引数はメタ情報として残るため、getGenericType()系のAPIで取得できる場合があります。実行時に型で分岐したい設計では、Class<T>を明示的に受け渡す方法が確実です。
ジェネリクスはパフォーマンスに影響しますか?
型消去のため、ジェネリクス自体による実行時オーバーヘッドはほとんどありません。ただしプリミティブ型をラッパーで扱う際のボクシングコストは発生します。大量の数値処理では、プリミティブ特化のAPI(IntStreamなど)の利用を検討します。