Урок по треку Java 7 мин

Дженерики, wildcard и стирание типов

Параметризованные типы, wildcard, PECS, стирание типов и его последствия.

Дженерики дают проверку типов на этапе компиляции: List<String> не примет Integer, и не нужны приведения при чтении. На собесе спрашивают три вещи: PECS, стирание и почему List<Integer> не наследник List<Number>.

Базовый синтаксис

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

static <T extends Comparable<T>> T max(List<T> list) { ... }

T extends Comparable<T> это ограничение сверху: подойдёт только тип, сравнимый сам с собой. Можно несколько: <T extends Number & Comparable<T>> (класс первым).

Примитивы параметром быть не могут: List<int> нельзя, только List<Integer>.

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

Integer наследник Number, но List<Integer> не наследник List<Number>. Иначе можно было бы сделать так:

List<Integer> ints = new ArrayList<>();
List<Number> nums = ints;    // не компилируется
nums.add(3.14);              // и в списке целых оказался бы Double

Массивы, наоборот, ковариантны (Integer[] это Number[]), и та же ошибка ловится только в рантайме ArrayStoreException.

Wildcard и PECS

  • List<? extends Number>: список чего-то, что является Number. Читать можно (получаем Number), добавлять нельзя (кроме null): неизвестно, какой именно подтип.
  • List<? super Integer>: список Integer или его предков. Добавлять Integer можно, читать только как Object.
  • List<?>: неизвестный тип, только чтение как Object.

Правило PECS: Producer Extends, Consumer Super. Источник данных объявляют через extends, приёмник через super.

static <T> void copy(List<? super T> dst, List<? extends T> src) {
    for (T t : src) dst.add(t);
}

Так устроен Collections.copy, а Comparator<? super T> встречается по всему JDK: компаратор для Number подходит и для Integer.

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

Дженерики существуют только для компилятора. В байткоде List<String> и List<Integer> это один класс List, параметр заменён на свою границу (Object или Comparable), а в местах чтения вставлены приведения. Отсюда ограничения:

  • нельзя new T() и new T[10];
  • нельзя obj instanceof List<String> (можно instanceof List<?>);
  • нельзя перегрузить void f(List<String>) и void f(List<Integer>): после стирания одна сигнатура;
  • статическое поле static T value невозможно: оно одно на все параметризации;
  • list.getClass() одинаков для любых T.

Если нужен тип в рантайме, передают Class<T> (type token) или используют TypeReference (Jackson), который вытаскивает параметр из сигнатуры анонимного подкласса: информация о параметрах в объявлениях классов, полей и методов в байткоде сохраняется.

Сырые типы и heap pollution

List raw = new ArrayList<String>() это сырой тип для совместимости со старым кодом. Через него можно положить что угодно, а ClassCastException вылетит далеко от места ошибки. Это и есть heap pollution. Компилятор предупреждает unchecked, такие предупреждения не глушат без причины. Varargs с дженериками (T... args) тоже создают массив стёртого типа, поэтому в JDK на безопасных методах стоит @SafeVarargs.

Частые ошибки

  • Ждать, что List<Object> примет List<String> в параметре метода (нужен List<?>).
  • Пытаться добавить элемент в List<? extends T>.
  • Использовать сырые типы и подавлять предупреждения.

Что спросят на собесе

  • Что такое стирание типов и какие у него последствия?
  • Объясните PECS на примере Collections.copy.
  • Почему List<Integer> нельзя присвоить в List<Number>, а Integer[] в Number[] можно?
  • Как получить тип дженерика в рантайме?
  • Чем List<?> отличается от List<Object> и сырого List?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.