Was ist ein Unterschied zwischen <? Super E> und <? erweitert E>?


147

Was ist der Unterschied zwischen <? super E>und <? extends E>?

Wenn Sie sich beispielsweise die Klasse ansehen, java.util.concurrent.LinkedBlockingQueuegibt es die folgende Signatur für den Konstruktor:

public LinkedBlockingQueue(Collection<? extends E> c)

und zum einen für die Methode:

public int drainTo(Collection<? super E> c)

Antworten:


182

Der erste sagt, dass es "ein Typ ist, der ein Vorfahr von E ist"; der zweite sagt, dass es "ein Typ ist, der eine Unterklasse von E ist". (In beiden Fällen ist E selbst in Ordnung.)

Der Konstruktor verwendet das ? extends EFormular, um sicherzustellen, dass beim Abrufen von Werten aus der Auflistung alle E oder eine Unterklasse sind (dh kompatibel). Die drainToMethode versucht, Werte in die Auflistung einzufügen, daher muss die Auflistung einen Elementtyp E oder eine Oberklasse haben .

Angenommen, Sie haben eine Klassenhierarchie wie diese:

Parent extends Object
Child extends Parent

und a LinkedBlockingQueue<Parent>. Sie können diese Übergabe in einer List<Child>erstellen, die alle Elemente sicher kopiert, da alle Childübergeordnete Elemente sind . Sie konnten a nicht übergeben, List<Object>da einige Elemente möglicherweise nicht kompatibel sind Parent.

Ebenso können Sie diese Warteschlange in eine entleeren, List<Object>weil jede Parenteine Object... aber Sie können sie nicht in eine entleeren, List<Child>weil List<Child>erwartet wird, dass alle ihre Elemente mit kompatibel sind Child.


25
+1. Das ist wirklich der praktische Unterschied. erstreckt sich zum holen, super zum einfügen.
Yishai

1
@ Jon was meinst du mit (in beiden Fällen ist E selbst in Ordnung.) Im ersten Absatz?
Geek

2
@Geek: Ich meine, wenn du so etwas hast ? extends InputStreamoder ? super InputStreamdann kannst du ein InputStreamals Argument verwenden.
Jon Skeet

Ich habe die PECS-Erklärung von Josh Block in effektivem Java nie wirklich bekommen. @Yishai, dies ist jedoch eine hilfreiche Art, sich zu erinnern. Vielleicht können wir eine neue Mnemonik vorschlagen, SAGE: Super -> Add / Get -> Extend
dcompiled

Wenn ich es also richtig lese, erfordert "<? Erweitert E>" das "?" ist eine Unterklasse von "E" und "<? super E>" erfordert, dass "E" eine Unterklasse von "?" ist, richtig?
El Suscriptor Justiciero

130

Die Gründe dafür hängen davon ab, wie Java Generika implementiert.

Ein Arrays-Beispiel

Mit Arrays können Sie dies tun (Arrays sind kovariant)

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

Aber was würde passieren, wenn Sie dies versuchen?

myNumber[0] = 3.14; //attempt of heap pollution

Diese letzte Zeile würde gut kompiliert werden, aber wenn Sie diesen Code ausführen, könnten Sie eine bekommen ArrayStoreException. Weil Sie versuchen, ein Double in ein Integer-Array einzufügen (unabhängig davon, ob Sie über eine Zahlenreferenz darauf zugreifen).

Dies bedeutet, dass Sie den Compiler täuschen können, aber Sie können das Laufzeitsystem nicht täuschen. Und das ist so, weil Arrays das sind, was wir als reifizierbare Typen bezeichnen . Dies bedeutet, dass Java zur Laufzeit weiß, dass dieses Array tatsächlich als Array von Ganzzahlen instanziiert wurde, auf die einfach über eine Referenz vom Typ zugegriffen wird Number[].

Wie Sie sehen, ist eine Sache der tatsächliche Typ des Objekts und eine andere die Art der Referenz, mit der Sie darauf zugreifen, oder?

Das Problem mit Java Generics

Das Problem bei generischen Java-Typen besteht nun darin, dass die Typinformationen vom Compiler verworfen werden und zur Laufzeit nicht verfügbar sind. Dieser Vorgang wird als Typlöschung bezeichnet . Es gibt gute Gründe, solche Generika in Java zu implementieren, aber das ist eine lange Geschichte, und sie hat unter anderem mit der binären Kompatibilität mit bereits vorhandenem Code zu tun (siehe Wie wir die Generika erhalten haben, die wir haben ).

Der wichtige Punkt hierbei ist jedoch, dass es zur Laufzeit keine Typinformationen gibt und es keine Möglichkeit gibt, sicherzustellen, dass wir keine Haufenverschmutzung begehen.

Zum Beispiel,

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);

List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution

Wenn der Java-Compiler Sie nicht daran hindert, kann Sie das Laufzeitsystem auch nicht daran hindern, da zur Laufzeit nicht festgestellt werden kann, dass diese Liste nur eine Liste von Ganzzahlen sein sollte. Mit der Java-Laufzeit können Sie alles, was Sie wollen, in diese Liste einfügen, wenn sie nur Ganzzahlen enthalten soll, da sie beim Erstellen als Ganzzahlliste deklariert wurde.

Daher haben die Entwickler von Java sichergestellt, dass Sie den Compiler nicht täuschen können. Wenn Sie den Compiler nicht täuschen können (wie wir es mit Arrays tun können), können Sie auch das Laufzeitsystem nicht täuschen.

Daher sagen wir, dass generische Typen nicht reifizierbar sind .

Offensichtlich würde dies den Polymorphismus behindern. Betrachten Sie das folgende Beispiel:

static long sum(Number[] numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Jetzt können Sie es so verwenden:

Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};

System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

Wenn Sie jedoch versuchen, denselben Code mit generischen Sammlungen zu implementieren, ist dies nicht erfolgreich:

static long sum(List<Number> numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Sie würden Compiler-Fehler bekommen, wenn Sie versuchen ...

List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);

System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error

Die Lösung besteht darin, zu lernen, zwei leistungsstarke Funktionen von Java-Generika zu verwenden, die als Kovarianz und Kontravarianz bekannt sind.

Kovarianz

Mit Kovarianz können Sie Elemente aus einer Struktur lesen, aber nichts hineinschreiben. All dies sind gültige Erklärungen.

List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();

Und Sie können lesen aus myNums:

Number n = myNums.get(0); 

Da Sie sicher sein können, dass die aktuelle Liste in eine Nummer hochgesendet werden kann (schließlich ist alles, was die Nummer erweitert, eine Nummer, oder?)

Sie dürfen jedoch nichts in eine kovariante Struktur einfügen.

myNumst.add(45L); //compiler error

Dies wäre nicht zulässig, da Java nicht garantieren kann, welcher Objekttyp in der generischen Struktur tatsächlich vorhanden ist. Es kann alles sein, was Number erweitert, aber der Compiler kann nicht sicher sein. Sie können also lesen, aber nicht schreiben.

Kontravarianz

Mit Kontravarianz können Sie das Gegenteil tun. Sie können Dinge in eine generische Struktur einfügen, aber Sie können nicht daraus vorlesen.

List<Object> myObjs = new List<Object>();
myObjs.add("Luke");
myObjs.add("Obi-wan");

List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);

In diesem Fall ist die tatsächliche Natur des Objekts eine Liste von Objekten, und durch Kontravarianz können Sie Zahlen einfügen, im Grunde genommen, weil alle Zahlen Objekt als gemeinsamen Vorfahren haben. Als solche sind alle Zahlen Objekte, und daher ist dies gültig.

Sie können jedoch nichts aus dieser kontravarianten Struktur sicher lesen, vorausgesetzt, Sie erhalten eine Zahl.

Number myNum = myNums.get(0); //compiler-error

Wie Sie sehen können, erhalten Sie zur Laufzeit eine ClassCastException, wenn der Compiler Ihnen das Schreiben dieser Zeile erlaubt.

Get / Put-Prinzip

Verwenden Sie daher die Kovarianz, wenn Sie nur generische Werte aus einer Struktur entfernen möchten, verwenden Sie die Kontravarianz, wenn Sie nur generische Werte in eine Struktur einfügen möchten, und verwenden Sie den genauen generischen Typ, wenn Sie beides ausführen möchten.

Das beste Beispiel, das ich habe, ist das folgende, das jede Art von Zahlen von einer Liste in eine andere Liste kopiert. Es nur bekommt Elemente aus der Quelle, und es nur bringt Elemente im Ziel.

public static void copy(List<? extends Number> source, List<? super Number> target) {
    for(Number number : source) {
        target(number);
    }
}

Dank der Kraft der Kovarianz und Kontravarianz funktioniert dies für einen Fall wie diesen:

List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();

copy(myInts, myObjs);
copy(myDoubles, myObjs);

27
Diese Antwort sollte nach oben stoßen. Schöne Erklärung.
Suresh Atta

1
@edwindalorzo, es gibt einen kleinen Tippfehler, den Sie unter Contravariance beheben möchten. Sie sagen List<Object> myObjs = new List<Object();(was den Abschluss >für die Sekunde fehlt Object).

Fantastische, einfache und klare Beispiele für diese subtilen Konzepte!
db1234

Etwas, das Sie hinzufügen könnten, um anderen zu helfen, sich an Dinge zu erinnern. Wenn Sie eine Methode aus einer Superklasse aufrufen möchten, verwenden Sie super.methodName. Bei der Verwendung <? super E>bedeutet es "etwas in der superRichtung" im Gegensatz zu etwas in der extendsRichtung. Beispiel: Objectist in der superRichtung von Number(da es eine Superklasse ist) und Integerist in der extendsRichtung (da es sich ausdehnt Number).
BrainStorm.exe

59

<? extends E>definiert Eals Obergrenze: "Dies kann umgewandelt werden E".

<? super E>definiert Eals die Untergrenze: " Ekann dazu gegossen werden."


6
Dies ist eine der besten einfachen / praktischen Zusammenfassungen des Unterschieds, den ich gesehen habe.
JAB

1
Seit Jahrzehnten (mit OOP) kämpfe ich gegen eine instinktive Umkehrung der Begriffe "oben" und "unten". Erschwerend! Für mich Objectist es von Natur aus eine niedrigere Klasse, trotz ihrer Position als ultimative Oberklasse (und vertikal in UML oder ähnlichen Vererbungsbäumen gezeichnet). Ich konnte dies trotz Äonen des Versuchs nie rückgängig machen.

3
@ tgm1024 "Superklasse" und "Unterklasse" müssen Ihnen viel Ärger bereiten.
David Moles

@ DavidMoles, warum? Sie folgen eindeutig überhaupt nicht dem, was ich sage. "Superklasse" ist vergleichbar mit "Obermenge"; Der Begriff der Spezialisierung ist eine Verringerung der Anwendbarkeit im Rahmen der IS-A-Beziehung. Apfel ist eine Frucht. Obst (Oberklasse) ist eine Obermenge, die Apple (Unterklasse) als Untermenge enthält. Diese verbale Beziehung ist in Ordnung. Was ich sage, bricht zusammen, ist die Vorstellung, dass "obere" und "untere" intrinsische Zuordnungen zu "Obermenge" und "Teilmenge" haben. Ober und Unter sollten als Begriffe in OO vermieden werden.

1
@ tgm1024 "Super-" kommt vom lateinischen super "oben, am" und "sub-" vom lateinischen sub "unter, unter". Das heißt, etymologisch ist Super oben und Sub unten.
David Moles

12

Ich werde versuchen, dies zu beantworten. Aber um eine wirklich gute Antwort zu erhalten, sollten Sie Joshua Blochs Buch Effective Java (2nd Edition) lesen. Er beschreibt die Mnemonik PECS, die für "Producer Extends, Consumer Super" steht.

Die Idee ist, dass Sie Erweiterungen verwenden sollten, wenn Ihr Code die generischen Werte aus dem Objekt verbraucht. Wenn Sie jedoch neue Werte für den generischen Typ erstellen, sollten Sie super verwenden.

Also zum Beispiel:

public void pushAll(Iterable<? extends E> src) {
  for (E e: src) 
    push(e);
}

Und

public void popAll(Collection<? super E> dst) {
  while (!isEmpty())
    dst.add(pop())
}

Aber wirklich sollten Sie dieses Buch lesen : http://java.sun.com/docs/books/effective/


12

<? super E> meint any object including E that is parent of E

<? extends E> meint any object including E that is child of E .


kurze und süße Antwort.
Chirag Soni

7

Möglicherweise möchten Sie nach den Begriffen contravariance ( <? super E>) und covariance ( <? extends E>) googeln . Ich fand, dass das Nützlichste beim Verständnis von Generika für mich war, die Methodensignatur von zu verstehen Collection.addAll:

public interface Collection<T> {
    public boolean addAll(Collection<? extends T> c);
}

So wie Sie in der Lage sein möchten, a Stringzu einem hinzuzufügen List<Object>:

List<Object> lo = ...
lo.add("Hello")

Sie sollten auch in der Lage sein, eine List<String>(oder eine beliebige Sammlung von Strings) über die folgende addAllMethode hinzuzufügen :

List<String> ls = ...
lo.addAll(ls)

Sie sollten jedoch erkennen, dass a List<Object>und a List<String>nicht gleichwertig sind und dass letztere auch keine Unterklasse der ersteren sind. Was benötigt wird, ist das Konzept eines kovarianten Typparameters - dh der<? extends T> Bits.

Sobald Sie dies haben, ist es einfach, sich Szenarien vorzustellen, in denen Sie auch Kontravarianz wünschen (überprüfen Sie die ComparableSchnittstelle).


4

Vor der Antwort; Bitte stellen Sie das klar

  1. Generics kompilieren nur die Zeitfunktion, um sicherzustellen, dass TYPE_SAFETY während RUNTIME nicht verfügbar ist.
  2. Nur eine Referenz mit Generika erzwingt die Typensicherheit. Wenn die Referenz nicht mit Generika deklariert ist, funktioniert sie ohne Typ safty.

Beispiel:

List stringList = new ArrayList<String>();
stringList.add(new Integer(10)); // will be successful.

Hoffe, dies wird Ihnen helfen, Wildcard klarer zu verstehen.

//NOTE CE - Compilation Error
//      4 - For

class A {}

class B extends A {}

public class Test {

    public static void main(String args[]) {

        A aObj = new A();
        B bObj = new B();

        //We can add object of same type (A) or its subType is legal
        List<A> list_A = new ArrayList<A>();
        list_A.add(aObj);
        list_A.add(bObj); // A aObj = new B(); //Valid
        //list_A.add(new String()); Compilation error (CE);
        //can't add other type   A aObj != new String();


        //We can add object of same type (B) or its subType is legal
        List<B> list_B = new ArrayList<B>();
        //list_B.add(aObj); CE; can't add super type obj to subclass reference
        //Above is wrong similar like B bObj = new A(); which is wrong
        list_B.add(bObj);



        //Wild card (?) must only come for the reference (left side)
        //Both the below are wrong;   
        //List<? super A> wildCard_Wrongly_Used = new ArrayList<? super A>();
        //List<? extends A> wildCard_Wrongly_Used = new ArrayList<? extends A>();


        //Both <? extends A>; and <? super A> reference will accept = new ArrayList<A>
        List<? super A> list_4__A_AND_SuperClass_A = new ArrayList<A>();
                        list_4__A_AND_SuperClass_A = new ArrayList<Object>();
                      //list_4_A_AND_SuperClass_A = new ArrayList<B>(); CE B is SubClass of A
                      //list_4_A_AND_SuperClass_A = new ArrayList<String>(); CE String is not super of A  
        List<? extends A> list_4__A_AND_SubClass_A = new ArrayList<A>();
                          list_4__A_AND_SubClass_A = new ArrayList<B>();
                        //list_4__A_AND_SubClass_A = new ArrayList<Object>(); CE Object is SuperClass of A


        //CE; super reference, only accepts list of A or its super classes.
        //List<? super A> list_4__A_AND_SuperClass_A = new ArrayList<String>(); 

        //CE; extends reference, only accepts list of A or its sub classes.
        //List<? extends A> list_4__A_AND_SubClass_A = new ArrayList<Object>();

        //With super keyword we can use the same reference to add objects
        //Any sub class object can be assigned to super class reference (A)                  
        list_4__A_AND_SuperClass_A.add(aObj);
        list_4__A_AND_SuperClass_A.add(bObj); // A aObj = new B();
        //list_4__A_AND_SuperClass_A.add(new Object()); // A aObj != new Object(); 
        //list_4__A_AND_SuperClass_A.add(new String()); CE can't add other type

        //We can't put anything into "? extends" structure. 
        //list_4__A_AND_SubClass_A.add(aObj); compilation error
        //list_4__A_AND_SubClass_A.add(bObj); compilation error
        //list_4__A_AND_SubClass_A.add("");   compilation error

        //The Reason is below        
        //List<Apple> apples = new ArrayList<Apple>();
        //List<? extends Fruit> fruits = apples;
        //fruits.add(new Strawberry()); THIS IS WORNG :)

        //Use the ? extends wildcard if you need to retrieve object from a data structure.
        //Use the ? super wildcard if you need to put objects in a data structure.
        //If you need to do both things, don't use any wildcard.


        //Another Solution
        //We need a strong reference(without wild card) to add objects 
        list_A = (ArrayList<A>) list_4__A_AND_SubClass_A;
        list_A.add(aObj);
        list_A.add(bObj);

        list_B = (List<B>) list_4__A_AND_SubClass_A;
        //list_B.add(aObj); compilation error
        list_B.add(bObj);

        private Map<Class<? extends Animal>, List<? extends Animal>> animalListMap;

        public void registerAnimal(Class<? extends Animal> animalClass, Animal animalObject) {

            if (animalListMap.containsKey(animalClass)) {
                //Append to the existing List
                 /*    The ? extends Animal is a wildcard bounded by the Animal class. So animalListMap.get(animalObject);
                 could return a List<Donkey>, List<Mouse>, List<Pikachu>, assuming Donkey, Mouse, and Pikachu were all sub classes of Animal. 
                 However, with the wildcard, you are telling the compiler that you don't care what the actual type is as long as it is a sub type of Animal.      
                 */   
                //List<? extends Animal> animalList = animalListMap.get(animalObject);
                //animalList.add(animalObject);  //Compilation Error because of List<? extends Animal>
                List<Animal> animalList = animalListMap.get(animalObject);
                animalList.add(animalObject);      


            } 
    }

    }
}



1
Gute Erklärung mit Code. Wenn Sie jedoch Kommentare in Code außerhalb des Codeblocks verwendet hätten, wäre dies besser zum Anzeigen und besser lesbar.
Prabhu

3

Ein Platzhalter mit einer Obergrenze sieht aus wie "? Erweitert Typ" und steht für die Familie aller Typen, die Untertypen des Typs sind, wobei der Typ Typ enthalten ist. Typ wird als Obergrenze bezeichnet.

Ein Platzhalter mit einer Untergrenze sieht aus wie "? Super Type" und steht für die Familie aller Typen, bei denen es sich um Supertypen des Typs handelt, wobei der Typ Type enthalten ist. Typ wird als Untergrenze bezeichnet.


1

Sie haben eine Parent-Klasse und eine Child-Klasse, die von der Parent-Klasse geerbt wurden. Die Parent-Klasse wird von einer anderen Klasse namens GrandParent-Klasse geerbt. Die Reihenfolge der Vererbung lautet also GrandParent> Parent> Child. Nun, <? erweitert Parent> - Dies akzeptiert Parent-Klasse oder eine Child-Klasse <? super Parent> - Dies akzeptiert die Parent-Klasse oder eine GrandParent-Klasse

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.