Зачем нужны Generics
- Безопасность типов на этапе компиляции (меньше
ClassCastException). - Переиспользование кода для разных типов.
- Самодокументируемость API:
Map<String, Integer>понятнее, чемMap.
Базовый синтаксис: <T>, <E>, <K, V>
Класс/интерфейс с параметром типа
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Box<String> sb = new Box<>("hi");
String s = sb.get(); // безопасно, без кастовНесколько параметров
public class Pair<K, V> {
private final K key; private final V value;
public Pair(K key, V value) { this.key = key; this.value = value; }
public K key() { return key; } public V value() { return value; }
}Дженерик-методы
public static <T> T first(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}Ограничения (bounds)
public class NumberBox<T extends Number> { /* ... */ }
// несколько ограничений:
public static <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}Wildcards: ?, ? extends, ? super + принцип PECS
Инвариантность: ключ к пониманию
List<Integer> не является ни List<Number>, ни подтипом, ни супертипом.
Массивы ковариантны (Number[] arr = new Integer[10]), generics — инвариантны.
Wildcard ?
List<?> — список неизвестного типа: можно читать как Object, но нельзя добавлять ничего (кроме null).
Верхняя граница: ? extends T (“поставщик/producer”)
void printNums(List<? extends Number> src) {
// можно читать как Number
Number n = src.get(0);
// src.add(42); // нельзя — тип точный не известен
}Нижняя граница: ? super T (“потребитель/consumer”)
void addIntegers(List<? super Integer> dst) {
dst.add(1); // можно добавлять Integer (и его подтипы)
// Integer x = dst.get(0); // вернётся Object, читать небезопасно
}Золотое правило PECS
- Producer Extends: если читаешь (источник), используешь
? extends. - Consumer Super: если пишешь (приёмник), используешь
? super.
Классический пример: Collections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }Источник — extends, приёмник — super.
Частые вопросы и ловушки
Почему List<Object> ≠ List<String>?
Инвариантность: иначе можно было бы впихнуть Integer в List<String> через ссылку на List<Object>.
Используй List<?>, если нужен “любой список только для чтения”.
Чем List<? extends Number> отличается от List<Number>?
- В
List<Number>можно добавлятьInteger,Long,Doubleи т. п. - В
List<? extends Number>нельзя добавлять (тип конкретный неизвестен), но можно безопасно читать какNumber.
Чем List<? super Integer> полезен?
Это “корзина” для записи объектов Integer. Читать оттуда можно только как Object.
Захват вайлдкарда (wildcard capture)
Иногда нужен “помощник”, чтобы работать с ?:
static void swapFirst(List<?> list) { swapHelper(list); }
private static <T> void swapHelper(List<T> list) {
if (list.size() > 1) {
T tmp = list.get(0);
list.set(0, list.get(1));
list.set(1, tmp);
}
}Type Inference (выведение типов)
“Ромбик” (diamond) с Java 7+
Map<String, List<Integer>> m = new HashMap<>();Выведение в дженерик-методах
var x = List.of(1, 2, 3); // List<Integer>
var y = first(List.of("a")); // StringИногда явно укажи <T> у вызова: MyClass.<Long>first(...).
Type Erasure (стирание типов)
Что это: параметры типов существуют только во время компиляции.
JVM видит “сырые” типы: List<T> → List. Отсюда ограничения:
Последствия стирания
- Нельзя создать массив параметризованного типа:
List<String>[] arr = new List<String>[10]; // ошибка- Нельзя
new T()иT.class:
T t = new T(); // ошибка
Class<T> c = T.class; // ошибкаОбход: передавать Class<T> в конструктор/метод:
class Factory<T> {
private final Class<T> type;
Factory(Class<T> type) { this.type = type; }
T create() throws Exception { return type.getDeclaredConstructor().newInstance(); }
}- Нельзя перегружать методы, различающиеся только параметрами типа:
void foo(List<String> x) {}
void foo(List<Integer> y) {} // одинаковая эрадикация → конфликтinstanceofтолько с вайлдкардом или сырой формой:
if (obj instanceof List<?>) { /* ок */ }
// if (obj instanceof List<String>) // ошибка компиляции- Мостовые методы (bridge methods): компилятор генерирует их, чтобы сохранить полиморфизм при стирании (часто видно в байткоде/дебаге).
- Reifiable vs non-reifiable:
- Reifiable (сохраняют инфо во время выполнения):
List<?>,List, массивы примитивов. - Non-reifiable:
List<String>,Map<Integer, String>— тип параметра стерт.
Type token / сохранение типа
Используют “носитель типа”:
abstract class TypeRef<T> {
final Type type = ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0];
}
TypeRef<List<String>> ref = new TypeRef<>() {};
System.out.println(ref.type); // java.util.List<java.lang.String>Generics и массивы/varargs
- Массивы ковариантны и проверяются в runtime; generics — инвариантны и проверяются в compile-time.
- Varargs с дженериками ведут к предупреждению
heap pollution.
Помечай метод@SafeVarargs(толькоstatic/final/private) и не смешивай внутренности:
@SafeVarargs
public static <T> List<T> asList(T... items) { return Arrays.asList(items); }Практические сигнатуры для интервью
Максимум по компаратору
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
if (list.isEmpty()) return null;
T best = list.get(0);
for (T t : list) if (t.compareTo(best) > 0) best = t;
return best;
}Копирование из источника в приёмник (PECS)
public static <T> void copyAll(List<? super T> dest, List<? extends T> src) {
for (T t : src) dest.add(t);
}Обобщённый репозиторий в стиле Spring
public interface Repository<T extends BaseEntity, ID> {
T save(T entity);
Optional<T> findById(ID id);
}Типичные ошибки
- ❌ Сырые типы (
List list) → теряешь безопасность, лезутClassCastException. - ❌ Возврат
List<? extends T>из метода API. Лучше вернутьList<T>; вайлдкарды — в аргументах, а не в возвращаемых типах. - ❌ Злоупотребление
? extendsтам, где нужно писать. Помни PECS. - ❌ Пытаться перегрузить методы, различающиеся только параметрами типа (стирание сломает).
- ❌
List<Object>вместоList<?>для “любой коллекции только для чтения”.
Best practices
- Придерживайся PECS:
extends— читаем,super— пишем - В публичных API используй вайлдкарды в параметрах, а не в возвращаемых типах
- Избегай сырых типов; если пришлось — изолируй и сопровождай
@SuppressWarnings("unchecked")с комментарием - Давай смысленные имена параметрам типа:
T,E,K,V,ID,R,U - Для фабрик/рефлексии принимай
Class<T>илиType(type token), не изобретайnew T()
Мини-шпаргалка отличий (на память)
List<?>— читаем какObject, не пишем.List<? extends T>— читаем какT, не пишем.List<? super T>— пишемT, читаем какObject.- Generics инвариантны; массивы ковариантны.
- Стирание типов: нет
new T(),instanceof List<String>и перегрузок “только по дженерикам”.