Получить доступ

Wildcards и правило PECS

Инвариантность дженериков против ковариантных массивов, ? extends/super, и когда что выбирать по PECS.

Дженерики инвариантны

List<String> НЕ является подтипом List<Object>, хотя String — подтип Object. Это инвариантность, и она намеренная: будь List<String> подтипом List<Object>, через ссылку List<Object> можно было бы положить в список Integer и сломать его типобезопасность.

Контраст с массивами: массивы ковариантны (String[] — подтип Object[]), и именно поэтому небезопасны — запись неподходящего типа компилируется, но падает в рантайме как ArrayStoreException. Дженерики ловят эту ошибку на компиляции, что лучше.

Object[] arr = new String[1];
arr[0] = 42;            // компилируется, но в рантайме ArrayStoreException
// List<Object> l = new ArrayList<String>(); // а это даже не скомпилируется

Безграничный wildcard ?

List<?> — это «список чего-то конкретного, но неизвестно чего». Он полезен, когда тип элементов не важен (например, метод, печатающий размер любого списка).

Из него можно читать как Object и нельзя ничего класть (кроме null): компилятор не знает фактический тип, поэтому запрещает запись, чтобы не нарушить типобезопасность.

? extends T — ковариантность (producer)

List<? extends Number> — список Number ИЛИ любого его подтипа (Integer, Double…). Из такого списка безопасно ЧИТАТЬ как Number, но нельзя в него ПИСАТЬ (кроме null): фактический тип может быть List<Integer>, и положить туда Double было бы ошибкой.

Это для источников данных (producer): метод, который только берёт элементы из коллекции.

double sum(List<? extends Number> xs) { // принимает List<Integer>, List<Double>...
    double s = 0;
    for (Number n : xs) s += n.doubleValue(); // читаем как Number — ок
    return s; // xs.add(...) — запрещено компилятором
}

? super T — контравариантность (consumer)

List<? super Integer> — список Integer ИЛИ любого его супертипа (Number, Object). В такой список безопасно ПИСАТЬ Integer (он подойдёт любому супертипу), но при чтении компилятор может гарантировать лишь Object.

Это для приёмников данных (consumer): метод, который только кладёт элементы в коллекцию.

void fillWithZeros(List<? super Integer> dst, int n) { // List<Integer>, List<Number>, List<Object>
    for (int i = 0; i < n; i++) dst.add(0); // писать Integer — ок
}

PECS

Правило выбора границы — PECS: Producer Extends, Consumer Super. Если параметр служит ИСТОЧНИКОМ элементов (из него читают) — ? extends T; если ПРИЁМНИКОМ (в него пишут) — ? super T; если и то и другое — точный тип T без wildcard.

Классика из JDK: Collections.copy(List<? super T> dest, List<? extends T> src) — приёмник super, источник extends. Так же устроены сигнатуры Stream.forEach(Consumer<? super T>), Comparator и многие другие — это делает API максимально гибким для вызывающих.

Правило PECS: ? extends T — producer, из коллекции читают; ? super T — consumer, в коллекцию пишут.
PECS: «? extends T» — читаем (producer), «? super T» — пишем (consumer).

Как отвечать на собеседовании

Объясните инвариантность дженериков через пример небезопасной записи и противопоставьте ковариантным массивам (ArrayStoreException в рантайме). Разложите ? extends (читать, producer) и ? super (писать, consumer) и сформулируйте PECS, подкрепив сигнатурой Collections.copy.

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем