У чому різниця між 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).