it-swarm.com.de

Ist List <Dog> eine Unterklasse von List <Animal>? Warum sind Java-Generics nicht implizit polymorph?

Ich bin etwas verwirrt darüber, wie Java-Generics Vererbung/Polymorphie handhaben.

Nehmen Sie die folgende Hierarchie an -

Tier (Elternteil)

Hund - Katze (Kinder)

Nehmen wir an, ich habe eine Methode doSomething(List<Animal> animals). Nach allen Regeln der Vererbung und des Polymorphismus würde ich davon ausgehen, dass ein List<Dog> ein List<Animal> ist und ein List<Cat> ein List<Animal> ist - und somit einer dieser Methoden übergeben werden könnte. Nicht so. Wenn ich dieses Verhalten erreichen möchte, muss ich der Methode explizit mitteilen, dass sie eine Liste von Unterklassen von Animal akzeptiert, indem sie doSomething(List<? extends Animal> animals) sagt. 

Ich verstehe, dass dies Javas Verhalten ist. Meine Frage ist warum? Warum ist Polymorphismus generell implizit, aber wenn es um Generika geht, muss dies angegeben werden?

672
froadie

Nein, ein List<Dog> ist nicht ein List<Animal>. Überlegen Sie, was Sie mit einem List<Animal> tun können - Sie können any animal hinzufügen ... einschließlich einer Katze. Können Sie einem Wurf von Welpen logischerweise eine Katze hinzufügen? Absolut nicht.

// 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?

Plötzlich haben Sie eine sehr verwirrte Katze.

Nun können Sie nicht eine Cat zu einem List<? extends Animal> hinzufügen, da Sie nicht wissen, dass es sich um einen List<Cat> handelt. Sie können einen Wert abrufen und wissen, dass es sich um eine Animal handelt, Sie können jedoch keine beliebigen Tiere hinzufügen. Umgekehrt gilt für List<? super Animal> - in diesem Fall können Sie sicher eine Animal hinzufügen, wissen jedoch nicht, was davon abgerufen werden kann, da es sich um einen List<Object> handeln könnte.

817
Jon Skeet

Was Sie suchen, heißt covariant type parameters. Das bedeutet, wenn ein Objekttyp in einer Methode durch einen anderen ersetzt werden kann (beispielsweise Animal kann durch Dog ersetzt werden), gilt dies auch für Ausdrücke, die diese Objekte verwenden (also List<Animal> könnte durch List<Dog> ersetzt werden). Das Problem ist, dass Kovarianz im Allgemeinen für veränderliche Listen nicht sicher ist. Angenommen, Sie haben einen List<Dog> und dieser wird als List<Animal> verwendet. Was passiert, wenn Sie versuchen, diesem List<Animal> eine Katze hinzuzufügen, die wirklich ein List<Dog> ist? Das automatische Zulassen von Typparametern als Kovarianz unterbricht das Typsystem.

Es wäre nützlich, eine Syntax hinzuzufügen, um die Angabe von Typparametern als Kovariante zu ermöglichen, wodurch ? extends Foo in Methodendeklarationen vermieden wird, dies erhöht jedoch die Komplexität.

72

Der Grund, warum ein List<Dog> kein List<Animal> ist, besteht darin, dass Sie zum Beispiel eine Cat in einen List<Animal> einfügen können, aber nicht in einen List<Dog>.... Das Lesen aus einem List<Dog> ähnelt beispielsweise dem Lesen aus einem List<Animal> - aber nicht dem Schreiben.

Die Generics in der Java-Sprache und der Abschnitt über Generics aus den Java-Tutorials haben eine sehr gute und ausführliche Erklärung, warum einige Dinge polymorph sind oder nicht und mit Generics zulässig sind.

42

Ich würde sagen, der Sinn von Generics ist, dass das nicht erlaubt ist. Betrachten Sie die Situation mit Arrays, die diese Art von Kovarianz zulassen:

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

Dieser Code lässt sich gut kompilieren, wirft jedoch einen Laufzeitfehler ab (Java.lang.ArrayStoreException: Java.lang.Boolean in der zweiten Zeile). Es ist nicht typsicher. Bei Generics geht es darum, die Kompilierungszeit zu erhöhen. Andernfalls könnten Sie einfach eine Klasse ohne Generics verwenden.

Jetzt gibt es Zeiten, in denen Sie flexibler sein müssen und dafür sind ? super Class und ? extends Class gedacht. Ersteres ist, wenn Sie in einen Typ Collection (zum Beispiel) einfügen müssen, und letzteres ist für den Typ, in dem Sie auf eine sichere Art lesen möchten. Der einzige Weg, beide gleichzeitig auszuführen, besteht darin, einen bestimmten Typ zu haben.

32
Yishai

Ein Punkt, den ich denke, sollte zu dem hinzugefügt werden, was andereAntworten erwähnen, dass dabei

List<Dog> ist kein List<Animal>in Java

es stimmt auch das

Eine Liste von Hunden ist eine Liste von Tieren auf Englisch (gut, unter einer angemessenen Interpretation)

Wie die Intuition des OP funktioniert - was natürlich völlig gilt - ist der letzte Satz. Wenn wir diese Intuition anwenden, erhalten wir jedoch eine Sprache, die in ihrem Typensystem nicht Java-ähnlich ist: Angenommen, unsere Sprache erlaubt es, eine Katze in unsere Liste der Hunde aufzunehmen. Was würde das heißen? Es würde bedeuten, dass die Liste keine Liste von Hunden mehr ist, sondern lediglich eine Liste von Tieren bleibt. Und eine Liste von Säugetieren und eine Liste von Vierecken.

Anders ausgedrückt: Ein List<Dog> in Java bedeutet nicht "eine Liste von Hunden" im Englischen, sondern "eine Liste, die Hunde haben kann, und nichts anderes".

Allgemeiner gesagt, die Intuition von OP eignet sich für eine Sprache, in der Operationen an Objekten ihren Typ ändern können, oder vielmehr sind die Objekttypen eine (dynamische) Funktion ihres Wertes. 

30
einpoklum

Um das Problem zu verstehen, ist es nützlich, einen Vergleich mit Arrays vorzunehmen.

List<Dog> ist not Unterklasse von List<Animal>.
AberDog[]ist Unterklasse von Animal[].

Arrays sind reifiable und covariant
Reifiable bedeutet, dass deren Typinformationen zur Laufzeit vollständig verfügbar sind. 
Daher bieten Arrays Laufzeitsicherheit, nicht aber Kompilierzeitsicherheit.

    // 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

Für Generika ist es umgekehrt:
Generics sind gelöscht und invariant
Daher können Generics keine Sicherheit für die Laufzeit bieten, sie bieten jedoch Sicherheit für die Kompilierungszeit. 
Wenn im folgenden Code Generika kovariant waren, ist es möglich, Heap-Verschmutzung in Zeile 3 zu erstellen.

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());
8
outdev

Die hier gegebenen Antworten haben mich nicht voll überzeugt. Also mache ich stattdessen ein anderes Beispiel.

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

klingt gut, nicht wahr? Sie können jedoch nur Consumers und Suppliers für Animals übergeben. Wenn Sie einen Mammal Verbraucher, aber einen Duck Lieferanten haben, sollten diese nicht passen, obwohl beide Tiere sind. Um dies zu verhindern, wurden zusätzliche Einschränkungen hinzugefügt.

Stattdessen müssen wir Beziehungen zwischen den von uns verwendeten Typen definieren.

Z.B.,

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

stellt sicher, dass wir nur einen Lieferanten einsetzen können, der uns den richtigen Objekttyp für den Verbraucher zur Verfügung stellt.

OTOH, wir könnten es auch tun

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

wohin wir in die andere Richtung gehen: Wir definieren den Typ des Supplier und beschränken, dass er in das Consumer eingefügt werden kann.

Wir können es sogar tun

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

wobei wir mit den intuitiven Beziehungen Life -> Animal -> Mammal -> Dog, Cat usw. sogar setzen könnten ein Mammal in einen Life Verbraucher, aber kein String in einen Life Verbraucher.

4
glglgl

Die Basislogik für ein solches Verhalten ist, dass Generics einem Mechanismus zum Löschen von Typen folgt. Zur Laufzeit haben Sie also keine Möglichkeit, den Typ von collection zu identifizieren, im Gegensatz zu arrays, wo es keinen solchen Löschvorgang gibt. Kommen wir also auf Ihre Frage zurück ...

Nehmen wir an, es gibt eine Methode, die unten angegeben ist:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

Wenn nun der Aufrufer Java die Möglichkeit bietet, dieser Methode eine Liste des Typs Animal hinzuzufügen, können Sie falsche Elemente in die Sammlung aufnehmen, und zur Laufzeit wird sie auch aufgrund von Typlöschung ausgeführt. Im Falle von Arrays erhalten Sie für solche Szenarien eine Laufzeitausnahme ...

Daher ist dieses Verhalten im Wesentlichen so implementiert, dass man der Sammlung nichts Falsches hinzufügen kann. Jetzt glaube ich, dass das Löschen von Typen vorhanden ist, um die Kompatibilität mit älterem Java ohne Generics zu gewährleisten.

4
Hitesh

Eigentlich können Sie eine Schnittstelle verwenden, um das zu erreichen, was Sie möchten.

public interface Animal {
    String getName();
    String getVoice();
}
public class Dog implements Animal{
    @Override 
    String getName(){return "Dog";}
    @Override
    String getVoice(){return "woof!";}

}

sie können die Sammlungen dann verwenden

List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
3
Angel Koh

Wenn Sie sicher sind, dass die Listenelemente Unterklassen dieses bestimmten Supertyps sind, können Sie die Liste mit diesem Ansatz umsetzen:

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

Dies ist nützlich, wenn Sie die Liste in einem Konstruktor übergeben oder iterieren möchten

1
sagits

Subtyping ist invariant für parametrisierte Typen. Auch wenn die Klasse Dog ein Subtyp von Animal ist, ist der parametrisierte Typ List<Dog> kein Subtyp von List<Animal>. Im Gegensatz dazu wird covariant subtyping von Arrays verwendet, daher ist das Array Type Dog[] ein Subtyp von Animal[].

Invariante Untertypen stellen sicher, dass die von Java erzwungenen Typeinschränkungen nicht verletzt werden. Beachten Sie den folgenden Code von @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);

Wie von @Jon Skeet festgestellt, ist dieser Code illegal, da er sonst die Typeinschränkungen verletzen würde, wenn er eine Katze zurückgibt, wenn ein Hund erwartet.

Es ist aufschlussreich, den obigen mit analogem Code für Arrays zu vergleichen.

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

Der Code ist legal. Löst jedoch eine Array-Speicherausnahme aus ..__ Ein Array trägt seinen Typ zur Laufzeit auf diese Weise, dass die JVM die Sicherheit des Kovarianten-Subtyps erzwingt. 

Um dies besser zu verstehen, betrachten wir den von javap der folgenden Klasse generierten Bytecode:

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");
    }
}

Mit dem Befehl javap -c Demonstration wird folgender Java-Bytecode angezeigt:

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
}

Beachten Sie, dass der übersetzte Code der Methodenkörper identisch ist. Der Compiler hat jeden parametrisierten Typ durch sein erasure ersetzt. Diese Eigenschaft ist entscheidend, was bedeutet, dass die Abwärtskompatibilität nicht beeinträchtigt wurde.

Zusammenfassend ist die Laufzeitsicherheit für parametrisierte Typen nicht möglich, da der Compiler jeden parametrisierten Typ durch seine Löschung ersetzt. Dadurch sind parametrisierte Typen nichts anderes als syntaktischer Zucker.

1
Root G

Die Antwort sowie andere Antworten sind korrekt. Ich werde diese Antworten mit einer Lösung ergänzen, die meiner Meinung nach hilfreich sein wird. Ich denke, das kommt oft beim Programmieren auf. Zu beachten ist, dass bei Sammlungen (Listen, Sets usw.) das Hauptproblem der Sammlung hinzugefügt wird. Hier bricht alles zusammen. Selbst das Entfernen ist in Ordnung. 

In den meisten Fällen können wir Collection<? extends T> anstelle von Collection<T> verwenden, und das sollte die erste Wahl sein. Ich finde jedoch Fälle, in denen das nicht einfach ist. Es ist an der Debatte, ob dies immer das Beste ist. Ich präsentiere hier eine Klasse DownCastCollection, mit der ein Collection<? extends T> in einen Collection<T> konvertiert werden kann (wir können ähnliche Klassen für List, Set, NavigableSet, .. definieren), die verwendet werden, wenn der Standardansatz sehr unpraktisch ist. Nachfolgend finden Sie ein Beispiel für die Verwendung (in diesem Fall könnten wir auch Collection<? extends Object> verwenden. Die Verwendung von DownCastCollection ist jedoch einfach zu veranschaulichen.).

/**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));
}

Nun die Klasse:

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

Das Problem wurde gut erkannt. Aber es gibt eine Lösung. make doSomething generic:

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

jetzt können Sie doSomething entweder mit List <Dog> oder List <Cat> oder List <Animal> aufrufen.

0
gerardw

Wir sollten auch berücksichtigen, wie der Compiler die generischen Klassen bedroht: "instanziiert" einen anderen Typ, wenn wir die generischen Argumente füllen.

Wir haben also ListOfAnimal, ListOfDog, ListOfCat usw., wobei es sich um verschiedene Klassen handelt, die vom Compiler bei der Angabe der generischen Argumente "erstellt" werden. Und dies ist eine flache Hierarchie (eigentlich ist List überhaupt keine Hierarchie).

Ein weiteres Argument, warum Kovarianz bei generischen Klassen nicht sinnvoll ist, ist die Tatsache, dass alle Klassen gleich sind - es sind List-Instanzen. Durch die Spezialisierung einer List durch Ausfüllen des generischen Arguments wird die Klasse nicht erweitert, sondern nur für dieses bestimmte generische Argument.

0
Cristik

Nehmen wir das Beispiel aus JavaSE Tutorial

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) {
        ...
    }
}

Warum also eine Liste von Hunden (Kreisen) nicht implizit als eine Liste von Tieren (Formen) betrachtet werden sollte, ist aus dieser Situation heraus:

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

Java-Architekten hatten also zwei Möglichkeiten, dieses Problem zu lösen:

  1. beachten Sie nicht, dass ein Untertyp implizit sein Supertyp ist, und geben Sie einen Kompilierungsfehler an, wie er jetzt auftritt

  2. betrachten Sie den Untertyp als seinen Supertyp und beschränken Sie ihn beim Kompilieren der Methode "add". Wenn also in der drawAll-Methode eine Liste von Kreisen, Untertyp von Shape, übergeben wird, sollte der Compiler dies erkennen und Sie mit Kompilierfehler einschränken Das).

Aus offensichtlichen Gründen entschied man sich für den ersten Weg. 

0
aurelius

eine andere Lösung ist die Erstellung einer neuen Liste

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