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 или его подтипов, но обеспечивает ограниченные гарантии при чтении. Это поведение вытекает из «Принципа Получить и Поместить», который гласит, что следует использовать подстановочные знаки, когда вам нужно либо получать значения из структуры данных, либо помещать значения в нее, но не одновременно.
Содержание
- Основы подстановочных знаков
- Объяснение Принципа Получить и Поместить
- Поведение List<? extends T>
- Поведение List<? super 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>:
- Операции получения (чтение): Когда вам нужно только извлекать элементы из коллекции, используйте
? extends T - Операции помещения (запись): Когда вам нужно только добавлять элементы в коллекцию, используйте
? super T - Оба типа операций: Когда вам нужно как считывать, так и записывать в коллекцию, используйте точный параметр типа без подстановочных знаков
Поведение 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 или его супертипы, что полезно в сценариях, таких как методы утилит коллекций, которые должны работать с разными типами безопасным для типов образом.
Практические примеры и варианты использования
Пример: Копирование между коллекциями
Вот практический пример, демонстрирующий разницу:
// Подстановочный знак с верхней границей - безопасно для чтения
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); // Безопасно, но теряется информация о типе
Пример: Методы утилит коллекций
// Метод, который только считывает из коллекции
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, когда вам нужно создать отношение между классами, чтобы код мог получать доступ к методам через элементы, используйте подстановочный знак с верхней границей:
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK - подстановочный знак с верхней границей разрешает это
Когда использовать каждый тип подстановочного знака
Используйте List<? extends T> Когда:
- Вам нужно только считывать элементы из коллекции
- Вы хотите максимальную гибкость при принятии разных подтипов
- Вы реализуете производителя, который предоставляет значения потребителям
- Вам нужна безопасная для типов итерация по элементам типа T или его подтипов
Распространенные сценарии:
- Методы утилит коллекций, обрабатывающие элементы
- Параметры методов, принимающие коллекции для чтения
- Обобщенные алгоритмы, работающие с любым подтипом T
Используйте List<? super T> Когда:
- Вам нужно только добавлять элементы в коллекцию
- Вы хотите принимать коллекции, которые могут содержать T или его супертипы
- Вы реализуете потребителя, который принимает значения от производителей
- Вам нужна безопасная для типов вставка элементов типа T или его подтипов
Распространенные сценарии:
- Методы утилит коллекций, заполняющие коллекции
- Параметры методов, принимающие коллекции для записи
- Обобщенные алгоритмы, хранящие элементы типа T
Используйте точный тип (без подстановочных знаков) Когда:
- Вам нужны операции чтения и записи
- Вам требуется точная информация о типе в обоих направлениях
- Вы работаете с конкретными ограничениями типов
Распространенные ошибки и лучшие практики
Распространенные ошибки
-
Попытка добавлять в
List<? extends T>javaList<? extends String> list = new ArrayList<>(); // list.add("hello"); // Ошибка компиляции! -
Предположение, что
List<? super T>сохраняет информацию о типе при чтенииjavaList<? super String> list = new ArrayList<>(); list.add("hello"); String str = list.get(0); // Ошибка компиляции! Нужно использовать Object -
Неоправданное использование подстановочных знаков, когда нужны операции чтения и записи
java// Не используйте подстановочные знаки здесь - используйте точный тип List<String> list = new ArrayList<>(); list.add("hello"); String str = list.get(0); // Работает идеально
Лучшие практики
- Последовательно следуйте Принципу Получить и Поместить во всем коде
- Используйте мнемоническое правило PECS (Producer Extends, Consumer Super) для запоминания принципа
- Рассмотрите возможность использования параметров типа метода, когда вам нужна большая гибкость
- Документируйте использование подстановочных знаков в вашем API для руководства пользователей
- Предпочитайте точные типы, когда вам нужны операции чтения и записи
Как отмечено в учебнике CodeJava.net, понимание, когда использовать extends и super подстановочные знаки, важно для создания гибких, но безопасных для типов операций с коллекциями.
Заключение
Понимание различий между List<? super T> и List<? extends T> необходимо для написания гибкого и безопасного для типов кода на Java. Ключевые выводы:
List<? extends T>предназначен для операций только чтения - вы можете получать элементы типа T или его подтипов, но не можете добавлять элементыList<? super T>предназначен для операций только записи - вы можете добавлять элементы типа T или его подтипов, но чтение дает вам наиболее общий супертип- Принцип Получить и Поместить предоставляет простое руководство: используйте extends для получения значений, super для помещения значений
- Подстановочные знаки увеличивают гибкость, но сопровождаются компромиссами в информации о типе
- Выбирайте правильную конструкцию в зависимости от того, вам нужно поведение производителя (extends) или потребителя (super)
Следуя этим принципам, вы можете создавать более гибкие API, сохраняя безопасность типов в ваших Java-приложениях.
Источники
- Различие между <? super T> и <? extends T> в Java
- Параметр типа vs Подстановочный знак в обобщенных типах Java | Baeldung
- Обобщенные типы Java с extends и super подстановочными знаками и Принцип Получить и Поместить
- Учебник по языку Java => Выбор между T, ? super T, и ? extends T
- Обобщенные типы Java за 5 минут “? extends” & “? super” объяснены с диаграммой
- Подстановочные знаки и подтипизация (Учебник Java™ > Обучение языку Java > Обобщенные типы)
- Руководства по использованию подстановочных знаков (Учебник Java™ > Обучение языку Java > Обобщенные типы)
- Серый блог: Учебник по обобщенным типам Java - Часть III - Подстановочные знаки
- Подстановочные знаки обобщенных типов Java
- Подстановочные знаки обобщенных типов Java