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