909 Stimmen

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

Ich bin ein wenig verwirrt darüber, wie Java Generika behandeln Vererbung / Polymorphismus.

Nehmen Sie die folgende Hierarchie an -

Tier (Elternteil)

Hund - Katze (Kinder)

Nehmen wir also an, ich habe eine Methode doSomething(List<Animal> animals) . Nach allen Regeln der Vererbung und des Polymorphismus würde ich annehmen, dass ein List<Dog> ist a List<Animal> et un List<Cat> ist a List<Animal> - und kann daher an diese Methode übergeben werden. Dem ist nicht so. Wenn ich dieses Verhalten erreichen will, muss ich der Methode explizit sagen, dass sie eine Liste einer beliebigen Unterklasse von Animal akzeptieren soll, indem ich sage doSomething(List<? extends Animal> animals) .

Ich verstehe, dass dies das Verhalten von Java ist. Meine Frage ist pourquoi ? Warum ist Polymorphismus im Allgemeinen implizit, aber wenn es um Generika geht, muss er angegeben werden?

22 Stimmen

Und eine völlig unabhängige Grammatikfrage, die mich gerade beschäftigt - sollte mein Titel "warum" lauten? sind nicht Javas Generika" oder "Warum ist nicht Java's generics"? Ist "Generika" Plural wegen des s oder Singular, weil es eine Einheit ist?

28 Stimmen

Generika, wie sie in Java verwendet werden, sind eine sehr schlechte Form des parametrischen Polymorphismus. Setzen Sie nicht zu viel Vertrauen in sie (wie ich es früher getan habe), denn eines Tages werden Sie hart an ihre erbärmlichen Grenzen stoßen: Surgeon extends Handable<Scalpel>, Handable<Sponge> KABOOM! Macht nicht berechnen [TM]. Das ist die Einschränkung der Java-Generik. Jede OOA/OOD kann gut in Java übersetzt werden (und MI kann sehr schön mit Java-Schnittstellen gemacht werden), aber Generika sind einfach nicht geeignet. Sie sind gut für "Sammlungen" und prozedurale Programmierung, dass sagte (das ist, was die meisten Java-Programmierer sowieso tun, so...).

8 Stimmen

Die Superklasse von List<Dog> ist nicht List<Animal>, sondern List<?> (d. h. eine Liste unbekannten Typs). Generics löscht Typinformationen im kompilierten Code. Dies geschieht, damit Code, der Generics verwendet (Java 5 und höher), mit früheren Versionen von Java ohne Generics kompatibel ist.

23voto

outdev Punkte 4886

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

List<Dog> es nicht Unterklasse von List<Animal> .
Aber Dog[] ist Unterklasse von Animal[] .

Arrays sind reifungsfähig und kovariant .
Berechenbar bedeutet, dass ihre Typinformationen zur Laufzeit vollständig verfügbar sind.
Daher bieten Arrays Typsicherheit zur Laufzeit, nicht aber zur Kompilierzeit.

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

Bei Generika verhält es sich umgekehrt:
Generika sind Gelöscht und unveränderlich .
Daher können Generika keine Typsicherheit zur Laufzeit bieten, aber sie bieten Typsicherheit zur Kompilierzeit.
Wären die Generika im folgenden Code kovariant, wäre es möglich, die Haldenverschmutzung in Zeile 3.

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

10voto

Mike Nakis Punkte 49916

Andere haben anständig erklärt, warum man nicht einfach eine Liste von Nachkommen in eine Liste von Superklassen umwandeln kann.

Viele Menschen besuchen diese Frage jedoch auf der Suche nach einer Lösung.

Die Lösung für dieses Problem seit der Java-Version 10 lautet also wie folgt:

(Anmerkung: S = Superklasse)

List<S> supers = List.copyOf( descendants );

Diese Funktion führt einen Cast durch, wenn es absolut sicher ist, oder eine Kopie, wenn ein Cast nicht sicher wäre.

Eine ausführliche Erklärung (die auch die in anderen Antworten erwähnten potenziellen Fallstricke berücksichtigt) finden Sie in der entsprechenden Frage und meiner Antwort von 2022: https://stackoverflow.com/a/72195980/773113

8voto

glglgl Punkte 84928

Die hier gegebenen Antworten haben mich nicht vollständig ü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? Aber man kann nur bestehen Consumer s und Supplier s für Animal s. Wenn Sie eine Mammal Verbraucher, sondern ein Duck Lieferant, sollten sie 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 Anbieter verwenden können, der uns die richtige Art von Objekt für den Verbraucher liefert.

OTOH, wir könnten genauso gut

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

wo wir den umgekehrten Weg gehen: wir definieren den Typ des Supplier und einschränken, dass sie in die Consumer .

Wir können sogar

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

wobei nach den intuitiven Beziehungen Life -> Animal -> Mammal -> Dog , Cat usw., wir könnten sogar eine Mammal in eine Life Verbraucher, aber nicht ein String in eine Life Verbraucher.

7voto

Hitesh Punkte 605

Die Grundlogik für ein solches Verhalten ist, dass Generics einem Mechanismus der Typenlöschung folgen. Zur Laufzeit haben Sie also keine Möglichkeit, den Typ von collection im Gegensatz zu arrays wo es keinen solchen Löschvorgang gibt. Um also auf Ihre Frage zurückzukommen...

Nehmen wir also an, es gibt eine Methode wie unten angegeben:

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

Nun, wenn Java erlaubt es dem Aufrufer, eine Liste des Typs Animal zu dieser Methode hinzuzufügen, dann kann es sein, dass man etwas Falsches in die Sammlung hinzufügt, und auch zur Laufzeit wird es aufgrund der Typlöschung ausgeführt. Während im Falle von Arrays erhalten Sie eine Laufzeit Ausnahme für solche Szenarien...

Im Wesentlichen ist dieses Verhalten also so implementiert, dass man nichts Falsches in die Sammlung aufnehmen kann. Ich glaube, dass die Typlöschung existiert, um Kompatibilität mit Legacy-Java ohne Generika zu gewährleisten: ....

5voto

Root G Punkte 1019

Subtypisierung ist invariant für parametrisierte Typen. Auch wenn die Klasse Dog ist ein Subtyp von Animal der parametrisierte Typ List<Dog> ist kein Subtyp von List<Animal> . Im Gegensatz dazu, Kovariante Subtypisierung wird von Arrays verwendet, so dass das Array Typ Dog[] ist ein Subtyp von Animal[] .

Die invariante Subtypisierung stellt sicher, dass die von Java erzwungenen Typbeschränkungen nicht verletzt werden. Betrachten 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 erwähnt, ist dieser Code illegal, da er sonst die Typbeschränkungen verletzen würde, indem er eine Katze zurückgibt, obwohl ein Hund erwartet wird.

Es ist aufschlussreich, den obigen Code mit dem analogen 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. Er löst jedoch ein Ausnahme beim Speichern von Arrays . Ein Array trägt seinen Typ zur Laufzeit, so kann die JVM erzwingen Typsicherheit der kovarianten Subtypisierung erzwingen.

Um dies besser zu verstehen, schauen wir uns den Bytecode an, der von javap der Klasse unten:

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 zeigt dies den folgenden Java-Bytecode:

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 ersetzte jeden parametrisierten Typ durch seinen Löschen . Diese Eigenschaft ist von entscheidender Bedeutung, da sie die Abwärtskompatibilität nicht beeinträchtigt.

Zusammenfassend lässt sich sagen, dass Laufzeitsicherheit für parametrisierte Typen nicht möglich ist, da der Compiler jeden parametrisierten Typ durch seine Löschung ersetzt. Damit sind parametrisierte Typen nichts weiter als syntaktischer Zucker.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X