В чем разница между <? super T> и <? extends T> в Java дженериках?
Краткий ответ: PECS — Producer Extends, Consumer Super
Ваша путаница абсолютно нормальна — это один из самых сложных моментов в дженериках Java. Запомните правило PECS (Producer Extends, Consumer Super), которое придумал Джошуа Блох:
- Producer Extends — если вы только читаете данные из структуры (она производит элементы), используйте
? extends T - Consumer Super — если вы только записываете данные в структуру (она потребляет элементы), используйте
? super T - Если вы и читаете, и пишете — не используйте wildcard вообще
Почему нельзя добавлять в extends T>?
Представьте, что у вас есть List<? extends Number>. Это может быть List<Integer>, List<Double> или List<BigDecimal>. Компилятор не знает, какой именно это список. Если бы он разрешил вам вызвать list.add(42), то:
- Если список на самом деле
List<Double>— вы положили бы Integer в список, ожидающий Double (ClassCastException) - Если список
List<BigDecimal>— та же проблема
Поэтому компилятор запрещает любые вызовы методов, которые принимают параметр типа T (кроме null). Вы можете только читать — get() возвращает Number (верхняя граница).
Почему можно добавлять в super T>, но читать только Object?
List<? super Integer> может быть List<Integer>, List<Number> или List<Object>. Компилятор знает, что любой из этих типов может принять Integer (так как Integer — подтип всех них). Поэтому add(42) безопасен.
Но при чтении: что вернет get(0)? Это может быть Integer, Number или Object. Единственный общий предок — Object. Поэтому компилятор возвращает Object.
Наглядные примеры
// Producer Extends — читаем числа из коллекции
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number num : list) { // безопасно читаем как Number
sum += num.doubleValue();
}
return sum;
}
// Consumer Super — добавляем числа в коллекцию
public static void addIntegers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i); // безопасно добавляем Integer
}
}
// Пример использования с иерархией классов
class Animal {}
class Dog extends Animal {}
class Puppy extends Dog {}
// Producer: читаем только собак
public void processDogs(List<? extends Dog> dogs) {
for (Dog d : dogs) { /* OK */ }
// dogs.add(new Puppy()); // ОШИБКА!
}
// Consumer: добавляем только собак
public void addDogs(List<? super Dog> dogs) {
dogs.add(new Puppy()); // OK — Puppy это Dog
dogs.add(new Dog()); // OK
// Dog d = dogs.get(0); // ОШИБКА — только Object
}
Практические рекомендации
- Для параметров методов: используйте
? extends Tдля входных данных (читаете),? super Tдля выходных (пишете) - Для возвращаемых типов: избегайте wildcard — это усложняет API
- Коллекции в Java:
Collections.copy(List<? super T> dest, List<? extends T> src)— идеальный пример PECS - Comparator/Comparable:
Collections.sort(List<T> list, Comparator<? super T> c)— Comparator потребляет элементы для сравнения
Запомните: extends — для чтения, super — для записи. Это правило спасет вас от большинства ошибок компиляции при работе с дженериками.
<<Альтернативный взгляд: ковариантность и контравариантность
Если вам ближе теория типов, то разницу можно объяснить через понятия ковариантности и контравариантности:
? extends T— ковариантность:List<Dog>является подтипомList<? extends Animal>. Направление наследования сохраняется. Вы можете читать элементы как Animal, но не можете их добавлять (кроме null).? super T— контравариантность:List<Animal>является подтипомList<? super Dog>. Направление наследования обратное. Вы можете добавлять Dog и его подтипы, но при чтении получаете только Object.
Простое правило для запоминания
Представьте, что вы работаете с коробками:
- Коробка с фруктами ( extends Fruit>) — вы знаете, что там фрукты, но не знаете какие именно. Можете взять фрукт (прочитать), но не можете положить туда яблоко — вдруг там апельсины?
- Коробка для яблок ( super Apple>) — вы знаете, что туда можно класть яблоки и их подтипы. Но когда берете фрукт — не знаете, что достанете (может быть яблоко, фрукт или Object).
Этот ментальный образ помогает быстро решить, какой wildcard использовать в конкретной ситуации. Если сомневаетесь — вспомните про коробки!
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги
Монорепозиторий с Java: организация кода и лучшие практики