私はJavaジェネリックが継承/多態性をどのように扱うかについて少し混乱しています。
次のような階層を仮定します -
動物(親)
犬 - 猫(子供)
それで、私はメソッドdoSomething(List<Animal> animals)
を持っているとしましょう。継承と多態性のすべての規則により、List<Dog>
はList<Animal>
、List<Cat>
はList<Animal>
- であると思いますそのため、どちらか一方をこのメソッドに渡すことができます。そうではありません。この振る舞いを実現したい場合は、doSomething(List<? extends Animal> animals)
と言って、Animalの任意のサブクラスのリストを受け入れるようにメソッドに明示的に指示する必要があります。
これがJavaの動作であることを私は理解しています。私の質問はなぜですか?多態性が一般的に暗黙的なのはなぜでしょうか。しかし、総称に関してはそれを指定しなければなりません。
いいえ、List<Dog>
はList<Animal>
ではありません。あなたがList<Animal>
で何ができるかを考えてください - あなたはそれに猫を含む...任意の動物を追加することができます。さて、あなたは論理的に子犬のごみに猫を追加することはできますか?絶対違う。
// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?
突然あなたはとても混乱した猫を飼っています。
さて、あなたはそれがList<? extends Animal>
であることを知らないのであなたはCat
をList<Cat>
に追加することができません。値を取得してそれがAnimal
になることを知ることはできますが、任意の動物を追加することはできません。その逆はList<? super Animal>
に当てはまります - その場合あなたはそれにAnimal
を安全に追加することができますが、それがList<Object>
であるかもしれないのであなたはそれから検索されるかもしれないものについて何も知らない。
探しているものは共変型 parametersと呼ばれます。これは、メソッド内であるタイプのオブジェクトを別のタイプに置き換えることができる場合(たとえば、Animal
をDog
に置き換えることができる場合)、それらのオブジェクトを使用する式にも同じことが適用される(つまりList<Animal>
はList<Dog>
に置き換えることができます。問題は、一般に可変リストでは共分散が安全ではないことです。 List<Dog>
があり、List<Animal>
として使用されているとします。実際にList<Animal>
であるこのList<Dog>
に猫を追加しようとするとどうなりますか?型パラメーターの共変を自動的に許可すると、型システムが壊れます。
型パラメーターを共変として指定できる構文を追加すると便利です。これにより、メソッド宣言で? extends Foo
が回避されますが、複雑さが増します。
List<Dog>
がList<Animal>
ではないのは、たとえば、Cat
をList<Animal>
に挿入することはできますが、List<Dog>
には挿入できないためです。可能な場合は、ワイルドカードを使用して総称をより拡張可能にできます。例えば、List<Dog>
からの読み取りは、List<Animal>
からの読み取りと似ていますが、書き込みはしません。
Java言語の ジェネリック およびJavaチュートリアルの ジェネリックに関するセクション いくつかの事柄がなぜ多態的であるのか、またはジェネリックスで許可されていないのかについて、非常に詳細な説明を持ってください。
Javaでは
List<Dog>
はList<Animal>
ではありません
それはまた本当である
犬のリストは - 英語での動物のリストです(まあ、合理的な解釈の下で)
OPの直感がどのように機能するか(もちろん完全に有効です)は、後者の文です。しかし、この直感を適用すると、その型システムではJava固有ではない言語が得られます。この言語で、犬のリストに猫を追加できるとします。どういう意味ですか?それは、そのリストが犬のリストではなくなり、動物のリストにとどまることを意味します。そして哺乳類のリスト、そして四足動物のリスト。
別の言い方をすると、JavaのList<Dog>
は英語の「犬のリスト」を意味するのではなく、「犬を飼うことができるリスト、それ以外に何もない」という意味です。
より一般的には、OPの直観は、オブジェクトに対する操作がそれらの型を変更できる言語に向いています。むしろ、オブジェクトの型はその値の(動的)関数です。 。
ジェネリックスの全体的な点は、それが許可されていないということです。そのタイプの共分散を可能にする配列の状況を考えてみます。
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
そのコードは正常にコンパイルされますが、実行時エラー(2行目のJava.lang.ArrayStoreException: Java.lang.Boolean
)がスローされます。安全ではありません。 Genericsのポイントは、コンパイル時型の安全性を追加することです。それ以外の場合は、総称なしで単純なクラスをそのまま使用できます。
今、あなたはもっと柔軟になる必要がある時があり、それが? super Class
と? extends Class
のためのものです。前者は(例えば)型Collection
に挿入する必要があるときで、後者は型安全な方法でそれから読む必要があるときです。しかし、両方を同時に行う唯一の方法は特定のタイプを持つことです。
問題を理解するためには、配列と比較するのが便利です。
List<Dog>
はnotList<Animal>
のサブクラスです。
しかしDog[]
はAnimal[]
のサブクラスです。
配列は 変更可能 で、共変です。
Reifiable はそれらの型情報が実行時に完全に利用可能であることを意味します。
したがって、配列はランタイム型の安全性を提供しますが、コンパイル時型の安全性は提供しません。
// All compiles but throws ArrayStoreException at runtime at last line
Dog[] dogs = new Dog[10];
Animal[] animals = dogs; // compiles
animals[0] = new Cat(); // throws ArrayStoreException at runtime
それはジェネリックの逆です:
総称は 消去され 、不変式はです。
したがって、ジェネリックはランタイム型の安全性を提供できませんが、コンパイル時型の安全性を提供します。
以下のコードで、ジェネリックが共変であれば、3行目で ヒープ汚染 を作ることが可能です。
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());
そのような振る舞いの基本的な論理は、Generics
が消去タイプのメカニズムに従うということです。そのため、そのような消去プロセスがないcollection
とは異なり、実行時にはarrays
のタイプを識別しても意味がありません。だからあなたの質問に戻ってきて...
以下のような方法があるとします。
add(List<Animal>){
//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}
JavaがこのメソッドにAnimalタイプのListの追加を許可している場合は、コレクションに間違ったものを追加する可能性があり、実行時にもタイプ消去のために実行されます。配列の場合は、そのようなシナリオでは実行時例外が発生します。
したがって、本質的にこの動作は、誤ったものをコレクションに追加できないように実装されています。今私はジェネリックせずにレガシーJavaとの互換性を与えるために型消去が存在すると思います....
ここで与えられた答えは私を完全に納得させませんでした。その代わりに、私は別の例を作ります。
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
consumer.accept(supplier.get());
}
いいですね。しかしConsumer
sにはSupplier
sとAnimal
sしか渡すことができません。もしあなたがMammal
消費者だがDuck
供給者がいるならば、両方とも動物ですが、それらは合いません。これを許可しないために、追加の制限が追加されました。
上記の代わりに、使用するタイプ間の関係を定義する必要があります。
E.
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
私たちが消費者のために正しいタイプの物を私たちに提供する供給業者だけを使うことができることを確実にします。
OTOH、私たちもできる
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
consumer.accept(supplier.get());
}
別の方法では、Supplier
の型を定義し、それをConsumer
に入れることができるように制限します。
できます
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
ここで、直観的な関係Life
- > Animal
- > Mammal
- > Dog
、Cat
などで、Mammal
をLife
コンシューマに入れることもできますが、String
をLife
コンシューマに入れることはできません。
実際にはあなたが望むものを達成するためにインターフェースを使うことができます。
public interface Animal {
String getName();
String getVoice();
}
public class Dog implements Animal{
@Override
String getName(){return "Dog";}
@Override
String getVoice(){return "woof!";}
}
あなたはその後、コレクションを使用することができます
List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
リスト項目がそのスーパータイプのサブクラスであることが確実な場合は、次の方法でリストをキャストできます。
(List<Animal>) (List<?>) dogs
これは、リストをコンストラクタに渡したり、それを繰り返したりしたい場合に便利です。
サブタイピングは、パラメータ化された型では 不変式 です。クラスDog
がAnimal
のサブタイプであるとしても、パラメータ化された型List<Dog>
はList<Animal>
のサブタイプではありません。対照的に、 共変 サブタイプは配列で使用されるので、配列型Dog[]
はAnimal[]
のサブタイプです。
不変サブタイプ化により、Javaによって強制される型制約に違反しません。 @Jon Skeetによって与えられた以下のコードを考えてください。
List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);
@ Jon Skeetが述べたように、このコードは違法です。さもなければ犬が予期したときに猫を返すことによって型制約に違反するからです。
上記を配列の類似のコードと比較することは有益です。
Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];
コードは合法です。ただし、 配列ストア例外 がスローされます。配列は実行時にその型を保持するため、JVMは共変サブタイピングの型安全性を強化できます。
これをさらに理解するために、以下のクラスのjavap
によって生成されたバイトコードを見てみましょう:
import Java.util.ArrayList;
import Java.util.List;
public class Demonstration {
public void normal() {
List normal = new ArrayList(1);
normal.add("lorem ipsum");
}
public void parameterized() {
List<String> parameterized = new ArrayList<>(1);
parameterized.add("lorem ipsum");
}
}
これは、コマンドjavap -c Demonstration
を使用して、次のJavaバイトコードを表示します。
Compiled from "Demonstration.Java"
public class Demonstration {
public Demonstration();
Code:
0: aload_0
1: invokespecial #1 // Method Java/lang/Object."<init>":()V
4: return
public void normal();
Code:
0: new #2 // class Java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method Java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod Java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
public void parameterized();
Code:
0: new #2 // class Java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method Java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod Java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
}
メソッド本体の翻訳済みコードが同一であることを確認してください。コンパイラは各パラメータ化された型をその 消去 に置き換えました。このプロパティは後方互換性を損なわないという意味で非常に重要です。
結論として、コンパイラは各パラメータ化された型をその消去によって置き換えるので、パラメータ化された型に対して実行時の安全性は不可能です。これにより、パラメータ化された型は構文糖にすぎません。
答え と他の答えは正しいです。私はそれらの答えに私が役に立つと思う解決策を追加するつもりです。私はこれがプログラミングでしばしば起こると思います。注目すべき1つのことは、コレクション(リスト、セットなど)にとっての主な問題はコレクションへの追加です。それが物事が崩壊するところです。取り外してもOKです。
ほとんどの場合、Collection<? extends T>
ではなくCollection<T>
を使用できます。これが最初の選択肢です。しかし、そうするのが容易ではないケースを見つけています。それが常に最善の方法であるかどうかについては議論の余地があります。ここでは、Collection<? extends T>
をCollection<T>
に変換することができるDownCastCollectionクラスを紹介します(List、Set、NavigableSetなどにも同様のクラスを定義できます)。これは標準的な方法では非常に不便です。以下はその使用方法の例です(この場合はCollection<? extends Object>
も使用できますが、DownCastCollectionを使用して説明するのは簡単にしておきます)。
/**Could use Collection<? extends Object> and that is the better choice.
* But I am doing this to illustrate how to use DownCastCollection. **/
public static void print(Collection<Object> col){
for(Object obj : col){
System.out.println(obj);
}
}
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c"));
print(new DownCastCollection<Object>(list));
}
今クラス:
import Java.util.AbstractCollection;
import Java.util.Collection;
import Java.util.Iterator;
import Java.util.NoSuchElementException;
public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;
public DownCastCollection(Collection<? extends E> delegate) {
super();
this.delegate = delegate;
}
@Override
public int size() {
return delegate ==null ? 0 : delegate.size();
}
@Override
public boolean isEmpty() {
return delegate==null || delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
if(isEmpty()) return false;
return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
Iterator<? extends E> delegateIterator;
protected MyIterator() {
super();
this.delegateIterator = delegate == null ? null :delegate.iterator();
}
@Override
public boolean hasNext() {
return delegateIterator != null && delegateIterator.hasNext();
}
@Override
public E next() {
if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
return delegateIterator.next();
}
@Override
public void remove() {
delegateIterator.remove();
}
}
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
@Override
public boolean add(E e) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
if(delegate == null) return false;
return delegate.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
if(delegate==null) return false;
return delegate.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.retainAll(c);
}
@Override
public void clear() {
if(delegate == null) return;
delegate.clear();
}
}
問題はよく識別されています。しかし解決策はあります。 makedoSomethingジェネリック:
<T extends Animal> void doSomething<List<T> animals) {
}
これで、List <Dog>、List <Cat>、またはList <Animal>のいずれかでdoSomethingを呼び出すことができます。
また、コンパイラがジェネリッククラスをどのように脅かしているかについても考慮する必要があります。ジェネリック引数を埋めるたびに、異なる型を "インスタンス化"します。
したがって、ListOfAnimal
、ListOfDog
、ListOfCat
などがあります。これらは、一般的な引数を指定したときにコンパイラによって「作成」される個別のクラスです。そしてこれはフラットな階層です(実際にはList
に関してはまったく階層ではありません)。
一般的なクラスの場合に共分散が意味をなさないもう一つの議論は、基本的にすべてのクラスが同じであるという事実です - List
インスタンスです。総称引数を埋めてList
を特殊化してもクラスは拡張されず、単にその特定の総称引数に対して機能するようになります。
JavaSE チュートリアル の例を見てみましょう。
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
それで、犬(円)のリストが暗黙のうちに動物(形)のリストとみなされるべきではないのは、このような状況のためです:
// drawAll method call
drawAll(circleList);
public void drawAll(List<Shape> shapes) {
shapes.add(new Rectangle());
}
そのため、Javaの「設計者」には、この問題に対処する2つの選択肢がありました。
現在発生しているように、サブタイプが暗黙のうちにスーパータイプであるとは考えず、コンパイルエラーを出します。
サブタイプをスーパータイプと見なし、コンパイル時に「add」メソッドを制限します(したがって、drawAllメソッドで、形状のサブタイプである円のリストが渡される場合、コンパイラはそれを検出し、コンパイルエラーを制限しますそれ)。
明白な理由で、それが最初の方法を選びました。
別の解決策は新しいリストを作ることです
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());