「ジェネリクスの制約」を一言でいうと
Java のジェネリクスには、「こう書けたら便利そうだけど、仕様上あえて禁止されていること」 がいくつもあります。
new T() が書けないT[](ジェネリック配列)が素直には作れないList<String> に対して instanceof ができないList<int> のようにプリミティブ型は使えない
ジェネリクスの型パラメータを static フィールドには使えない
などです。
これらは全部バラバラの不便に見えますが、根っこにあるのは
「Java のジェネリクスが“型消去”という仕組みで実装されている」ことです。
ここから、一つずつ丁寧に噛みくだいていきます。
制約その1:プリミティブ型(int, double など)は使えない
なぜ List<int> は書けないのか
Java でジェネリクスを使うとき、こういうコードはコンパイルエラーになります。
List<int> list = new ArrayList<>(); // コンパイルエラー
Java代わりに、ラッパークラスを使う必要があります。
List<Integer> list = new ArrayList<>();
Java理由は、ジェネリクスの型パラメータは「参照型(クラスやインターフェース)」しか取れないからです。
int や double は「プリミティブ型」であって、「参照型」ではありません。
Java のジェネリクスはクラスレベルで動いていて、List<T> の T には、必ずクラス(String, Integer, User など)が入る設計になっています。
プリミティブを扱いたいときは、Integer や Double などのラッパークラスを使い、
必要であればオートボクシング/アンボクシングに任せる、という割り切りになります。
制約その2:new T() や new T[] が書けない
なぜ new T() はダメなのか
次のようなコードはコンパイルエラーになります。
public class Box<T> {
public T create() {
return new T(); // コンパイルエラー
}
}
Java「T のインスタンスを作りたいだけなのに、なんでダメ?」と思いますよね。
理由は、「型消去」のせいです。
コンパイル時には T が何か分かっていても、
実行時には Box<String> も Box<Integer> も、どちらも単なる Box として動きます。
つまり、実行時には「T が何のクラスか」が分かりません。
new T() を実行しようとしたとき、
実行時に「どのコンストラクタを呼ぶべきか」が決められないため、この構文自体が禁止されています。
T のインスタンスを作りたい場合は、
コンストラクタやファクトリを外から渡す、といった工夫が必要になります。
例えばこうです。
public class Box<T> {
private final java.util.function.Supplier<T> supplier;
public Box(java.util.function.Supplier<T> supplier) {
this.supplier = supplier;
}
public T create() {
return supplier.get(); // supplier が実際の new を知っている
}
}
Java使う側はこう書きます。
Box<String> box = new Box<>(() -> new String("hello"));
String s = box.create();
Java「T 自体は何か知らないけど、“T を作る方法” を外から渡してもらう」
という発想です。
なぜ new T[](ジェネリック配列)もダメなのか
同じ理由で、次のようなコードもコンパイルエラーになります。
public class Holder<T> {
private T[] array;
public Holder(int size) {
array = new T[size]; // コンパイルエラー
}
}
Java配列は実行時にも「要素型」を覚えていて、
間違った型を入れようとすると ArrayStoreException を投げます。
しかし、型消去された世界では「実行時に T が何か分からない」ので、
「T 型の配列」を作ることができません。
よく使われる回避策としては、
@SuppressWarnings("unchecked")
public Holder(int size) {
array = (T[]) new Object[size];
}
Javaのように、「Object 配列を作って、T[] にキャストする」という技があります。
ただし、これはコンパイラから警告が出るような“危険な技”なので、
初心者のうちは、そもそも「配列ではなく List などのコレクションを使う」設計に寄せた方が安全です。
制約その3:instanceof でパラメータ化された型はチェックできない
なぜ instanceof List<String> が書けないのか
次のコードはコンパイルエラーになります。
Object obj = new ArrayList<String>();
if (obj instanceof List<String>) { // コンパイルエラー
}
Java正しくは、こうです。
if (obj instanceof List) { // OK
}
Java理由はやはり、「実行時には List<String> も List<Integer> も同じ List としてしか存在しない」からです。
instanceof は実行時に型をチェックする構文なので、
「実行時に残っていない型情報(型パラメータ)を条件にする」のは無理があります。
「少なくとも List かどうか」なら分かるので、instanceof List は許されています。
「中身が String の List か?」は、実行時には判断できない。
これは「型消去」を採用したことの直接的な制約です。
制約その4:static 文脈で型パラメータは使えない
なぜ static T value; が書けないのか
例えば、次のようなクラスはコンパイルエラーになります。
public class Box<T> {
private T value;
private static T staticValue; // コンパイルエラー
}
Java理由は、「型パラメータ T は“インスタンスごと”に違う可能性がある」のに、static は「クラス全体に一つだけ」の領域だからです。
Box<String> と Box<Integer> があったとして、static T staticValue に対して、
String を入れたいのか、Integer を入れたいのか、
クラスレベルでは決めようがありません。
実行時には T が消えてしまうことも相まって、
「static フィールドや static メソッドのシグネチャに、クラスの T を直接使う」ことはできません。
static メソッドでジェネリクスを使いたい場合は、
そのメソッド自身に型パラメータを宣言します。
public class Util {
public static <T> T identity(T value) {
return value;
}
}
Javaクラスの <T> ではなく、
メソッドの <T> として宣言するイメージです。
制約その5:ジェネリック型の配列は基本的に禁止されている
なぜ List<String>[] がダメなのか
次のような宣言もコンパイルエラーになります。
List<String>[] array = new List<String>[10]; // コンパイルエラー
Java型消去と配列の性質を組み合わせると、とても危険なことが起き得るからです。
もしこれが許されてしまうと、こんなコードが書けます(あくまで「仮に」の話)。
List<String>[] array = new List[10];
Object[] objArray = array; // 配列は共変なのでコンパイル上は通る仮定
objArray[0] = List.of(123); // 実は List<Integer> を突っ込めてしまう仮定
String s = array[0].get(0); // 中身は Integer なのに…
Java実体は「List<String> の配列」だと思っている場所に、
「List<Integer> を突っ込めてしまう」状況が生まれます。
しかし、実行時には List<String> と List<Integer> の区別が付かない(型消去)ので、
配列のように「不正な代入を実行時に検知して防ぐ」こともできません。
この矛盾を防ぐために、
「ジェネリック型(パラメータ化された型)の配列生成は、言語仕様として禁止」
という強い制約を入れています。
実務的には、
配列ではなく List<List<String>> のようなコレクションを使う
どうしても配列が必要なら、List<?>[] のように raw に近い形+キャストで頑張る(非推奨寄り)
といった方向に寄せていくことになります。
制約その6:型引数は実行時には分からない(再掲)
いわゆる「reified(再ified)ではない」
Kotlin や Scala など、
一部の言語には「型引数が実行時にも分かる」(reified 型パラメータ)という機能があります。
Java のジェネリクスはそうではありません。
型パラメータはコンパイル時にだけ存在し、実行時には消えます(型消去)。
その結果、例えばこういうことはできません。
new ArrayList<T>() の T に基づいて、実行時のクラス情報を取得するT.class と書いてクラスオブジェクトを直接取る
どうしてもクラスが必要なときは、
「型パラメータとは別に Class<T> を引数でもらう」という形になります。
public <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
Java呼び出し側はこうです。
String s = createInstance(String.class);
Java「T の型そのもの」は実行時には消えてしまうので、
どうしても必要な場合は、Class<T> という形で別途渡してやる必要がある、ということです。
制約を味方にする考え方
「なんでできないの?」を「じゃあどう設計する?」に変える
ジェネリクスの制約は、一見ただの不便に見えます。
new T() が書けない
配列とジェネリクスの相性が悪い
static と型パラメータが素直に組み合わさらない
でも、これらは全部「型消去」「後方互換性」「実行時コストを増やさない」といった設計方針の副作用です。
重要なのは、
「Java のジェネリクスは“コンパイル時に型安全性を高める仕組み”であり、実行時には基本的に“普通のクラスとキャスト”に還元される」
という本質を理解した上で、
ここは配列ではなく List を使おう
T のインスタンス生成は外からファクトリとして渡してもらおう
ここは静的なユーティリティだから、クラスジェネリクスではなくジェネリクスメソッドにしよう
といった設計に落とし直していくことです。
まとめ:ジェネリクスの制約を自分の中でこう整理する
Java のジェネリクスには、
プリミティブ型は使えない(List<int> はダメ、List<Integer> を使う)new T() や new T[] は書けない(型消去のため)instanceof List<String> は書けない(実行時に型パラメータ情報がない)
クラスの T を static フィールドに使えない
ジェネリック型の配列(List<String>[])は作れない
といった代表的な制約があります。
これらはバラバラなようでいて、ほとんどが「型消去」と「配列の実行時チェック」の組み合わせから来ています。

