この理由は、Javaがジェネリックを実装する方法に基づいています。
配列の例
配列を使用すると、これを行うことができます(他の人が説明しているように、配列は共変です)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
しかし、これを行おうとするとどうなるでしょうか。
Number[0] = 3.14; //attempt of heap pollution
この最後の行は問題なくコンパイルされますが、このコードを実行すると、を取得できますArrayStoreException
。doubleを整数配列に入れようとしているため(数値参照を介してアクセスされているかどうかに関係なく)。
これは、コンパイラをだますことはできますが、ランタイム型システムをだますことはできないことを意味します。これは、配列が再利用可能な型と呼ばれるものだからです。これは、実行時にJavaが、この配列が実際には整数の配列としてインスタンス化されたことを認識していることを意味します。整数の配列は、型の参照を介してアクセスされるだけNumber[]
です。
ご覧のとおり、1つはオブジェクトの実際のタイプであり、もう1つはオブジェクトへのアクセスに使用する参照のタイプです。
Javaジェネリックの問題
現在、Java汎用型の問題は、型情報がコンパイラーによって破棄され、実行時に使用できないことです。このプロセスは型消去と呼ばれます。このようなジェネリックスをJavaに実装するのには十分な理由がありますが、それは長い話であり、既存のコードとのバイナリ互換性と関係があります。
ただし、ここで重要な点は、実行時に型情報がないため、ヒープ汚染が発生していないことを確認する方法がないことです。
例えば、
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Javaコンパイラがこれを停止しない場合、実行時にこのリストが整数のみのリストであると判断する方法がないため、実行時型システムも停止できません。Javaランタイムでは、整数のみを含める必要がある場合に、必要なものをこのリストに入れることができます。これは、作成時に整数のリストとして宣言されているためです。
そのため、Javaの設計者は、コンパイラーをだますことができないようにしました。コンパイラーをだますことができない場合(配列でできるように)、ランタイム型システムもだますことはできません。
そのため、ジェネリック型は再構築不可能であると言います。
明らかに、これは多型を妨げるでしょう。次の例を考えてみましょう。
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
これで、次のように使用できます。
Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));
ただし、ジェネリックコレクションを使用して同じコードを実装しようとすると、成功しません。
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
しようとするとコンパイラエラーが発生します...
List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error
解決策は、共変性と反変性として知られるJavaジェネリックの2つの強力な機能の使用法を学ぶことです。
共分散
共分散を使用すると、構造からアイテムを読み取ることはできますが、構造に何も書き込むことはできません。これらはすべて有効な宣言です。
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()
そして、あなたはから読むことができますmyNums
:
Number n = myNums.get(0);
実際のリストに含まれているものが何であれ、それを数値にアップキャストできることを確認できるため(数値を拡張するものはすべて数値ですよね?)
ただし、共変構造に何かを入れることは許可されていません。
myNumst.add(45L); //compiler error
Javaは、汎用構造内のオブジェクトの実際のタイプを保証できないため、これは許可されません。Numberを拡張するものであれば何でもかまいませんが、コンパイラーは確信が持てません。したがって、読み取りはできますが、書き込みはできません。
共変性
共変性を使用すると、反対のことができます。物事を一般的な構造に入れることはできますが、そこから読み取ることはできません。
List<Object> myObjs = new List<Object();
myObjs.add("Luke");
myObjs.add("Obi-wan");
List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);
この場合、オブジェクトの実際の性質はオブジェクトのリストであり、基本的にすべての数値が共通の祖先としてオブジェクトを持っているため、反変性によって数値を入れることができます。そのため、すべての数値はオブジェクトであるため、これは有効です。
ただし、数値を取得すると仮定すると、この反変構造から何も安全に読み取ることはできません。
Number myNum = myNums.get(0); //compiler-error
ご覧のとおり、コンパイラがこの行の記述を許可している場合、実行時にClassCastExceptionが発生します。
Get/Putの原則
そのため、構造体からジェネリック値のみを取得する場合は共分散を使用し、構造体にジェネリック値のみを挿入する場合は反変性を使用し、両方を実行する場合は正確なジェネリック型を使用します。
私が持っている最良の例は、あるリストから別のリストにあらゆる種類の番号をコピーする次の例です。ソースからアイテムを取得するだけで、アイテムを運命に置くだけです。
public static void copy(List<? extends Number> source, List<? super Number> destiny) {
for(Number number : source) {
destiny.add(number);
}
}
共変性と反変性の力のおかげで、これは次のような場合に機能します。
List<Integer> myInts = asList(1,2,3,4);
List<Integer> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);