web-dev-qa-db-ja.com

List <Dog>はList <Animal>のサブクラスですか? Javaジェネリックが暗黙的に多相ではないのはなぜですか?

私はJavaジェネリックが継承/多態性をどのように扱うかについて少し混乱しています。

次のような階層を仮定します -

動物(親)

- (子供)

それで、私はメソッドdoSomething(List<Animal> animals)を持っているとしましょう。継承と多態性のすべての規則により、List<Dog>List<Animal>List<Cat>List<Animal> - であると思いますそのため、どちらか一方をこのメソッドに渡すことができます。そうではありません。この振る舞いを実現したい場合は、doSomething(List<? extends Animal> animals)と言って、Animalの任意のサブクラスのリストを受け入れるようにメソッドに明示的に指示する必要があります。

これがJavaの動作であることを私は理解しています。私の質問はなぜですか?多態性が一般的に暗黙的なのはなぜでしょうか。しかし、総称に関してはそれを指定しなければなりません。

705
froadie

いいえ、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>であることを知らないのであなたはCatList<Cat>に追加することができません。値を取得してそれがAnimalになることを知ることはできますが、任意の動物を追加することはできません。その逆はList<? super Animal>に当てはまります - その場合あなたはそれにAnimalを安全に追加することができますが、それがList<Object>であるかもしれないのであなたはそれから検索されるかもしれないものについて何も知らない。

839
Jon Skeet

探しているものは共変型 parametersと呼ばれます。これは、メソッド内であるタイプのオブジェクトを別のタイプに置き換えることができる場合(たとえば、AnimalDogに置き換えることができる場合)、それらのオブジェクトを使用する式にも同じことが適用される(つまりList<Animal>List<Dog>に置き換えることができます。問題は、一般に可変リストでは共分散が安全ではないことです。 List<Dog>があり、List<Animal>として使用されているとします。実際にList<Animal>であるこのList<Dog>に猫を追加しようとするとどうなりますか?型パラメーターの共変を自動的に許可すると、型システムが壊れます。

型パラメーターを共変として指定できる構文を追加すると便利です。これにより、メソッド宣言で? extends Fooが回避されますが、複雑さが増します。

74

List<Dog>List<Animal>ではないのは、たとえば、CatList<Animal>に挿入することはできますが、List<Dog>には挿入できないためです。可能な場合は、ワイルドカードを使用して総称をより拡張可能にできます。例えば、List<Dog>からの読み取りは、List<Animal>からの読み取りと似ていますが、書き込みはしません。

Java言語の ジェネリック およびJavaチュートリアルの ジェネリックに関するセクション いくつかの事柄がなぜ多態的であるのか、またはジェネリックスで許可されていないのかについて、非常に詳細な説明を持ってください。

43

他のが答えるもの に追加されるべきだと思う点はしながら

JavaではList<Dog>List<Animal>ではありません

それはまた本当である

犬のリストは - 英語での動物のリストです(まあ、合理的な解釈の下で)

OPの直感がどのように機能するか(もちろん完全に有効です)は、後者の文です。しかし、この直感を適用すると、その型システムではJava固有ではない言語が得られます。この言語で、犬のリストに猫を追加できるとします。どういう意味ですか?それは、そのリストが犬のリストではなくなり、動物のリストにとどまることを意味します。そして哺乳類のリスト、そして四足動物のリスト。

別の言い方をすると、JavaのList<Dog>は英語の「犬のリスト」を意味するのではなく、「犬を飼うことができるリスト、それ以外に何もない」という意味です。

より一般的には、OPの直観は、オブジェクトに対する操作がそれらの型を変更できる言語に向いています。むしろ、オブジェクトの型はその値の(動的)関数です。 。

33
einpoklum

ジェネリックスの全体的な点は、それが許可されていないということです。そのタイプの共分散を可能にする配列の状況を考えてみます。

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

そのコードは正常にコンパイルされますが、実行時エラー(2行目のJava.lang.ArrayStoreException: Java.lang.Boolean)がスローされます。安全ではありません。 Genericsのポイントは、コンパイル時型の安全性を追加することです。それ以外の場合は、総称なしで単純なクラスをそのまま使用できます。

今、あなたはもっと柔軟になる必要がある時があり、それが? super Class? extends Classのためのものです。前者は(例えば)型Collectionに挿入する必要があるときで、後者は型安全な方法でそれから読む必要があるときです。しかし、両方を同時に行う唯一の方法は特定のタイプを持つことです。

32
Yishai

問題を理解するためには、配列と比較するのが便利です。

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());
8
outdev

そのような振る舞いの基本的な論理は、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との互換性を与えるために型消去が存在すると思います....

5
Hitesh

ここで与えられた答えは私を完全に納得させませんでした。その代わりに、私は別の例を作ります。

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

いいですね。しかしConsumersにはSuppliersとAnimalsしか渡すことができません。もしあなたが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 - > DogCatなどで、MammalLifeコンシューマに入れることもできますが、StringLifeコンシューマに入れることはできません。

4
glglgl

実際にはあなたが望むものを達成するためにインターフェースを使うことができます。

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());
3
Angel Koh

リスト項目がそのスーパータイプのサブクラスであることが確実な場合は、次の方法でリストをキャストできます。

(List<Animal>) (List<?>) dogs

これは、リストをコンストラクタに渡したり、それを繰り返したりしたい場合に便利です。

1
sagits

サブタイピングは、パラメータ化された型では 不変式 です。クラスDogAnimalのサブタイプであるとしても、パラメータ化された型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
Root G

答え と他の答えは正しいです。私はそれらの答えに私が役に立つと思う解決策を追加するつもりです。私はこれがプログラミングでしばしば起こると思います。注目すべき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();

}

}

1
dan b

問題はよく識別されています。しかし解決策はあります。 makedoSomethingジェネリック:

<T extends Animal> void doSomething<List<T> animals) {
}

これで、List <Dog>、List <Cat>、またはList <Animal>のいずれかでdoSomethingを呼び出すことができます。

0
gerardw

また、コンパイラがジェネリッククラスをどのように脅かしているかについても考慮する必要があります。ジェネリック引数を埋めるたびに、異なる型を "インスタンス化"します。

したがって、ListOfAnimalListOfDogListOfCatなどがあります。これらは、一般的な引数を指定したときにコンパイラによって「作成」される個別のクラスです。そしてこれはフラットな階層です(実際にはListに関してはまったく階層ではありません)。

一般的なクラスの場合に共分散が意味をなさないもう一つの議論は、基本的にすべてのクラスが同じであるという事実です - Listインスタンスです。総称引数を埋めてListを特殊化してもクラスは拡張されず、単にその特定の総称引数に対して機能するようになります。

0
Cristik

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つの選択肢がありました。

  1. 現在発生しているように、サブタイプが暗黙のうちにスーパータイプであるとは考えず、コンパイルエラーを出します。

  2. サブタイプをスーパータイプと見なし、コンパイル時に「add」メソッドを制限します(したがって、drawAllメソッドで、形状のサブタイプである円のリストが渡される場合、コンパイラはそれを検出し、コンパイルエラーを制限しますそれ)。

明白な理由で、それが最初の方法を選びました。

0
aurelius

別の解決策は新しいリストを作ることです

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
0
ejaenv