Яка різниця між <? супер E> і <? подовжує E>?


147

У чому різниця між <? super E>і <? extends E>?

Наприклад, коли ви дивитесь на клас, java.util.concurrent.LinkedBlockingQueueдля конструктора є такий підпис:

public LinkedBlockingQueue(Collection<? extends E> c)

і для одного методу:

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

Відповіді:


182

Перший говорить, що це "якийсь тип, який є родоначальником Е"; другий говорить, що це "певний тип, який є підкласом E". (В обох випадках E добре.)

Таким чином, конструктор використовує ? extends Eформу, щоб гарантувати, що коли він отримує значення з колекції, всі вони будуть E або деякий підклас (тобто сумісний). drainToМетод намагається покласти значення в колекцію, так що колекція повинна мати тип елемента E або суперклас .

Наприклад, припустимо, що у вас є така ієрархія класів:

Parent extends Object
Child extends Parent

і а LinkedBlockingQueue<Parent>. Ви можете сконструювати цей прохід у List<Child>якому буде безпечно копіювати всі елементи, тому що кожен Childє батьківським. Ви не можете перейти через, List<Object>тому що деякі елементи можуть бути не сумісні з Parent.

Так само ви можете злити цю чергу в a, List<Object>тому що кожна Parent- це Object..., але ви не змогли перетягнути її в, List<Child>оскільки List<Child>очікуєте, що всі її елементи будуть сумісні Child.


25
+1. Це справді відмінна відмінність. розширюється для отримання, супер для вставки
Yishai

1
@ Jon, що ти маєш на увазі під цим (в обох випадках з E у мене все нормально) у першому абзаці?
Geek

2
@Geek: Я маю на увазі, що якщо у вас є щось на кшталт ? extends InputStreamабо ? super InputStreamтоді ви можете використовувати InputStreamаргумент як аргумент.
Джон Скіт

Я ніколи не отримав пояснення PECS Джоша Блока в ефективній Java. Однак @Yishai, це корисний спосіб запам'ятати. Можливо, ми можемо запропонувати новий мнемонічний, SAGE: Super -> Add / Get -> Extend
dcompiled

Отже, якщо я правильно прочитав, "<? Extends E>" вимагає цього "?" є підкласом "E", і "<? super E>" вимагає, щоб "E" був підкласом "?", так?
El Suscriptor Justiciero

130

Причини цього базуються на тому, як Java реалізує дженерики.

Приклад масивів

З масивами ви можете це зробити (масиви є коваріантними)

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

Але що буде, якщо ви спробуєте це зробити?

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

Цей останній рядок складеться чудово, але якщо запустити цей код, ви можете отримати ArrayStoreException. Тому що ви намагаєтеся поставити двійник у цілий масив (незалежно від того, щоб отримати доступ через посилання числа).

Це означає, що ви можете обдурити компілятор, але не можете обдурити систему типу виконання. І це так, тому що масиви - це те, що ми називаємо повторюваними типами . Це означає, що під час виконання Java знає, що цей масив був фактично створений як масив цілих чисел, до якого просто трапляється доступ через посилання типу Number[].

Отже, як ви бачите, одна річ - це фактичний тип об’єкта, а інша - тип посилання, який ви використовуєте для доступу до нього, правда?

Проблема з Java Generics

Тепер проблема з загальними типами Java полягає в тому, що компілятор відкидає інформацію про тип і вона недоступна під час виконання. Цей процес називається стиранням типу . Існують вагомі причини для застосування подібних дженериків на Java, але це довга історія, і це стосується, крім усього іншого, бінарної сумісності з попередньо існуючим кодом (див. Як ми отримали загальні дженерики ).

Але важливим моментом є те, що оскільки під час виконання відсутня інформація про тип, немає ніякого способу гарантувати, що ми не здійснюємо забруднення від купи.

Наприклад,

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

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

Якщо компілятор Java не заважає вам це робити, система типу виконання не може зупинити вас і тому, що під час виконання немає можливості визначити, що цей список повинен бути лише списком цілих чисел. Час виконання Java дозволить вам помістити все, що завгодно, до цього списку, коли він повинен містити лише цілі числа, адже коли він був створений, він оголошувався як список цілих чисел.

Таким чином, дизайнери Java переконалися, що ви не можете обдурити компілятор. Якщо ви не можете обдурити компілятор (як це можна зробити з масивами), ви також не можете обдурити систему типу виконання.

Як такий, ми кажемо, що родові типи не підлягають повторенню .

Очевидно, це перешкоджатиме поліморфізму. Розглянемо наступний приклад:

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

Тепер ви можете використовувати його так:

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

Але якщо ви спробуєте реалізувати той самий код із загальними колекціями, вам це не вдасться:

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

Ви отримаєте помилки компілятора, якщо спробуєте ...

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

Рішення полягає в тому, щоб навчитися використовувати дві потужні функції дженериків Java, відомі як коваріація та противаріантність.

Коваріація

За допомогою коваріації ви можете читати елементи зі структури, але ви нічого не можете записати в неї. Все це дійсні декларації.

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

І ви можете прочитати з myNums:

Number n = myNums.get(0); 

Тому що ви можете бути впевнені, що незалежно від фактичного списку його можна перенести на число (адже все, що розширює число, є числом, правда?)

Однак вам не дозволяється нічого вкладати в коваріантну структуру.

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

Це не дозволено, оскільки Java не може гарантувати, що є фактичним типом об'єкта в загальній структурі. Це може бути все, що розширює номер, але компілятор не може бути впевнений. Так ви можете читати, але не писати.

Суперечність

При противаріантності ви можете зробити навпаки. Ви можете помістити речі в загальну структуру, але ви не можете її прочитати.

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

У цьому випадку фактична природа об'єкта - це Перелік об'єктів, і за допомогою протиріччя можна ввести Числа до нього, в основному тому, що всі числа мають Об'єкт як їх спільного предка. Таким чином, усі Числа є об'єктами, і тому це дійсно.

Однак з цієї противаріантної структури ви нічого не можете прочитати, якщо ви отримаєте число.

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

Як бачите, якби компілятор дозволив вам написати цей рядок, ви отримаєте ClassCastException під час виконання.

Отримати / поставити принцип

Таким чином, використовуйте коваріацію, коли ви маєте намір вийняти загальні значення зі структури, використовуйте протиріччя, коли ви маєте намір ввести загальні значення в структуру та використовувати точний загальний тип, коли ви маєте намір зробити обидва.

Найкращий приклад, який я маю, є наступним, який копіює будь-які номери з одного списку в інший список. Він отримує лише предмети з джерела, і він лише ставить елементи в ціль.

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

Завдяки повноваженням коваріації та протиріччя це працює для такого випадку:

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
Ця відповідь має стикатися до вершини. Приємне пояснення.
Суреш Атта

1
@edwindalorzo, є невелика помилка, яку ви хочете виправити в програмі Contravariance. Ви кажете List<Object> myObjs = new List<Object();(чого не вистачає закриття >на секунду Object).

Фантастичні, легкі та зрозумілі приклади цих тонких концепцій!
db1234

Щось ви можете додати, щоб допомогти іншим запам'ятати речі. Коли ви хочете викликати метод із суперкласу, ви використовуєте super.methodName. При використанні <? super E>це означає "щось у superнапрямку" на відміну від чогось у extendsнапрямку. Приклад: Objectзнаходиться в superнапрямку Number(оскільки це супер клас) і Integerзнаходиться в extendsнапрямку (оскільки воно поширюється Number).
BrainStorm.exe

59

<? extends E>визначає Eяк верхню межу: "Це може бути передано E".

<? super E>визначає Eяк нижню межу: " Eможе бути передано до цього".


6
Це одне з найкращих простих / практичних підсумків різниці, що я бачив.
JAB

1
Уже десятиліттями (з ООП) я борюся з інстинктивною інверсією понять «верхній» і «нижній». Обтяжуючі! Для мене, Objectпо суті, це клас нижчого рівня, незважаючи на те, що він є найвищим суперкласом (і вертикально намальований у UML або подібних деревах спадкування). Я ніколи не міг скасувати це, незважаючи на еони спроб.

3
@ tgm1024 "надклас" та "підклас" повинні доставити вам багато клопоту.
Девід Молес

@DavidMoles, чому? Ви явно взагалі не стежите за тим, що я говорю. "суперклас" схожий на "суперсеть"; поняття спеціалізації - це зменшення застосовності у відносинах IS-A. Яблучний фрукт IS-A. Фрукт (суперклас) - це супермножина, що включає Apple (підклас) як підмножину. Це словесні стосунки прекрасні. Те, що я говорю, розбивається на думку про те, що "верхній" і "нижній" мають внутрішні відображення на "надмножина" та "підмножина". Верхні та нижні слід уникати як терміни в ОО.

1
@ Tgm1024 «Супер-» походить від латинського супера «вище, на» і «суб» від латинського підрозділами «під землею, під». Тобто, етимологічно супер є вгору, а суб знижується.
Девід Молес

12

Я спробую відповісти на це. Але щоб отримати дійсно гарну відповідь, слід переглянути книгу «Ефективна Java» Джошуа Блоха (2-е видання). Він описує мнемонічну PECS, яка розшифровується як "Producer Extends, Consumer Super".

Ідея полягає в тому, що якщо ви використовуєте код об'єктних значень від об'єкта, ви повинні використовувати extends. але якщо ви створюєте нові значення для загального типу, вам слід використовувати super.

Так, наприклад:

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

І

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

Але дійсно варто переглянути цю книгу: http://java.sun.com/docs/books/effective/


12

<? super E> засоби any object including E that is parent of E

<? extends E> засоби any object including E that is child of E .


коротка і мила відповідь.
chirag soni

7

Ви можете скористатися Google для термінів контраваріантність ( <? super E>) та коваріація ( <? extends E>). Я виявив, що найбільш корисним, коли я розумію генеріки, було для мене розуміння підпису методу Collection.addAll:

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

Так само, як ви хочете додати Stringдо List<Object>:

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

Ви також повинні мати можливість додати List<String>(або будь-яку колекцію Strings) за допомогою addAllметоду:

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

Однак ви повинні усвідомити, що a List<Object>і a List<String>не є еквівалентом, і останній не є підкласом першого. Потрібно поняття параметра коваріантного типу - тобто <? extends T>біта.

Після цього ви можете легко придумувати сценарії, в яких також потрібно протиріччя (перевірте Comparableінтерфейс).


4

Перед відповіддю; Будь ласка, майте це зрозуміти

  1. Загальна функція компілює лише час для забезпечення TYPE_SAFETY, але вона не буде доступною під час RUNTIME.
  2. Тільки посилання на Generics змусить забезпечити безпеку типу; якщо посилання не декларується з дженериками, то воно буде працювати без типу safty.

Приклад:

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

Сподіваємось, це допоможе вам зрозуміти підстановку більш чітко.

//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
Гарне пояснення з кодом. Але якби ви використовували коментарі в коді за межами блоку коду, було б краще для перегляду та більш читабельного.
Прабху

3

Підстановочний знак із верхньою межею виглядає як "? Extends Type" і означає сімейство всіх типів, які є підтипами Type, включаючи тип Type. Тип називається верхньою межею.

Підстановочний знак із нижньою межею виглядає як "супер тип" і означає сімейство всіх типів, що є супертипами типу, включаючи тип типу. Тип називається нижньою межею.


1

У вас є клас батьків і клас дочір, успадкований від батьківського класу. Батьківський клас успадковується від іншого класу під назвою клас GrandParent. Тепер, <? розширює Parent> - Це приймає батьківський клас або дочірній клас <? super Parent> - Це приймає батьківський клас або клас GrandParent

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.