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.

4voto

Angel Koh Punkte 11327

Sie können eine Schnittstelle verwenden, um 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!";}

}

können Sie die Sammlungen dann mit

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

4voto

Renato Probst Punkte 5557

Wenn Sie sicher sind, dass es sich bei den Listenelementen um Unterklassen des jeweiligen Supertyps handelt, können Sie die Liste auf diese Weise umwandeln:

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

Dies ist nützlich, wenn Sie die Liste innerhalb eines Konstruktors übergeben oder über sie iterieren wollen.

3voto

dan b Punkte 1162

En Antwort sowie die anderen Antworten sind richtig. Ich werde diese Antworten mit einer Lösung ergänzen, die meiner Meinung nach hilfreich sein wird. Ich denke, dieses Problem tritt in der Programmierung häufig auf. Zu beachten ist, dass bei Sammlungen (Listen, Mengen usw.) das Hauptproblem das Hinzufügen zur Sammlung ist. Das ist der Punkt, an dem die Dinge zusammenbrechen. Auch das Entfernen ist in Ordnung.

In den meisten Fällen können wir Collection<? extends T> eher als Collection<T> und das sollte die erste Wahl sein. Ich stelle jedoch fest, dass es in manchen Fällen nicht einfach ist, dies zu tun. Ob das immer die beste Lösung ist, sei dahingestellt. Ich stelle hier eine Klasse DownCastCollection vor, die eine Collection<? extends T> zu einer Collection<T> (wir können ähnliche Klassen für List, Set, NavigableSet, definieren), die verwendet werden, wenn die Verwendung des Standardansatzes sehr unbequem ist. Im Folgenden finden Sie ein Beispiel für die Verwendung dieser Klasse (wir könnten auch Collection<? extends Object> in diesem Fall, aber ich halte es einfach, um 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));
}

Jetzt 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();

}

}

2voto

Yttrill Punkte 4531

Das Problem wurde korrekt als mit der Abweichung zusammenhängend identifiziert, aber die Details sind nicht korrekt. Eine rein funktionale Liste ist ein kovarianter Datenfunktor, d. h. wenn ein Typ Sub ein Subtyp von Super ist, dann ist eine Liste von Sub definitiv ein Subtyp einer Liste von Super.

Die Veränderbarkeit einer Liste ist hier jedoch nicht das grundlegende Problem. Das Problem ist die Veränderbarkeit im Allgemeinen. Das Problem ist wohlbekannt und wird als Kovarianzproblem bezeichnet. Es wurde, glaube ich, zuerst von Castagna identifiziert und zerstört die Objektorientierung als allgemeines Paradigma vollständig und vollständig. Es basiert auf den zuvor etablierten Varianzregeln von Cardelli und Reynolds.

Betrachten wir - etwas vereinfachend - die Zuordnung eines Objekts B vom Typ T zu einem Objekt A vom Typ T als Mutation. Dies geschieht ohne Verlust an Allgemeinheit: eine Mutation von A kann als A = f (A) geschrieben werden, wobei f: T -> T. Das Problem ist natürlich, dass Funktionen zwar in ihrem Kodomain kovariant sind, aber in ihrem Domain kontravariant, aber bei Zuweisungen sind Domain und Kodomain dasselbe, also ist die Zuweisung invariant!

Daraus folgt verallgemeinernd, dass Subtypen nicht mutiert werden können. Bei der Objektorientierung ist die Mutation jedoch von grundlegender Bedeutung, so dass die Objektorientierung von Natur aus fehlerhaft ist.

Hier ein einfaches Beispiel: In einer rein funktionalen Umgebung ist eine symmetrische Matrix eindeutig eine Matrix, sie ist ein Untertyp, kein Problem. Fügen wir nun der Matrix die Fähigkeit hinzu, ein einzelnes Element an den Koordinaten (x,y) zu setzen, wobei sich kein anderes Element ändern darf. Jetzt ist die symmetrische Matrix kein Untertyp mehr, denn wenn Sie (x,y) ändern, haben Sie auch (y,x) geändert. Die funktionale Operation ist delta: Sym -> Mat, wenn Sie ein Element einer symmetrischen Matrix ändern, erhalten Sie eine allgemeine nicht-symmetrische Matrix zurück. Wenn Sie also eine Methode zum Ändern eines Elements in Mat aufgenommen haben, ist Sym kein Untertyp. In der Tat gibt es mit ziemlicher Sicherheit KEINE richtigen Untertypen.

Um es einfacher auszudrücken: Wenn Sie einen allgemeinen Datentyp mit einer Vielzahl von Mutatoren haben, die seine Allgemeinheit ausnutzen, können Sie sicher sein, dass ein geeigneter Untertyp unmöglich alle diese Mutationen unterstützen kann: Wenn er das könnte, wäre er genauso allgemein wie der Supertyp, was der Spezifikation des "geeigneten" Untertyps widerspricht.

Die Tatsache, dass Java die Subtypisierung von veränderbaren Listen verhindert, geht am eigentlichen Problem vorbei: Warum verwenden Sie objektorientierten Müll wie Java, wenn es schon vor mehreren Jahrzehnten in Verruf geraten ist?

Auf jeden Fall gibt es hier eine vernünftige Diskussion:

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

0voto

aurelius Punkte 3701

Nehmen wir das Beispiel von JavaSE Lehrgang

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

Dies ist der Grund, warum eine Liste von Hunden (Kreise) nicht implizit als Liste von Tieren (Formen) betrachtet werden sollte:

// drawAll method call
drawAll(circleList);

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

Die Java-"Architekten" hatten also 2 Möglichkeiten, um dieses Problem zu lösen:

  1. nicht berücksichtigen, dass ein Subtyp implizit sein Supertyp ist, und einen Kompilierfehler ausgeben, wie es jetzt geschieht

  2. den Subtyp als seinen Supertyp betrachten und bei der Kompilierung die "add"-Methode einschränken (wenn also in der drawAll-Methode eine Liste von Kreisen, Subtyp von shape, übergeben würde, sollte der Compiler dies erkennen und Sie mit einem Kompilierungsfehler daran hindern, dies zu tun).

Aus offensichtlichen Gründen wurde der erste Weg gewählt.

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