ジョシュア・ブロッホによる効果的なJavaから、
共変とは、XがYのサブタイプである場合、X []もY []のサブタイプであることを意味します。文字列はオブジェクトのサブタイプなので、配列は共変です
String[] is subtype of Object[]
不変式は単に、XがYのサブタイプであるかどうかに関係なく、
List<X> will not be subType of List<Y>.
私の質問は、なぜ配列をJavaで共変にするという決定ですか? なぜ配列は不変ですが、リストは共変なのですか? など、他のSO投稿がありますが、それらはScalaに焦点を当てているようで、私はできませんフォロー。
wikipedia 経由:
JavaとC#の初期のバージョンにはジェネリック(別名、パラメトリックポリモーフィズム)が含まれていませんでした。
このような設定では、配列を不変にすることにより、有用な多態性プログラムが除外されます。たとえば、配列をシャッフルする関数、または要素で
Object.equals
メソッドを使用して2つの配列の等価性をテストする関数を記述することを検討してください。実装は、配列に格納されている要素の正確なタイプに依存しないため、すべてのタイプの配列で機能する単一の関数を記述することが可能です。タイプの関数を実装するのは簡単ですboolean equalArrays (Object[] a1, Object[] a2); void shuffleArray(Object[] a);
ただし、配列型が不変として扱われた場合、
Object[]
型の配列でのみこれらの関数を呼び出すことができます。たとえば、文字列の配列をシャッフルできませんでした。したがって、JavaとC#は両方とも配列型を共変的に扱います。たとえば、C#では
string[]
はobject[]
のサブタイプで、JavaString[]
はObject[]
のサブタイプです。
これは、「配列が共変なのはなぜですか?」、またはより正確には、「なぜwere配列が共変at時間? "
ジェネリックが導入されたとき、それらは Jon Skeetによるこの回答 で指摘されている理由により、意図的に共変されませんでした。
いいえ、
List<Dog>
はList<Animal>
ではありません。List<Animal>
で何ができるかを考えてみてください-猫を含め、動物を追加できます。さて、猫を子犬のくずに論理的に追加できますか?絶対違う。// Illegal code - because otherwise life would be Bad List<Dog> dogs = new List<Dog>(); List<Animal> animals = dogs; // Awooga awooga animals.add(new Cat()); Dog dog = dogs.get(0); // This should be safe, right?
突然、veryの混乱した猫がいます。
wildcards により共分散(および反分散)の表現が可能になったため、ウィキペディアの記事で説明されている配列を共変にする最初の動機はジェネリックには適用されませんでした。
boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
その理由は、実行時にすべての配列が要素タイプを知っているのに対し、ジェネリックコレクションはタイプの消去のためではないからです。
例えば:
String[] strings = new String[2];
Object[] objects = strings; // valid, String[] is Object[]
objects[0] = 12; // error, would cause Java.lang.ArrayStoreException: Java.lang.Integer during runtime
これが汎用コレクションで許可された場合:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings; // let's say it is valid
objects.add(12); // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this
ただし、後でリストにアクセスしようとすると問題が発生します。
String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
パラメトリックタイプの重要な機能は、ポリモーフィックアルゴリズム、つまりArrays.sort()
などのパラメーター値に関係なくデータ構造を操作するアルゴリズムを作成できることです。
ジェネリックでは、ワイルドカードタイプで行われます。
<E extends Comparable<E>> void sort(E[]);
真に役立つために、ワイルドカードタイプにはワイルドカードキャプチャが必要であり、それにはタイプパラメータの概念が必要です。配列がJavaに追加された時点では利用できなかったため、参照型共変の配列を作成することで、ポリモーフィックアルゴリズムを許可するはるかに簡単な方法が許可されました。
void sort(Comparable[]);
ただし、その単純さにより、静的型システムに抜け穴ができました。
String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException
参照型の配列へのすべての書き込みアクセスの実行時チェックが必要です。
一言で言えば、ジェネリックによって具体化された新しいアプローチは、型システムをより複雑にしますが、静的なタイプセーフでもありますが、古いアプローチは単純で、静的なタイプセーフではありません。この言語の設計者は、より単純なアプローチを選択し、ほとんど問題を引き起こさない型システムの小さな抜け穴を塞ぐよりも重要なことをしました。その後、Javaが確立され、差し迫ったニーズに対応したときに、ジェネリックに対して適切に行うためのリソースがありました(ただし、配列用に変更すると、既存のJavaプログラムが破損します) 。
配列は少なくとも2つの理由で共変です。
これは、共変に変わることのない情報を保持するコレクションに役立ちます。 Tのコレクションが共変であるためには、そのバッキングストアも共変でなければなりません。バッキングストアとしてT[]
を使用しない不変のT
コレクションを設計することはできますが(たとえば、ツリーまたはリンクリストを使用して)、このようなコレクションは実行される可能性が低くなります。配列。共変不変コレクションを提供するより良い方法は、バッキングストアを使用できる「共変不変配列」型を定義することであると主張するかもしれませんが、単純に配列共分散を許可する方がおそらく簡単でした。
配列は頻繁にコードによって変更されます。このコードは、配列内にどのような種類のものが入るかはわかりませんが、同じ配列から読み取られなかったものは配列に入れません。この代表的な例は、コードの並べ替えです。概念的には、要素を交換または置換するメソッドを配列タイプに含めることができた可能性があり(このようなメソッドは任意の配列タイプに等しく適用できます)、または配列への参照を保持する「配列マニピュレーター」オブジェクトを定義しますそれから読み取られており、以前に読み取られた項目を元の配列に格納するメソッドを含めることができます。配列が共変でない場合、ユーザーコードはそのような型を定義できませんが、ランタイムには特殊なメソッドが含まれている可能性があります。
配列が共変であるという事実は、見苦しいハックと見なされるかもしれませんが、ほとんどの場合、作業コードの作成を容易にします。
ジェネリックは不変です:from JSL 4.10 :
...サブタイピングはジェネリック型を介して拡張されません:T <:Uは
C<T>
<:C<U>
...を意味しません.
さらに数行、JLSは次のことも説明しています。
配列は共変です(最初の箇条書き):
4.10.3配列型間のサブタイプ化
そもそも、配列を共変にする間違った決定をしたと思います。 here で説明したように型の安全性を壊し、後方互換性のためにそれらに固執し、その後ジェネリックに対して同じ間違いをしないようにしました。そして、それが Joshua Bloch が本「Effective Java(second edition)」の項目25の配列よりもリストを好む理由の1つです。
私の見解:コードが配列A []を予期し、B []をB []に指定する場合、BがAのサブクラスである場合、心配することは2つだけです。配列要素を読み取るとどうなるそれ。したがって、すべての場合でタイプセーフが維持されるように言語規則を記述することは難しくありません(主な規則は、AをB []に固定しようとするとArrayStoreException
がスローされる可能性があることです)。ただし、ジェネリックでは、クラスを宣言するときにSomeClass<T>
、クラスの本体でT
を使用する方法はいくつもありますが、動作するにはあまりにも複雑すぎると思います許可される場合と許可されない場合についてのルールを記述するために、可能なすべての組み合わせを出力します。