Generic型の配列を作るとgeneric array creationやunchecked conversionが出る
結論
配列はアレなので使ってはいけない
JavaのGenericsとは
JavaのGenericsでは、型パラメーターは最終的にバイトコードになったら消えてObject
になってしまって、Generic型からその型パラメーターで示された型を実際に使う側がキャストして使うというものなんですね。だからこのようなコードに対して:
import java.util.List;
import java.util.ArrayList;
class Main {
public static void main(String[] args) {
List<Integer> l = new ArrayList<Integer>();
l.add(1);
Integer a = l.get(0);
}
}
このようなバイトコードが出力されます:
Compiled from "Main.java"
class Main extends java.lang.Object{
Main();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2; //class java/util/ArrayList
3: dup
4: invokespecial #3; //Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #4; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #5, 2; //InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: aload_1
20: iconst_0
21: invokeinterface #6, 2; //InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
26: checkcast #7; //class java/lang/Integer
29: astore_2
30: return
}
ArrayList
の型パラメーターが消えていて、要素を取り出すときに checkcast
でキャストしてますね。
総称配列を作るときのエラー・警告
例えば次のようなコードはgeneric array createionエラーになります:
ArrayList<Integer>[] list = new ArrayList<Integer>[10];
これは配列をアロケートするときに、バイトコードの中で型情報が必要になるのに、ArrayList<Integer>
は(バイトコードレベルでは)実際は存在せず、ArrayList
が存在しているからです。
なので次のようなコードを書きます:
ArrayList<Integer>[] list = new ArrayList[10];
ArrayList<Integer>[] list = new ArrayList<?>[10];
しかし今度はunchecked conversionの警告が起きます。Javaが型チェックをするときは、 ArrayList
と ArrayList<Integer>
は違う型ですから。
追記
generic array creationがなぜ禁止なのかきちんと理解してなかった。
Generic型の配列が作れると仮定しましょう。次のコードはエラー・警告なしでコンパイル出来ます:
ArrayList[] list = new ArrayList<Integer>[10];
ArrayList<String> stringArray = new ArrayList<String>();
stringArray.add("Some String");
list[0] = stringArray;
ArrayList<Integer> intArray = list[0];
Integer intObj = intArray.get(0); // ClassCastException occur!
一方でGeneric型以外の配列の場合は、次のようになります:
Number[] list = new Integer[10];
list[0] = Double.valueOf(1); // ArrayStoreException occur!
Generic型以外の配列の場合、型の違うオブジェクトの参照を配列に代入しようとすると、 ArrayStoreException
が発生しますが、Generic型の配列の場合はその例外は発生せずに、値を取り出したときに ClassCastException
が発生するようになります。これがGeneric型の配列が作れない理由のようです。
また、総称配列が作れなくても、上のコードは次のように書き換えると、unchecked conversion warningが出るけれども、 ClassCastException
が発生するように実行することが出来ます:
ArrayList[] list = new ArrayList[10];
ArrayList<String> stringArray = new ArrayList<String>();
stringArray.add("Some String");
list[0] = stringArray;
ArrayList<Integer> intArray = list[0]; // unchecked conversion warning
Integer intObj = intArray.get(0); // ClassCastException occur!
次のように書き換えると、今度はきちんと list[0] = stringArray
のところで型エラーを起こしてくれます:
ArrayList<Integer>[] list = new ArrayList[10]; // unchecked conversion warning
ArrayList<String> stringArray = new ArrayList<String>();
stringArray.add("Some String");
list[0] = stringArray; // Incompatible types error
ArrayList<Integer> intArray = list[0];
Integer intObj = intArray.get(0);
配列は使ってはいけない
総称配列のエラー・警告はJavaレベルの型とJVMレベルの型が一致していないことが原因に見えますが、実は配列を使わなければこのような問題は回避されるようです。(僕はGenericsによってJavaとJVMの型が一致しなくなったこともなんかアレな気がするんですが)
また、配列がcovariantなのもまた変なエラーの原因となるので、配列さえ使わなければ……というのがかなり前から言われているようです。