В чём разница между extends T и super T
Разница между extends T> и super T> заключается в направлении иерархии наследования и роли контейнера (чтение или запись).
🟢 Junior Level
Разница между <? extends T> и <? super T> заключается в направлении иерархии наследования и роли контейнера (чтение или запись).
Оба синтаксиса представляют собой подстановочные знаки с ограничениями (Bounded Wildcards) и подчиняются золотому правилу PECS (Producer Extends, Consumer Super):
| Wildcard | Направление иерархии | Роль (PECS) | Операции |
|---|---|---|---|
<? extends T> |
Тип $T$ и все, кто ниже него (наследники) | Producer (поставщик) | Можно читать как $T$. Запись запрещена (кроме null). |
<? super T> |
Тип $T$ и все, кто выше него (предки) | Consumer (приемник) | Можно записывать объекты типа $T$. Читать можно только как Object. |
// 1. <? extends T> — только чтение (Producer)
List<? extends Number> producers = List.of(1, 2.5, 3L);
Number n = producers.get(0); // ✅ Читать безопасно: любой элемент гарантированно является Number
// producers.add(10); // ❌ Ошибка компиляции: запись запрещена!
// 2. <? super T> — только запись (Consumer)
List<? super Integer> consumers = new ArrayList<Number>();
consumers.add(42); // ✅ Записывать безопасно: Integer всегда поместится в Number или Object
// Integer val = consumers.get(0); // ❌ Ошибка компиляции: читать можно только как Object
Object obj = consumers.get(0); // ✅ Читать безопасно только как базовый Object
🟡 Middle Level
1. Почему <? extends T> запрещает запись элементов?
Рассмотрим, почему компилятор блокирует вызов .add():
List<Integer> intList = new ArrayList<>();
List<? extends Number> numbers = intList; // Допустимо: Integer extends Number
// Представим, что компилятор разрешил бы запись:
// numbers.add(3.14); // ❌ Ошибка!
Если бы компилятор разрешил добавить Double (3.14) в список numbers, он физически поместил бы вещественное число в исходный список List<Integer>! При последующем вызове Integer val = intList.get(0) программа упала бы с фатальным ClassCastException.
Поскольку ? extends Number означает: «список некоторого неизвестного подтипа Number (это может быть List<Integer>, List<Double> или List<Long>)», компилятор полностью запрещает запись, защищая целостность памяти. Единственное значение, которое разрешено передать в .add() — это литерал null, так как null принадлежит любому ссылочному типу.
2. Почему <? super T> при чтении возвращает только Object?
В случае List<? super Integer> фактическим списком в куче может быть:
List<Integer>List<Number>List<Object>
Если фактическим списком является List<Number>, в нем уже могут лежать элементы Double или Float. Поэтому компилятор не может гарантировать, что извлеченный элемент будет являться Integer. Единственный гарантированный общий предок любого суперкласса Integer — это java.lang.Object.
3. Классический пример из JDK: Collections.copy
Эталонная иллюстрация совместной работы extends и super:
// src — поставщик данных (Producer -> extends)
// dest — приемник данных (Consumer -> super)
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i)); // Читаем из src как T, пишем в dest как T!
}
}
🔴 Senior Level
Теория типов: Ковариантность, Контравариантность и Инвариантность
Дженерики в Java по умолчанию строго инвариантны: \(\text{Integer} \le \text{Number} \not\Rightarrow \text{List}\langle\text{Integer}\rangle \le \text{List}\langle\text{Number}\rangle\)
Подстановочные знаки (Wildcards) позволяют ослабить инвариантность в местах использования (Use-Site Variance):
- Ковариантность (
? extends T): сохраняет направление наследования в иерархии типов. \(\text{Sub} \le \text{Super} \implies \text{List}\langle\text{Sub}\rangle \le \text{List}\langle?\text{ extends Super}\rangle\) - Контравариантность (
? super T): обращает (инвертирует) направление наследования. \(\text{Sub} \le \text{Super} \implies \text{List}\langle\text{Super}\rangle \le \text{List}\langle?\text{ super Sub}\rangle\)
Захват подстановочного знака (Wildcard Capture) и паттерн Helper
Попытка прочитать элемент из List<?> и записать его обратно приводит к ошибке Wildcard Capture:
public void swapFirstTwo(List<?> list) {
// ❌ Ошибка компиляции:
// capture of ? cannot be applied to capture of ?
// list.set(0, list.get(0));
}
Причина: Компилятор присваивает каждому вхождению ? свежую внутреннюю анонимную переменную типа (например, capture#1 of ? при чтении и capture#2 of ? при записи). Поскольку компилятор не может доказать, что два анонимных типа идентичны, операция блокируется.
Паттерн решения: Вспомогательный generic-метод (Private Helper)
public void swapFirstTwo(List<?> list) {
swapHelper(list); // Захват wildcard в явный тип T
}
private <T> void swapHelper(List<T> list) {
list.set(0, list.get(0)); // ✅ Успешно: тип T однозначно определен!
}
Use-Site Variance (Java) vs Declaration-Site Variance (Kotlin / C#)
- Declaration-Site Variance (Kotlin, Scala, C#): тип объявляется ковариантным или контравариантным непосредственно в заголовке класса:
class Producer<out T>. Клиентский код избавлен от необходимости писать wildcards при каждом вызове. - Use-Site Variance (Java): класс
List<E>всегда инвариантен, а вариативность объявляется разработчиком по месту использования в аргументах методов. Это требует строгого следования правилу PECS в API, но обеспечивает максимальную гибкость: одну и ту же коллекцию можно в одном методе использовать как Producer, а в другом — как Consumer.
4 Tricky Questions
1. Почему в List<? extends Number> нельзя положить даже объект Integer, хотя Integer гарантированно является Number?
Ответ:
Потому что тип List<? extends Number> означает: «список некоторого конкретного, но неизвестного компилятору подтипа Number».
Фактическим экземпляром в памяти может оказаться List<Double> или List<Long>. Если бы компилятор разрешил положить туда Integer, мы бы нарушили целостность списка List<Double>, вызвав отложенный ClassCastException при чтении. Единственный объект, который гарантированно совместим с любым неизвестным ссылочным типом — это null.
2. Что возвращает метод list.get(0), если list имеет тип List<? super Integer>, и почему именно этот тип?
Ответ:
Метод возвращает значение типа java.lang.Object.
Тип ? super Integer означает «список некоторого неизвестного супертипа Integer» (например, List<Number> или List<Object>). В такой список помимо целых чисел в рантайме могут быть законно добавлены Double, Float или любые другие объекты супертипа. Компилятор не имеет права утверждать, что возвращаемое значение является Integer или Number. Единственным предком, общеприменимым ко всем элементам любого возможного суперкласса в Java, является Object.
3. Что такое ошибка Wildcard Capture (например, capture#1 of ?) и как ее решить?
Ответ:
Ошибка Wildcard Capture возникает, когда компилятор встречает подстановочный знак ? и назначает ему уникальный внутренний идентификатор типа (например, capture of ?). Если попытаться прочитать элемент из такого списка через get() и тут же записать его в тот же список через set(), компилятор откажет в компиляции: он воспринимает тип извлеченного элемента и тип параметра метода set() как два независимых несовместимых захвата типа.
Решением является Паттерн Helper Method: публичный метод принимает List<?> и сразу же вызывает приватный обобщенный вспомогательный метод <T> void helper(List<T> list). Вспомогательный метод материализует анонимный wildcard в конкретную переменную типа $T$, связывая чтение и запись в рамках одной сигнатуры.
4. Можно ли объявить локальную переменную как List<? super T>, и имеет ли это практический смысл?
Ответ: Синтаксически это полностью допустимо:
List<? super Integer> list = new ArrayList<Number>();
list.add(10);
Однако в реальном коде объявлять локальные переменные с wildcards (? extends или ? super) практически никогда не имеет смысла и считается антипаттерном. Wildcards созданы для сигнатур публичных интерфейсов и аргументов методов (Use-Site Variance), где они обеспечивают полиморфизм входящих параметров. Внутри тела метода разработчик знает конкретный тип структуры и должен объявлять точный параметризованный тип (List<Number> или List<Integer>), избегая потери удобства работы с методами чтения/записи.
🎯 Шпаргалка для интервью
Главное о Bounded Wildcards
- Формула PECS: Producer Extends, Consumer Super.
<? extends T>(Producer):- Чтение: возвращает
T. - Запись: запрещена (разрешен только
null). - Ковариантность: сохраняет направление иерархии.
- Чтение: возвращает
<? super T>(Consumer):- Чтение: возвращает только
Object. - Запись: разрешена для объектов типа $T$ и его подтипов.
- Контравариантность: инвертирует направление иерархии.
- Чтение: возвращает только
- Если нужно И читать, И писать: Не используйте wildcards! Используйте точный инвариантный тип
<T>. - Wildcard Capture: Ошибка несоответствия типов при работе с
?. Решается паттерном приватного generic-хелпера<T> void helper(List<T> list).