Что такое Generics в Java и как они работают?

Ответ

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

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

  • Type-safety: Компилятор проверяет корректность типов.
  • Reusability: Один обобщенный код для разных типов.
  • Устранение кастов: Избегание ClassCastException.

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

public class Container<T> {
    private T value;

    public void setValue(T value) { this.value = value; }
    public T getValue() { return value; }
}
// Использование
Container<String> stringContainer = new Container<>();
stringContainer.setValue("Hello");
String s = stringContainer.getValue(); // Без приведения типа

Как работают (Type Erasure): Во время компиляции информация о generic-типах стирается.

  • Параметр типа T заменяется на его верхнюю границу (по умолчанию Object).
  • Генерируются необходимые приведения типов (casts).
  • При компиляции Container<String> становится Container<Object>.

Ограничения (Bounds):

// T должен быть подтипом Number
public class NumericContainer<T extends Number> { ... }
// ? - wildcard (подстановочный знак)
public void processList(List<? extends Number> list) { ... } // Producer (чтение)
public void addToList(List<? super Integer> list) { ... } // Consumer (запись)

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

  • Producer Extends (? extends T) — если структура является источником данных (читаем).
  • Consumer Super (? super T) — если структура является приемником данных (записываем).

Ответ 18+ 🔞

Слушай, а вот эти ваши дженерики — это вообще пиздец какая удобная штука, если разобраться. Ну, типа, чтобы не писать один и тот же класс на каждый чих, под каждый тип, понимаешь? Один раз написал — и потом подсовываешь туда что угодно: строки, числа, свои объекты... Главное, чтобы компилятор не охуел от твоего креатива.

Вот смотри, в чём суть-то, блядь. Раньше-то как было? Хранил ты всё в Object, потому что он всему родитель. Засунул туда строку, достал — ага, надо кастовать: (String) obj. И если ты, мудак, случайно туда не строку сунешь, а, допустим, Integer, то в рантайме тебе пизда — ClassCastException прямо в ебало. А с дженериками компилятор, этот бдительный сукин сын, сразу тебя за руку ловит: «Куда, пидор? Ты же сюда только String договорился класть!».

Вот, например, контейнер для чего угодно. Пишем один раз и ебёмся с типом потом:

public class Контейнер<T> { // <--- Видишь эту Т? Это типа шаблон. Подставим потом что угодно.
    private T содержимое;

    public void засунуть(T штука) { this.содержимое = штука; }
    public T вытащить() { return содержимое; } // И кастовать не надо, ёпта!
}

// А используем вот так:
Контейнер<String> дляСтрок = new Контейнер<>();
дляСтрок.засунуть("Привет, мир!");
String s = дляСтрок.вытащить(); // Всё, чисто, без плясок с бубном!

А как внутри-то работает, интересно? А хуй его знает! Шучу. Работает через стирание типов, ядрёна вошь. Компилятор, хитрая жопа, на этапе компиляции все эти <String>, <Integer> выкидывает к хуям и заменяет на Object (или на верхнюю границу, если она есть). А потом сам же, небось, в нужных местах невидимые касты подставляет. То есть в байт-коде твой Контейнер<String> и Контейнер<Integer> — это один и тот же, блядь, класс. Магия, да? Чистый фокус-покус для твоей же безопасности.

Но это ещё не всё! Можно же и ограничения ставить, чтобы не любой тип совали. Типа, «только числа, мудаки!».

public class ЧисловойКонтейнер<T extends Number> { ... } // Теперь T — это только Number или его наследник (Integer, Double).
ЧисловойКонтейнер<Integer> ок = new ЧисловойКонтейнер<>();
ЧисловойКонтейнер<String> неОк = new ЧисловойКонтейнер<>(); // Компилятор: «Иди нахуй!».

А ещё есть эти... как их... вайлдкарды, ?, подстановочные знаки. Вот где мозг можно ебнуть, если не понять простое правило PECS.

Запоминай, как «Отче наш»:

  • Producer Extends (? extends T) — если из коллекции ТОЛЬКО ЧИТАЕШЬ (она тебе производит данные). List<? extends Number> — можешь читать как Number.
  • Consumer Super (? super T) — если в коллекцию ТОЛЬКО ПИШЕШЬ (она твой потребитель). List<? super Integer> — можешь писать Integer и его наследников.

Проще говоря, если метод забирает объекты из коллекции — используй extends. Если метод суёт объекты в коллекцию — используй super. А если и то, и другое — тогда, блядь, не используй ? вообще, используй конкретный тип. И всё, и никакой хуйни!

Вот и весь сказ, блядь. Кажется сложным, но как вникнешь — жить без них уже не сможешь, как без рук. Главное — безопасность типов и никаких лишних кастов. Компилятор доволен, ты доволен. Красота, ёпта!