RU | EN | DE

Зачем нужны 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. Отсюда ограничения:

Последствия стирания

  1. Нельзя создать массив параметризованного типа:
List<String>[] arr = new List<String>[10]; // ошибка
  1. Нельзя 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(); }
}
  1. Нельзя перегружать методы, различающиеся только параметрами типа:
void foo(List<String> x) {}
void foo(List<Integer> y) {} // одинаковая эрадикация → конфликт
  1. instanceof только с вайлдкардом или сырой формой:
if (obj instanceof List<?>) { /* ок */ }
// if (obj instanceof List<String>) // ошибка компиляции
  1. Мостовые методы (bridge methods): компилятор генерирует их, чтобы сохранить полиморфизм при стирании (часто видно в байткоде/дебаге).
  2. 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> и перегрузок “только по дженерикам”.