Питання 16 · Розділ 20

У чому різниця між і

Обмеження зверху: тип T або будь-який його нащадок.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

<? extends T> — коваріантність (Producer).

  • Можна читати елементи як тип T.
  • Не можна записувати (крім null).
List<? extends Number> numbers = List.of(1, 2, 3);
Number n = numbers.get(0);  // ✅ можна читати
// numbers.add(4);  // ❌ не можна писати

<? super T> — контраваріантність (Consumer).

  • Можна записувати елементи типу T.
  • Читати можна тільки як Object.
List<? super Integer> integers = new ArrayList<>();
integers.add(42);  // ✅ можна писати
// Integer i = integers.get(0);  // ❌ можна читати тільки як Object

Просте правило (PECS):

  • Producer Extends — читаємо дані → ? extends T
  • Consumer Super — записуємо дані → ? super T

🟡 Middle Level

Коваріантність — <? extends T>

Обмеження зверху: тип T або будь-який його нащадок.

List<? extends Number> nums = new ArrayList<Integer>();
Number n = nums.get(0);  // ✅ Integer — це Number
// nums.add(42);  // ❌ компілятор не знає, Integer це чи Double

Чому не можна писати: List<? extends Number> може бути List<Integer> або List<Double>. Якщо дозволити запис Double, а там Integer — порушення цілісності.

Контраваріантність — <? super T>

Обмеження знизу: тип T або будь-який його батько.

List<? super Integer> target = new ArrayList<Number>();
target.add(42);  // ✅ Integer поміститься в Number або Object
Object o = target.get(0);  // ✅ читати можна тільки як Object

Чому не можна читати типізовано: List<? super Integer> може бути List<Number> або List<Object>. Не гарантується, що там лежать саме Integer.

Порівняння

Характеристика <? extends T> <? super T>
Роль Producer (джерело) Consumer (приймач)
Читання ✅ Як T ❌ Тільки як Object
Запис ❌ Заборонена ✅ Як T
Ієрархія T і всі, хто нижче T і всі, хто вище

Capture of Wildcard

Часта помилка:

void reverse(List<?> list) {
    // list.set(0, list.get(0)); // ПОМИЛКА: capture of ?
}

Компілятор не може співставити тип, який «дістав», з типом для запису. Рішення: допоміжний generic-метод, який «захоплює» тип:

void reverse(List<?> list) { reverseHelper(list); }
private <T> void reverseHelper(List<T> list) {
    // тепер T відомий
}

🔴 Senior Level

Варіативність на місці використання (Use-site variance)

Java використовує варіативність на місці використання — в параметрах методу. Це відрізняється від Kotlin/Scala, де є варіативність на місці оголошення (out T / in T).

Wildcards НЕ запобігають алокаціям

Використання wildcards в сигнатурах методів не запобігає алокаціям. ArrayList<Integer> алокує Integer objects незалежно від того, переданий він як List<? extends Number> чи ні. Wildcards — це тільки compile-time type checking.

Unbounded Wildcard <?>

Це фактично <? extends Object>. Корисно, коли код використовує тільки методи Object:

void printSize(List<?> list) {
    System.out.println(list.size());  // тільки методи Object
}

Best Practices

// ✅ extends — для вилучення даних (Producer)
public double sum(List<? extends Number> numbers) { ... }

// ✅ super — для накопичення даних (Consumer)
public void fill(List<? super Integer> list, int value) { ... }

// ✅ Без wildcard коли потрібно і read, і write
public <T> void swap(List<T> list, int i, int j) { ... }

// ❌ Extends для запису
// ❌ Super для читання конкретного типу

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • <? extends T> — коваріантність (Producer): можна читати як T, не можна записувати (крім null)
  • <? super T> — контраваріантність (Consumer): можна записувати T, читати тільки як Object
  • List<String> НЕ наслідує List<Object> — дженерики інваріантні
  • Правило PECS: Producer Extends, Consumer Super
  • Wildcards — use-site variance (на відміну від Kotlin’s declaration-site variance)
  • <?> = <? extends Object> — для випадків коли потрібні тільки методи Object

Часті уточнюючі запитання:

  • Чому <? extends T> забороняє запис? — Компілятор не знає реальний тип (може бути List), запис порушить цілісність
  • Що можна записати в List<? super Integer>? — Integer та його нащадків, бо вони помістяться в будь-який супертип
  • Що таке capture of wildcard? — Компілятор не може зв’язати get() з set() для ?, потрібен generic helper метод
  • Коли wildcard НЕ потрібен? — Коли потрібно і читати, і писати — використовувати <T>

Червоні прапорці (НЕ говорити):

  • ❌ “List<? extends Number> може бути List" — Так, але не можна додати Integer в нього
  • ❌ “<? super T дозволяє читати як T” — Можна читати тільки як Object
  • ❌ “Wildcards запобігають алокаціям” — Це тільки compile-time перевірка
  • ❌ “List<?> — це read-only список” — Компілятор блокує записи через невідомий тип, не immutable

Пов’язані теми:

  • [[11. Що таке дженерики (Generics) в Java]]
  • [[13. Що таке type erasure (стирання типів)]]
  • [[17. Що таке PECS (Producer Extends Consumer Super)]]
  • [[21. У чому різниця між List<?> і List]]
  • [[24. Як працюють дженерики з наслідуванням]]