配列は共変であるのにジェネリックは不変なのはなぜですか?


160

Joshua BlochのEffective Javaから、

  1. 配列は、2つの重要な点でジェネリック型と異なります。最初の配列は共変です。ジェネリックスは不変です。
  2. 共変とは、XがYのサブタイプの場合、X []もY []のサブタイプになることを意味します。文字列はオブジェクトのサブタイプなので、配列は共変です

    String[] is subtype of Object[]

    不変とは、XがYのサブタイプであるかどうかに関係なく、

     List<X> will not be subType of List<Y>.

私の質問は、なぜJavaで配列を共変にする決定をしたのですか?なぜ配列は不変なのにリストは共変なのかなどの他のSOの投稿があります。、しかしそれらはScalaに焦点を合わせているようで、私はついていけません。


1
ジェネリックが後で追加されたからではないですか?
Sotirios Delimanolis 2013

1
配列とコレクションの比較は不公平だと思います。コレクションはバックグラウンドで配列を使用します!!
Ahmed Adel Ismail

4
@ EL-conteDe-monteTereBentikhたとえば、すべてのコレクションとは限りませんLinkedList
Paul Bellora 2013

@PaulBellora私はマップがコレクションの実装者とは異なることを知っていますが、コレクションは一般的に配列に依存していることをSCPJ6で読みました!!
Ahmed Adel Ismail

ArrayStoreExceptionがないためです。as配列のコレクションに間違った要素を挿入したとき。コレクションは検索時にのみこれを見つけることができ、キャストのためにそれも同じです。したがって、ジェネリックスはこの問題を確実に解決します。
Kanagavelu Sugumar 16年

回答:


150

ウィキペディア経由:

JavaおよびC#の初期のバージョンにはジェネリック(別名パラメトリックポリモーフィズム)が含まれていませんでした。

このような設定では、配列を不変にすると、有用な多態性プログラムが除外されます。たとえば、配列をシャッフルする関数、またはObject.equals要素のメソッドを使用して2つの配列が等しいかどうかをテストする関数を書くことを検討してください。実装は、配列に格納されている要素の正確なタイプに依存しないため、すべてのタイプの配列で機能する単一の関数を記述できるはずです。タイプの機能を実装するのは簡単です

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

ただし、配列型が不変として扱われた場合、これらの関数を呼び出すことができるのは、その型の配列に対してのみObject[]です。たとえば、文字列の配列をシャッフルすることはできません。

したがって、JavaとC#はどちらも配列型を共変的に扱います。たとえば、C#string[]ではのサブタイプでobject[]あり、Java String[]ではのサブタイプですObject[]

これは質問より正確に「なぜ?配列は共変である」、またはに答え、「なぜだったの配列は共変をした時に?」

ジェネリックが導入されたとき、Jon Skeetこの回答で指摘した理由により、ジェネリックは意図的に共変にされませんでした:

いいえ、a 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?

突然あなたは非常に混乱した猫を飼っています。

ウィキペディアの記事で説明されている配列を共変にする元の動機はジェネリックには当てはまりませんでした。これは、ワイルドカードによって共分散(および反変)の表現が可能になったためです。次に例を示します。

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);

3
はい、配列はポリモーフィックな動作を許可しますが、(ジェネリックのコンパイル時の例外とは異なり)ランタイム例外を導入します。例:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn 2013

21
そのとおりです。配列には再構成可能な型がありArrayStoreException、必要に応じてをスローします。明らかにこれは当時、価値のある妥協案と考えられていました。今日とは対照的に、多くの人は、アレイの共分散を振り返ってみると、誤りと見なしています。
ポールベローラ2013

1
「多くの」がそれを間違いと見なすのはなぜですか?配列の共分散がない場合よりもはるかに便利です。ArrayStoreExceptionをどのくらいの頻度で見ましたか。彼らは非常にまれです。ここでの皮肉なのは、許されないimoです... Javaでこれまでに発生した非常に最悪のミスの1つは、使用サイトの差異、つまりワイルドカードです。
スコット

3
@ScottMcKinney:「なぜ「多くの」それを間違いと見なすのですか?」AIUI、これは、配列の共分散がすべての配列割り当て操作で動的な型テストを必要とするためです(ただし、コンパイラーの最適化はおそらく役立つかもしれません)。これにより、実行時のオーバーヘッドが大幅に増加する可能性があります。
Dominique Devriese、2015年

おかげで、ドミニク、しかし私の観察から、それが間違いであると「多くの」理由が、他のいくつかの人が言ったことのオウムの線に沿っているのではないかと思われます。繰り返しになりますが、配列の共分散を再検討すると、損傷を与えるよりもはるかに便利です。繰り返しになりますが、Javaが実際に犯した大きな間違いは、ワイルドカードによる使用サイトの一般的な差異でした。「多くの」が認めたいと思うよりも多くの問題を引き起こしています。
スコット

30

その理由は、実行時にすべての配列がその要素の型を知っているのに対し、ジェネリックコレクションは型の消去のためにそうではないからです。

例えば:

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

配列が共変である理由についてのコメントであるPaul Belloraの答えの方が適切だと思います。配列を不変にした場合、それで問題ありません。あなたはそれでタイプ消去をするでしょう。型消去プロパティの主な理由は、下位互換性のためですか?
eagertoLearn 2013

@ user2708477はい、型の消去は下位互換性のために導入されました。そして、はい、私の答えはタイトルの質問に答えようとします、なぜジェネリックは不変ですか。
Katona

配列がその型を知っているという事実は、共分散により、コードが配列に収まらない配列に何かを格納するように要求できることを意味します-これは、そのような格納が許可されることを意味しません。その結果、配列を共変にすることによってもたらされる危険のレベルは、型を知らなかった場合よりもはるかに低くなります。
スーパーキャット2013

@supercat、正解、私が指摘したいのは、型の消去が行われているジェネリックスの場合、ランタイムチェックの最小限の安全性では共分散が実装できなかったということです
Katona

1
私は個人的に、この答えが、コレクションができなかったのになぜ配列が共変であるかについての正しい説明を提供すると思います。ありがとう!
asgs

22

この助けになるかもしれません:-

ジェネリックは共変ではありません

Java言語の配列は共変です。つまり、IntegerがNumberを拡張する場合(そうする場合)、IntegerもNumberであるだけでなく、Integer []もでありNumber[]、自由に渡したり割り当てたりできます。Integer[]ここでa Number[]が呼び出されます。(より正式には、NumberがIntegerのスーパータイプである場合、Number[]はスーパータイプですInteger[]。)ジェネリック型についても同様であると考えるかもしれません-これList<Number>はのスーパータイプであり、a が期待される場所List<Integer>を渡すことができます。残念ながら、そのようには動作しません。List<Integer>List<Number>

それがそのように機能しないことには十分な理由があることがわかります:それはジェネリックジェネリックが提供することになっていたタイプセーフティを壊すでしょう。あなたが割り当てることができます想像List<Integer>しますList<Number>。次に、次のコードを使用すると、Integerではないものをに入れることができますList<Integer>

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

lnはなのでList<Number>、Floatを追加することは完全に合法であるようです。しかし、lnがでエイリアスされている場合li、liの定義に暗黙的に含まれる型安全性の約束を破ります。これは、整数のリストであるため、ジェネリック型を共変にすることはできません。


3
配列の場合ArrayStoreException、実行時に取得します。
Sotirios Delimanolis 2013

4
私の質問はWHY、アレイが共変になったことです。Sotiriosが述べたように、配列を使用すると、実行時にArrayStoreExceptionが発生します。配列を不変にすると、コンパイル時にこのエラーを検出できますか?
eagertoLearn 2013

@eagertoLearn:Javaの主要なセマンティックの弱点の1つは、その型システムではAnimal、「他の場所から受け取ったアイテムを受け入れる必要のない、の派生物のみを保持する配列」を「を含むだけの配列」から区別できないことですAnimal。外部から提供されるへの参照を受け入れる用意がある必要があります。前者を必要とするコードAnimalはの配列を受け入れるCat必要がありますが、後者を必要とするコードは受け入れてはなりません。コンパイラが2つのタイプを区別できる場合は、コンパイル時のチェックを提供できます。残念ながら、それらを区別する唯一のものは...
スーパーキャット'16

...コードが実際に何かを格納しようとするかどうかであり、実行時までそれを知る方法はありません。
スーパーキャット2014年

3

配列は少なくとも2つの理由で共変です。

  • 共変に変化しない情報を保持するコレクションに役立ちます。Tのコレクションが共変であるためには、そのバッキングストアも共変でなければなりません。をバッキングストアとしてT使用しない不変のコレクションを設計することはできますがT[](たとえば、ツリーまたはリンクリストを使用して)、そのようなコレクションは、配列によってサポートされるコレクションと同様に実行される可能性は低くなります。共変不変コレクションを提供するより良い方法は、バッキングストアを使用できる「共変不変配列」タイプを定義することであると主張する人もいるでしょうが、単に配列共分散を許可する方がおそらく簡単でした。

  • 配列は、どのようなタイプのものになるかを知らないコードによって頻繁に変更されますが、同じ配列から読み取られなかったものは配列に入れません。これの主な例は、ソートコードです。概念的には、配列タイプに要素を交換または置換するメソッドを含める(そのようなメソッドは任意の配列タイプに等しく適用可能である)か、配列および1つ以上のものへの参照を保持する「配列マニピュレータ」オブジェクトを定義することが可能だったかもしれませんそれはそこから読み取られたものであり、以前に読み取られた項目をそれらが来た配列に格納するメソッドを含めることができます。配列が共変でない場合、ユーザーコードはそのような型を定義できませんが、ランタイムにいくつかの特殊なメソッドが含まれている可能性があります。

配列が共変であるという事実は醜いハックと見なされるかもしれませんが、ほとんどの場合、動作するコードの作成を容易にします。


1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.-良い点
eagertoLearn 2013

3

パラメトリックタイプの重要な機能は、ポリモーフィックアルゴリズム、つまりなどのパラメーター値に関係なくデータ構造を操作するアルゴリズムを作成できること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

ジェネリックは不変です:JSL 4.10以降

...サブタイピングはジェネリック型を介して拡張されません:T <:UはC<T><:C<U>...

さらに数行、JLSは
配列が共変であることも説明しています(最初の箇条書き)。

4.10.3配列型間のサブタイピング

ここに画像の説明を入力してください


2

配列共変を作る最初の場所で彼らは間違った決定をしたと思います。ここで説明したように、型の安全性が損なわれ、下位互換性のためにそれが行き詰まり、その後、ジェネリックに対して同じミスを犯さないように努めました。そして、それがJoshua Blochが本「Effective Java(second edition)」のアイテム25にあるリストよりもリストを優先する理由の1つです。


Josh Blockは、Javaのコレクションフレームワーク(1.2)の作者であり、Javaのジェネリック(1.5)の作者でもありました。だから、誰もが不満を言うジェネリックを作った人は、偶然にも本を書いた人が彼らがより良い方法だと言っているのですか?大きな驚きではありません!
cpurdy

1

私の見解:コードが配列A []を期待していて、それにB []を指定した場合、BはAのサブクラスです。心配しなければならないことが2つだけあります。それ。したがって、すべてのケースで型の安全性が維持されるようにする言語規則を書くことは難しくありません(主な規則はArrayStoreException、AをB []に挿入しようとするとスローされるということです)。ただし、ジェネリックの場合、クラスを宣言するとき、クラスの本文で使用するSomeClass<T>方法Tはいくつでもあり得ます。また、すべての可能な組み合わせを計算して、物事は許可され、許可されていない場合。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.