読者です 読者をやめる 読者になる 読者になる

型パラメーターへのキャストは絶対に行ってはいけない

Java

通常、ジェネリクスで型指定されたコレクションには指定された型のインスタンスしか格納されない。

例えば以下のコードで表される list には Integer クラスのインスタンスしか格納されない。

List<Integer> list = new ArrayList<Integer>();

上記の list に対して以下のコードを書くとコンパイルエラーとなる。

list.add( "not a number" );

しかし、キャストを行うことでコンパイルエラーは回避できる。

list.add( (Integer)"not a number" );


このキャストでコンパイルエラーが回避できる性質を利用すると、ジェネリクスを用いた型安全性は脆くも崩壊する。
上記コード例では Integer へキャストを行っていたが、この Integer を型パラメーターにし、型パラメーターへのキャストを行うことでジェネリクスを用いた型安全性は完全に崩壊する。

以下、型安全を崩壊させるコード例。

public class Sample {
    public static void main(String[] args) {
        List<Integer> list = getList();
        System.out.println(list);
    }
    
    public static <T> List<T> getList()
    {
        List<T> list = new ArrayList<T>();
        list.add((T)"string");
        list.add((T)Integer.valueOf(1));
        list.add((T)new Object());
        return list;
    }
}

このコードはコンパイルエラーは発生しない。実行時も例外は発生しない。
実行結果は以下のようになる。list には Integer クラスのインスタンスしか格納されないはずだが、文字列や Object クラスのインスタンスも格納されている。

[string, 1, java.lang.Object@10b30a7]


なぜこのようなことが起こるのか?
それは、Javaジェネリクスで用いられる型パラメーターの実態は単なる Object として扱われるからだ。Objectとして扱うため、キャストは常に成功する。メソッドで値の受け渡しをする際、List と List のような具体的な型が指定されているものはコンパイル時のチェックで弾かれるが、List を含んだ場合はチェックが行われない。

結果として、型パラメーターへのキャストを行うことで、実行時に型パラメーターの指定を行ったものの全く無関係なインスタンスが型パラメーターのチェックをすり抜けて利用されてしまう。つまりジェネリクスが存在しない頃の List や Set を用いるのと同等になる。

余談として、C++のテンプレートは Java のように似非ジェネリクスではなく具体的な型を指定したコードを生成するためこのような現象は起きない。Java のように型チェックをサボっていないのだ。


ジェネリクスが意味を成さないため、以下のコードはエラーを吐き出す。list には Integer 以外のオブジェクトも格納されているため、キャストに失敗するからだ。

    public static void main(String[] args) {
        List<Integer> list = getList();
        for(Integer i : list)
        	System.out.println(i);
    }

//実行結果
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
	at sample.Sample.main(Sample.java:9)

以上の事から、型パラメーターへのキャストは絶対に行ってはいけない。