В чем практическая разница между "ArrayList<A>" и "ArrayList<? Extends A>"?

Я посмотрел на вопросы q1, q2, q3, но они не совсем соответствуют моему вопросу.

Обратите внимание, что ArrayList<A> and ArrayList<? extends A> должны использоваться для объявления переменной или параметра (не для создания нового универсального класса).

Оба выражения эквивалентны при объявлении атрибута объекта (случай 1)?:

class Foo {

  private ArrayList<A> aList; // == ArrayList<? extends A> aList;
}

РЕДАКТИРОВАТЬ: оба выражения эквивалентны с точки зрения того, какие объекты разрешено добавлять в aList?, но отличается в том же смысле, что и в следующем случае?

но отличаются ли они при использовании в объявлении параметра (случай 2)?

void methodFoo(ArrayList<A> al)  !=  void methodFoo(ArrayList<? extends A> al)

потому что первый только позволяет передавать объекты ArrayList, в то время как второй будет как "более разрешающий", позволяющий отправлятьArrayList<A1> а также ArrayList<A2> (до тех пор, пока А1 и А2 расширяет А)?

Если это правильно, есть ли другой сценарий, в котором два выражения эффективно различаются?

Спасибо,

6 ответов

Решение

Давайте посмотрим на некоторые практические примеры. Скажем, у вас есть:

List<Number> list;

Это означает, что все, что присвоено этой переменной или полю, занимает Number и выводы Number, так что вы всегда знаете, чего ожидать. Integer можно добавить в этот список с Integer продолжается Number, Тем не менее, вы не можете назначить, скажем, ArrayList<Long> к этому списку.

Но рассмотрим этот случай:

List<? extends Number> list;

Этот говорит: эй, это список чего-то, что расширяет Number, но никто не знает, что именно. Что это значит? Это означает, что вы можете назначить, например, ArrayList<Long> в этот список, который вы не могли в первом случае. Вы все еще знаете, что все, что выводит этот список, будет Number, но вы не можете положить Integer в этом больше.

Существует также противоположный случай:

List<? super Number> list;

Напечатав, что вы говорите: это список Number или его суперклассы. Здесь все становится наоборот. Список теперь может ссылаться на ArrayList<Object> а также ArrayList<Number>, Теперь мы не знаем, что выведет этот список. Это будет Number? Это будет Object? Но теперь мы знаем, что можем поставить Number в этом списке, а также любой подкласс Number лайк Integer или же Long,

Между прочим, есть правило, согласно которому производитель расширяется - потребитель супер (PECS для краткости). Если вам нужен список для вывода значений, это производитель, это второй случай. Если вам нужен список для принятия значений, это потребитель, это третий случай. Если вам нужны оба, не используйте подстановочные знаки (это первый случай).

Я надеюсь, что это проясняет дело.

Это объяснит разницу:

public class GenericsTest {
  private ArrayList<A> la;
  private ArrayList<? extends A> lexta;

  void doListA(ArrayList<A> la) {}
  void doListExtA(ArrayList<? extends A> lexta) {}

  void tester() {
    la = new ArrayList<SubA>(); // Compiler error: Type mismatch
    doListA(new ArrayList<SubA>());  // Compiler error: Type mismatch
    lexta = new ArrayList<SubA>();
    doListExtA(new ArrayList<SubA>());
  }

  static class A {}
  static class SubA extends A {}
}

Как видите, вызов метода и присвоение поля переменной / экземпляра имеют одинаковые правила. Посмотрите на вызов метода как на присваивание вашего аргумента объявленному параметру.

ArrayList<A> означает конкретный класс А, где как ArrayList<? extends A> означает класс A или любой класс, который расширяет A (подкласс A), что делает его более общим

С помощью private ArrayList<A> aList; как объявление переменной на самом деле не эквивалентно использованию подстановочного знака private ArrayList<? extends A> aList;

Версия с подстановочными символами позволит вам назначить любые списки массивов типов, которые расширяют сами A и A, но откажутся добавлять элементы в список, так как он не может решить, является ли он безопасным типом. С ArrayList<A> с другой стороны, вы можете назначать только ArrayLists (или расширения ArrayList) типа A, а затем вы можете добавить элементы A и любые элементы, расширяющие A, до него.

К вашему сведению: вы должны предпочесть использование более абстрактного типа для объявления ваших переменных / параметров, таких как List<A> или же Collection<A>,

Сначала это трудно понять, но наследование не применимо к генерикам, т. Е. Если B расширяет A, List<B> не является "подклассом" (не может быть назначен) List<A>, В дальнейшем, List<? extends A> не является "подклассом" (не может быть назначен) List<A>,

Основное отличие состоит в том, что если универсальная форма используется в качестве аргумента или возвращаемого типа в методе в базовом классе (или интерфейсе), это позволяет большему диапазону сигнатур типов считать переопределение вместо перегрузки.

Переопределение-перегрузка функции в Java

Например, следующий код является допустимым (в Java 7):

interface A
{
    List<? extends Number> getSomeNumbers();
}

class B implements A
{
    @Override
    public ArrayList<Integer> getSomeNumbers()
    {
        return new ArrayList<>();
    }
}

Разница между перечислениями расширяет ZipEntry> и перечисление ?

Все это означает, что иногда вы можете написать код, который требует меньше приведения, когда его использует кто-то другой. Что должно не только сократить объем печати, но и устранить возможные сбои.

Конечно, проблема с Java-дженериками заключается в том, что они были представлены способом, который был ограничен обратной совместимостью. Так что не все работает так, как вы могли бы подумать, и детали становятся довольно противоречивыми относительно того, что именно работает, а что нет.

http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html

Другие вопросы по тегам