Другое

Java Generics: List<? super T> против <? extends T> - объяснение

Узнайте ключевые различия между wildcard List<? super T> и List<? extends T> в Java. Понимайте, почему можно добавлять элементы в один тип, но не в другой, с практическими примерами и лучшими практиками.

В чем разница между List<? super T> и List<? extends T> в Java?

Я заметил, что когда я использую List<? extends T>, я не могу добавлять элементы в него с помощью list.add(e), но я могу добавлять элементы в List<? super T>. Можете ли вы объяснить разницу между этими двумя обобщенными конструкциями Java и почему они ведут себя по-разному при добавлении элементов?

Основное различие между List<? super T> и List<? extends T> в Java заключается в их дисперсии и шаблонах использования: List<? extends T> (подстановочный знак с верхней границей) позволяет считывать элементы типа T или его подтипов, но предотвращает добавление элементов, в то время как List<? super T> (подстановочный знак с нижней границей) позволяет добавлять элементы типа T или его подтипов, но обеспечивает ограниченные гарантии при чтении. Это поведение вытекает из «Принципа Получить и Поместить», который гласит, что следует использовать подстановочные знаки, когда вам нужно либо получать значения из структуры данных, либо помещать значения в нее, но не одновременно.

Содержание


Основы подстановочных знаков

Подстановочные знаки Java обеспечивают гибкость в обобщенном программировании, позволяя писать код, который работает с семействами типов, а не с одним конкретным типом. Вопросительный знак ? представляет неизвестный тип, который может быть ограничен двумя способами:

  • Подстановочный знак с верхней границей: List<? extends T> - представляет список, который может содержать экземпляры типа T или любого из его подтипов
  • Подстановочный знак с нижней границей: List<? super T> - представляет список, который может содержать экземпляры типа T или любого из его супертипов

Эти конструкции особенно полезны, когда вы хотите создать более гибкие API, которые могут работать с разными иерархиями типов, не жертвуя безопасностью типов.

Согласно документации Oracle Java, подстановочные знаки следуют принципу “in” и “out”, где “in” переменная определяется с использованием подстановочного знака с верхней границей с помощью ключевого слова extends, а “out” переменная определяется иначе.


Объяснение Принципа Получить и Поместить

Принцип Получить и Поместить - это основное руководство по использованию подстановочных знаков в обобщенных типах Java, как объясняется в нескольких авторитетных источниках:

“Используйте подстановочный знак extends, когда вам нужно только получать значения из структуры, используйте подстановочный знак super, когда вам нужно только помещать значения в структуру, и не используйте подстановочные знаки, когда вы делаете и то, и другое.”

Этот принцип помогает объяснить, почему можно добавлять элементы в List<? super T>, но не в List<? extends T>:

  1. Операции получения (чтение): Когда вам нужно только извлекать элементы из коллекции, используйте ? extends T
  2. Операции помещения (запись): Когда вам нужно только добавлять элементы в коллекцию, используйте ? super T
  3. Оба типа операций: Когда вам нужно как считывать, так и записывать в коллекцию, используйте точный параметр типа без подстановочных знаков

Поведение List<? extends T>

Когда вы объявляете переменную как List<? extends T>, вы создаете подстановочный знак с верхней границей, который может содержать любой список, тип элемента которого является T или подтипом T. Вот как он ведет себя:

Операции чтения

  • Вы можете безопасно считывать элементы из списка и рассматривать их как тип T
  • Любой извлеченный элемент будет типа T или его подтипа, поэтому вы можете присвоить его переменной типа T
  • Пример: List<? extends Number> numberList = new ArrayList<Integer>(); - вы можете считывать значения Integer и рассматривать их как Number

Операции записи

  • Вы не можете добавлять элементы в List<? extends T> с помощью list.add(e)
  • Компилятор предотвращает это, потому что он не может гарантировать, что конкретный тип элемента, который добавляется, совместим с фактическим типом списка
  • Например, если у вас есть List<? extends Number>, список может указывать на List<Double>, поэтому добавление Integer нарушит безопасность типов

Как объясняется на Baeldung, подстановочный знак с верхней границей, такой как List<? extends Number>, представляет список Number или его подтипов (например, Double или Integer), но вы не можете добавлять произвольные элементы в него.

Существование этого ограничения

Ограничение существует для поддержания безопасности типов. Если бы вы могли добавлять элементы в List<? extends T>, вы могли бы попытаться добавить подтип, несовместимый с фактическим типом списка, что привело бы к ошибкам типов во время выполнения.


Поведение List<? super T>

Когда вы объявляете переменную как List<? super T>, вы создаете подстановочный знак с нижней границей, который может содержать любой список, тип элемента которого является T или супертипом T. Вот как он ведет себя:

Операции записи

  • Вы можете добавлять элементы типа T или любого подтипа T в List<? super T>
  • Компилятор разрешает это, потому что любой подтип T может быть безопасно присвоен супертипу T
  • Пример: List<? super Integer> integerList = new ArrayList<Number>(); - вы можете добавлять значения Integer в него

Операции чтения

  • Вы можете считывать элементы из List<? super T>, но они будут рассматриваться как наиболее общий супертип (обычно Object)
  • Вы теряете конкретную информацию о типе при чтении, потому что список может содержать элементы различных супертипов
  • Пример: Если у вас есть List<? super Integer>, считывание элементов дает вам Object, а не Integer

Как отмечено в объяснении на Java-Success.com, все элементы, вставленные в List<? super T>, являются либо экземплярами T, либо экземплярами суперкласса T, обеспечивая безопасность типов при добавлении, но ограничивая информацию о типе при чтении.

Существование этой гибкости

Эта гибкость позволяет добавлять элементы типа T в коллекции, которые могут содержать T или его супертипы, что полезно в сценариях, таких как методы утилит коллекций, которые должны работать с разными типами безопасным для типов образом.


Практические примеры и варианты использования

Пример: Копирование между коллекциями

Вот практический пример, демонстрирующий разницу:

java
// Подстановочный знак с верхней границей - безопасно для чтения
List<? extends Number> source = Arrays.asList(1, 2.5, 3);
List<Number> destination = new ArrayList<>();

// Вы можете считывать из источника:
Number num = source.get(0); // Безопасно - возвращает Number

// Но вы не можете добавлять в источник:
// source.add(10); // Ошибка компиляции!

// Подстановочный знак с нижней границей - безопасно для записи
List<? super Integer> intDest = new ArrayList<>();
intDest.add(10); // Безопасно - Integer можно добавить в Integer или супертип
intDest.add(20); // Безопасно

// Но чтение дает Object:
Object obj = intDest.get(0); // Безопасно, но теряется информация о типе

Пример: Методы утилит коллекций

java
// Метод, который только считывает из коллекции
public static double sum(List<? extends Number> numbers) {
    double sum = 0.0;
    for (Number num : numbers) {
        sum += num.doubleValue(); // Безопасное чтение
    }
    return sum;
}

// Метод, который только записывает в коллекцию
public static void fill(List<? super Integer> list, Integer value) {
    for (int i = 0; i < 5; i++) {
        list.add(value); // Безопасная запись
    }
}

Пример: Операции с коллекциями безопасные для типов

Согласно учебнику Oracle Java, когда вам нужно создать отношение между классами, чтобы код мог получать доступ к методам через элементы, используйте подстановочный знак с верхней границей:

java
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK - подстановочный знак с верхней границей разрешает это

Когда использовать каждый тип подстановочного знака

Используйте List<? extends T> Когда:

  1. Вам нужно только считывать элементы из коллекции
  2. Вы хотите максимальную гибкость при принятии разных подтипов
  3. Вы реализуете производителя, который предоставляет значения потребителям
  4. Вам нужна безопасная для типов итерация по элементам типа T или его подтипов

Распространенные сценарии:

  • Методы утилит коллекций, обрабатывающие элементы
  • Параметры методов, принимающие коллекции для чтения
  • Обобщенные алгоритмы, работающие с любым подтипом T

Используйте List<? super T> Когда:

  1. Вам нужно только добавлять элементы в коллекцию
  2. Вы хотите принимать коллекции, которые могут содержать T или его супертипы
  3. Вы реализуете потребителя, который принимает значения от производителей
  4. Вам нужна безопасная для типов вставка элементов типа T или его подтипов

Распространенные сценарии:

  • Методы утилит коллекций, заполняющие коллекции
  • Параметры методов, принимающие коллекции для записи
  • Обобщенные алгоритмы, хранящие элементы типа T

Используйте точный тип (без подстановочных знаков) Когда:

  1. Вам нужны операции чтения и записи
  2. Вам требуется точная информация о типе в обоих направлениях
  3. Вы работаете с конкретными ограничениями типов

Распространенные ошибки и лучшие практики

Распространенные ошибки

  1. Попытка добавлять в List<? extends T>

    java
    List<? extends String> list = new ArrayList<>();
    // list.add("hello"); // Ошибка компиляции!
    
  2. Предположение, что List<? super T> сохраняет информацию о типе при чтении

    java
    List<? super String> list = new ArrayList<>();
    list.add("hello");
    String str = list.get(0); // Ошибка компиляции! Нужно использовать Object
    
  3. Неоправданное использование подстановочных знаков, когда нужны операции чтения и записи

    java
    // Не используйте подстановочные знаки здесь - используйте точный тип
    List<String> list = new ArrayList<>();
    list.add("hello");
    String str = list.get(0); // Работает идеально
    

Лучшие практики

  1. Последовательно следуйте Принципу Получить и Поместить во всем коде
  2. Используйте мнемоническое правило PECS (Producer Extends, Consumer Super) для запоминания принципа
  3. Рассмотрите возможность использования параметров типа метода, когда вам нужна большая гибкость
  4. Документируйте использование подстановочных знаков в вашем API для руководства пользователей
  5. Предпочитайте точные типы, когда вам нужны операции чтения и записи

Как отмечено в учебнике CodeJava.net, понимание, когда использовать extends и super подстановочные знаки, важно для создания гибких, но безопасных для типов операций с коллекциями.


Заключение

Понимание различий между List<? super T> и List<? extends T> необходимо для написания гибкого и безопасного для типов кода на Java. Ключевые выводы:

  1. List<? extends T> предназначен для операций только чтения - вы можете получать элементы типа T или его подтипов, но не можете добавлять элементы
  2. List<? super T> предназначен для операций только записи - вы можете добавлять элементы типа T или его подтипов, но чтение дает вам наиболее общий супертип
  3. Принцип Получить и Поместить предоставляет простое руководство: используйте extends для получения значений, super для помещения значений
  4. Подстановочные знаки увеличивают гибкость, но сопровождаются компромиссами в информации о типе
  5. Выбирайте правильную конструкцию в зависимости от того, вам нужно поведение производителя (extends) или потребителя (super)

Следуя этим принципам, вы можете создавать более гибкие API, сохраняя безопасность типов в ваших Java-приложениях.

Источники

  1. Различие между <? super T> и <? extends T> в Java
  2. Параметр типа vs Подстановочный знак в обобщенных типах Java | Baeldung
  3. Обобщенные типы Java с extends и super подстановочными знаками и Принцип Получить и Поместить
  4. Учебник по языку Java => Выбор между T, ? super T, и ? extends T
  5. Обобщенные типы Java за 5 минут “? extends” & “? super” объяснены с диаграммой
  6. Подстановочные знаки и подтипизация (Учебник Java™ > Обучение языку Java > Обобщенные типы)
  7. Руководства по использованию подстановочных знаков (Учебник Java™ > Обучение языку Java > Обобщенные типы)
  8. Серый блог: Учебник по обобщенным типам Java - Часть III - Подстановочные знаки
  9. Подстановочные знаки обобщенных типов Java
  10. Подстановочные знаки обобщенных типов Java
Авторы
Проверено модерацией
Модерация