Тема 9. Generics
Generics в Java нужны для того, чтобы писать переиспользуемый код без потери типобезопасности. Проще говоря, вы описываете алгоритм один раз, а компилятор следит, чтобы в него не передавали неподходящие типы. Для новичка это особенно важно: generics уменьшают количество поздних runtime-ошибок и убирают лишние cast в коде.
1. Проблема, которую решают generics
До generics часто использовали raw types. Такой код компилируется, но ошибка может всплыть только во время выполнения:
List list = new ArrayList();
list.add("Alice");
list.add(100);
String name = (String) list.get(1); // ClassCastException
С generics ошибка ловится раньше:
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(100); // compile-time error
String first = names.get(0);
Главная идея: лучше получить ошибку на этапе компиляции, чем в production.
2. Базовый синтаксис и конвенции
List<String> означает «список строк», Map<String, Integer> — «ключ строка, значение число». Обычно используют такие обозначения тип-параметров:
| Параметр | Типичный смысл |
|---|---|
T | произвольный тип |
E | элемент коллекции |
K / V | ключ / значение |
R | результат функции |
Пример generic-метода:
public static <T> void printAll(List<T> items) {
for (T item : items) {
System.out.println(item);
}
}
Пример generic-класса:
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
3. extends, super и правило PECS
Wildcards позволяют делать API гибче:
public static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) {
total += n.doubleValue();
}
return total;
}
public static void addDefaults(List<? super Integer> out) {
out.add(10);
out.add(20);
}
Логика:
? extends Number: безопасно читать какNumber, но небезопасно писать.? super Integer: безопасно писатьInteger, читать обычно какObject.
Правило PECS:
- Producer Extends — источник данных на чтение.
- Consumer Super — приемник данных на запись.
4. Почему List<Integer> не List<Number>
Generics в Java инвариантны. Это значит, что List<Integer> не является подтипом List<Number>:
List<Integer> ints = List.of(1, 2, 3);
// List<Number> nums = ints; // compile error
Если бы это было разрешено, можно было бы добавить Double в список Integer и сломать типовую целостность.
5. Bounded type parameters
Иногда generic-тип должен поддерживать конкретный контракт:
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
Здесь T обязан быть сравнимым с тем же типом.
Несколько ограничений тоже возможны:
<T extends Number & Comparable<T>>
Сначала класс, затем интерфейсы.
6. Type erasure и практические последствия
В Java generics реализованы через стирание типов (type erasure). Компилятор проверяет типы, но в runtime многие конкретные параметры типа уже недоступны.
Из этого следуют ограничения:
- Нельзя сделать
new T(). - Нельзя надежно проверить
obj instanceof List<String>. - Нельзя создать
new List<String>[10].
Поэтому generics — это в первую очередь compile-time гарантия.
7. Проектирование API с generics
Плохая сигнатура слишком узкая:
void copy(List<Object> dst, List<Object> src)
Хорошая и переиспользуемая:
public static <T> void copy(List<? super T> dst, List<? extends T> src) {
for (T item : src) {
dst.add(item);
}
}
Ещё практичный пример для чтения:
public static void printNumbers(List<? extends Number> nums) {
nums.forEach(System.out::println);
}
И для записи:
public static void fillWithZeros(List<? super Integer> target, int n) {
for (int i = 0; i < n; i++) {
target.add(0);
}
}
8. Частые ошибки новичков
- Использование raw types (
List list) вместо параметризованных (List<String>). - Путаница
extendsиsuper. - Избыточные unchecked cast, чтобы «обойти» компилятор.
- Слишком сложные generic-подписи там, где обычный тип читался бы проще.
9. Практический чеклист
- Всегда задавайте типы коллекций.
- Для чтения проектируйте сигнатуры через
extends. - Для записи — через
super. - Старайтесь, чтобы generic API читался без ментальной перегрузки.
- Если сигнатура сложная, добавьте короткий Javadoc с примером использования.
Что важно запомнить
Generics — это не «сложный синтаксис ради синтаксиса», а механизм надежности: меньше runtime-ошибок, меньше ручных привидений типов и более понятные контракты API для всей команды.