私はなぜそれをしてはいけないのかを知っています。しかし、なぜこれが不可能なのかを素人に説明する方法はありますか。これは素人にも簡単に説明できますAnimal animal = new Dog();
。犬は動物の一種ですが、犬のリストは動物のリストではありません。
13 に答える
Dogsのリストを作成するとします。次にこれをList<Animal>として宣言し、同僚に渡します。彼は、不合理ではありませんが、猫を入れることができると信じています。
その後、彼はそれをあなたに返します。これで、Dogs のリストが作成され、その中にCatが含まれています。カオスが起こります。
この制限は、リストの可変性によるものであることに注意することが重要です。たとえば Scala では、DogsのリストがAnimalsのリストであることを宣言できます。これは、Scala のリストが (デフォルトで) 不変であるため、 DogsのリストにCatを追加すると、 Animalsの新しいリストが得られるためです。
あなたが探している答えは、共分散と反分散と呼ばれる概念を扱うことです。一部の言語はこれらをサポートしていますが (たとえば、.NET 4 ではサポートが追加されています)、基本的な問題のいくつかは次のようなコードで示されています。
List<Animal> animals = new List<Dog>();
animals.Add(myDog); // works fine - this is a list of Dogs
animals.Add(myCat); // would compile fine if this were allowed, but would crash!
Cat は animal から派生するため、コンパイル時のチェックにより、List に追加できることが示されます。ただし、実行時に、Cat を Dogs のリストに追加することはできません。
したがって、直感的には単純に見えるかもしれませんが、これらの問題は実際には非常に複雑です。
.NET 4 の co/contravariance に関する MSDN の概要は次のとおりです。 Java のサポートがどのようなものかわかりません。
私ができる最良の素人の答えはこれです:ジェネリックを設計する際に、Javaの配列型システムに対して行われたのと同じ決定を繰り返したくないため、それを unsafe にしました。
これは配列で可能です:
Object[] objArray = new String[] { "Hello!" };
objArray[0] = new Object();
このコードは、配列の型システムが Java で機能する方法により、問題なくコンパイルされます。ArrayStoreException
実行時に が発生します。
ジェネリックに対してこのような安全でない動作を許可しないという決定が下されました。
他の場所も参照してください: Java Arrays Break Type Safety、多くの人がJava Design Flawsの 1 つと見なしています。
List<Animal>は、猫やタコなどの動物を挿入できるオブジェクトです。ArrayList<Dog>はそうではありません。
あなたがやろうとしていることは次のとおりです。
List<? extends Animal> animals = new ArrayList<Dog>()
それはうまくいくはずです。
最も簡単な答えは、猫と犬を無視することだと思います。それらは関係ありません。重要なのはリスト自体です。
List<Dog>
と
List<Animal>
犬は動物に由来するさまざまなタイプであり、これにはまったく関係がありません。
このステートメントは無効です
List<Animal> dogs = new List<Dog>();
同じ理由でこれは
AnimalList dogs = new DogList();
DogはAnimalから継承できますが、によって生成されたリストクラスは
List<Animal>
によって生成されたリストクラスから継承しません
List<Dog>
2つのクラスが関連しているため、それらをジェネリックパラメーターとして使用すると、それらのジェネリッククラスも関連するようになると考えるのは誤りです。あなたは確かに犬を追加することができますが
List<Animal>
それはそれを意味するものではありません
List<Dog>
のサブクラスです
List<Animal>
これができるとします。a を渡された人List<Animal>
ができると合理的に期待することの 1 つは、それに a を追加するGiraffe
ことです。誰かが を に追加しようとするとどうGiraffe
なりanimals
ますか? 実行時エラー?それは、コンパイル時の型付けの目的を無効にしているように思われます。
持っている場合は注意してください
List<Dog> dogs = new ArrayList<Dog>()
それなら、できれば
List<Animal> animals = dogs;
これはにはなりません。動物の基礎となるデータ構造は依然として であるため、 を に挿入しようとすると、実際には動作しない に挿入されます (象は明らかに大きすぎます ;-)。dogs
List<Animal>
ArrayList<Dog>
Elephant
animals
ArrayList<Dog>
リストを変更できなかった場合、あなたの推論は完全に健全です。残念ながら、 a List<>
は命令的に操作されます。つまりList<Animal>
、新しいを追加することで変更できますAnimal
。List<Dog>
を aとして使用することが許可されているList<Animal>
場合、 も含むリストになる可能性がありますCat
。
List<>
が (Scala のように) 変更できない場合、 AList<Dog>
を として扱うことができますList<Animal>
。たとえば、C# では、共変および反変のジェネリック型引数を使用してこの動作が可能になります。
これは、より一般的なリスコフ置換プリンシパルのインスタンスです。
ここで突然変異が問題を引き起こすという事実は、他の場所で発生します。Square
タイプとを検討してくださいRectangle
。
ですか?Square
_ Rectangle
確かに -- 数学的な観点から。
読み取り可能なプロパティRectangle
を提供するクラスを定義できます。getWidth
getHeight
これらのプロパティに基づいて、area
またはを計算するメソッドを追加することもできます。perimeter
次に、両方Square
をサブクラス化して作成し、同じ値を返すクラスを定義できます。Rectangle
getWidth
getHeight
setWidth
しかし、 orを介して変更を許可し始めるとどうなるsetHeight
でしょうか?
現在、Square
は の適切なサブクラスではありませんRectangle
。これらのプロパティの 1 つを変更すると、不変条件を維持するためにもう一方のプロパティを静かに変更する必要があり、Liskov の代入プリンシパルに違反します。a の幅を変更するSquare
と、予期しない副作用が発生します。正方形のままにするには、高さも変更する必要がありますが、幅だけを変更するように要求しました。
Square
a を使用できたときはいつでも、 your を使用することはできませんRectangle
。したがって、突然変異が存在する場合、 aは!でSquare
はありません。Rectangle
Rectangle
新しい幅または新しい高さで四角形を複製する方法を知っている新しいメソッドを作成すると、複製プロセス中にSquare
安全に に移ることができますが、元の値を変更することはもうありません。Rectangle
同様に、そのインターフェイスが新しいアイテムをリストに追加List<Dog>
できるようにする場合、 を にすることはできません。List<Animal>
まず、動物界を定義しましょう。
interface Animal {
}
class Dog implements Animal{
Integer dogTag() {
return 0;
}
}
class Doberman extends Dog {
}
2 つのパラメーター化されたインターフェイスを考えてみましょう。
interface Container<T> {
T get();
}
interface Comparator<T> {
int compare(T a, T b);
}
そして、これらの実装T
は ですDog
。
class DogContainer implements Container<Dog> {
private Dog dog;
public Dog get() {
dog = new Dog();
return dog;
}
}
class DogComparator implements Comparator<Dog> {
public int compare(Dog a, Dog b) {
return a.dogTag().compareTo(b.dogTag());
}
}
あなたが求めていることは、このContainer
インターフェースのコンテキストでは非常に合理的です:
Container<Dog> kennel = new DogContainer();
// Invalid Java because of invariance.
// Container<Animal> zoo = new DogContainer();
// But we can annotate the type argument in the type of zoo to make
// to make it co-variant.
Container<? extends Animal> zoo = new DogContainer();
では、なぜ Java はこれを自動的に行わないのでしょうか? これが にとって何を意味するかを考えてみましょうComparator
。
Comparator<Dog> dogComp = new DogComparator();
// Invalid Java, and nonsensical -- we couldn't use our DogComparator to compare cats!
// Comparator<Animal> animalComp = new DogComparator();
// Invalid Java, because Comparator is invariant in T
// Comparator<Doberman> dobermanComp = new DogComparator();
// So we introduce a contra-variance annotation on the type of dobermanComp.
Comparator<? super Doberman> dobermanComp = new DogComparator();
Java が に自動的Container<Dog>
に割り当てられると、 aが に割り当てられることContainer<Animal>
も予想されますが、これは意味がありません。2 つの Cat を比較するにはどうすればよいでしょうか。Comparator<Dog>
Comparator<Animal>
Comparator<Dog>
Container
とはどう違いComparator
ますか?コンテナは type の値を生成しT
ますが、それらComparator
を消費します。これらは、型パラメーターの共変および反変の使用法に対応します。
型パラメーターが両方の位置で使用され、インターフェイスがinvariantになる場合があります。
interface Adder<T> {
T plus(T a, T b);
}
Adder<Integer> addInt = new Adder<Integer>() {
public Integer plus(Integer a, Integer b) {
return a + b;
}
};
Adder<? extends Object> aObj = addInt;
// Obscure compile error, because it there Adder is not usable
// unless T is invariant.
//aObj.plus(new Object(), new Object());
後方互換性の理由から、Java のデフォルトはinvarianceです。変数、フィールド、パラメーター、またはメソッドの戻り値の型と一緒に、? extends X
またはその型に対して、適切な分散を明示的に選択する必要があります。? super X
これは本当に面倒です。ジェネリック型を使用するたびに、この決定を下す必要があります。Container
確かにとの作者はComparator
これをきっぱりと宣言できるはずです。
これは「Declaration Site Variance」と呼ばれ、Scala で利用できます。
trait Container[+T] { ... }
trait Comparator[-T] { ... }
これは、ジェネリック型が不変であるためです。
継承することで、実際にはいくつかのクラスに共通の型を作成しています。ここに、一般的な動物タイプがあります。Animal のタイプで配列を作成し、同様のタイプの値を保持することで使用しています(継承されたタイプの dog 、 cat など)。
例えば:
dim animalobj as new List(Animal)
animalobj(0)=new dog()
animalobj(1)=new Cat()
.......
とった?