Generics и wildcards в Java
Попробовал написать o instanceof List<String> и получил ошибку компиляции: Object cannot be safely cast to List<String>. Попробовал объявить два метода, один для List<String>, другой для List<Integer> - тоже отказ: name clash, m(List<Integer>) and m(List<String>) have the same erasure.
Оба отказа - следствие одной вещи, стирания типов. На собеседовании проверяют, видишь ли ты за зоопарком правил эту единственную причину: понял стирание - девяносто процентов странностей выводятся сами.
// Формулировки: «почему List<Cat> нельзя передать как List<Animal>?», «что такое PECS (producer extends, consumer super)?», «почему instanceof с дженериком не компилируется?»
Стирание и инвариантность
Дженерики - проверка на этапе компиляции. В байт-коде параметры типов стёрты, и List<String> с List<Integer> превращаются в один и тот же класс List. Отсюда обе ошибки из начала темы: проверять в рантайме нечего, а два метода после стирания становятся одним.
Второе следствие - инвариантность: List<Cat> НЕ считается подтипом List<Animal>. Если бы компилятор это разрешил, через ссылку типа List<Animal> в список котов можно было бы положить собаку, и типобезопасность рухнула бы молча.
// Контраст, который любят на собеседовании: массивы, наоборот, ковариантны. Cat[] присваивается в Animal[] без единого возражения - я проверил, компилируется и работает. А запись собаки в такой массив падает в рантайме с ArrayStoreException. Дженерики выбрали строгость на компиляции, массивы - свободу с миной внутри.
- стирание типов
- параметры типов исчезают после компиляции
- инвариантность
- List<Cat> не подтип List<Animal>, в отличие от массивов
Wildcards и PECS
Инвариантность мешает писать общие методы, и для этого есть подстановочные знаки. Написал метод с параметром List<? extends Animal> и попробовал добавить туда кота. Компилятор ответил: incompatible types: Cat cannot be converted to CAP#1, где CAP#1 - свежая типовая переменная.
Расшифровывается это так. Знак «extends» значит «какой-то конкретный подтип Animal, но какой именно - неизвестно». Под ссылкой может лежать список котов, а может собак. Класть туда нельзя ничего, кроме null, зато читать безопасно: что бы там ни было, это точно Animal.
Зеркальный случай - List<? super Cat>, то есть «какой-то надтип кота». Туда кот войдёт всегда, а вот читать оттуда можно лишь как Object. Мнемоника PECS ровно про это: коллекция отдаёт (producer) - пишем extends, коллекция принимает (consumer) - пишем super.
// Отдельно про знак вопроса без границ: из List<?> можно читать как Object и добавлять только null. Это НЕ то же самое, что список без параметра вообще - тот отключает проверки дженериков целиком и оставлен для совместимости со старым кодом.
void feed(List<? extends Animal> zoo) {
Animal a = zoo.get(0); // читать можно
zoo.add(new Cat()); // не компилируется: Cat cannot be converted to CAP#1
}- PECS
- producer extends, consumer super
- сырой тип
- дженерик без параметра: проверки выключены целиком
Что ещё нельзя и почему
Из стирания выводится весь список запретов. Нельзя создать массив параметризованного типа - рантайм не знает, какой это тип. У статических членов нет доступа к параметру типа класса, потому что параметр живёт в экземпляре. И перегрузки, различающиеся только параметром типа, не компилируются - мы это уже видели.
Переменное число аргументов с дженериками создаёт массив стёртого типа, отсюда предупреждение о загрязнении кучи: переменная одного типа начинает ссылаться на объект другого. Аннотация @SafeVarargs - это осознанная подпись «я не выпускаю этот массив наружу», а не способ заглушить предупреждение.
// Достать параметр типа в рантайме можно только окольно - через сигнатуры полей и методов или через анонимный подкласс, который сохраняет параметр в метаданных супертипа. Именно поэтому в Jackson пишут TypeReference с фигурными скобками на конце: это анонимный класс, а не декоративный синтаксис.
- загрязнение кучи
- переменная типа ссылается на объект другого типа
- анонимный подкласс
- приём, сохраняющий параметр типа в метаданных
Как отвечать: «Почему в List<? extends Animal> нельзя добавить элемент?»
Потому что «extends» означает «какой-то конкретный подтип Animal, но компилятор не знает какой». Под этой ссылкой может лежать список котов, и если бы добавить собаку разрешили, список котов оказался бы испорчен без единого предупреждения. Поэтому запись запрещена целиком - даже добавить Animal нельзя, можно разве что null. Я специально смотрел, как это выглядит: компилятор говорит, что Cat не может быть преобразован в CAP#1, то есть в свежую типовую переменную, которой он обозначил этот неизвестный подтип. Читать при этом безопасно: что бы там ни лежало, это точно Animal. Это и есть первая половина мнемоники PECS: коллекция-источник объявляется через extends. Когда мне надо класть, я переворачиваю на super: в List<? super Cat> кот войдёт всегда, а чтение отдаст только Object. И для контраста помню, что массивы устроены наоборот - они ковариантны, Cat[] присваивается в Animal[], а расплата приходит в рантайме через ArrayStoreException.
Сильный ответ: не правило наизусть, а вывод из угрозы типобезопасности, с точной формулировкой компилятора и парным случаем super. Контраст с массивами показывает, что человек понимает, какой размен выбрали дженерики.
На чём валят
- −Считают, что List<String> можно передать туда, где ждут List<Object>. Дженерики инвариантны, это не массивы.
- −Пишут instanceof с параметром типа. Ответ компилятора: Object cannot be safely cast to List<String>.
- −Пробуют добавить элемент в List<? extends Animal>. Нельзя ничего, кроме null: это источник, а не приёмник.
- −Переходят на сырой тип «чтобы скомпилировалось». Проверки выключаются целиком, и ClassCastException переезжает в рантайм к коллеге.
- −Объявляют перегрузки, различающиеся только параметром типа. После стирания это один метод: name clash, have the same erasure.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 16, остальные разбираются в тренажёре.
- Что означает подстановка ? extends Number (wildcard)?A)Супертип Number: в такой список безопасно добавлять и Integer, и Double, и другие наследники NumberB)Точно тип Number и ничего кроме: ни Integer, ни Double в такой список ни прочитать, ни записать не получитсяC)Неизвестный подтип Number — из такого списка можно читать, но не добавлятьD)Обобщение, разрешающее одновременно и чтение, и запись наследников Number без ограничений
показать ответ и разбор
+C)Неизвестный подтип Number — из такого списка можно читать, но не добавлять// разбор: List<? extends Number> — «список какого-то конкретного, но неизвестного подтипа Number». Читать из него можно как Number (producer), а добавлять нельзя (кроме null): компилятор не знает, это List<Integer> или List<Double>, и не даст нарушить типобезопасность. Зеркально ? super Integer — consumer: туда можно писать Integer, а читать только как Object. Отсюда правило PECS.
- Что советует принцип PECS (Producer Extends, Consumer Super)?A)Producer объявляют через super, а consumer — через extends; порядок именно такой по смыслуB)Extends и super взаимозаменяемы, PECS — лишь стилевое соглашение об именовании обобщённых методовC)Обобщённый параметр следует объявлять сразу и с extends, и с super, чтобы покрыть оба случаяD)extends — если из структуры читаем; super — если в неё пишем
показать ответ и разбор
+D)extends — если из структуры читаем; super — если в неё пишем// разбор: PECS помогает выбрать wildcard. Если параметр — ИСТОЧНИК данных, из которого метод читает (producer), берут ? extends T (например, copy(dst, src) — src это ? extends T). Если параметр — ПРИЁМНИК, куда метод пишет (consumer), берут ? super T (dst это ? super T). Так метод максимально гибок по принимаемым типам, оставаясь типобезопасным. Пример из JDK — Collections.copy.
- Что такое raw type (например, List без параметра)?A)Использование обобщённого типа без параметра — теряет проверки типовB)Специальный примитивный тип-список из ранних версий Java, независимый от обобщённого ListC)Обобщённый тип с параметром Object, который компилятор проверяет строже, чем обычный List<Object>D)Неизменяемый список, параметр которого фиксируется в рантайме и не может быть изменён после создания
показать ответ и разбор
+A)Использование обобщённого типа без параметра — теряет проверки типов// разбор: Raw type — обобщённый класс, использованный БЕЗ типа-параметра (List вместо List<String>). Он существует ради совместимости с кодом до Java 5, но отключает проверки дженериков: компилятор выдаёт unchecked-warnings, и легко положить не тот тип, получив ClassCastException в рантайме. В новом коде raw types не используют. List<Object> — не то же самое: он типобезопасен, но не принимает List<String>.
- Зачем ограничивают тип-параметр: <T extends Comparable<T>>?A)Чтобы метод принимал тип T, включая примитивы, без ограничений на аргументB)Чтобы внутри метода вызывать методы этой границы (например, compareTo)C)Чтобы в рантайме сохранить точный тип T и получить возможность создавать массивы этого типа new T[]D)Чтобы автоматически отсортировать переданную коллекцию перед выполнением тела обобщённого метода
показать ответ и разбор
+B)Чтобы внутри метода вызывать методы этой границы (например, compareTo)// разбор: Ограничение <T extends Comparable<T>> говорит: T обязан реализовывать Comparable. Тогда внутри обобщённого метода можно вызывать compareTo у объектов T (без ограничения T стёрся бы до Object, и compareTo был бы недоступен). Так пишут, например, generic-метод max/сортировку. Границ может быть несколько (& через интерфейсы). Стирание при этом убирает T до этой самой границы.
- Можно ли создать массив дженериков: new T[10] или new List<String>[10]?A)Да, обе конструкции легальны и создают массив соответствующего обобщённого типа в рантаймеB)Да, но только внутри статических методов, где тип-параметр известен компилятору на этапе сборкиC)Нет, из-за стирания типов; обходят через List или Object[]+кастD)Нет, потому что массивы в Java запрещено создавать динамически — их размер обязан быть константой
показать ответ и разбор
+C)Нет, из-за стирания типов; обходят через List или Object[]+каст// разбор: Массивы reifiable (знают тип в рантайме и проверяют запись — ArrayStoreException), а дженерики стёрты. Их смешение сломало бы типобезопасность, поэтому new T[] и new List<String>[] запрещены компилятором. На практике используют List<List<String>> вместо массива списков, либо создают Object[]/(T[]) с подавлением unchecked-варнинга (как внутри ArrayList). Это классический вопрос на стыке массивов и дженериков.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.