Параметризованные классы

Определение

Параметризованный класс получает тип как параметр и использует его в полях и методах. Так один алгоритм работает с разными типами, а компилятор проверяет согласованность.

public final class Box<T> {
    private final T value;
 
    public Box(T value) {
        this.value = value;
    }
 
    public T value() {
        return value;
    }
}
 
Box<String> text = new Box<>("ready");
String value = text.value(); // приведение не нужно

T — не специальное слово, а имя параметра типа. Для нескольких ролей часто используют K, V, E, но говорящее имя тоже допустимо.

Ограничения типа

Bound сообщает, какие операции доступны внутри класса:

public final class Range<T extends Comparable<? super T>> {
    private final T min;
    private final T max;
 
    public Range(T min, T max) {
        if (min.compareTo(max) > 0) {
            throw new IllegalArgumentException("min > max");
        }
        this.min = min;
        this.max = max;
    }
}

Несколько ограничений записываются через &; класс, если он есть, должен стоять первым:

<T extends Number & Comparable<T>>

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

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

Для отношений между параметризованными типами применяют wildcard:

double sum(List<? extends Number> source) {
    return source.stream()
            .mapToDouble(Number::doubleValue)
            .sum();
}
 
void addDefaults(List<? super Integer> target) {
    target.add(0);
}

Правило PECS:

  • producer — ? extends T: безопасно читать T, нельзя добавлять произвольный T;
  • consumer — ? super T: можно добавлять T, читать можно только как Object.

Стирание типов

Java реализует generics в основном через стирание. После компиляции JVM обычно не хранит аргумент T у обычного объекта:

  • нельзя создать new T();
  • нельзя написать new T[10];
  • нельзя проверить instanceof Box<String>;
  • статическое поле не может иметь тип параметра класса — статическое состояние общее для всех экземпляров.
// Нельзя:
// private static T cached;
// T[] values = new T[10];

При необходимости фабрику передают явно:

static <T> T create(Supplier<T> factory) {
    return factory.get();
}

Generic-класс и generic-метод

Метод может иметь собственный параметр типа независимо от класса:

public static <T> Box<T> of(T value) {
    return new Box<>(value);
}

<T> до возвращаемого типа объявляет параметр метода. Diamond <> позволяет компилятору вывести аргумент типа из контекста.

Ловушки

  • Считать Container<Child> подтипом Container<Parent>.
  • Использовать raw type и терять проверку компилятора.
  • Пытаться создавать T или массив T без переданной фабрики/типа.
  • Возвращать wildcard без необходимости: вызывающему коду труднее использовать результат.
  • Путать ограничение <T extends Number> с wildcard ? extends Number: первое объявляет тип, второе описывает семейство уже параметризованных типов.

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

  1. Зачем параметризовать класс?
  2. Почему List<Integer> не является List<Number>?
  3. Как работает PECS?
  4. Что означает стирание типов и какие ограничения оно создаёт?
  5. Чем параметр типа метода отличается от параметра типа класса?
  6. Почему нельзя создать new T()?

Связанные заметки

Обобщения generics · Raw Type · List · Функциональные интерфейсы · Преобразование типов Java