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 лекция
Недоступен
Интерфейсы коллекций
Интерфейсы коллекций
Комментарии (6)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Gans Electro Уровень 42
15 июля 2026
Довольно сложно понять для новичков, об этой теме лучше несколько лекций дать
Timofey Novitsky Уровень 36
21 мая 2026
Почему интерфейс Map не наследует Collection? - Потому что Map не реалиует Iterable - неверно -- т.е. реализует, лол?
nightmarakesh Уровень 45
27 июля 2026
второй вариант ответа поглощает первый
Riga Уровень 1
28 февраля 2026
Какой метод NavigableSet возвращает наибольший элемент, строго меньший заданного?
Alex Blizz Уровень 18
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) - пройдет без проблем