В чем разница между <? супер 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». (В обоих случаях с самой 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... но вы не можете слить ее в a, List<Child>поскольку List<Child>ожидается, что все ее элементы будут совместимы Child.


25
+1. Это действительно практическая разница. расширяет, чтобы выбрать, супер, чтобы вставить.
Ишай

1
@Jon, что вы подразумеваете под (в обоих случаях с самой E все в порядке?) В первом абзаце?
Компьютерщик

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 . Потому что вы пытаетесь поместить двойное число в массив целых чисел (независимо от того, к чему вы обращаетесь через ссылку на число).

Это означает, что вы можете обмануть компилятор, но вы не можете обмануть систему типов времени выполнения. И это так, потому что массивы - это то, что мы называем типами reifiable . Это означает, что во время выполнения 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 не может гарантировать, что является фактическим типом объекта в общей структуре. Это может быть что угодно, что расширяет Number, но компилятор не может быть уверен. Так что вы можете читать, но не писать.

контрвариация

С противоположностью вы можете сделать наоборот. Вы можете поместить вещи в общую структуру, но вы не можете читать из нее.

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. Яблоко - это фрукт. Fruit (суперкласс) - это надмножество, включающее Apple (подкласс) в качестве подмножества. С этими словесными отношениями все в порядке. То, что я говорю, ломается, так это понятие, что «верхний» и «нижний» имеют внутренние отображения на «надмножество» и «подмножество». Верхний и нижний следует избегать в терминах ОО.

1
@ tgm1024 «Супер» происходит от латинского супер «выше, на», а «суб» от латинского слагаемого «под, под». То есть, этимологически, супер вверх, а суб вниз.
Дэвид Моулз

12

Я собираюсь попытаться ответить на это. Но чтобы получить действительно хороший ответ, вам следует обратиться к книге Джошуа Блоха «Эффективная Java» (2-е издание). Он описывает мнемонический PECS, который расшифровывается как «Продюсер продлевает, потребительский супер».

Идея состоит в том, что если ваш код потребляет общие значения от объекта, вам следует использовать 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 .


краткий и сладкий ответ.
Чираг Сони

7

Возможно, вы захотите поискать в Google термины contravariance ( <? super E>) и covariance ( <? 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>(или любую коллекцию String) с помощью addAllметода:

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

Однако вы должны понимать, что a List<Object>и a List<String>не эквивалентны, и последний не является подклассом первого. Что нужно, так это концепция параметра ковариантного типа - то есть <? extends T>бит.

Как только вы это сделаете, очень просто подумать о сценариях, в которых вы также хотите использовать контравариантность (проверьте Comparableинтерфейс).


4

Перед ответом; Пожалуйста, будьте уверены, что

  1. Generics только функция времени компиляции для обеспечения 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

Подстановочный знак с верхней границей выглядит как «расширяет тип» и обозначает семейство всех типов, которые являются подтипами типа, включая тип «тип». Тип называется верхней границей.

Подстановочный знак с нижней границей выглядит как «супер тип» и обозначает семейство всех типов, которые являются супертипами типа, включая тип «тип». Тип называется нижней границей.


1

У вас есть родительский класс и дочерний класс, унаследованные от родительского класса. Родительский класс унаследован от другого класса, называемого GrandParent Class. Поэтому порядок наследования - GrandParent> Parent> Child. Теперь, <? extends Parent> - Это принимает родительский класс или дочерний класс <? super Parent> - Это принимает родительский класс или класс GrandParent

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.