コンパイル時警告に注意

http://aoking.hatenablog.jp/entry/20100427/1272326219での主張は

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;
    }
}

といったように型変数Tにキャストを行った場合に「このコードはコンパイルエラーは発生しない。実行時も例外は発生しない。」ゆえに「型パラメーターへのキャストは絶対に行ってはいけない。」というものですが、解説がいくらか間違っているので指摘しておきます。

キャスト時のコンパイラの挙動

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

に対して、以下のようにキャストを行うことでコンパイルエラーが回避できるように書かれていますが、これは明確な誤りです。

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

実際にコンパイルを行ってみれば分かりますが

エラー: 変換できない型
list.add( (Integer)"not a number" );
^
期待値: Integer
検出値: String

となります。

list.add((T)"string");

についても

注意:blog\Sample.javaの操作は、未チェックまたは安全ではありません。
注意:詳細は、-Xlint:uncheckedオプションを指定して再コンパイルしてください。

と警告されることが分かるでしょう。-Xlint:uncheckedオプションを指定して詳細を見てみましょう。

Sample.java:18: 警告:[unchecked] 無検査キャスト
list.add( (T)"string");
^
期待値: T
検出値: String
Tが型変数の場合:
メソッド getList()で宣言されているT extends Object
Sample.java:19: 警告:[unchecked] 無検査キャスト
list.add( (T)Integer.valueOf(1));
^
期待値: T
検出値: Integer
Tが型変数の場合:
メソッド getList()で宣言されているT extends Object
Sample.java:20: 警告:[unchecked] 無検査キャスト
list.add( (T)new Object());
^
期待値: T
検出値: Object
Tが型変数の場合:
メソッド getList()で宣言されているT extends Object
警告3個

このように、Javaコンパイラは危険箇所を把握した上で、この場合の挙動をどうするかをプログラマに委ねています。

その上で、プログラマによって@SuppressWarnings("unchecked")が指定されていれば、プログラマの責任によってこれを無視することができます。

境界についての補足

Javaジェネリクスで用いられる型パラメーターの実態は単なる Object として扱われるからだ。Objectとして扱うため、キャストは常に成功する。

この記述は、境界の指定のない場合は概ね正しいと言えます。上限境界の指定されている場合、上限境界の型として扱われるためキャストに失敗する場合があります。
例えばTの境界にStringを設定した場合

    public static <T extends String> 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;
    }

なお、Object型はStringの親クラスであるため、コンパイル時のキャストは通過しますが実行時にキャストエラーとなります。型を明示することでダウンキャストが行えるのはC#同等ですね。

java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String

Javaジェネリクスを扱う場合の心構え

Javaジェネリクス周りのコンパイルコンパイルエラーとはならず、警告となる部分が多くあります。「それだと型安全にならないよ」と警告されているのを無視しながら「型安全じゃない!」と叫ぶのは制止を振りきって失敗をしているようなものです。ちゃんとコンパイラの声を聞きましょう。

  • 警告を無視しない
  • 警告を@SuppressWarnings("unchecked")で黙殺しない

元エントリの主張はある意味では正しいわけですが、警告を無視した場合の問題事例を個別に指摘しても埒が明かないので、根本的には「警告を無視すんな」とまとめるのがより妥当だろうと思います。

なお、EclipseのようなIDEではコンパイラのオプション設定でこれらの警告をエラー扱いにすることもできます。活用しましょう。

JavaジェネリクスはJava5から追加実装されたものですが、ソースコード後方互換性を持つためにraw型を導入しました。これにより「本当は型安全ではないキャスト」を記述できるようになっていますが、こうしたコードはコンパイル警告が出るようになっています。

raw型や安全ではないキャストを用いると警告されるようになっています。なので、上記の「警告を無視しない」ためにより具体的に記述すると

  • raw型を使わない
  • 型変数の安全ではないキャストを用いない

とする必要があります。

JavaジェネリクスC#にはないワイルドカードの機能をもっていますが、この機能を用いた場合の型安全性については非常に複雑です。それでも厳密な型安全性を求めてGenerics Hellを彷徨うか、適当なところで諦めて@SuppressWarnings("unchecked")するかはJavaではプログラマの責任となっています。

それでは楽しいジェネリクスライフを。