Javaのジェネリクス(Generics、総称型)は、List<String>の<String>のように、クラスやメソッドが扱う型を「型引数」として外から渡す仕組みです。J2SE 5.0(2004年)でJSR 14として入り、誤った型の混入を実行時のClassCastExceptionからコンパイルエラーへ前倒しし、取り出し時のキャストを不要にしました。この記事はJava固有の書き方と制約に絞り、型パラメータTの宣言、? extendsと? superの使い分け、型消去で「書けないコード」を、JDK 25.0.4とJDK 27でコンパイルしたときのjavacのメッセージ付きで解説します。ジェネリクスの一般概念やC#・TypeScript・Goとの違いはジェネリクスとは|型パラメータの仕組みと主要言語での違いにまとめています。
まとめ:Javaジェネリクスの要点
class Box<T>の<T>は型パラメータの宣言、Box<String>の<String>は型引数。Java 5から使え、Java 7で右辺を省略するダイヤモンド<>が入りました。- メソッド引数の型を広げるのはワイルドカード。値を取り出す引数は
? extends T、値を入れる引数は? super T、両方を行うならワイルドカードを使わずTにします。 List<Integer>はList<Number>に代入できません(不変)。配列は共変なので代入できますが、その参照を通じて実際の要素型に適合しない値を格納すると、実行時にArrayStoreExceptionになります。- 型引数はコンパイル後に消えます(型消去)。
new T()、new T[10]、o instanceof List<String>、型引数だけが違うオーバーロードはコンパイルエラーです。 List<int>は2026年10月時点の最新版JDK 27でも書けません。JDK 28で値オブジェクト(JEP 401)がプレビューに入りますが、プリミティブ型を型引数にするJEP 402はまだDraftです。
以下、宣言の書き方から順に、つまずきやすい制約とその回避策を見ていきます。記事中の出力は、Temurin 25.0.4.1とZulu 27(27+35)で、成功する例とエラーになる例を別ファイルに分けてjavac -J-Duser.language=en -Xlint:unchecked,rawtypesでコンパイルし、javaで実行、javap -cとjavap -vで逆アセンブルして取ったものです。
ジェネリクス導入前後のコード比較:キャストとClassCastExceptionの解消
Java 1.4までのコレクションは要素をObjectとして保持していました。取り出すたびにキャストが必要で、別の型を入れてもコンパイルは通り、実行して初めてClassCastExceptionで発覚します。
// Java 1.4 までの書き方(今のjavacではunchecked操作の注記が出る)
List names = new ArrayList();
names.add("Sato");
names.add(Integer.valueOf("1")); // 通ってしまう
String s = (String) names.get(1); // 実行時に ClassCastException
// ジェネリクスを使う書き方
List<String> names2 = new ArrayList<>();
names2.add("Sato");
String s2 = names2.get(0); // キャスト不要
names2.add(1); // コンパイルエラー
最後の行に対して、JDK 25のjavacはerror: incompatible types: int cannot be converted to String(日本語ロケールでは「不適合な型: intをStringに変換できません」)を出します。型の誤りが見つかる場所が、実行時からコンパイル時に移りました。
型パラメータTの宣言場所と書き方
<T>を書ける場所は、クラス・インターフェース・レコードの名前の後、メソッドの戻り値の型の前、コンストラクタの名前の前です。
ジェネリッククラスとダイヤモンド演算子(Java 7・Java 9)
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
record Pair<A, B>(A first, B second) {} // レコードも型パラメータを持てる
Box<String> box = new Box<>(); // 右辺の型引数はダイヤモンドで省略
box.set("hello");
String s = box.get();
右辺の型引数を<>で省略できるのはJava SE 7からで、匿名クラスでのnew Comparator<>() { ... }はJava 9のJEP 213で許可されました。
注意したいのはvarとの併用です。var list = new ArrayList<>();と書くと推論の手がかりが無いためArrayList<Object>になり、JDK 25でlist.add(1)とlist.add("str")の両方がエラーなく通りました。varを使うなら右辺に型引数を書き、ダイヤモンドを使うなら左辺に型を書く、のどちらかに統一します。
ジェネリックメソッドと型引数の明示
public static <T> T firstOf(List<T> list) {
return list.get(0);
}
Integer n = firstOf(List.of(10, 20, 30)); // T = Integer と推論
List<String> empty = Collections.<String>emptyList(); // 型引数を明示する書き方
クラスの型パラメータはstaticメソッドやstaticフィールドからは使えません。static T field;と書くとnon-static type variable T cannot be referenced from a static contextになります。ユーティリティをstaticで書きたいときは、メソッド側に<T>を宣言します。
型パラメータ名の慣習:E・K・N・T・V
名前は任意ですが、Oracleのチュートリアルが示す1文字の慣習がJava SE APIでも使われています。
| 名前 | 由来 | Java SEでの例 |
|---|---|---|
| E | Element | List<E>・Set<E> |
| K / V | Key / Value | Map<K,V> |
| T | Type | Optional<T>・Stream<T> |
| R | Result | Function<T,R> |
| N | Number | 数値型を受ける型パラメータ |
| S, U, V | 2番目以降の型 | BiFunction<T,U,R> |
Rはチュートリアルの一覧には無く、java.util.functionで使われている名前です。
境界型パラメータ:T extends Numberと複数境界
<T extends Number>と書くと、TはNumberのサブタイプに限られ、メソッド内でdoubleValue()などを呼べます。インターフェースを境界にする場合もimplementsではなくextendsと書きます。
static <T extends Number & Comparable<T>> T clampMax(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
境界は&で複数並べられ、そのうち1つがクラスなら先頭に書く決まりです。標準ライブラリの例がCollections.maxで、JDK 25でリフレクションから取ったシグネチャは次のとおりでした。
public static <T extends java.lang.Object & java.lang.Comparable<? super T>> T
java.util.Collections.max(java.util.Collection<? extends T>)
先頭のObjectは、型消去後の戻り値型をObjectに保つためのものです。ジェネリクス導入前からCollections.max()を呼んでいる既存のクラスファイルが、戻り値Objectのシグネチャに依存しているためだとOracleのチュートリアルは説明しています。Comparable<? super T>の部分は、親クラスでComparableを実装したサブクラスも受け付けるための下限境界です。
ワイルドカード3種とextends・superの使い分け
ワイルドカード?は、メソッドの引数で受け入れる型引数の範囲を広げる書き方です。
? extends:要素追加の制限とjavacのCAP#1エラー
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue(); // 読み取りはNumberとして可能
return total;
}
List<? extends Number> ln = new ArrayList<Integer>();
ln.add(1); // コンパイルエラー
sumにはList<Integer>もList<Double>も渡せます。その代わり、実体がIntegerのリストかDoubleのリストかをコンパイラは知らないため、null以外を追加できません。javacのエラーはincompatible types: int cannot be converted to CAP#1で、続けてCAP#1 extends Number from capture of ? extends Numberと表示されます。CAP#1は「この?に入っている、名前の無い何かの型」を表す捕捉型です。
? super:書き込み側に使う下限境界
static void fill(List<? super Integer> list) {
list.add(1);
list.add(2); // Integer は必ず入る
Object o = list.get(0); // 取り出しは Object としてしか受けられない
}
fill(new ArrayList<Integer>());
fill(new ArrayList<Number>());
fill(new ArrayList<Object>());
? super Integerは「Integerか、その親の型のリスト」を受けます。どのリストでもIntegerは入るので追加はでき、逆に取り出した値の型はObjectとしか言えません。
判断基準:Collections.copyのシグネチャで見るPECS
使い分けの覚え方として、書籍『Effective Java』が広めた「PECS(Producer Extends, Consumer Super)」があります。値を供給する引数(Producer)はextends、値を受け取る引数(Consumer)はsuperです。JDK 25のCollections.copyはそのまま次の形をしています。
public static <T> void copy(List<? super T> dest, List<? extends T> src)
Oracleのワイルドカード指針は同じ考えを「in変数/out変数」と呼び、さらに設計上の指針を2つ示しています。同じ引数を読み書き両方に使うならワイルドカードを使わないことと、戻り値の型にワイルドカードを使わないことです。戻り値がList<? extends Number>だと、呼び出し側から直接追加できる値は、型の上ではnullだけです。ワイルドカードは引数で受け入れる範囲を広げるために使い、戻り値はList<T>のように呼び出し側がそのまま操作できる型で返します。
List<?>・List<Object>・raw型Listの違い
似た見た目の3つは、受け付ける値と警告の有無が違います。Class<?>の<?>も「型引数は問わない」という意味です。
| 型 | List<String>を代入 | add("x") |
javacの扱い |
|---|---|---|---|
List<?> |
できる | 不可(nullのみ) | 警告なし |
List<Object> |
できない | できる | 代入時にエラー |
List(raw型) |
できる | できる | uncheckedとrawtypesの警告 |
raw型は互換性のために残っているだけで、新しいコードで使う理由はありません。中身の型を問わずに受け取りたいならList<?>を使います。
List<Integer>をList<Number>に代入できない理由と配列との違い
IntegerはNumberのサブクラスですが、List<Integer>はList<Number>のサブタイプではありません。この性質を不変(invariant)と呼びます。配列は逆に共変で、Integer[]をNumber[]やObject[]に代入できます。その結果がどうなるかを、JDK 25で実行した結果で比べます。
Object[] objs = new String[1];
objs[0] = 1; // コンパイルは通る → 実行時 java.lang.ArrayStoreException: java.lang.Integer
List<Number> nums = new ArrayList<Integer>();
// error: incompatible types: ArrayList<Integer> cannot be converted to List<Number>
もしList<Integer>をList<Number>として扱えたら、Doubleを追加できてしまい、配列と同じ事故が起きます。配列は実行時に要素の型を保持しているのでArrayStoreExceptionで止められますが、型消去されたジェネリクスは実行時に止める手段が無いため、コンパイル時に禁じています。Number系のリストをまとめて受けたいときは、前章のList<? extends Number>を使います。
型消去でコンパイル後に消えるもの
Javaのジェネリクスは型消去(type erasure)で実装されています。型引数はコンパイル時の検査にだけ使われ、クラスファイルの命令列からは消えます。ジェネリクス導入前のライブラリや既存コードとの互換性を保つための設計です。現在のjavacが生成したクラスファイルを古いJVMで実行できるという意味ではありません。
getClass()が同じになる例とjavapで見るcheckcast
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
実行時にはどちらも同じArrayListクラスです。String s = list.get(0);をjavap -cで逆アセンブルすると、List.getの戻り値はObjectのままで、直後にコンパイラがcheckcast命令を挿入しています。
2: invokeinterface #19, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
7: checkcast #25 // class java/lang/String
キャストはコンパイラが代わりに書いています。バイトコードの読み方はJava仮想マシン(JVM)とは?仕組み・メモリ構成・JITを実機の出力で解説で扱っています。
ブリッジメソッド:compareTo(Object)が自動生成される理由
class Br implements Comparable<Br>でcompareTo(Br o)だけを書いても、消去後のComparableが要求するのはcompareTo(Object)です。javacはこの食い違いを埋めるメソッドを自動で足します。javap -vの出力では、次のようにACC_BRIDGEとACC_SYNTHETICが付いた2つ目のcompareToが見えます。
public int compareTo(java.lang.Object);
descriptor: (Ljava/lang/Object;)I
flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
2: checkcast #8 // class Br
5: invokevirtual #27 // Method compareTo:(LBr;)I
スタックトレースにcompareToが2段重なって出てきた場合は、ブリッジメソッドを経由している可能性があります。再帰呼び出しなどでも同名のフレームは並ぶため、javap -vで確認します。仕組みはOracleのチュートリアルに例があります。
型消去が原因で書けないコード7項目とjavacのエラー文
Oracleの「Restrictions on Generics」が挙げる制約を、JDK 25のjavac(英語ロケール)で1つずつコンパイルしたときのメッセージです。JDK 27でも同じ文言でした。
| 書けないコード | javacのエラー |
|---|---|
List<int> |
unexpected type(required: reference) |
new T() |
unexpected type(found: type parameter T) |
static T field; |
non-static type variable T cannot be referenced… |
o instanceof List<String> |
Object cannot be safely cast to List<String> |
new T[10]・new List<String>[3] |
generic array creation |
class Ex<T> extends Exception |
a generic class may not extend java.lang.Throwable |
c(List<String>)とc(List<Integer>) |
name clash: … have the same erasure |
new T()が必要なら後述のClass<T>を渡す方法などでインスタンスを生成し、型引数だけが違うオーバーロードはメソッド名を変えて解決します。
raw型・unchecked警告・ヒープ汚染の扱い
javacのunchecked注記と-Xlintによる警告の詳細表示
raw型のadd呼び出しのようなunchecked操作を含むファイルを普通にコンパイルすると、出力はNote: Warn.java uses unchecked or unsafe operations.とNote: Recompile with -Xlint:unchecked for details.の2行だけで、ビルドは成功します。-Xlint:unchecked,rawtypesを付けて初めて、どの行が問題かが出ます。
warning: [rawtypes] found raw type: List
warning: [unchecked] unchecked call to add(E) as a member of the raw type List
warning: [unchecked] unchecked conversion
required: List<Integer>
found: List
MavenやGradleのコンパイラ設定に-Xlint:uncheckedを入れておき、どうしても避けられない箇所だけ@SuppressWarnings("unchecked")を付けます。付ける範囲はメソッド全体ではなく、警告が出る1つの変数宣言に絞ると、他の警告を巻き込みません。
可変長引数のヒープ汚染と@SafeVarargs(Java 7・Java 9)
可変長引数は内部で配列になるため、List<String>...では型引数の情報を持たないList[]の配列が作られます。その配列に別の型引数のリストを入れたときに起きるのが、JLSがヒープ汚染(heap pollution)と呼ぶ状態です。
static void pollute(List<String>... lists) {
Object[] arr = lists;
arr[0] = List.of(42); // 実行時の要素型はListなので、List.of(42)も格納できてしまう
String s = lists[0].get(0); // ここでClassCastException
}
JDK 25で実行すると、例外が出るのは汚染した行ではなく取り出した行で、メッセージはclass java.lang.Integer cannot be cast to class java.lang.Stringでした。原因と発覚箇所が離れるのがヒープ汚染の厄介な点です。宣言側にはPossible heap pollution from parameterized vararg type List<String>、呼び出し側にはunchecked generic array creation for varargs parameterの警告が出ます。
可変長引数の配列を外へ漏らさず、要素を書き換えないと保証できるメソッドにだけ@SafeVarargsを付けて警告を消します。付けられるのはstaticメソッド、finalメソッド、コンストラクタと、Java 9からはprivateメソッドで、それ以外に付けるとコンパイルエラーです。上のpolluteのように配列へ代入するメソッドに付けてはいけません。
実行時に型引数が必要なときの2つの回避策
JSONの変換のように実行時に型を知りたい処理には、単一のクラスを渡すClass<T>と、List<String>のような型引数付きの型まで渡せるスーパータイプトークンの2つの手段があります。
Class<T>によるインスタンス生成と型判定
static <T> T create(Class<T> type) throws ReflectiveOperationException {
return type.getDeclaredConstructor().newInstance(); // new T() の代わり
}
String s = create(String.class);
Class<T>があればtype.isInstance(obj)で型判定もできます。ただしList<String>.classとは書けず、型引数を持つ型は表せません。
匿名サブクラスによる型引数の保持(スーパータイプトークン)
型消去で消えるのは命令列の中の型引数で、クラスやフィールドの宣言に書いた型引数はシグネチャとしてクラスファイルに残ります。これを利用して、匿名サブクラスを作るときに型引数を固定し、getGenericSuperclass()で読み出すのがスーパータイプトークンです。
abstract class TypeRef<T> {
final Type type;
protected TypeRef() {
type = ((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[0];
}
}
System.out.println(new TypeRef<List<String>>() {}.type);
// 出力: java.util.List<java.lang.String>
末尾の{}が匿名サブクラスを作る部分です。この例のTypeRefは抽象クラスなので、{}を外すとコンパイルエラーになります。JacksonのTypeReferenceやSpringのParameterizedTypeReferenceはこの仕組みです。Jacksonでの使い方はJacksonをMavenで導入する手順|2.22と3.2の依存の違いとObjectMapper・JsonNodeの使い方、getGenericType()などのリフレクションAPIはJavaリフレクションとは?invoke・アノテーション・Proxyの使い方とJava 17以降の制限で扱っています。
Java 16以降の変化:instanceofとレコードパターンでのジェネリクス
「instanceofに型引数は書けない」はJava 16以降は正確ではありません。Java SE 16でinstanceofの右辺がreifiable型でなければならない要件が外れ、左辺の静的な型から安全にキャストできると分かる場合はパラメータ化型を書けるようになりました。List<String>型の変数に対するls instanceof ArrayList<String> alはJDK 25でコンパイルでき、Object型の変数に対するo instanceof List<String>は前章の表のとおりエラーのままです。
List<String> ls = new ArrayList<>(List.of("x"));
if (ls instanceof ArrayList<String> al) { ... } // Java 16以降はOK
record Box<T>(T value) {}
Object o = new Box<>("hello");
if (o instanceof Box(String s)) { // Java 21以降、型引数は推論される
System.out.println(s.length()); // 5
}
レコードパターンで型引数を省略したときの推論は、Java 21で正式になったJEP 440の仕様です。一方でo instanceof List lのような型パターンは推論されずraw型として扱われる、とJEP 440に明記されています。レコード自体の書き方はJavaのレコードクラス(record)とは?使い方・コンストラクタ・継承できない理由とエラー例をJava 25で実測を参照してください。
プリミティブ型を型引数にできない理由とProject Valhallaの現在地
List<int>が書けないのは、消去後のTがObjectになり、Objectを継承しないintを入れる場所が無いからです。実際にはList<Integer>に対してlist.add(1)と書け、オートボクシングでIntegerに包まれます。
Integerの==比較が127と128で結果を変える落とし穴
==はボクシングされた値の参照を比べます。JDK 25でInteger i1 = 127, i2 = 127のi1 == i2はtrue、128同士ではfalseでした。JLS 5.1.7は、整数の定数式について-128〜127の同じ値をボクシングした結果の参照同一性を保証しますが、128では保証しないためです。範囲外でも実装や設定によって同一になることがあります。List<Integer>から取り出した値はequalsで比べ、大量の数値はボクシングの無いIntStreamやint[]で扱います。
JEP 401(値オブジェクト)とJEP 402の状態:2026年10月時点
この制約に取り組んでいるのがProject Valhallaです。2026年10月28日時点のOpenJDKの表示では、JEP 401: Value Objects (Preview)がIntegratedでRelease 28、つまり2027年3月9日GA予定のJDK 28でプレビュー機能として入ります。JEP 401はIntegerやLocalDateを値クラスへ移行することと、List<T>などの型引数に値クラスを使えることを挙げていますが、intそのものを型引数にする話ではありません。それを目標に掲げるJEP 402: Enhanced Primitive Boxing (Preview)の状態はDraftで、対象リリースは決まっていません。
実務の判断としては、当面List<int>が書ける前提で設計しないこと、プレビュー機能はjavac --release 28 --enable-previewを付けないと使えないので本番コードに入れないこと、の2点です。Java 27までの版ごとの変更はJava 27とは|非LTSの半年運用とG1・オブジェクトヘッダ既定化の影響にまとめています。Javaで型安全な業務システムを一から組む場合は、弊社のフルスクラッチ開発でも設計から支援しています。
よくある質問
Javaの<>(山括弧)は何を意味しますか?
型を指定する記号です。宣言側のclass Box<T>は「型パラメータTを持つ」、使う側のBox<String>は「TにStringを当てはめる」という意味です。中身が空のnew ArrayList<>()はダイヤモンド演算子で、型引数を左辺から推論させます(Java 7以降)。
ジェネリクスはJavaのどのバージョンから使えますか?
J2SE 5.0(内部バージョン1.5、2004年公開)からで、仕様はJSR 14です。その後、ダイヤモンド演算子(Java 7)、匿名クラスでのダイヤモンド(Java 9)、instanceofでのパラメータ化型(Java 16)、レコードパターンの型引数推論(Java 21)が加わりました。
<T extends Number>と<? extends Number>はどう使い分けますか?
引数と戻り値の型を結び付けたいときは<T extends Number>です。<T extends Number> T max(List<T> list)なら、List<Integer>を渡すと戻り値もIntegerになります。引数を読むだけで型を外に返さないならList<? extends Number>で十分で、宣言も短くなります。
型をTにするのとObjectにするのでは何が違いますか?
上限境界を指定しないTは消去後にObjectになりますが、境界を指定した型変数は左端の境界の消去型になります。主な違いはコンパイル時の型検査です。Objectで受けると、取り出すたびにキャストが要り、別の型が混ざっても検出できません。Tにすると、呼び出し側の型が呼び出しごとに固定され、誤った型の代入がコンパイルエラーになります。
ジェネリクスを使うと実行速度は遅くなりますか?
ジェネリクス自体が追加するのは取り出し時のcheckcast命令程度で、型ごとにクラスが複製されることもありません。速度に効くのはボクシングで、List<Integer>の要素はintの値ではなくIntegerオブジェクトへの参照です(-128〜127の値はキャッシュされた同じオブジェクトを共有することがあります)。数値を大量に扱うならIntStreamやint[]を検討します。