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.

0voto

Cristik Punkte 28307

Wir sollten auch in Betracht ziehen, wie der Compiler die generischen Klassen bedroht: Er "instanziiert" einen anderen Typ, wenn wir die generischen Argumente ausfüllen.

Wir haben also ListOfAnimal , ListOfDog , ListOfCat usw., bei denen es sich um unterschiedliche Klassen handelt, die vom Compiler "erstellt" werden, wenn wir die generischen Argumente angeben. Und dies ist eine flache Hierarchie (eigentlich in Bezug auf List überhaupt keine Hierarchie ist).

Ein weiteres Argument, warum die Kovarianz im Fall von generischen Klassen keinen Sinn macht, ist die Tatsache, dass alle Klassen im Grunde gleich sind - sind List Instanzen. Die Spezialisierung einer List durch Ausfüllen des generischen Arguments erweitert die Klasse nicht, sondern macht sie nur für dieses bestimmte generische Argument funktionsfähig.

0voto

gerardw Punkte 5059

Das Problem ist gut erkannt worden. Aber es gibt eine Lösung: Machen Sie 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.

0voto

ejaenv Punkte 1695

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

0voto

Luke Hutchison Punkte 6996

Im Anschluss an die 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?

Im Grunde genommen besteht das Problem hier darin, dass dogs y animals eine Referenz teilen. Das bedeutet, dass eine Möglichkeit, dies zu bewerkstelligen, darin bestünde, die gesamte Liste zu kopieren, was die Referenzgleichheit aufheben 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 nicht nachträglich direkt zuordnen animals entweder zu dogs o cats :

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

Sie können also nicht den falschen Subtyp von Animal in die Liste aufzunehmen, da es keinen falschen Untertyp gibt - jedes Objekt des Untertyps ? extends Animal kann hinzugefügt werden zu animals .

Dies ändert natürlich die Semantik, da die Listen animals y dogs nicht mehr gemeinsam genutzt werden, so dass das Hinzufügen zu einer Liste nicht zu einer anderen führt (was genau das ist, was Sie wollen, um das Problem zu vermeiden, dass eine Cat zu einer Liste hinzugefügt werden, die nur Folgendes enthalten soll Dog Objekte). Außerdem kann das Kopieren der gesamten Liste ineffizient sein. Allerdings wird dadurch das Problem der Typgleichheit gelöst, indem die Referenzgleichheit aufgehoben wird.

0voto

Shubham Arya Punkte 495

Ich sehe, dass die Frage bereits mehrfach beantwortet wurde, möchte aber dennoch meine Meinung zu dieser Frage äußern.

Lassen Sie uns nun eine vereinfachte Hierarchie der Tierklassen erstellen.

abstract class Animal {
    void eat() {
        System.out.println("animal eating");
    }
}

class Dog extends Animal {
    void bark() { }
}

class Cat extends Animal {
    void meow() { }
}

Werfen wir nun einen Blick auf unseren alten Freund Arrays, von dem wir wissen, dass er implizit Polymorphismus unterstützt -

class TestAnimals {
    public static void main(String[] args) {
        Animal[] animals = {new Dog(), new Cat(), new Dog()};
        Dog[] dogs = {new Dog(), new Dog(), new Dog()};
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(Animal[] animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

Die Klasse lässt sich gut kompilieren, und wenn wir die obige Klasse ausführen, erhalten wir die folgende Ausgabe

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

Hier ist zu beachten, dass die takeAnimals()-Methode so definiert ist, dass sie alles aufnehmen kann, was vom Typ Tier ist, sie kann ein Array vom Typ Tier aufnehmen und sie kann auch ein Array von Hund aufnehmen, weil Hund-ist-ein-Tier. Dies ist also Polymorphismus in Aktion.

Lassen Sie uns nun den gleichen Ansatz mit Generika anwenden,

Ändern wir nun unseren Code ein wenig und verwenden ArrayLists anstelle von Arrays -

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());
        takeAnimals(animals);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

Die obige Klasse wird kompiliert und erzeugt die Ausgabe -

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

Wir wissen also, dass es funktioniert. Nun können wir diese Klasse ein wenig anpassen, um den Typ Animal polymorph zu verwenden.

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());

        ArrayList<Dog> dogs = new ArrayList<Dog>();
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

Es sollte kein Problem bei der Kompilierung der obigen Klasse geben, da die takeAnimals()-Methode so konzipiert ist, dass sie jede ArrayList des Typs Animal und Dog-is-a-Animal aufnehmen kann, so dass dies hier kein Problem darstellen sollte.

Aber leider wirft der Compiler einen Fehler und erlaubt uns nicht, eine Dog ArrayList an eine Variable zu übergeben, die Animal ArrayList erwartet.

Sie fragen warum?

Stellen Sie sich vor, JAVA würde es zulassen, dass die ArrayList "Dog" - Hunde - in die ArrayList "Animal" - Tiere - eingefügt wird, und in der takeAnimals()-Methode macht jemand etwas wie -

animals.add(new Cat());

denken, dass dies machbar sein sollte, weil es idealerweise ein Tier ArrayList ist und Sie in der Lage sein sollten, jede Katze hinzuzufügen, wie Katze-ist-auch-ein-Tier, aber in Wirklichkeit übergeben Sie einen Hund-Typ ArrayList es.

Jetzt denken Sie sicher, dass dasselbe auch mit den Arrays hätte passieren müssen. Sie haben Recht, wenn Sie so denken.

Wenn jemand versucht, das Gleiche mit Arrays zu tun, dann werden Arrays auch einen Fehler auslösen, aber Arrays behandeln diesen Fehler zur Laufzeit, während ArrayLists diesen Fehler zur Kompilierzeit behandeln.

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