Java | Java 詳細・モダン文法:ジェネリクス – ジェネリクスの制約

Java Java
スポンサーリンク

「ジェネリクスの制約」を一言でいうと

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

理由は、ジェネリクスの型パラメータは「参照型(クラスやインターフェース)」しか取れないからです。

intdouble は「プリミティブ型」であって、「参照型」ではありません。

Java のジェネリクスはクラスレベルで動いていて、
List<T> の T には、必ずクラス(String, Integer, User など)が入る設計になっています。

プリミティブを扱いたいときは、IntegerDouble などのラッパークラスを使い、
必要であればオートボクシング/アンボクシングに任せる、という割り切りになります。


制約その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>[])は作れない

といった代表的な制約があります。

これらはバラバラなようでいて、ほとんどが「型消去」と「配列の実行時チェック」の組み合わせから来ています。

タイトルとURLをコピーしました