Логотип Workflow

Article

Updated at:

Generics

Тема 9. Generics

Generics в Java нужны для того, чтобы писать переиспользуемый код без потери типобезопасности. Проще говоря, вы описываете алгоритм один раз, а компилятор следит, чтобы в него не передавали неподходящие типы. Для новичка это особенно важно: generics уменьшают количество поздних runtime-ошибок и убирают лишние cast в коде.

Generics safety

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:

  1. Producer Extends — источник данных на чтение.
  2. 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 многие конкретные параметры типа уже недоступны.

Из этого следуют ограничения:

  1. Нельзя сделать new T().
  2. Нельзя надежно проверить obj instanceof List<String>.
  3. Нельзя создать 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. Частые ошибки новичков

  1. Использование raw types (List list) вместо параметризованных (List<String>).
  2. Путаница extends и super.
  3. Избыточные unchecked cast, чтобы «обойти» компилятор.
  4. Слишком сложные generic-подписи там, где обычный тип читался бы проще.

9. Практический чеклист

  1. Всегда задавайте типы коллекций.
  2. Для чтения проектируйте сигнатуры через extends.
  3. Для записи — через super.
  4. Старайтесь, чтобы generic API читался без ментальной перегрузки.
  5. Если сигнатура сложная, добавьте короткий Javadoc с примером использования.

Что важно запомнить

Generics — это не «сложный синтаксис ради синтаксиса», а механизм надежности: меньше runtime-ошибок, меньше ручных привидений типов и более понятные контракты API для всей команды.

Авторизуйтесь чтоб пройти тесты

Quiz

Проверьте, что вы усвоили

Practice

Интерактивная практика

Выполните задания и сразу проверьте ответ.