Что такое Generics (обобщения) в 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.

Ответ 18+ 🔞

Давай разберём эту штуку, которая всех новичков в ступор вгоняет, а потом оказывается, что жить без неё нельзя. Generics, обобщения, эти ваши <T> в угловых скобках.

Представь себе, ты — царь и бог на своей кухне. У тебя есть один волшебный контейнер, в который можно положить что угодно: и хлеб, и гвозди, и носки. Это старый стиль, List list = new ArrayList(). Кладёшь туда строку "hello", а когда достаёшь — охуеваешь, потому что это оказывается кирпич. И получаешь ClassCastException прямо в ебало во время работы программы. Пиздец и расстройство.

List list = new ArrayList(); // Это контейнер для всего, он же свалка
list.add("hello");
String s = (String) list.get(0); // Надо гадать и приводить тип
Integer i = (Integer) list.get(0); // Runtime: БАБАХ! ClassCastException, всё сломалось!

А теперь Generics. Это как если бы ты взял этот контейнер и наклеил на него ярлык: «ТОЛЬКО ДЛЯ СТРОК, СУКА». Или «ТОЛЬКО ЦИФРЫ». Компилятор — это такой дотошный охранник, который смотрит на ярлык и не пускает мимоходом засунутый в список кирпич. Ошибка видна сразу, когда пишешь код, а не когда программа уже у клиента на сервере ебнулась.

List<String> list = new ArrayList<>(); // Ярлык "String" наклеен
list.add("hello");
String s = list.get(0); // Всё чисто, никаких танцев с (String)
// list.add(123); // Компилятор: "Нихуя себе, дружок! Ты куда это integer в список для строк суёшь? Иди нахуй!" Ошибка компиляции.

Зачем это, блядь, нужно?

  1. Безопасность типов: Охранник-компилятор не пропустит левое. Ошибки — на этапе написания, а не в проде. Волнение ебать — ноль.
  2. Убрать эти ёбаные приведения: Заебался уже писать (String) something. Теперь код чистый.
  3. Один раз написал — для всех типов работает: Создал алгоритм для коробки Box<T>, и она работает и для строк, и для чисел, и для твоих кастомных объектов.

Создаём свою коробку с дженериком:

public class Box<T> { // T — это типа заглушка. Можешь назвать как угодно: Type, E, Shmotka
    private T content; // Здесь будет лежать что-то типа T

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

// Используем
Box<String> stringBox = new Box<>();
stringBox.setContent("Письмо с угрозами");
// stringBox.setContent(42); // Не прокатит! Компилятор заругается.

Box<Integer> intBox = new Box<>();
intBox.setContent(42); // А вот тут — красота.

А теперь дикие карты (Wildcards). Это когда ты не знаешь точно тип, но знаешь его «родословную».

  1. <?> — «Мне похуй, что там» (Unbounded).

    public void printList(List<?> list) {
        for (Object elem : list) System.out.println(elem); // Всё приводится к Object
    }
    // Передать можно список чего угодно, но положить в него ничего нового нельзя. Только смотреть.
  2. <? extends Number> — «Дай мне Number или любого его сына/внука» (Upper Bounded).

    public double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number n : list) sum += n.doubleValue(); // У всех наследников Number есть doubleValue()
        return sum;
    }
    // Можно передать List<Integer>, List<Double>. Но, внимание, хуй ты добавишь в этот список что-то новое! Только читать.
  3. <? super Integer> — «Дай мне Integer или его папу/дедушку» (Lower Bounded).

    public void addNumbers(List<? super Integer> list) {
        list.add(1); // А вот тут — можно добавлять Integer!
        list.add(2);
    }
    // Можно передать List<Integer>, List<Number>, List<Object>. Потому что везде можно положить циферку.

Главная подстава (Type Erasure): Java, чтоб её, на этапе компиляции стирает всю информацию о дженериках. Во время выполнения твой List<String> превращается просто в List. Поэтому некоторые вещи невозможны:

// НЕ РАБОТАЕТ, НЕ МЕЧТАЙ:
if (list instanceof List<String>) { ... } // Ошибка компиляции. Во время выполнения никаких String в типе нет.
T instance = new T(); // Ошибка. Какой T, блядь? Он же стёрся.
T[] array = new T[10]; // Ошибка. Массив создаётся в runtime, а T уже нет.

Решение? Для массивов часто используют List<T> (списки), а если уж очень надо массив — костыль с приведением и аннотацией, чтобы компилятор не ныл:

@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[size]; // Делай так, если очень припёрло, но это небезопасно.

Золотое правило PECS (Producer Extends, Consumer Super): Запоминай, чтобы не ебать мозг себе и другим:

  • PRODUCER (поставщик, читаешь из коллекции) → Используй extends (? extends T).
  • CONSUMER (потребитель, пишешь в коллекцию) → Используй super (? super T).

Итог: Дженерики — это не просто синтаксический сахар, это банда-анал безопасности твоего кода. Используй всегда. Пиши List<String>, а не просто List. И будет тебе счастье, а не внезапный ClassCastException посреди ночи на проде.