Дженерики (generics) в Java — это механизм параметризации типов: класс, интерфейс или метод объявляется с параметром типа (<T>), а конкретный тип (String, Integer) подставляется при использовании. Нужны они для того, чтобы ошибку «положили в коллекцию не тот тип» ловил компилятор, а не пользователь во время работы программы, и чтобы код не был заставлен ручными приведениями (String) obj.
Содержание
- Мини-словарь: пять терминов, которые путают
- Зачем нужны дженерики: ошибка до и после
- Минимальный рабочий пример: класс, метод и ограничение
- Инвариантность: почему
List<Integer>— неList<Number> - Wildcard:
? extends,? superи правило PECS - Стирание типов и его последствия
- Raw types и загрязнение кучи
- Соглашение об именах параметров
- Если не компилируется: симптом -> причина
- Выводы
- Где применяется / связь с практикой
- FAQ
Важная граница: дженерики работают на этапе компиляции. В байт-коде параметры типов в основном стираются (type erasure), поэтому List<String> и List<Integer> во время выполнения — один и тот же класс ArrayList. Из этого следуют почти все ограничения дженериков, о которых ниже. Примеры проверены на OpenJDK 25.0.4 (Java 25 LTS) 24.09.2026.
Мини-словарь: пять терминов, которые путают
| Термин | Пример | Что это |
|---|---|---|
| Параметр типа | T в class Box<T> |
«переменная» для типа в объявлении |
| Аргумент типа | String в Box<String> |
конкретный тип при использовании |
| Параметризованный тип | List<String> |
тип с подставленным аргументом |
| Raw type (сырой тип) | List без <...> |
обобщенный тип без аргумента, наследие до Java 5 |
| Wildcard | List<? extends Number> |
«какой-то тип» с границей, только в местах использования |
Зачем нужны дженерики: ошибка до и после
Дженерики появились в Java 5 (2004). До этого коллекции хранили Object, и тип проверялся только приведением при чтении. Вот как это выглядит (raw type — так писали до Java 5):
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List names = new ArrayList(); // raw type: без параметра
names.add("Анна");
names.add(42); // компилятор не возражает
for (Object o : names) {
String s = (String) o; // приведение вручную
System.out.println(s.toUpperCase());
}
}
}
Компиляция проходит, javac лишь предупреждает Note: Main.java uses unchecked or unsafe operations. (при запуске без компиляции java Main.java вместо Note сразу печатаются подробные предупреждения [unchecked] unchecked call to add(E) as a member of the raw type List). При запуске первая строка печатается, а на второй программа падает:
АННА
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at Main.main(Main.java:10)
Исправление — указать тип элементов: List<String> names = new ArrayList<>();. Теперь та же строка names.add(42); не компилируется:
Main.java:8: error: incompatible types: int cannot be converted to String
names.add(42);
^
Ошибка переехала из рантайма (у пользователя) в компиляцию (у разработчика), а приведение (String) o больше не нужно: for (String s : names). Это и есть главный ответ на вопрос, для чего нужны дженерики: типобезопасность без ручных приведений плюс один код для многих типов.
Минимальный рабочий пример: класс, метод и ограничение
Один файл показывает три формы дженериков сразу. Сохраните как Main.java, запустите java Main.java (Java 11+ умеет запускать один файл без отдельного javac).
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class Main {
// Обобщенный класс: T - параметр типа
static class Box<T> {
private final T value;
Box(T value) { this.value = value; }
T get() { return value; }
<R> Box<R> map(Function<? super T, ? extends R> f) {
return new Box<>(f.apply(value));
}
}
// Обобщенный метод: свой параметр <T> перед типом результата
static <T> T firstOrDefault(List<T> list, T fallback) {
return list.isEmpty() ? fallback : list.get(0);
}
// Ограничение сверху: T умеет сравниваться сам с собой
static <T extends Comparable<? super T>> T max(List<T> list) {
T best = list.get(0);
for (T x : list) {
if (x.compareTo(best) > 0) best = x;
}
return best;
}
public static void main(String[] args) {
Box<String> box = new Box<>("generics");
String s = box.get(); // без приведения
Box<Integer> len = box.map(String::length);
System.out.println(s + " -> " + len.get());
List<String> empty = new ArrayList<>();
System.out.println(firstOrDefault(empty, "нет данных"));
System.out.println(max(List.of(3, 17, 5)));
System.out.println(max(List.of("груша", "яблоко", "апельсин")));
}
}
В консоли будет:
generics -> 8
нет данных
17
яблоко
Разбор по частям:
- Обобщенный класс
Box<T>:Tможно использовать как тип поля, параметра и результата.new Box<>("generics")— «ромбовидный оператор» (diamond, с Java 7): аргумент типа компилятор выводит сам из левой части. - Обобщенный метод
<T> T firstOrDefault(...): параметр<T>объявлен у самого метода, класс при этом может быть необобщенным. Тип выводится из аргументов; явно его можно указать так:Main.<String>firstOrDefault(empty, "x"). - Ограничение
T extends Comparable<? super T>: вmaxможно передать только то, что умеетcompareTo. Внутри метода уTдоступны методы границы. Строки сравниваются по кодам символов, а не по правилам локали, поэтому «яблоко» оказалось максимумом. Граница примера: на пустом спискеmaxупадет сIndexOutOfBoundsExceptionнаlist.get(0), в рабочем коде пустой ввод проверяют заранее или возвращаютOptional<T>.
У extends в дженериках смысл «является подтипом» — и для классов, и для интерфейсов. Границ может быть несколько: <T extends Number & Comparable<T>>, класс идет первым.
Инвариантность: почему List<Integer> — не List<Number>
Integer — наследник Number, но List<Integer> не наследник List<Number>. Параметризованные типы инвариантны. Проверка компилятором:
Main.java:11: error: incompatible types: List<Integer> cannot be converted to List<Number>
List<Number> nums = ints;
^
Причина практическая: если бы присваивание прошло, через nums.add(3.14) в список целых можно было бы положить Double. Массивы в Java, наоборот, ковариантны (Object[] arr = new String[1] компилируется), и та же ошибка всплывает только при запуске как ArrayStoreException. Дженерики закрывают эту дыру на этапе компиляции.
Wildcard: ? extends, ? super и правило PECS
Если метод должен принимать и List<Integer>, и List<Double>, нужен wildcard. Направление зависит от того, читает метод из коллекции или пишет в нее.
import java.util.ArrayList;
import java.util.List;
public class Main {
// ? extends: только читаем (производитель)
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
// ? super: только кладем (потребитель)
static void fillWithOnes(List<? super Integer> target, int count) {
for (int i = 0; i < count; i++) target.add(1);
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(0.5, 1.5);
System.out.println(sum(ints) + " " + sum(doubles));
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
fillWithOnes(numbers, 2);
fillWithOnes(objects, 3);
System.out.println(numbers + " " + objects);
}
}
Результат: 6.0 2.0 и [1, 1] [1, 1, 1]. А попытка записать в List<? extends Number> не компилируется, потому что реальный тип элементов неизвестен (это может быть и List<Double>):
Main.java:6: error: incompatible types: int cannot be converted to CAP#1
nums.add(1);
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
Правило PECS (Producer Extends, Consumer Super, формулировка Джошуа Блоха из Effective Java) сводит выбор к таблице:
| Что делает метод с коллекцией | Wildcard | Можно | Нельзя |
|---|---|---|---|
| Только читает (коллекция — производитель) | ? extends T |
получать элементы как T |
добавлять (кроме null) |
| Только пишет (коллекция — потребитель) | ? super T |
добавлять T |
читать как T (только как Object) |
| И читает, и пишет | T без wildcard |
все | — |
| Тип не важен (размер, очистка) | ? |
методы, не зависящие от типа | добавлять элементы |
Живой пример из JDK: Collections.copy(List<? super T> dest, List<? extends T> src) — источник производит, приемник потребляет.
Стирание типов и его последствия
Компилятор проверяет типы, а затем стирает параметры: T заменяется на границу (Object или, например, Comparable), в местах чтения вставляются приведения. Так дженерики сохранили совместимость с кодом до Java 5. Проверка в Java 25:
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true
System.out.println(a.getClass().getName()); // java.util.ArrayList
Что из этого следует на практике (ошибки javac 25 из реального прогона):
| Что пытаемся сделать | Ошибка javac | Как обойти |
|---|---|---|
new T() |
unexpected type ... found: type parameter T |
передать фабрику Supplier<T> или Class<T> |
new T[n] |
generic array creation |
List<T> или массив от переданного Class<T> |
o instanceof List<String> (o — Object) |
Object cannot be safely cast to List<String> |
o instanceof List<?>, элементы проверять отдельно |
List<int> |
unexpected type ... required: reference found: int |
List<Integer> (автоупаковка) |
два print(List<String>) и print(List<Integer>) |
name clash: ... have the same erasure |
разные имена методов |
Обход через фабрику, проверенный прогоном:
static <T> List<T> fill(Supplier<T> factory, int n) {
List<T> out = new ArrayList<>();
for (int i = 0; i < n; i++) out.add(factory.get());
return out;
}
// fill(StringBuilder::new, 2).size() -> 2
Raw types и загрязнение кучи
Raw type (List без аргумента) оставлен ради старого кода. Смешивание сырого и параметризованного типа отключает проверку и дает загрязнение кучи (heap pollution): в List<String> оказывается Integer, а падает программа не в месте ошибки, а позже, при чтении.
List<String> a = new ArrayList<>();
List raw = a; // warning: [rawtypes]
raw.add(42); // warning: [unchecked], но компилируется
System.out.println(a.size()); // 1
String first = a.get(0); // ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
Практическое правило: не писать raw types в новом коде, собирать проект с -Xlint:unchecked,rawtypes и не подавлять предупреждение @SuppressWarnings("unchecked"), пока не доказано, что приведение безопасно.
Соглашение об именах параметров
Имена параметров типа по соглашению — одна заглавная буква, чтобы их не путать с именами классов:
| Буква | Смысл | Где встречается |
|---|---|---|
T |
Type, тип | Box<T>, Optional<T> |
E |
Element, элемент | List<E>, Set<E> |
K, V |
Key, Value | Map<K, V> |
N |
Number | числовые утилиты |
R |
Result | Function<T, R> |
S, U |
второй, третий тип | BiFunction<T, U, R> |
Это соглашение, не правило языка: имя <Item> скомпилируется, но читатель кода примет его за класс.
Если не компилируется: симптом -> причина
| Симптом | Вероятная причина | Что сделать |
|---|---|---|
List<Integer> cannot be converted to List<Number> |
инвариантность | параметр метода List<? extends Number> |
cannot be converted to CAP#1 |
запись в ? extends |
? super T или конкретный T |
Note: ... uses unchecked or unsafe operations |
raw type или непроверенное приведение | пересобрать с -Xlint:unchecked и найти строку |
ClassCastException там, где приведений не видно |
heap pollution через raw type | искать raw types выше по потоку данных |
generic array creation |
массив из T |
List<T> |
Выводы
- Дженерики — параметризация типов:
Box<T>,List<String>,<T> T method(...). Главная польза — ошибки типов ловит компилятор, приведения не нужны. - Параметризованные типы инвариантны:
List<Integer>неList<Number>. Гибкость дают wildcard? extends(чтение) и? super(запись) по правилу PECS. - Ограничение
T extends Xоткрывает методыXвнутри обобщенного кода; границ может быть несколько. - Из-за стирания типов нельзя
new T(),new T[],instanceof List<String>,List<int>и перегрузка по аргументу типа. - Raw types оставлены для совместимости и ведут к heap pollution; в новом коде — только параметризованные типы и сборка с
-Xlint.
Где применяется / связь с практикой
Дженерики — повседневный инструмент Java-разработчика: на них построены коллекции, Optional, Stream API, функциональные интерфейсы, репозитории Spring Data и типизированные DTO. На собеседованиях про PECS и стирание типов спрашивают почти всегда, потому что без них не понять сигнатуры JDK. Как устроены сами коллекции, на которых дженерики применяются чаще всего, разобрано в статье о коллекциях в Java.
Освойте тему на практике
Если хочется системно пройти обобщенное программирование, коллекции, Stream API и многопоточность на реальных задачах, посмотрите курс «Java разработчик. Продвинутый уровень». Разобрать отдельную тему с преподавателем бесплатно можно на открытых уроках Otus.
FAQ
Чем дженерики Java отличаются от шаблонов C++?
Шаблон C++ порождает отдельный код для каждого аргумента типа, а в Java один класс после стирания обслуживает все параметризации. Поэтому в Java нельзя параметризовать примитивом и узнать T во время выполнения.
Можно ли узнать тип T во время выполнения?
Напрямую нет. Обычно передают Class<T> в конструктор; тип из сигнатуры наследника (class Repo extends Base<User>) можно прочитать через рефлексию getGenericSuperclass().
Что такое List<?> и чем он отличается от raw List?
List<?> — список неизвестного, но конкретного типа: компилятор не даст добавить в него элемент, кроме null. Raw List проверку отключает и позволяет положить что угодно.
Есть ли дженерики у статических полей?
Нет: параметр типа класса принадлежит экземпляру, а статическое поле общее для всех параметризаций, поэтому static T value не компилируется. У статических методов свой параметр объявляется отдельно: static <T> ....



