クラスDogがクラスAnimalを拡張すると仮定します:この多形ステートメントが許可されない理由:
List<Animal> myList = new ArrayList<Dog>();
ただし、プレーン配列では許可されています。
Animal[] x=new Dog[3];
クラスDogがクラスAnimalを拡張すると仮定します:この多形ステートメントが許可されない理由:
List<Animal> myList = new ArrayList<Dog>();
ただし、プレーン配列では許可されています。
Animal[] x=new Dog[3];
この理由は、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<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);
配列は、2つの重要な点でジェネリック型とは異なります。まず、配列は共変です。この恐ろしい言葉は、SubがSuperのサブタイプである場合、配列型Sub[]がSuper[]のサブタイプであることを意味します。対照的に、ジェネリックスは不変です。Type1とType2の2つの異なるタイプの場合、List<Type1>はList<Type2>のサブタイプでもスーパータイプでもありません。
[..]配列とジェネリックスの2つ目の大きな違いは、配列が具体化されていることです[JLS、4.7]。これは、配列が実行時に要素タイプを認識して適用することを意味します。
[..]対照的に、ジェネリックスは消去によって実装されます[JLS、4.6]。つまり、コンパイル時にのみ型制約を適用し、実行時に要素型情報を破棄(または消去)します。消去は、ジェネリック型がジェネリックを使用しないレガシーコードと自由に相互運用できるようにするものです(項目23)。これらの根本的な違いのために、配列とジェネリックはうまく混ざりません。たとえば、ジェネリック型、パラメーター化された型、または型パラメーターの配列を作成することは違法です。これらの配列作成式はいずれも有効ではありません:new List <E> []、new List <String> []、newE[]。すべての場合、コンパイル時にジェネリック配列の作成エラーが発生します。[..]
PrenticeHall-効果的なJava2ndEdition
それはとても興味深いです。答えはわかりませんが、犬のリストを動物のリストに入れたい場合は、次のように機能します。
List<Animal> myList = new ArrayList<Animal>();
myList.addAll(new ArrayList<Dog>());
コレクションのバージョンをコーディングしてコンパイルする方法は次のとおりです。
List<? extends Animal> myList = new ArrayList<Dog>();
配列でこれを必要としない理由は、型消去によるものです。非プリミティブの配列はすべて正しくObject[]
、Java配列は型付きクラスではありません(コレクションのように)。言語はそれに応えるように設計されたことはありません。
配列とジェネリックは混在しません。
List<Animal> myList = new ArrayList<Dog>();
その場合、猫を犬に入れることができるため、不可能です。
private void example() {
List<Animal> dogs = new ArrayList<Dog>();
addCat(dogs);
// oops, cat in dogs here
}
private void addCat(List<Animal> animals) {
animals.add(new Cat());
}
一方で
List<? extends Animal> myList = new ArrayList<Dog>();
可能ですが、その場合、汎用パラメーターを持つメソッドを使用することはできません(nullのみが受け入れられます)。
private void addCat(List<? extends Animal> animals) {
animals.add(null); // it's ok
animals.add(new Cat()); // compilation error here
}
最終的な答えは、Javaがそのように指定されているため、そのようになっているということです。もっと正確に言えば、それがJava仕様が進化した方法だからです*。
Java設計者の実際の考え方はわかりませんが、次のことを考慮してください。
List<Animal> myList = new ArrayList<Dog>();
myList.add(new Cat()); // compilation error
対
Animal[] x = new Dog[3];
x[0] = new Cat(); // runtime error
ここでスローされるランタイムエラーはですArrayStoreException
。これは、非プリミティブの任意の配列への任意の割り当てでスローされる可能性があります。
上記のような例のために、Javaの配列型の処理が間違っていると主張することができます。
*Java配列の型付けはJava1.0より前に指定されていましたが、ジェネリック型はJava1.5でのみ追加されたことに注意してください。Java言語には、下位互換性の包括的なメタ要件があります。つまり、言語拡張機能は古いコードを壊してはなりません。とりわけ、これは、配列の型指定が機能する方法など、過去の間違いを修正することができないことを意味します。(それが間違いだったと認められたと仮定して...)
ジェネリック型の側では、型消去はコンパイルエラーを説明しません。消去されていないジェネリック型を使用したコンパイル型チェックが原因で、コンパイルエラーが実際に発生しています。
実際、チェックを外した型キャストを使用して(警告を無視して)コンパイルエラーを覆すことができ、実行時ArrayList<Dog>
に実際にCat
オブジェクトが含まれる状況に陥ることがあります。(これは型消去の結果です!)ただし、チェックされていない変換を使用したコンパイルエラーの破壊は、予期しない場所でランタイムエラーを引き起こす可能性があることに注意してください...間違った場合。それが悪い考えである理由です。
ジェネリックスが登場する前の時代には、任意の型の配列を並べ替えることができるルーチンを作成するには、(1)共変形式で読み取り専用配列を作成し、型に依存しない方法で要素を交換または再配置できるか、(2)作成する必要がありました。安全に読み取ることができ、以前に同じ配列から読み取ったものを使用して安全に書き込むことができる、または(3)配列に要素を比較するための型に依存しない手段を提供する、共変方式の読み取り/書き込み配列。共変および反変のジェネリックインターフェイスが最初から言語に含まれていた場合、実行時に型チェックを実行する必要性とそのような型チェックの可能性を回避できたため、最初のアプローチが最適だった可能性があります。失敗する可能性があります。それにもかかわらず、そのような一般的なサポートが存在しなかったので、ありませんでした。