JavaRush /Курси /JAVA 25 SELF /Підстановочні типи (wildcards) у дженериках

Підстановочні типи (wildcards) у дженериках

JAVA 25 SELF
Рівень 27 , Лекція 4
Відкрита

1. Інваріантність дженериків: 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. Межі параметрів типу

Іноді потрібно обмежити, з якими типами може працювати дженерик‑клас або метод. Для цього використовують межі (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); // OK
    // 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).

Порівняння: масиви коваріантні, дженерики інваріантні

  • Масиви коваріантні: Integer[] можна присвоїти змінній типу Number[].
  • Дженерики інваріантні: List<Integer> не можна присвоїти змінній типу List<Number>.

Підстановочні типи дозволяють частково «помʼякшити» інваріантність дженериків.

5. Дженерик‑методи та виведення типів; стирання типів (type erasure)

Дженерик‑методи

Можна оголошувати дженерик‑методи, які працюють з будь‑якими типами:

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

Обмеження дженериків: стирання типів

  • У Java дженерики реалізовано за допомогою стирання типів: інформація про параметри типів видаляється після компіляції.
  • У байткоді немає різниці між List<String> і List<Integer>.
  • Не можна створювати масиви дженерик‑типів: 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); // OK: 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);

Фільтрація з підстановочним типом

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: «Стирання» типу під час перевантаження.
Не можна перевантажувати методи лише за дженерик‑параметрами — після стирання типів сигнатури збігаються.

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

Помилка № 4: Масиви дженерик‑типів.
Не можна створювати масиви параметризованих типів через стирання типів.

List<String>[] arr = new List<String>[10]; // Помилка компіляції!
1
Опитування
Інтерфейси колекцій, рівень 27, лекція 4
Недоступний
Інтерфейси колекцій
Інтерфейси колекцій
Коментарі (1)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Grimnir Рівень 31
20 лютого 2026
Важкувато написана лекція