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


770

Ich bin etwas verwirrt darüber, wie Java-Generika mit Vererbung / Polymorphismus umgehen.

Nehmen Sie die folgende Hierarchie an -

Tier (Eltern)

Hund - Katze (Kinder)

Angenommen, ich habe eine Methode doSomething(List<Animal> animals). Nach allen Regeln der Vererbung und Polymorphismus, würde ich davon ausgehen , dass ein List<Dog> ist ein List<Animal>und List<Cat> ist ein List<Animal>- und so entweder konnte man an diese Methode übergeben werden. Nicht so. Wenn ich dieses Verhalten erreichen möchte, muss ich die Methode explizit anweisen, eine Liste aller Unterklassen von Tieren zu akzeptieren, indem ich sage doSomething(List<? extends Animal> animals).

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


17
Und eine völlig unabhängige Grammatikfrage, die mich jetzt beschäftigt - sollte mein Titel "Warum sind nicht Javas Generika" oder "Warum sind Javas Generika nicht" sein? Ist "Generika" Plural wegen des s oder Singular, weil es eine Einheit ist?
Froadie

25
Generika wie in Java sind eine sehr schlechte Form des parametrischen Polymorphismus. Vertraue ihnen nicht zu sehr (wie ich es früher getan habe ), denn eines Tages wirst du ihre erbärmlichen Grenzen hart treffen: Der Chirurg erweitert Handable <Scalpel>, Handable <Sponge> KABOOM! Hat nicht berechnen [TM]. Es gibt Ihre Java-Generika-Einschränkung. Jedes OOA / OOD kann gut in Java übersetzt werden (und MI kann sehr gut über Java-Schnittstellen ausgeführt werden), aber Generika schneiden es einfach nicht. Sie eignen sich gut für "Sammlungen" und prozedurale Programmierung (was die meisten Java-Programmierer sowieso tun ...).
SyntaxT3rr0r

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


9
@froadie, da niemand zu antworten schien ... es sollte definitiv "warum sind nicht Javas Generika ..." sein. Das andere Problem ist, dass "generisch" tatsächlich ein Adjektiv ist, und daher bezieht sich "generisch" auf ein durch "generisch" modifiziertes Pluralnomen. Sie könnten sagen "diese Funktion ist generisch", aber das wäre umständlicher als zu sagen "diese Funktion ist generisch". Es ist jedoch etwas umständlich zu sagen, dass "Java generische Funktionen und Klassen hat", anstatt nur "Java hat generische Funktionen". Als jemand, der seine Masterarbeit über Adjektive geschrieben hat, sind Sie auf eine sehr interessante Frage gestoßen!
Dantiston

Antworten:


916

Nein, a List<Dog>ist nicht a List<Animal>. Überlegen Sie, was Sie mit einem tun können List<Animal>- Sie können jedes Tier hinzufügen ... einschließlich einer Katze. Können Sie einem Wurf Welpen logisch 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 hast du eine sehr verwirrte Katze.

Jetzt können Sie ein nichtCat zu einem hinzufügen , List<? extends Animal>weil Sie nicht wissen, dass es ein ist List<Cat>. Sie können einen Wert abrufen und wissen, dass es sich um einen Wert handelt Animal, aber Sie können keine beliebigen Tiere hinzufügen. Das Gegenteil gilt für List<? super Animal>- in diesem Fall können Sie ein Animalsicher hinzufügen , aber Sie wissen nichts darüber, was daraus abgerufen werden könnte, da es ein sein könnte List<Object>.


50
Interessanterweise jede Liste von Hunden ist in der Tat eine Liste von Tieren, wie Intuition uns sagt. Der Punkt ist, dass nicht jede Liste von Tieren eine Liste von Hunden ist, daher ist die Mutatation der Liste durch Hinzufügen einer Katze das Problem.
Ingo

66
@Ingo: Nein, nicht wirklich: Sie können eine Katze zu einer Liste von Tieren hinzufügen, aber Sie können keine Katze zu einer Liste von Hunden hinzufügen. Eine Liste von Hunden ist nur dann eine Liste von Tieren, wenn Sie sie als schreibgeschützt betrachten.
Jon Skeet

12
@ JonSkeet - Natürlich, aber wer schreibt vor, dass das Erstellen einer neuen Liste aus einer Katze und einer Liste von Hunden tatsächlich die Liste der Hunde ändert? Dies ist eine willkürliche Implementierungsentscheidung in Java. Eine, die Logik und Intuition widerspricht.
Ingo

7
@Ingo: Ich hätte das nicht "sicher" benutzt, um damit zu beginnen. Wenn Sie eine Liste mit der Aufschrift "Hotels, in die wir vielleicht gehen möchten" oben haben und dann jemand einen Swimmingpool hinzugefügt hat, würden Sie das für gültig halten? Nein - es ist eine Liste von Hotels, die keine Liste von Gebäuden ist. Und es ist nicht so, als hätte ich sogar gesagt "Eine Liste von Hunden ist keine Liste von Tieren" - ich habe es in Code-Begriffen , in einer Code-Schriftart, ausgedrückt . Ich glaube wirklich nicht, dass es hier Unklarheiten gibt. Die Verwendung von Unterklassen wäre sowieso falsch - es geht um Zuweisungskompatibilität, nicht um Unterklassen.
Jon Skeet

14
@ruakh: Das Problem ist, dass Sie dann etwas zur Ausführungszeit streichen, was zur Kompilierungszeit blockiert werden kann. Und ich würde argumentieren, dass Array-Kovarianz zunächst ein Designfehler war.
Jon Skeet

84

Was Sie suchen, nennt man kovariante Typparameter . Dies bedeutet, dass, wenn ein Objekttyp in einer Methode durch einen anderen ersetzt werden kann (z. B. Animaldurch ersetzt werden kann Dog), dies auch für Ausdrücke gilt, die diese Objekte verwenden (also List<Animal>durch ersetzt werden könnten List<Dog>). Das Problem ist, dass Kovarianz für veränderbare Listen im Allgemeinen nicht sicher ist. Angenommen, Sie haben eine List<Dog>, und sie wird als verwendet List<Animal>. Was passiert, wenn Sie versuchen, eine Katze hinzuzufügen, List<Animal>die wirklich eine ist List<Dog>? Das automatische Zulassen, dass Typparameter kovariant sind, unterbricht das Typensystem.

Es wäre nützlich, eine Syntax hinzuzufügen, damit Typparameter als Kovariante angegeben werden können. Dadurch werden die ? extends FooDeklarationen in Methoden vermieden , dies erhöht jedoch die Komplexität.


44

Der Grund, warum a List<Dog>kein a ist List<Animal>, ist, dass Sie beispielsweise ein Catin ein einfügen können List<Animal>, aber nicht in ein List<Dog>... Sie können Platzhalter verwenden, um Generika nach Möglichkeit erweiterbarer zu machen. Zum Beispiel ist das Lesen von a List<Dog>dem Lesen von a ähnlich List<Animal>- aber nicht das Schreiben.

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


36

Ein Punkt, den ich denke, sollte zu dem hinzugefügt werden, was andere Antworten erwähnen, ist das während

List<Dog>ist kein List<Animal> Java

es ist auch wahr, dass

Eine Liste von Hunden ist eine Liste von Tieren in englischer Sprache (nun, unter einer vernünftigen Interpretation)

Die Art und Weise, wie die Intuition des OP funktioniert - was natürlich völlig gültig ist - ist der letztere Satz. Wenn wir diese Intuition anwenden, erhalten wir jedoch eine Sprache, die in ihrem Typensystem nicht Java-artig ist: Angenommen, unsere Sprache erlaubt das Hinzufügen einer Katze zu unserer Liste von Hunden. Was würde das bedeuten? Dies würde bedeuten, dass die Liste keine Liste von Hunden mehr ist und lediglich eine Liste von Tieren bleibt. Und eine Liste von Säugetieren und eine Liste von Quadrapeds.

Anders ausgedrückt: A bedeutet List<Dog>auf Java nicht "eine Liste von Hunden" auf Englisch, sondern "eine Liste, die Hunde haben kann, und sonst nichts".

Im Allgemeinen eignet sich die Intuition von OP für eine Sprache, in der Operationen an Objekten ihren Typ ändern können , oder vielmehr, dass die Typen eines Objekts eine (dynamische) Funktion seines Werts sind.


Ja, die menschliche Sprache ist verschwommener. Wenn Sie jedoch ein anderes Tier zur Liste der Hunde hinzufügen, handelt es sich immer noch um eine Liste der Tiere, jedoch nicht mehr um eine Liste der Hunde. Der Unterschied besteht darin, dass ein Mensch mit der Fuzzy-Logik normalerweise kein Problem damit hat, dies zu realisieren.
Vlasec

Als jemand, der die ständigen Vergleiche mit Arrays noch verwirrender findet, hat mich diese Antwort überzeugt. Mein Problem war die Sprachintuition.
FLonLon

32

Ich würde sagen, der springende Punkt bei Generics ist, dass es das nicht zulässt. Betrachten Sie die Situation mit Arrays, die diese Art von Kovarianz zulassen:

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

Dieser Code wird gut kompiliert, löst jedoch einen Laufzeitfehler aus ( java.lang.ArrayStoreException: java.lang.Booleanin der zweiten Zeile). Es ist nicht typsicher. Bei Generics geht es darum, die Sicherheit für den Kompilierungszeittyp hinzuzufügen. Andernfalls können Sie sich einfach an eine einfache Klasse ohne Generics halten.

Jetzt gibt es Zeiten, in denen Sie flexibler sein müssen, ? super Classund dafür ? extends Classsind die und da . Ersteres ist, wenn Sie in einen Typ einfügen müssen Collection(zum Beispiel), und letzteres ist, wenn Sie typsicher daraus lesen müssen. Die einzige Möglichkeit, beides gleichzeitig zu tun, besteht darin, einen bestimmten Typ zu haben.


13
Array-Kovarianz ist wohl ein Fehler beim Sprachdesign. Beachten Sie, dass aufgrund der Typlöschung das gleiche Verhalten für die generische Erfassung technisch nicht möglich ist.
Michael Borgwardt

" Ich würde sagen, der springende Punkt bei Generics ist, dass das nicht erlaubt ist. " Sie können nie sicher sein: Java und Scalas Typensysteme sind nicht stichhaltig: Die existenzielle Krise der Nullzeiger (vorgestellt auf der OOPSLA 2016) (seitdem korrigiert, wie es scheint)
David Tonhofer

15

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

List<Dog>ist keine Unterklasse von List<Animal>.
Ist Dog[] aber Unterklasse von Animal[].

Arrays sind reifizierbar und kovariant .
Reifizierbar bedeutet, dass ihre Typinformationen zur Laufzeit vollständig verfügbar sind.
Daher bieten Arrays Sicherheit vom Typ Laufzeit, jedoch keine Sicherheit vom Typ Kompilierungszeit.

    // 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 ist es umgekehrt: Generika werden gelöscht und sind unveränderlich .
Daher können Generika keine Sicherheit vom Typ Laufzeit bieten, sie bieten jedoch Sicherheit vom Typ Kompilierungszeit.
Wenn im folgenden Code Generika kovariant wären, könnte in Zeile 3 eine Haufenverschmutzung auftreten .

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

2
Es könnte argumentiert werden, dass gerade deswegen Arrays in Java kaputt sind ,
leonbloy

Kovariante Arrays sind ein "Feature" des Compilers.
Cristik

6

Die hier gegebenen Antworten haben mich nicht ganz ü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 aber nur Consumers und Suppliers für Animals übergeben. Wenn Sie einen MammalVerbraucher, aber einen DuckLieferanten haben, sollten diese nicht passen, obwohl beide Tiere sind. Um dies zu verbieten, wurden zusätzliche Einschränkungen hinzugefügt.

Anstelle der oben genannten 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 verwenden können, der uns den richtigen Objekttyp für den Verbraucher bietet.

OTOH, das könnten wir auch

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

wo wir den anderen Weg gehen: Wir definieren den Typ des Supplierund beschränken, dass es in das gesetzt werden kann Consumer.

Wir können es sogar tun

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

wo die intuitiven Beziehungen mit Life-> Animal-> Mammal-> Dog, Catusw., könnten wir sogar ein setzen Mammalin einen LifeVerbraucher, aber nicht Stringin einen LifeVerbraucher.


1
Unter den 4 Versionen ist # 2 wahrscheinlich falsch. zB können wir es nicht mit (Consumer<Runnable>, Supplier<Dog>)while Dogals Subtyp vonAnimal & Runnable
ZhongYu

5

Die Basislogik für ein solches Verhalten besteht darin Generics, einem Mechanismus der Typlöschung zu folgen. Also während der Laufzeit haben Sie keine Möglichkeit , wenn die Identifizierung der Art der im collectionGegensatz zu arraysdenen es keine solchen Löschprozess. Kommen wir also auf Ihre Frage zurück ...

Angenommen, es gibt eine Methode wie folgt:

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

Wenn Java dem Aufrufer erlaubt, dieser Methode eine Liste vom Typ Animal hinzuzufügen, können Sie der Sammlung möglicherweise etwas Falsches hinzufügen, und auch zur Laufzeit wird es aufgrund des Löschens des Typs ausgeführt. Während bei Arrays eine Laufzeitausnahme für solche Szenarien angezeigt wird ...

Somit wird dieses Verhalten im Wesentlichen so implementiert, dass man der Sammlung nichts Falsches hinzufügen kann. Jetzt glaube ich, dass es eine Typlöschung gibt, um die Kompatibilität mit Legacy-Java ohne Generika zu gewährleisten.


4

Die Subtypisierung ist für parametrisierte Typen unveränderlich . Selbst wenn die Klasse Dogein Subtyp von ist Animal, ist der parametrisierte Typ List<Dog>kein Subtyp von List<Animal>. Im Gegensatz dazu wird die kovariante Subtypisierung von Arrays verwendet, sodass der Array-Typ Dog[]ein Subtyp von ist Animal[].

Durch die invariante Untertypisierung wird sichergestellt, dass die von Java erzwungenen Typeinschrä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 angegeben, ist dieser Code illegal, da er sonst die Typbeschränkungen verletzen würde, indem eine Katze zurückgegeben wird, wenn ein Hund dies erwartet.

Es ist lehrreich, das Obige 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, sodass JVM die Typensicherheit der kovarianten Subtypisierung erzwingen kann.

Um dies weiter zu verstehen, schauen wir uns den Bytecode an, der javapvon der folgenden Klasse generiert wurde :

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

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


3

Tatsächlich können Sie eine Schnittstelle verwenden, um das zu erreichen, was Sie wollen.

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 dann die Sammlungen mit verwenden

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

1

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

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

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


2
Dies wird mehr Probleme verursachen, als es tatsächlich löst
Ferrybig

Wenn Sie versuchen, eine Katze zur Liste hinzuzufügen, wird dies sicher Probleme verursachen, aber für Schleifenzwecke denke ich, dass dies die einzige nicht ausführliche Antwort ist.
Sagits

1

Die Antwort sowie andere Antworten sind korrekt. Ich werde diese Antworten mit einer Lösung ergänzen, die ich für hilfreich halte. Ich denke, das kommt beim Programmieren oft vor. Zu beachten ist, dass bei Sammlungen (Listen, Sets usw.) das Hauptproblem darin besteht, die Sammlung zu erweitern. Dort brechen die Dinge zusammen. Auch das Entfernen ist in Ordnung.

In den meisten Fällen können wir dann Collection<? extends T>eher verwenden Collection<T>und das sollte die erste Wahl sein. Ich finde jedoch Fälle, in denen dies nicht einfach ist. Es steht zur Debatte, ob dies immer das Beste ist. Ich präsentiere hier eine Klasse DownCastCollection, die die Konvertierung von a Collection<? extends T>in a Collection<T>(wir können ähnliche Klassen für List, Set, NavigableSet, .. definieren) verwenden kann, um verwendet zu werden, wenn der Standardansatz verwendet wird, ist sehr unpraktisch. Im Folgenden finden Sie ein Beispiel für die Verwendung (wir könnten es auch Collection<? extends Object>in diesem Fall verwenden, aber ich halte es einfach, die Verwendung von DownCastCollection 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();

}

}}


Dies ist eine gute Idee, so sehr, dass sie bereits in Java SE vorhanden ist. ;; )Collections.unmodifiableCollection
Radiodef

1
Richtig, aber die von mir definierte Sammlung kann geändert werden.
Dan B

Ja, es kann geändert werden. Collection<? extends E>Behandelt dieses Verhalten jedoch bereits korrekt, es sei denn, Sie verwenden es auf eine Weise, die nicht typsicher ist (z. B. wenn Sie es auf etwas anderes übertragen). Der einzige Vorteil, den ich dort sehe, ist, dass beim Aufrufen der addOperation eine Ausnahme ausgelöst wird, selbst wenn Sie sie gewirkt haben.
Vlasec

0

Nehmen wir das Beispiel aus dem 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 eine Liste von Hunden (Kreisen) nicht implizit als Liste von Tieren (Formen) betrachtet werden sollte, liegt an dieser Situation:

// drawAll method call
drawAll(circleList);


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

Java "Architekten" hatten also zwei Möglichkeiten, um dieses Problem anzugehen:

  1. Denken Sie nicht daran, dass ein Subtyp implizit ein Supertyp ist, und geben Sie einen Kompilierungsfehler aus, wie es jetzt geschieht

  2. Betrachten Sie den Subtyp als Supertyp und beschränken Sie beim Kompilieren die "add" -Methode (wenn also in der drawAll-Methode eine Liste von Kreisen, Subtyp der Form, übergeben würde, sollte der Compiler dies erkennen und Sie mit Kompilierungsfehlern einschränken Das).

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


0

Wir sollten auch berücksichtigen, wie der Compiler die generischen Klassen bedroht: In "instanziiert" ein anderer Typ, wenn wir die generischen Argumente ausfüllen.

So haben wir ListOfAnimal, ListOfDog, ListOfCat, usw., die verschiedenen Klassen sind , dass am Ende wird durch den Compiler „erzeugt“ wird , wenn wir die allgemeinen Argumente angeben. Und dies ist eine flache Hierarchie (eigentlich Listist dies überhaupt keine Hierarchie).

Ein weiteres Argument, warum Kovarianz bei generischen Klassen keinen Sinn ergibt, ist die Tatsache, dass im Grunde alle Klassen gleich sind - ListInstanzen sind. Das Spezialisieren von a Listdurch Ausfüllen des generischen Arguments erweitert die Klasse nicht, sondern funktioniert nur für dieses bestimmte generische Argument.


0

Das Problem wurde gut identifiziert. Aber es gibt eine Lösung; mach doSomething generisch:

<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

Eine andere Lösung besteht darin, eine neue Liste zu erstellen

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

0

Weiter zur Antwort von Jon Skeet, der diesen Beispielcode verwendet:

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

Auf der tiefsten Ebene ist das Problem hier das dogsund animalsteilen Sie eine Referenz. Das bedeutet, dass eine Möglichkeit, diese Arbeit zu machen, darin besteht, die gesamte Liste zu kopieren, was die Referenzgleichheit brechen würde:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

Nach dem Anruf List<Animal> animals = new ArrayList<>(dogs);können Sie animalsentweder dogsoder nicht direkt zuweisen cats:

// These are both illegal
dogs = animals;
cats = animals;

Daher können Sie den falschen Subtyp von nicht Animalin die Liste aufnehmen, da es keinen falschen Subtyp gibt - jedes Objekt des Subtyps ? extends Animalkann hinzugefügt werden animals.

Offensichtlich ändert sich die Semantik, da die Listen animalsund dogssind nicht mehr freigegeben, so zu einer Liste hinzuzufügen hinzufügen nicht auf die anderen (das ist genau das , was Sie wollen, um das Problem zu vermeiden , dass ein Catzu einer Liste hinzugefügt werden könnte , die nur soll DogObjekte enthalten ). Das Kopieren der gesamten Liste kann auch ineffizient sein. Dies löst jedoch das Typäquivalenzproblem, indem die Referenzgleichheit gebrochen wird.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.