Что такое дженерики (Generics) в Java?

Ответ

Дженерики (Generics) — это механизм параметризации типов в Java, позволяющий создавать классы, интерфейсы и методы, которые работают с различными типами данных, обеспечивая безопасность типов на этапе компиляции.

Основная цель:

  • Типобезопасность: Компилятор проверяет соответствие типов, предотвращая ошибки ClassCastException во время выполнения.
  • Устранение приведения типов: Код становится чище, исчезает необходимость в явных кастах (String).
  • Повторное использование кода: Один обобщённый алгоритм или структура данных может работать с разными типами.

Пример обобщённого класса:

public class Container<T> {
    private T item;

    public void setItem(T item) { this.item = item; }
    public T getItem() { return item; }
}

// Использование с конкретным типом
Container<String> stringContainer = new Container<>();
stringContainer.setItem("Text");
String value = stringContainer.getItem(); // Без приведения типа!

Container<Integer> intContainer = new Container<>();
intContainer.setItem(123);

Важные особенности и ограничения:

  • Стирание типов (Type Erasure): Информация о generic-типах удаляется во время компиляции и недоступна в runtime. List<String> и List<Integer> в runtime — это просто List.
  • Нельзя использовать примитивы: Вместо T нельзя подставить int. Используются классы-обёртки: Integer, Double и т.д.
  • Ограничения при создании экземпляров: Нельзя создать объект generic-типа: new T() или массив new T[size].
  • Wildcards (?, ? extends, ? super): Позволяют создавать гибкие API, работающие с семействами типов.

Ответ 18+ 🔞

Давай разберём эту штуку, которая всех новичков вводит в ступор, а потом оказывается, что она везде и без неё нихуя. Дженерики, ёпта. Ну, обобщённое программирование, если по-умному.

Представь себе, блядь, ты пишешь класс Container. Класс-коробка. Раньше, до дженериков, туда можно было положить Object — то есть что угодно, хоть хуй с горы. А когда достаёшь — надо было кастовать: «А, это же была строка!», — и писать (String). И если ошибся — получал ClassCastException прямо в рантайме, в самый неподходящий момент, ядрёна вошь.

Основная идея, блядь, проще пареной репы:

  1. Типобезопасность. Компилятор становится твоим личным надзирателем. Ты ему говоришь: «Слушай, эта коробка — только для строк». И он тебе не даст туда засунуть, например, Integer. Всё проверит на этапе компиляции, и ClassCastException в рантайме — как корова языком слизала.
  2. Убрать эти ёбаные приведения типов. Код становится чище, не надо каждый раз писать (String). Достал из коробки — и уже знаешь, что там.
  3. Переиспользование кода. Написал один раз алгоритм для работы с «чем угодно» (но типобезопасно!), и юзай его и для строк, и для чисел, и для своих кастомных классов. Овердохуища экономии.

Вот смотри, как это выглядит:

public class Container<T> { // Вот эта буква T — это и есть наш параметр типа. Назвать можно как угодно: E, K, V. Главное — объявить в угловых скобках.
    private T item; // Здесь будет лежать наш "что угодно", но конкретного типа.

    public void setItem(T item) { this.item = item; }
    public T getItem() { return item; } // Возвращает T, а не Object! Никакого каста!
}

// А теперь магия, блядь:
Container<String> stringContainer = new Container<>(); // Говорим компилятору: "Эта коробка — СТРОГО для строк, понял?"
stringContainer.setItem("Text"); // Ок.
// stringContainer.setItem(123); // А вот это уже НЕ ОК. Компилятор заорёт: "Ты чё, мудак? Я же сказал — только String!"
String value = stringContainer.getItem(); // Всё! Просто присваиваем String. Никакого (String)!

Container<Integer> intContainer = new Container<>(); // А это уже коробка для чисел.
intContainer.setItem(123);
Integer number = intContainer.getItem(); // И тут всё типобезопасно.

А теперь про подводные ебучки, про которые все молчат, пока ты не наступишь:

  • Стирание типов (Type Erasure). Это, блядь, самое главное и самое обидное. Вся эта красивая информация о <String> и <Integer> стирается при компиляции. В рантайме Container<String> и Container<Integer> — это один и тот же класс Container. Компилятор просто подставляет нужные касты там, где ты вызываешь getItem(). Поэтому, например, нельзя сделать new T() или new T[] — компилятор нихуя не знает, какой там T в рантайме.
  • Примитивы не катят. Нельзя написать Container<int>. Только классы-обёртки: Integer, Double. Волнение ебать, но что поделать.
  • Wildcards (?, ? extends, ? super). Вот это уже для продвинутых шалунов. Когда нужно сделать метод, который работает, например, со списком любых чисел (List<? extends Number> — можно передать и List<Integer>, и List<Double>). Или метод, который добавляет элементы в потребительский список (List<? super Integer> — можно передать List<Integer>, List<Number>, List<Object>). Голова сначала болит, но когда врубаешься — мощный инструмент, чих-пых тебя в сраку.

Короче, дженерики — это чтобы не выстрелить себе в ногу, работая с разными типами. Сначала кажется, что это лишняя писанина, а потом без них жить не можешь.