JavaRush /Курсы /JAVA 25 SELF /Generics wildcards

Generics wildcards

JAVA 25 SELF
27 уровень , 4 лекция
Открыта

1. Инвариантность generics: List<Number> ≠ List<Integer>

В Java обобщения устроены строго: они инвариантны. Это означает, что даже если один тип является подтипом другого (например, Integer — это подтип Number), коллекции с этими типами никак не связаны.

Представьте: у вас есть коробка с яблоками (List<Integer>), а кто-то говорит, что это вообще «коробка с фруктами» (List<Number>). Вроде бы логично, яблоки же — фрукты. Но тогда в эту коробку можно будет положить банан (Double), и всё сломается — коробка-то на самом деле «яблочная».

Пример кода:

List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Ошибка компиляции!

Компилятор здесь специально строг: он не даёт превратить список «яблок» в список «фруктов». Иначе мы получили бы возможность добавить в него что угодно, и программа упала бы уже во время выполнения.

Поэтому List<Number> и List<Integer> — это два совершенно разных типа, несмотря на то что Integer сам по себе является подтипом Number.

В отличие от массивов:

Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // Разрешено (массивы ковариантны)
numArr[0] = 3.14; // Ошибка во время выполнения (ArrayStoreException)

Вывод: дженерики инвариантны для обеспечения безопасности типов на этапе компиляции.

2. Границы типов-параметров

Иногда нужно ограничить, с какими типами может работать generic-класс или метод. Для этого используют ограничения (bounds).

Верхняя граница (extends)

class Stats<T extends Number> {
    private T[] nums;
    // ...
}

Теперь Stats может работать только с типами, которые являются наследниками Number (Integer, Double, Float и т.д.). Попытка создать Stats<String> вызовет ошибку компиляции.

Ограничение с интерфейсом

class Sorter<T extends Comparable<T>> {
    void sort(List<T> list) { /* ... */ }
}

Теперь Sorter может работать только с типами, которые реализуют интерфейс Comparable.

Множественные границы

Можно указать сразу несколько ограничений через &:

class MyClass<T extends Number & Comparable<T>> { /* ... */ }

T должен быть наследником Number и реализовывать Comparable<T>.

Порядок важен: сначала класс, потом интерфейсы.

3. Знакомство с wildcard: ?

Wildcard — это «подстановочный» тип, который говорит: «Здесь может быть какой-то тип, но я не знаю, какой именно».

Примеры:

  • List<?> — список чего угодно.
  • List<? extends Number> — список любого типа, который является наследником Number (например, Integer, Double).
  • List<? super Integer> — список любого типа, который является предком Integer (например, Integer, Number, Object).

Зачем нужны wildcards?

Они позволяют писать методы, которые работают с коллекциями разных, но связанных типов.

Пример: только для чтения (producer)

void printNumbers(List<? extends Number> list) {
    for (Number n : list) {
        System.out.println(n);
    }
    // list.add(123); // Ошибка! Нельзя добавлять элементы
}
  • Можно читать элементы как Number.
  • Нельзя добавлять элементы (кроме null).

Пример: только для записи (consumer)

void addIntegers(List<? super Integer> list) {
    list.add(42); // ОК
    // Integer x = list.get(0); // Ошибка! Не знаем, какой тип вернётся
}
  • Можно добавлять элементы типа Integer (или его подтипов).
  • Нельзя безопасно читать элементы как Integer (можно только как Object).

Правило PECS

PECS — «Producer Extends, Consumer Super»:

  • Producer — Extends: если коллекция только отдаёт данные (producer), используйте ? extends T.
  • Consumer — Super: если коллекция только принимает данные (consumer), используйте ? super T.

Запомнить просто: extends — для чтения (producer), super — для записи (consumer).

Сравнение: массивы ковариантны, generics инвариантны

  • Массивы ковариантны: Integer[] можно присвоить переменной типа Number[].
  • Generics инвариантны: List<Integer> нельзя присвоить переменной типа List<Number>.

Wildcards позволяют частично «смягчить» инвариантность generics.

5. Generic-методы и вывод типов; ограничения (type erasure)

Generic-методы

Можно объявлять generic-методы, которые работают с любыми типами:

public static <T> void printList(List<T> list) {
    for (T elem : list) {
        System.out.println(elem);
    }
}

Вывод типа (type inference)

Java может «догадаться» о типе параметра:

List<String> strings = List.of("a", "b");
printList(strings); // T = String

Ограничения generics: стирание типов

  • В Java дженерики реализованы через стирание типов: информация о типах параметров удаляется после компиляции.
  • В байткоде нет разницы между List<String> и List<Integer>.
  • Нельзя создавать массивы generic-типов: new List<String>[10] — ошибка.
  • Нельзя использовать instanceof с параметрами типа: obj instanceof List<String> — ошибка.

6. Практика на коллекциях и Stream API

Копирование элементов между списками

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {
        dest.add(item);
    }
}
  • src — producer (? extends T)
  • dest — consumer (? super T)

Использование:

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // ОК: Number — супертип Integer

Stream API и wildcards

List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;

numbers.stream()
    .map(Number::doubleValue)
    .forEach(System.out::println);

Фильтрация с wildcard

public static void printAll(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

7. Частые ошибки

Ошибка №1: Raw types (сырые типы).
Использование «сырых» типов отключает проверку типов и ведёт к ошибкам времени выполнения.

List list = new ArrayList(); // raw type — плохо!
list.add("строка");
list.add(123); // Можно добавить что угодно

String s = (String) list.get(1); // ClassCastException!

Никогда не используйте raw types. Всегда указывайте параметры типа: List<String>, List<Integer>.

Ошибка №2: Небезопасные преобразования с extends.
Попытка добавлять элементы в коллекцию с ? extends ... приведёт к ошибке компиляции.

List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // Ошибка компиляции!

Ошибка №3: «Затирание» типа при перегрузке.
Нельзя перегружать методы только по generic-параметрам — после стирания типов сигнатуры совпадают.

public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // Ошибка компиляции!

Ошибка №4: Массивы generic-типов.
Нельзя создавать массивы параметризованных типов из‑за стирания типов.

List<String>[] arr = new List<String>[10]; // Ошибка компиляции!
1
Задача
JAVA 25 SELF, 27 уровень, 4 лекция
Недоступна
Подсчёт общей стоимости активов 💰
Подсчёт общей стоимости активов 💰
1
Задача
JAVA 25 SELF, 27 уровень, 4 лекция
Недоступна
Универсальная система логирования событий 📜
Универсальная система логирования событий 📜
1
Опрос
Интерфейсы коллекций, 27 уровень, 4 лекция
Недоступен
Интерфейсы коллекций
Интерфейсы коллекций
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Riga Уровень 29
28 февраля 2026
Какой метод NavigableSet возвращает наибольший элемент, строго меньший заданного?
Alex Blizz Уровень 29
24 января 2026
Вопрос, методы

public static <T> void copy(List<? super T> dest, List<? extends T> src)
и

public static <T> void copy(List<T> dest, List<T> src)
будут одинаково работать или нет?
iseverin Уровень 66
1 февраля 2026
Нет, во втором случае ограничение жестко задано одним типом. В первом разрешено расширение типа, т.е. копировать тип-в-тип или наследника в родителя на любом удалении от базового типа. Если взять из того что проходили, то можно иерархию java.lang.Object java.lang.Throwable java.lang.Exception java.lang.RuntimeException java.lang.NullPointerException Если <T> = <Exception>, то можно будет перекладывать NullPointerException/RuntimeException/Exception в Exception/Throwable/Object т.е. copy(List<NullPointerException> listOfNPE, List<Object> listOfObjects) - пройдет без проблем