Разберите, почему компилятор отвергает точное описание типа коллекции через Class, хотя объект создан с параметром String:
import java.util.ArrayList;
class Demo {
static Class<?> runtimeType = new ArrayList<String>().getClass();
static Class<ArrayList<String>> preciseType = new ArrayList<String>().getClass();
}
Первая строка компилируется, а вторая — нет: во время выполнения объект Class описывает только класс ArrayList, но не его параметр String. Параметры обобщённых типов стираются, поэтому getClass() не может вернуть значение типа Class<ArrayList<String>>.
Generics появились в Java 5, но язык сохранил совместимость с уже существующим байткодом и raw-типами. Поэтому обобщения реализованы главным образом на этапе компиляции, а не через создание отдельного runtime-класса для каждой параметризации.
После компиляции ArrayList<String> и ArrayList<Integer> используют один и тот же runtime-класс ArrayList. Это позволяет старому и новому коду взаимодействовать, но ограничивает отражение информации о параметрах типов.
Разработчик может ожидать, что new ArrayList<String>().getClass() сохранит сведения о String. На практике такая информация уже недоступна в runtime, поэтому попытка использовать Class<ArrayList<String>> приводит к ошибке типов.
Неверное предположение особенно опасно в рефлексивных фабриках, контейнерах зависимостей и сериализаторах: объект Class<List<String>> нельзя получить обычным вызовом getClass() и использовать как полноценное описание параметризованного типа.
Метод Object.getClass() возвращает объект типа Class, соответствующий фактическому runtime-классу объекта. Для выражения new ArrayList<String>() это класс ArrayList, а параметр String существует только в метаданных, проверяемых компилятором, и не является частью runtime-идентичности класса.
Поэтому безопасное объявление использует Class<?>:
Class<?> означает «объект Class некоторого неизвестного типа». Из него можно получить имя класса, создать экземпляр при наличии доступного конструктора и проверить принадлежность объекта, но нельзя извлечь параметр String у ArrayList.
Запись ArrayList<String>.class недопустима: литералы .class требуют reifiable-тип, то есть тип, сведения о котором доступны после стирания. Допустимы ArrayList.class, String.class и Class<?>, но не ArrayList<String>.class.
Если приложению нужно сохранить параметры обобщённого типа, применяется отдельное описание типа, например java.lang.reflect.Type. Оно может содержать ParameterizedType, включающий сырой класс ArrayList и аргумент String. Это уже не результат getClass(), а явно переданный или построенный type token.
Небезопасное приведение вроде (Class<ArrayList<String>>) (Class<?>) ArrayList.class иногда технически возможно с предупреждением, но оно не добавляет информации о String. Такое значение фактически описывает только ArrayList, поэтому приведение допустимо лишь как ограниченный compile-time контракт, а не как способ восстановить стёртый параметр.
Представим фабрику, которая получает Class<T> и создаёт объект типа T. Для String.class или ArrayList.class такой API подходит: runtime знает конкретный класс. Но передача Class<ArrayList<String>> не позволяет фабрике понять, какие элементы должна содержать коллекция.
Вариант с Class<?> прост и надёжен, но сохраняет только сырой класс. Вариант с отдельным Class<E> для элемента, например Class<String>, подходит фабрике коллекций, если ей достаточно знать тип элемента, однако не описывает вложенные параметры вроде Map<String, List<Integer>>.
Для универсального сериализатора лучше передавать Type или собственный типовой токен, содержащий структуру параметризации. Это сложнее в API и требует обработки ParameterizedType, зато сохраняет необходимую информацию после стирания и предотвращает неверную десериализацию.
Дополнительный вопрос: можно ли проверить ArrayList<String> через instanceof?
Нет. Проверка value instanceof ArrayList<String> запрещена, потому что JVM не может проверить параметр String после стирания. Допустима проверка value instanceof ArrayList<?>: она проверяет только факт принадлежности к ArrayList, а неизвестный параметр типа безопасно обозначается wildcard.
Дополнительный вопрос: почему Class<ArrayList> и Class<?> не эквивалентны?
Class<ArrayList> означает, что объект Class описывает именно сырой тип ArrayList. Class<?> допускает описание любого класса и поэтому является более общим типом. Объект ArrayList.class можно присвоить обеим переменным, но через Class<ArrayList> API предоставляет более узкий compile-time контракт.
Дополнительный вопрос: можно ли по объекту ArrayList<String> узнать String через reflection?
Нет, если речь идёт о runtime-классе самого объекта: его класс — ArrayList, а информация о фактическом аргументе типа не хранится в экземпляре. Узнать String можно только из внешнего контекста, например из объявления поля List<String> или из явно переданного Type, где параметризация записана в generic-сигнатуре.