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:
Это нужно для безопасности: если бы 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 решает это безопасно, запрещая запись.
• правило PECS и пример Collections.copy с обоими видами wildcards (если дошёл до 2-го слоя).