Jaka jest różnica między <? super E> i <? rozszerza E>?

147

Jaka jest różnica między <? super E>i <? extends E>?

Na przykład, gdy spojrzysz na klasę, zobaczysz java.util.concurrent.LinkedBlockingQueuenastępujący podpis dla konstruktora:

public LinkedBlockingQueue(Collection<? extends E> c)

a dla jednej metody:

public int drainTo(Collection<? super E> c)
Tomasz Błachowicz
źródło

Odpowiedzi:

182

Pierwsza mówi, że jest to „jakiś typ, który jest przodkiem E”; druga mówi, że jest to „jakiś typ, który jest podklasą E”. (W obu przypadkach samo E jest w porządku.)

Więc konstruktor używa ? extends Eformularza, więc gwarantuje, że kiedy pobierze wartości z kolekcji, wszystkie będą E lub jakąś podklasą (tj. Jest zgodna). drainToMetoda stara się umieścić wartości w kolekcji, więc kolekcja musi mieć typ elementu z E lub superklasę .

Na przykład załóżmy, że masz taką hierarchię klas:

Parent extends Object
Child extends Parent

i a LinkedBlockingQueue<Parent>. Możesz skonstruować to przekazanie w a, List<Child>które bezpiecznie skopiuje wszystkie elementy, ponieważ każdy Childjest rodzicem. Nie możesz przekazać a, List<Object>ponieważ niektóre elementy mogą nie być zgodne z Parent.

Podobnie możesz spuścić tę kolejkę do a, List<Object>ponieważ every Parentjest Object..., ale nie możesz go spuścić do a, List<Child>ponieważ List<Child>oczekuje, że wszystkie jego elementy będą zgodne z Child.

Jon Skeet
źródło
25
+1. To jest naprawdę praktyczna różnica. rozszerza się do pobrania, super do wstawienia.
Yishai
1
@Jon, co masz na myśli, mówiąc (w obu przypadkach samo E jest w porządku) w pierwszym akapicie?
Geek
2
@Geek: Mam na myśli to, że jeśli masz coś takiego ? extends InputStreamlub ? super InputStreammożesz użyć InputStreamjako argumentu.
Jon Skeet
Tak naprawdę nigdy nie otrzymałem wyjaśnienia PECS Josha Blocka w efektywnej Javie. Jednak @Yishai, jest to pomocny sposób na zapamiętanie. Być może możemy zaproponować nowy mnemonik, SAGE: Super -> Add / Get -> Extend
dcompiled
Więc jeśli dobrze przeczytam, „<? Extends E>” wymaga tego „?” jest podklasą „E”, a „<? super E>” wymaga, aby „E” było podklasą „?”, prawda?
El Suscriptor Justiciero
130

Powody tego są oparte na tym, jak Java implementuje typy generyczne.

Przykład tablic

Dzięki tablicom możesz to zrobić (tablice są kowariantne)

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

Ale co by się stało, gdybyś spróbował to zrobić?

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

Ta ostatnia linia skompilowałaby się dobrze, ale jeśli uruchomisz ten kod, możesz uzyskać plik ArrayStoreException. Ponieważ próbujesz wstawić podwójną do tablicy liczb całkowitych (niezależnie od tego, czy uzyskasz dostęp przez odwołanie liczbowe).

Oznacza to, że możesz oszukać kompilator, ale nie możesz oszukać systemu typów środowiska uruchomieniowego. Dzieje się tak, ponieważ tablice nazywamy typami reifiable . Oznacza to, że w czasie wykonywania Java wie, że ta tablica została faktycznie utworzona jako tablica liczb całkowitych, do której dostęp uzyskuje się przez odwołanie typu Number[].

Jak więc widać, jedną rzeczą jest rzeczywisty typ obiektu, a inną typ odniesienia, którego używasz, aby uzyskać do niego dostęp, prawda?

Problem z generycznymi językami Java

Problem z typami ogólnymi Java polega na tym, że kompilator odrzuca informacje o typie i nie są one dostępne w czasie wykonywania. Ten proces nazywa się wymazywaniem typu . Jest dobry powód do implementowania takich typów generycznych w Javie, ale to długa historia i musi to być związane, między innymi, z binarną zgodnością z wcześniej istniejącym kodem (zobacz Jak mamy dostępne typy generyczne ).

Ale ważne jest tutaj to, że ponieważ w czasie wykonywania nie ma informacji o typie, nie ma sposobu, aby upewnić się, że nie popełnimy zanieczyszczenia sterty.

Na przykład,

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

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

Jeśli kompilator Java nie powstrzyma Cię przed tym, system typów środowiska wykonawczego również nie może Cię powstrzymać, ponieważ w czasie wykonywania nie ma sposobu, aby określić, że ta lista miała być tylko listą liczb całkowitych. Środowisko wykonawcze Javy pozwoliłoby umieścić na tej liście cokolwiek zechcesz, kiedy powinna zawierać tylko liczby całkowite, ponieważ podczas tworzenia została zadeklarowana jako lista liczb całkowitych.

W związku z tym projektanci Java upewnili się, że nie można oszukać kompilatora. Jeśli nie możesz oszukać kompilatora (tak jak możemy to zrobić z tablicami), nie możesz również oszukać systemu typów wykonawczych.

W związku z tym mówimy, że typy generyczne nie podlegają ponownej wymianie .

Najwyraźniej utrudniłoby to polimorfizm. Rozważmy następujący przykład:

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

Teraz możesz go użyć w ten sposób:

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

Ale jeśli spróbujesz zaimplementować ten sam kod w kolekcjach ogólnych, nie odniesiesz sukcesu:

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

Otrzymasz błędy kompilatora, jeśli spróbujesz ...

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

Rozwiązaniem jest nauczenie się korzystania z dwóch potężnych funkcji generycznych języka Java, znanych jako kowariancja i kontrawariancja.

Kowariancja

Dzięki kowariancji możesz czytać elementy ze struktury, ale nie możesz niczego do niej zapisać. To wszystko są ważne deklaracje.

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

Możesz przeczytać myNums:

Number n = myNums.get(0); 

Ponieważ możesz być pewien, że cokolwiek zawiera rzeczywista lista, można ją przesłać dalej do liczby (w końcu wszystko, co rozszerza liczbę, jest liczbą, prawda?)

Nie możesz jednak umieszczać niczego w kowariantnej strukturze.

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

Nie byłoby to dozwolone, ponieważ Java nie może zagwarantować, jaki jest rzeczywisty typ obiektu w strukturze ogólnej. Może to być wszystko, co rozszerza Number, ale kompilator nie może być tego pewien. Możesz więc czytać, ale nie pisać.

Sprzeczność

Z kontrawariancją możesz zrobić odwrotnie. Możesz umieścić rzeczy w ogólnej strukturze, ale nie możesz z niej odczytać.

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

W tym przypadku rzeczywistą naturą obiektu jest lista obiektów, a przez kontrawariancję możesz wstawić do niej liczby, zasadniczo dlatego, że wszystkie liczby mają wspólnego przodka obiektu. Jako takie, wszystkie liczby są obiektami i dlatego jest to poprawne.

Jednak nie możesz bezpiecznie odczytać niczego z tej kontrawariantnej struktury, zakładając, że otrzymasz liczbę.

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

Jak widać, gdyby kompilator zezwolił na napisanie tej linii, w czasie wykonywania wystąpiłby wyjątek ClassCastException.

Zasada Get / Put

W związku z tym użyj kowariancji, gdy zamierzasz tylko wyprowadzić wartości ogólne ze struktury, użyj kontrawariancji, gdy zamierzasz umieścić tylko wartości ogólne w strukturze i użyj dokładnego typu ogólnego, jeśli zamierzasz zrobić jedno i drugie.

Najlepszym przykładem, jaki mam, jest następujący, który kopiuje wszelkiego rodzaju liczby z jednej listy do innej listy. To tylko staje się przedmioty od źródła, a jedynie stawia pozycji w cel.

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

Dzięki możliwościom kowariancji i kontrawariancji działa to w takim przypadku:

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);
Edwin Dalorzo
źródło
27
Ta odpowiedź powinna uderzać w górę. Niezłe wyjaśnienie.
Suresh Atta
1
@edwindalorzo, jest mała literówka, którą chcesz naprawić w Contravariance. Mówisz List<Object> myObjs = new List<Object();(co brakuje zamknięcia >na sekundę Object).
Fantastyczne, łatwe i jasne przykłady tych subtelnych koncepcji!
db1234
Coś, co możesz dodać, aby pomóc innym zapamiętać rzeczy. Jeśli chcesz wywołać metodę z super klasy, użyj super.methodName. Podczas używania <? super E>oznacza „coś w superkierunku” w przeciwieństwie do czegoś w extendskierunku. Przykład: Objectjest w superkierunku Number(ponieważ jest to superklasa) i Integerjest w extendskierunku (ponieważ się rozciąga Number).
BrainStorm.exe,
59

<? extends E>definiuje Ejako górną granicę: „To może być rzutowane na E”.

<? super E>definiuje Ejako dolną granicę: „ Emożna do tego rzutować”.

David Moles
źródło
6
To jedno z najlepszych prostych / praktycznych podsumowań różnic, jakie widziałem.
JAB,
1
Od dziesięcioleci (z OOP) walczę z instynktowną inwersją pojęć „górny” i „dolny”. Obciążający! Dla mnie Objectjest z natury klasą niższego poziomu, mimo że jest pozycją jako ostateczną superklasę (i jest rysowana pionowo w UML lub podobnych drzewach dziedziczenia). Nigdy nie byłem w stanie tego cofnąć, pomimo eonów prób.
3
@ tgm1024 „superklasa” i „podklasa” muszą sprawić Ci wiele kłopotów.
David Moles
@DavidMoles, dlaczego? Wyraźnie w ogóle nie podążasz za tym, co mówię. „superklasa” jest podobna do „superklasy”; pojęcie specjalizacji to ograniczenie stosowalności w ramach relacji IS-A. Jabłko to owoc. Owoce (nadklasa) to nadzbiór obejmujący Apple (podklasę) jako podzbiór. Ten werbalny związek jest w porządku. To, co mówię, rozbija pogląd, że „górny” i „dolny” mają wewnętrzne odwzorowania na „nadzbiór” i „podzbiór”. Należy unikać górnych i dolnych terminów w OO.
1
@ tgm1024 „Super-” pochodzi od łacińskiego Super „powyżej, na” i „sub” od łacińskiego sub „pod, poniżej”. To znaczy, etymologicznie, super jest w górę, a sub jest w dół.
David Moles,
12

Spróbuję odpowiedzieć na to pytanie. Ale aby uzyskać naprawdę dobrą odpowiedź, powinieneś zajrzeć do książki Joshua Blocha Effective Java (2nd Edition). Opisuje mnemonik PECS, który oznacza „Producer Extends, Consumer Super”.

Chodzi o to, że jeśli kod pobiera wartości ogólne z obiektu, powinieneś użyć extends. ale jeśli tworzysz nowe wartości dla typu ogólnego, powinieneś użyć super.

Na przykład:

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

I

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

Ale naprawdę powinieneś sprawdzić tę książkę: http://java.sun.com/docs/books/effective/

codefinger
źródło
12

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

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

M Sach
źródło
krótka i słodka odpowiedź.
chirag soni
7

Możesz wyszukać w Google terminy kontrawariancja ( <? super E>) i kowariancja ( <? extends E>). Odkryłem, że najbardziej użyteczną rzeczą podczas rozumienia typów ogólnych jest zrozumienie sygnatury metody Collection.addAll:

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

Tak jak chciałbyś móc dodać a Stringdo List<Object>:

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

Powinieneś także móc dodać List<String>(lub dowolną kolekcję String) za pomocą addAllmetody:

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

Należy jednak zdać sobie sprawę, że a List<Object>i a List<String>nie są równoważne, a drugie nie jest podklasą pierwszego. Potrzebne jest pojęcie kowariantnego parametru typu - czyli <? extends T>bitu.

Gdy już to zrobisz, łatwo jest wymyślić scenariusze, w których chcesz również kontrawariancji (sprawdź Comparableinterfejs).

oxbow_lakes
źródło
4

Przed odpowiedzią; Jasne, proszę

  1. Generics tylko funkcja czasu kompilacji, aby zapewnić TYPE_SAFETY, nie będzie dostępna podczas RUNTIME.
  2. Tylko odniesienie do Generics wymusi bezpieczeństwo typu; jeśli referencja nie zostanie zadeklarowana za pomocą typów generycznych, będzie działać bez zabezpieczenia typu.

Przykład:

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

Mam nadzieję, że pomoże ci to lepiej zrozumieć symbole wieloznaczne.

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


            } 
    }

    }
}
Kanagavelu Sugumar
źródło
@Dana Zobacz także stackoverflow.com/questions/15255929/…
Kanagavelu Sugumar
1
Dobre wyjaśnienie z kodem. Ale gdybyś użył komentarzy w kodzie poza blokiem kodu, byłoby to lepsze do przeglądania i bardziej czytelne.
Prabhu
3

Symbol wieloznaczny z górną granicą wygląda jak „? Rozszerza typ” i oznacza rodzinę wszystkich typów, które są podtypami typu, przy czym typ jest uwzględniony. Typ jest nazywany górną granicą.

Symbol wieloznaczny z dolną granicą wygląda jak „? Super Type” i oznacza rodzinę wszystkich typów, które są nadtypami typu Type, przy czym typ Type jest uwzględniony. Typ nazywa się dolną granicą.

vinaynag
źródło
1

Masz klasę Parent i klasę Child dziedziczoną z klasy Parent.Klasa Parent jest dziedziczona z innej klasy o nazwie GrandParent Class, więc kolejność dziedziczenia to Dziadek> Rodzic> Dziecko. Teraz <? extends Parent> - akceptuje klasę rodzica lub klasę podrzędną <? super Parent> - akceptuje klasę Parent lub klasę GrandParent

Crox
źródło