Логотип Workflow

Article

Generics

Тема 9. Generics

Generics в Java это механизм параметризации типов: вы пишете один универсальный код и при этом сохраняете строгую типобезопасность на этапе компиляции.

Если коротко, generics решают две проблемы:

  1. Избавляют от небезопасных приведения типов (cast) в runtime.
  2. Делают API самодокументируемым: по сигнатуре сразу видно, с какими типами работает код.

Generics safety

Почему generics вообще появились

До generics часто использовали «сырой» тип (raw type):

List list = new ArrayList();
list.add("Alice");
list.add(100);
String name = (String) list.get(1); // ClassCastException в runtime

Компилятор не может защитить вас заранее. Ошибка всплывает поздно, иногда уже в продакшене.

С generics:

List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(100); // ошибка компиляции
String first = names.get(0);

Базовый синтаксис

  • T, E, K, V это имена параметров типа (конвенция).
  • List<String> значит: список, который хранит только String.
  • Map<String, Integer> значит: ключ String, значение Integer.

Типовые обозначения:

  • T (Type) — произвольный тип;
  • E (Element) — элемент коллекции;
  • K/V (Key/Value) — карта;
  • R (Result) — тип результата функции.

Пример 1. Обобщенный метод

public static <T> void printAll(List<T> items) {
    for (T item : items) {
        System.out.println(item);
    }
}

Использование:

printAll(List.of("A", "B", "C"));
printAll(List.of(1, 2, 3));

Один метод работает с разными типами без потери проверок компилятора.

Пример 2. Обобщенный класс

public class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

Использование:

Box<String> textBox = new Box<>();
textBox.set("hello");
String text = textBox.get();

Box<Integer> intBox = new Box<>();
intBox.set(42);
int number = intBox.get();

Пример 3. Ограничения extends и super

public static double sum(List<? extends Number> nums) {
    double total = 0.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: главное правило wildcards

Producer Extends, Consumer Super:

  1. Если источник производит данные для чтения, используйте extends.
  2. Если приемник потребляет данные для записи, используйте super.

Это ключ к правильным сигнатурам API.

Invariance: важное ограничение Java Generics

В Java List<Integer> не является подтипом List<Number>.

Нельзя:

List<Integer> ints = List.of(1, 2, 3);
// List<Number> nums = ints; // ошибка

Почему: иначе в nums можно было бы добавить Double, сломав List<Integer>.

Type Erasure (стирание типов)

Generics в Java реализованы через стирание типов:

  • во время компиляции проверяются ограничения типов;
  • в байткоде конкретные type-аргументы в основном стираются.

Практические последствия:

  1. Нельзя сделать new T().
  2. Нельзя напрямую проверять obj instanceof List<String>.
  3. Нельзя создать массив параметризованного типа new List<String>[10].

Ограниченные параметры типа (bounded type parameters)

public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

Здесь T обязан поддерживать сравнение с таким же T.

Можно и несколько ограничений:

<T extends Number & Comparable<T>>

Сначала класс, затем интерфейсы.

Generics + Collections: как проектировать сигнатуры

Плохая сигнатура:

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);
    }
}

Работает гибко и безопасно.

Частые ошибки новичков

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

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

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

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

  • Generics это инструмент качества API и раннего обнаружения ошибок.
  • Главная выгода: меньше runtime-сбоев и меньше ручных cast'ов.
  • Хорошо спроектированные generic-сигнатуры делают код и безопаснее, и удобнее для команды.

Quiz

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

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