При разборе класса нужно определить результат компиляции и причину.
import java.util.List;
class Converter {
static void convert(List<String> values) {}
static void convert(List<Integer> values) {}
}
Скомпилируется ли этот класс?
Класс не скомпилируется: методы имеют одинаковую сигнатуру после стирания типов. Для JVM оба параметра превращаются в List, поэтому получить два метода convert(List) невозможно.
Generics появились в Java для статической проверки типов и устранения множества явных приведений, сохранив совместимость с существующим кодом без параметризации типов. Для этого Java использует в основном стирание типов, а не создание отдельного runtime-типа для каждой параметризации.
Такой подход позволяет обобщённому коду взаимодействовать с кодом, написанным до появления generics, но ограничивает использование параметров типов во время выполнения.
На уровне исходного кода List<String> и List<Integer> выглядят разными типами. Однако виртуальная машина должна однозначно выбрать вызываемый метод по runtime-сигнатуре, а параметр типа String или Integer в этой сигнатуре не сохраняется.
Если бы оба метода были разрешены, после компиляции они имели бы один и тот же JVM-дескриптор. Поэтому компилятор сообщает о конфликте методов ещё до генерации байткода.
При стирании типов параметризация List<T> заменяется на сырой тип List. Поэтому исходные объявления концептуально превращаются в:
Это не перегрузка, а повторное объявление одного метода. Тип элемента списка не используется для выбора перегрузки, потому что String и Integer отсутствуют в runtime-сигнатуре параметра.
Стирание действует и для других параметров типов. Например, List<?> и List<String> также нельзя использовать как две перегрузки: оба параметра стираются до List. Для переменной типа стирается первый верхний bound: у <T extends Number> результатом будет Number, а при отсутствии bound — Object.
Это ограничение отличается от обычной проверки совместимости типов. Компилятор по-прежнему знает, что конкретный список имеет элементы String или Integer, и проверяет операции на этапе компиляции, но эта информация не участвует в выборе метода во время выполнения.
Практические варианты решения:
convertStrings и convertIntegers;Попытка заменить методы на convert(List<?> values) и convert(List<String> values) проблему не устраняет: конфликт после стирания сохраняется.
В библиотеке импорта разработчик хотел добавить отдельную обработку списков идентификаторов и списков числовых значений через две перегрузки importData(List<String>) и importData(List<Long>). Компиляция завершилась конфликтом из-за стирания типов.
Рассматривались три варианта. Разные имена делают API очевидным, но увеличивают его размер. Один метод importData(List<?>) уменьшает API, однако требует общего алгоритма и не позволяет безопасно выполнять операции, специфичные для конкретного типа. Явная стратегия или адаптер сохраняет разные алгоритмы, но добавляет объекты и усложняет вызов.
Выбрали разные имена, поскольку правила импорта для строк и чисел отличались, а различие было важной частью публичного API. В результате исчез конфликт сигнатур, а вызывающий код стал явно отражать выбранный формат данных.
List<String> и List<?>?Нет. Оба параметра после стирания становятся List, поэтому возникает конфликт сигнатур. Различие wildcard и конкретного параметра важно для статической проверки операций, но не для JVM-сигнатуры.
List<String> и List<Integer> нельзя различить по типу аргумента во время выполнения?Из-за стирания типов параметр String или Integer не сохраняется как часть runtime-типа списка. Объект обычно создаётся как экземпляр одного класса реализации, например ArrayList, независимо от параметра компиляции. Проверки, зависящие от типа элемента, должны выполняться явно или через доступный runtime-класс самого элемента.
Object?Нет. Если у переменной типа есть bound, стирание происходит до его первого верхнего ограничения. Для <T extends Number & Comparable<T>> стиранием будет Number; дополнительные bounds участвуют в проверке компилятором, но не становятся основным типом параметра в JVM-сигнатуре.