Обобщения generics

Generics параметризуют класс, интерфейс или метод типом. Они переносят проверку совместимости в компиляцию и уменьшают число явных приведений.

final class Box<T> {
    private final T value;
 
    Box(T value) {
        this.value = value;
    }
 
    T get() {
        return value;
    }
}

T — параметр типа в объявлении, String в Box<String> — аргумент типа.

Инвариантность

List<Integer> не является подтипом List<Number>, хотя Integer — подтип Number. Иначе в список целых можно было бы добавить Double.

List<Integer> integers = List.of(1, 2);
List<? extends Number> numbers = integers;
Number first = numbers.get(0);
// numbers.add(3); // запрещено: точный тип списка неизвестен

Wildcards и PECS

  • ? extends T — producer: безопасно читать как T, нельзя добавлять;
  • ? super T — consumer: можно добавлять T, чтение даёт Object;
  • ? — неизвестный тип, когда конкретный параметр не важен.
static <T> void copy(
        List<? extends T> source,
        List<? super T> target) {
    target.addAll(source);
}

PECS — удобная подсказка для входных параметров API, но не замена пониманию, кто читает и кто записывает.

Bounds

static <T extends Comparable<? super T>> T max(List<? extends T> values) {
    return values.stream().max(Comparator.naturalOrder()).orElseThrow();
}

У параметра может быть верхняя граница: сначала класс, затем интерфейсы через &. Нижняя граница доступна wildcard (? super T), но не объявлению параметра типа.

Type erasure

Большинство информации о параметрах стирается после компиляции. В байт-коде остаются границы, casts и при необходимости bridge methods. Поэтому нельзя:

  • создать new T();
  • написать T.class;
  • проверить value instanceof List<String>;
  • создать new T[10];
  • использовать примитив как List<int>;
  • иметь static-поле типа параметра класса.

Можно проверить instanceof List<?>. Отдельные сведения о generics в сигнатурах сохраняются для reflection, но объект обычно не знает, что он List<String>.

Raw type (List без аргумента) отключает часть проверок и может привести к heap pollution: переменная параметризованного типа фактически содержит объект другого типа. Предупреждение unchecked нельзя подавлять без анализа.

Связанные темы: Коллекции, Интерфейсы Java, Iterable.

Вопросы для собеседования

  1. Почему List<Integer> не List<Number>? Generics инвариантны.
  2. Что означает PECS? Producer extends, consumer super.
  3. Что возвращает чтение из List<? super Integer>? Только Object без дополнительного знания.
  4. Почему нельзя new T()? После erasure JVM не знает конкретный T.
  5. Что такое raw type? Использование generic-типа без аргумента, теряющее проверки.
  6. Можно ли перегрузить методы m(List<String>) и m(List<Integer>)? Нет, после стирания сигнатуры совпадут.
  7. Зачем bridge method? Сохранить полиморфизм после erasure.