Что такое Generics (обобщения) в Java и какие проблемы они решают?

«Что такое Generics (обобщения) в Java и какие проблемы они решают?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Generics (обобщения) — это механизм Java, позволяющий создавать типобезопасные классы, интерфейсы и методы с параметризованными типами. Тип указывается в угловых скобках: <T>.

Основные преимущества:

  1. Безопасность типов (Type Safety): Ошибки несоответствия типов обнаруживаются на этапе компиляции, а не во время выполнения.
  2. Устранение приведения типов (Casts): Код становится чище, без постоянных (String) list.get(0).
  3. Повторное использование кода: Один алгоритм может работать с разными типами данных.

Без Generics (старый стиль):

List list = new ArrayList(); // Raw type
list.add("hello");
String s = (String) list.get(0); // Требуется явное приведение
Integer i = (Integer) list.get(0); // ClassCastException во время РАБОТЫ!

С Generics:

List<String> list = new ArrayList<>(); // Тип указан
list.add("hello");
String s = list.get(0); // Приведение не нужно
// list.add(123); // ОШИБКА КОМПИЛЯЦИИ - нельзя добавить Integer в List<String>

Создание generic-класса:

public class Box<T> {
    private T content;

    public void setContent(T content) { this.content = content; }
    public T getContent() { return content; }
}

// Использование
Box<String> stringBox = new Box<>();
stringBox.setContent("Text");
Box<Integer> intBox = new Box<>();
intBox.setContent(42);

Wildcards (подстановочные типы):

// 1. Unbounded Wildcard - любой тип
public void printList(List<?> list) {
    for (Object elem : list) System.out.println(elem);
}

// 2. Upper Bounded Wildcard - T и его ПОДКЛАССЫ
public double sumOfList(List<? extends Number> list) {
    double sum = 0.0;
    for (Number n : list) sum += n.doubleValue();
    return sum;
}
// Можно передать List<Integer>, List<Double>, но НЕЛЬЗЯ добавлять элементы!

// 3. Lower Bounded Wildcard - T и его НАДКЛАССЫ
public void addNumbers(List<? super Integer> list) {
    list.add(1); // Можно добавлять Integer
    list.add(2);
}
// Можно передать List<Integer>, List<Number>, List<Object>

Ограничения Generics (Type Erasure):

  1. Стирание типов: Информация о generic-типах удаляется во время компиляции и недоступна во время выполнения.
    // НЕ РАБОТАЕТ во время выполнения:
    if (list instanceof List<String>) { ... } // Ошибка компиляции
    T instance = new T(); // Ошибка компиляции
    T[] array = new T[10]; // Ошибка компиляции
  2. Решение для массивов: Использовать List<T> вместо массивов или создавать массив через рефлексию (не рекомендуется).
  3. Нельзя использовать примитивы: Только ссылочные типы. Используйте List<Integer>, а не List<int>.

Best Practices:

  1. Всегда используйте Generics для коллекций и собственных generic-классов.
  2. Предпочитайте List<String> raw type List.
  3. Используйте var с Generics для сокращения кода: var list = new ArrayList<String>();
  4. Для создания generic-массивов используйте обходной путь:
    @SuppressWarnings("unchecked")
    T[] array = (T[]) new Object[size];
  5. Помните о PECS (Producer Extends, Consumer Super):
    • Если коллекция производит элементы (T возвращается) — используйте ? extends T.
    • Если коллекция потребляет элементы (T принимается) — используйте ? super T.