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]; // Помилка компіляції!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ