Дженерики дают проверку типов на этапе компиляции: 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?