Зачем дженерики
Дженерики дают параметризацию типом: коллекция/класс/метод работает с конкретным типом, а компилятор это проверяет. Ошибка типа ловится до запуска, и не нужны ручные приведения.
До Java 5 коллекции были «сырыми» (List без параметра): клали в них что угодно, доставали Object и приводили вручную — а ошибка вылезала в рантайме как ClassCastException. Дженерики переносят эту проверку на компиляцию.
List<String> list = new ArrayList<>();
list.add("x");
String s = list.get(0); // без приведения — тип проверен компилятором
Что такое type erasure
Дженерики в Java реализованы стиранием (type erasure): компилятор проверяет типы по параметрам, а затем стирает их — заменяет параметр на его границу (или Object, если границы нет) и вставляет нужные приведения. В байткод информация о конкретном T не попадает.
Так сделали ради обратной совместимости при вводе дженериков в Java 5: старый и новый код должны были работать вместе на той же JVM. Цена — дженерики существуют только на этапе компиляции (это «не reified» типы, в отличие, например, от C# или Kotlin inline reified).
// то, что пишем: // то, во что превращается после стирания:
List<String> l = ...; // List l = ...;
String s = l.get(0); // String s = (String) l.get(0); // cast вставил компилятор
Последствия стирания
Раз конкретный тип стёрт, в рантайме его нет: new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() (один класс List). Отсюда запреты: нельзя T.class, new T(), new T[], нельзя instanceof List<String> (только instanceof List<?>), нельзя иметь static-поле типа T, и нельзя завести два метода, чьи сигнатуры различаются только параметром дженерика (после стирания они совпадут).
Как обойти, когда тип реально нужен в рантайме: передавать его явным токеном Class<T> в метод/конструктор (фабрики так и делают: clazz.getDeclaredConstructor().newInstance()), либо приёмом super type token (анонимный подкласс параметризованного типа, у которого тип-аргумент можно достать рефлексией — так работает чтение generic-типов в Jackson/Spring).
// типобезопасная фабрика через Class-токен — обходит запрет new T()
static <T> T create(Class<T> type) throws Exception {
return type.getDeclaredConstructor().newInstance();
}
Дженерик-методы и вывод типов
Метод может быть параметризован независимо от класса: <T> T first(List<T> xs). Тип-аргумент обычно не пишут явно — компилятор выводит его из аргументов (type inference). Тот же вывод стоит за diamond (new ArrayList<>()) и за var.
Иногда вывод не справляется (например, из null или вложенных вызовов) — тогда тип задают явно: Collections.<String>emptyList().
Ограниченные параметры (bounds)
Параметр типа можно ограничить: <T extends Comparable<T>> разрешает вызывать на T методы границы (здесь compareTo). Без границы T стирается до Object, и доступны только методы Object; с границей — стирается до неё, и доступны её методы.
Границ может быть несколько: <T extends Number & Comparable<T>> (класс, если есть, идёт первым). Это делает дженерик-код одновременно гибким и типобезопасным.
Raw types — почему нельзя
Raw type — это использование дженерик-класса без параметра (List вместо List<String>). Он существует только ради совместимости с дописьменным кодом и опасен: использование raw type ОТКЛЮЧАЕТ проверки дженериков для всего объекта, возвращая риск ClassCastException в рантайме и unchecked-предупреждения.
Правило: не использовать raw types в новом коде. Если нужен «любой тип» — это List<?> (типобезопасный wildcard), а не сырой List.
Как отвечать на собеседовании
Объясните erasure как механизм (проверка → стирание до границы/Object → вставка cast) и его мотив (совместимость с Java 5), перечислите последствия (один класс в рантайме; нет new T()/T[]/instanceof generic) и обход через Class-токен/super type token. Добавьте bounded types (зачем — методы границы) и почему raw types запрещены (отключают проверки).