Avatar ·

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

Приветствую, коллеги! Я начал изучать Java дженерики и столкнулся с wildcards (шаблонами). Читал документацию и смотрел видео, но до сих пор путаюсь в двух конструкциях: `` и ``. Я понимаю, что `? extends T` означает "какой-то тип, который является подтипом T", а `? super T` — "какой-то тип, который является супертипом T". Но на практике у меня постоянно возникают ошибки компиляции. Например, у меня есть такой код с List: ```java public class Test { public static void addNumbers(List list) { list.add(42); // Это работает } public static void readNumbers(List list) { Integer num = list.get(0); // Это работает list.add(42); // Ошибка компиляции! Почему? } } ``` Почему я не могу добавить элемент в `List`, хотя могу читать из него? И наоборот — почему я могу добавлять в `List`, но при чтении оттуда получаю только Object? Объясните, пожалуйста, на простых примерах, когда использовать один вариант, а когда другой. И как это связано с принципом PECS (Producer Extends, Consumer Super)? Заранее спасибо! <<>>

Краткий ответ: PECS — Producer Extends, Consumer Super

Ваша путаница абсолютно нормальна — это один из самых сложных моментов в дженериках Java. Запомните правило PECS (Producer Extends, Consumer Super), которое придумал Джошуа Блох:

  • Producer Extends — если вы только читаете данные из структуры (она производит элементы), используйте ? extends T
  • Consumer Super — если вы только записываете данные в структуру (она потребляет элементы), используйте ? super T
  • Если вы и читаете, и пишете — не используйте wildcard вообще

Почему нельзя добавлять в ?

Представьте, что у вас есть List<? extends Number>. Это может быть List<Integer>, List<Double> или List<BigDecimal>. Компилятор не знает, какой именно это список. Если бы он разрешил вам вызвать list.add(42), то:

  • Если список на самом деле List<Double> — вы положили бы Integer в список, ожидающий Double (ClassCastException)
  • Если список List<BigDecimal> — та же проблема

Поэтому компилятор запрещает любые вызовы методов, которые принимают параметр типа T (кроме null). Вы можете только читать — get() возвращает Number (верхняя граница).

Почему можно добавлять в , но читать только 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.

Простое правило для запоминания

Представьте, что вы работаете с коробками:

  • Коробка с фруктами () — вы знаете, что там фрукты, но не знаете какие именно. Можете взять фрукт (прочитать), но не можете положить туда яблоко — вдруг там апельсины?
  • Коробка для яблок () — вы знаете, что туда можно класть яблоки и их подтипы. Но когда берете фрукт — не знаете, что достанете (может быть яблоко, фрукт или Object).

Этот ментальный образ помогает быстро решить, какой wildcard использовать в конкретной ситуации. Если сомневаетесь — вспомните про коробки!