Что именно о параметризации обобщённого типа можно узнать через reflection после стирания типов, а что недоступно во время выполнения?
После стирания типов JVM не использует параметризацию объекта для выбора поведения: например, во время выполнения нельзя надёжно определить, содержит ли конкретный объект List значения String или Integer. Однако компилятор может сохранить сведения о параметрах типов в атрибутах сигнатуры class-файла, поэтому reflection способен прочитать параметризацию объявлений классов, полей и методов.
Generics появились в Java для статической проверки типов и устранения необходимости в явных приведениях при работе с коллекциями. Для сохранения совместимости уже скомпилированного кода Java использует в основном стирание типов, а не создание отдельной runtime-версии класса для каждого набора параметров.
Чтобы инструменты, компилятор и reflection не теряли всю информацию о декларациях, параметризованные сигнатуры могут сохраняться в class-файле отдельно от обычных runtime-типов. Эти сведения описывают объявления, но не превращают параметры типов в полноценные runtime-метаданные каждого объекта.
Предположим, метод получает объект типа List<?>. Нельзя сделать вывод, что исходный объект был создан как List<String>: в runtime его класс обычно остаётся ArrayList, а параметры String или Integer не участвуют в идентификации класса.
Попытка принять решение на основе такой недоступной информации приводит либо к ошибке компиляции, либо к небезопасному приведению типов. Это особенно важно для сериализации, dependency injection, ORM и универсальных библиотек, где разработчик может ошибочно ожидать, что reflection восстановит фактические аргументы типа любого объекта.
Reflection может прочитать параметры типов там, где они записаны в сигнатуре объявления. Например, у класса-наследника можно получить обобщённый тип непосредственного суперкласса, у поля — его ParameterizedType, а у метода — обобщённые типы параметров и результата.
Здесь String доступен, потому что он указан в объявлении UserBox extends Box<String>, и эта сигнатура сохранена в class-файле. Информация относится к объявлению класса UserBox, а не к тому, что JVM обнаружила параметр String внутри конкретного экземпляра.
У объекта new ArrayList<String>() runtime-класс — ArrayList. Другой объект new ArrayList<Integer>() имеет тот же runtime-класс, поэтому обычные операции getClass() и instanceof не различают эти параметризации.
Важно различать два источника информации:
Стирание также означает, что нельзя безопасно проверить произвольный объект условием вида «это именно List<String>». Можно проверить только reifiable-форму вроде List<?>, а проверку содержимого нужно выполнять отдельно, если это вообще допустимо по контракту.
Компромисс такого дизайна — совместимость и отсутствие runtime-стоимости для большинства generic-операций ценой неполной информации о параметрах типов. Если библиотеке нужно сохранить тип для runtime-операций, его обычно передают явно через Class<T>, специальный объект описания типа или сохраняемую декларацию, но это уже отдельный контракт API, а не автоматическое свойство стирания.
Библиотека сериализации получает Object и должна понять, во что преобразовать JSON. Разработчик пытается извлечь параметр типа вызовом value.getClass(), но для List<String> получает только ArrayList.class; восстановить String из экземпляра нельзя.
Первый вариант — анализировать элементы коллекции. Он не работает для пустой коллекции, зависит от однородности данных и может нарушить границы безопасности типов.
Второй вариант — принимать только Class<T>. Он подходит для простых типов, но не описывает вложенные параметры вроде List<String> или Map<String, Integer>.
Выбранное решение — принимать явное описание полного типа, например объект метаданных, содержащий List<String>. Плюс подхода — точная информация доступна во время выполнения; минус — вызывающий код должен явно передать тип, потому что Java не может надёжно восстановить его из одного объекта. В результате пустые коллекции и сложные вложенные типы обрабатываются предсказуемо.
Дополнительный вопрос: Сохраняются ли параметры типов в class-файле всегда?
Ответ: Не следует считать, что любая информация о параметрах типов доступна безусловно. Компилятор обычно записывает generic-сигнатуры деклараций в class-файл, но reflection видит только те сведения, которые присутствуют в соответствующем объявлении и доступны загруженному классу. Удаление метаданных инструментами, генерация байткода без generic-сигнатуры или использование raw-типа могут сделать параметризацию недоступной.
Дополнительный вопрос: Можно ли по generic-сигнатуре суперкласса определить тип параметра конкретного объекта?
Ответ: Можно определить параметр, зафиксированный в объявлении самого класса или его цепочки наследования, но нельзя автоматически получить фактический тип, с которым объект якобы был создан. Например, для UserBox extends Box<String> reflection знает String из декларации UserBox. Это не означает, что аналогичный механизм извлечёт параметр из любого экземпляра Box<T> или из локальной переменной, потому что стирание не хранит такие значения как состояние объекта.
Дополнительный вопрос: Почему наличие generic-метаданных не позволяет перегружать методы только параметрами типа?
Ответ: Перегрузка определяется JVM по имени метода и стёртым типам параметров, а generic-сигнатура служит дополнительным описанием для компилятора и reflection. Поэтому методы, различающиеся только параметризацией, после стирания получают одну и ту же сигнатуру и конфликтуют. Generic-метаданные не создают отдельный runtime-диспетчер и не устраняют это ограничение.