Обобщения
Сырые типы, обобщённые методы, стирание типов, ограниченные параметры типа, wildcards, PECS и загрязнение кучи.
9 вопросов
MiddleТеорияОчень частоЧто такое стирание типов и какие ограничения рантайма оно накладывает на обобщения?
Что такое стирание типов и какие ограничения рантайма оно накладывает на обобщения?
Обобщения существуют только на этапе компиляции: компилятор стирает каждый параметр типа до его границы или до Object, если границы нет, и вставляет приведения. Поэтому в рантайме List<String> и List<Integer> — это идентичный сырой List без сохранённой информации о типе элемента. Это запрещает new T(), примитивные аргументы вроде List<int> и любую проверку обобщённого типа в рантайме вроде instanceof List<String>.
Типичные ошибки
- ✗Полагать, что аргумент типа доступен в рантайме, например ожидать компиляции
instanceof List<String> - ✗Пытаться написать
new T()илиnew T[], что стирание делает невозможным - ✗Перегружать два метода, отличающихся лишь обобщённым аргументом, которые после стирания совпадают
Уточняющие вопросы
- →Как стирание приводит к ситуации засорения кучи с
@SuppressWarnings("unchecked")? - →Почему для создания обобщённого массива в рантайме нужно передать токен
Class<T>?
JuniorТеорияЧастоЧем параметр типа обобщённого метода отличается от параметра обобщённого класса?
Чем параметр типа обобщённого метода отличается от параметра обобщённого класса?
Обобщённый метод объявляет собственный <T> перед типом возврата, и компилятор выводит его заново на каждом вызове. Класс при этом может быть вовсе не обобщённым — поэтому Collections.emptyList() работает как static. <T> класса привязан один раз к экземпляру, и static-метод им пользоваться не может.
Типичные ошибки
- ✗Считать, что обобщённый метод требует, чтобы его класс был обобщённым
- ✗Ожидать, что static-метод может обратиться к
<T>самого класса - ✗Полагать, что
<T>метода привязан к экземпляру, а не к вызову
Уточняющие вопросы
- →Почему static-метод может объявить свой
<T>, но не может использовать<T>класса? - →Как компилятор выводит аргумент типа обобщённого метода в месте вызова?
JuniorТеорияЧастоЧто такое сырой (raw) List и чем он отличается от List<Object> и List<?>?
Что такое сырой (raw) List и чем он отличается от List<Object> и List<?>?
Сырой (raw) List — обобщённый тип без аргумента типа, наследие эпохи до generics. Компилятор отключает для него проверку типов, поэтому положить можно что угодно, получив лишь unchecked-предупреждение. List<Object> проверяется полностью и хранит любые объекты. List<?> тоже проверяется, но тип элемента неизвестен, поэтому добавить можно только null.
Типичные ошибки
- ✗Считать сырой
ListиList<Object>взаимозаменяемыми — проверку типов теряет только сырая форма - ✗Пытаться сделать
addненулевого элемента вList<?>, тип элемента которого неизвестен - ✗Игнорировать unchecked-предупреждение — им компилятор сообщает, что больше не гарантирует типобезопасность
Уточняющие вопросы
- →Почему присваивание
List<String>в сыройListотключает проверки для всего объекта? - →Почему из
List<?>можно читать, но добавлять в него можно толькоnull?
MiddleТеорияЧастоЧто на деле даёт ограничение в <T extends Comparable<T>>?
Что на деле даёт ограничение в <T extends Comparable<T>>?
Верхняя граница позволяет компилятору вызывать члены ограничивающего типа на T; без неё T — лишь Object, и стирается граница до себя, а не до Object. Несколько границ пишут через &, класс первым. Граница ограничивает, чем можно подставить T — это не проверка рантайма, и super-границу имеет только wildcard, но не параметр типа.
Типичные ошибки
- ✗Считать, что необограниченный
Tуже позволяет вызывать методыComparable - ✗Полагать, что граница проверяется в рантайме, а не компилятором
- ✗Ожидать
super-границу у параметра типа, которую допускает только wildcard
Уточняющие вопросы
- →Почему в
<T extends Number & Comparable<T>>Numberдолжен стоять первым? - →Как меняется стёртая сигнатура, когда у
Tпоявляется верхняя граница?
MiddleТеорияЧастоЧем отличаются List<?>, List<? extends T> и List<? super T>?
Чем отличаются List<?>, List<? extends T> и List<? super T>?
List<?> — неограниченный wildcard: тип элемента неизвестен, поэтому читать элементы можно лишь как Object, а добавить — только null. List<? extends T> — производитель с верхней границей: элементы читаются как T, но добавить всё равно ничего нельзя, кроме null, ведь точный подтип неизвестен. List<? super T> — потребитель с нижней границей: добавить можно T или его подтипы, но читаются элементы только как Object.
Типичные ошибки
- ✗Пытаться сделать
addненулевого элемента вList<? extends T>, точный подтип которого неизвестен - ✗Ожидать чтения элементов как
TизList<? super T>, который отдаёт лишьObject - ✗Считать, что
List<?>принимает любой элемент как сыройList, а не толькоnull
Уточняющие вопросы
- →Почему из
List<? extends T>можно прочитатьT, но никогда нельзя добавить? - →Когда стоит выбрать
List<? super T>вместоList<? extends T>?
MiddleТеорияИногдаЧто такое загрязнение кучи и почему обобщённый varargs-параметр им рискует?
Что такое загрязнение кучи и почему обобщённый varargs-параметр им рискует?
Загрязнение кучи — это когда переменная параметризованного типа указывает на объект не того типа: ссылка List<String>, а внутри лежит Integer. Стирание это пропускает: приведение непроверяемое, поэтому плохое значение всплывает лишь позже — на неявном приведении при чтении, далеко от причины. Классический источник — обобщённый varargs: foo(List<String>... lists) компилируется в реифицированный List[], который ковариантность массивов позволяет загрязнить.
Типичные ошибки
- ✗Путать загрязнение кучи с утечкой памяти вместо нарушенного инварианта типов
- ✗Ждать
ClassCastExceptionна плохой записи, а не на каком-то позднем, никак не связанном чтении - ✗Вешать
@SafeVarargsна метод, который пишет в свой varargs-массив или возвращает его наружу
Уточняющие вопросы
- →Почему обобщённый varargs-параметр реифицируется в массив, тогда как тип его элемента стирается?
- →На каких методах допустим
@SafeVarargsи что именно обещает эта аннотация?
MiddleТеорияИногдаКак правило выбора wildcard PECS решает между ? extends T и ? super T?
Как правило выбора wildcard PECS решает между ? extends T и ? super T?
PECS — producer-extends, consumer-super. Параметр, который только производит значения T для чтения, объявляется как ? extends T; тот, что только потребляет записываемые T, — как ? super T; а тот, что делает и то и другое, берёт обычный T. Чтение из ? extends T безопасно, ведь каждый элемент — как минимум T; запись в ? super T безопасна, ведь T помещается в любой контейнер супертипа.
Типичные ошибки
- ✗Менять их местами — ставить
? super Tна источник, из которого только читают - ✗Ставить wildcard на тип возврата, из-за чего с wildcard приходится возиться каждому вызывающему
- ✗Хвататься за wildcard на параметре, который и читает, и пишет, где годится только точный
T
Уточняющие вопросы
- →Почему в
List<? super T>можно добавитьT, а вList<? extends T>— никогда? - →Почему
Stream.mapпринимаетFunction<? super T, ? extends R>, а неFunction<T, R>?
SeniorТеорияРедкоПочему в обобщённом классе нельзя написать new T[10] и что делать вместо этого?
Почему в обобщённом классе нельзя написать new T[10] и что делать вместо этого?
Массивы реифицированы, а обобщения стёрты. Массив хранит тип компонента в рантайме и сверяет с ним каждую запись, но new T[10] стёрся бы до new Object[10], то есть проверял бы записи против Object, и его объявленный тип оказался бы ложью. Поэтому компилятор запрещает создание обобщённого массива. Обходят это через (T[]) new Object[10] с непроверяемым предупреждением, через Array.newInstance с токеном Class<T> либо взяв List<T>.
Типичные ошибки
- ✗Считать, что массивы стираются как обобщения, хотя они несут тип компонента в рантайме
- ✗Полагать, что приведение
(T[]) new Object[10]проверяется, а не является непроверяемым обещанием, которое держите вы - ✗Выпускать такой
T[]наружу из класса, где неявное приведение у вызывающего бросаетClassCastException
Уточняющие вопросы
- →Почему
ArrayListхранит элементы в полеObject[], а не вT[]? - →Как токен
Class<T>позволяетArray.newInstanceпостроить по-настоящему типизированный массив?
SeniorТеорияРедкоМожет ли static-поле или метод использовать собственный параметр типа T своего класса?
Может ли static-поле или метод использовать собственный параметр типа T своего класса?
Нет. <T> класса привязан к экземпляру, а static-член принадлежит самому классу — к тому же после стирания за Box<String> и Box<Integer> стоит один класс Box, так что статическому контексту нечего понимать под T. static T value; и static T get() — ошибки компиляции. Вместо этого static-метод вправе объявить собственный параметр, выводимый на каждом вызове, — так и устроен Collections.emptyList().
Типичные ошибки
- ✗Ждать, что
static T value;скомпилируется просто потому, что класс объявляет<T> - ✗Считать, что static-метод наследует
<T>класса, а не объявляет собственный - ✗Полагать, что
static-вложенный класс может использовать параметр типа внешнего класса, как это делает внутренний
Уточняющие вопросы
- →Как
static-обобщённый метод выводит собственный параметр типа в месте вызова? - →Почему внутренний класс может использовать
Tвнешнего класса, аstatic-вложенный — нет?