Wildcards (?)

Wildcards (?) — «какой-то тип, неважно какой». Нужны когда метод должен принимать коллекции разных параметризаций, но работать только в одном направлении: либо читать из них, либо писать в них.

Проблема без wildcards:

void printAll(List<Number> list) { ... }

List<Integer> ints = List.of(1, 2, 3);
printAll(ints);  // ❌ ошибка компиляции!
                 // List<Integer> — не подтип List<Number>

List<Integer> — не List<Number>, даже хотя Integer extends Number:

Типы (наследование) Number Integer extends ✓

Дженерики (НЕТ наследования!) List<Number> List<Integer> не подтип ✗

Это нужно для безопасности: если бы List<Integer> был подтипом List<Number>, можно было бы добавить Double в список Integer’ов через ссылку типа List<Number>.

? extends T — только читать (коварьянтный):

void printAll(List<? extends Number> list) {  // принимает List<Integer>, List<Double>...
    for (Number n : list) System.out.println(n);  // читаем как Number ✅
    // list.add(1);  ❌ нельзя — компилятор не знает точный тип
}

? super T — только писать (контрвариантный):

void addNumbers(List<? super Integer> list) {  // принимает List<Integer>, List<Number>, List<Object>
    list.add(1);   // ✅ Integer точно подойдёт любому супертипу
    list.add(42);  // ✅
    // Integer n = list.get(0);  ❌ при чтении можно получить только Object
}
Копнуть глубже

Правило PECS (Producer Extends, Consumer Super) — мнемоника от Joshua Bloch:

  • Коллекция производит данные (вы из неё читаете) → ? extends
  • Коллекция потребляет данные (вы в неё пишете) → ? super

Классический пример — Collections.copy(dest, src):

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src)   // src — Producer, отдаёт T → extends
        dest.add(item);  // dest — Consumer, принимает T → super
}

List<Integer> src  = List.of(1, 2, 3);
List<Number>  dest = new ArrayList<>();
Collections.copy(dest, src);  // работает: src=List<Integer>, dest=List<Number>

Без wildcards пришлось бы писать copy(List<T>, List<T>) — тогда оба параметра должны быть одним и тем же типом, и вызвать copy(dest, src) выше не получилось бы.

Unbounded wildcard <?> — когда тип совсем не важен:

void printSize(List<?> list) {
    System.out.println(list.size()); // только size(), не трогаем элементы
}

Отличается от List<Object>: List<?> принимает любую параметризацию, List<Object> — только буквально List<Object>.

Частый вопрос на собесе: «Почему List<Integer> не является подтипом List<Number>?» Ответ: из-за безопасности типов — иначе через List<Number> можно было бы добавить Double в List<Integer>. Wildcard ? extends Number решает это безопасно, запрещая запись.

🎤 Закрыл тему, если можешь объяснить:
• почему List<Integer> не является подтипом List<Number> и как это исправляет wildcard;
• правило PECS и пример Collections.copy с обоими видами wildcards (если дошёл до 2-го слоя).