При разборе класса нужно определить результат компиляции и причину. пример с кодом Скомпилируется ли этот к...

При разборе класса нужно определить результат компиляции и причину.

import java.util.List;

class Converter {
    static void convert(List<String> values) {}
    static void convert(List<Integer> values) {}
}

Скомпилируется ли этот класс?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Класс не скомпилируется: методы имеют одинаковую сигнатуру после стирания типов. Для JVM оба параметра превращаются в List, поэтому получить два метода convert(List) невозможно.

Исторический контекст

Generics появились в Java для статической проверки типов и устранения множества явных приведений, сохранив совместимость с существующим кодом без параметризации типов. Для этого Java использует в основном стирание типов, а не создание отдельного runtime-типа для каждой параметризации.

Такой подход позволяет обобщённому коду взаимодействовать с кодом, написанным до появления generics, но ограничивает использование параметров типов во время выполнения.

Постановка проблемы

На уровне исходного кода List<String> и List<Integer> выглядят разными типами. Однако виртуальная машина должна однозначно выбрать вызываемый метод по runtime-сигнатуре, а параметр типа String или Integer в этой сигнатуре не сохраняется.

Если бы оба метода были разрешены, после компиляции они имели бы один и тот же JVM-дескриптор. Поэтому компилятор сообщает о конфликте методов ещё до генерации байткода.

Подробное решение

При стирании типов параметризация List<T> заменяется на сырой тип List. Поэтому исходные объявления концептуально превращаются в:

static void convert(List values) {} static void convert(List values) {}

Это не перегрузка, а повторное объявление одного метода. Тип элемента списка не используется для выбора перегрузки, потому что String и Integer отсутствуют в runtime-сигнатуре параметра.

Стирание действует и для других параметров типов. Например, List<?> и List<String> также нельзя использовать как две перегрузки: оба параметра стираются до List. Для переменной типа стирается первый верхний bound: у <T extends Number> результатом будет Number, а при отсутствии bound — Object.

Это ограничение отличается от обычной проверки совместимости типов. Компилятор по-прежнему знает, что конкретный список имеет элементы String или Integer, и проверяет операции на этапе компиляции, но эта информация не участвует в выборе метода во время выполнения.

Практические варианты решения:

  • дать методам разные имена, например convertStrings и convertIntegers;
  • использовать один обобщённый метод, если алгоритм одинаков для всех типов элементов;
  • передавать явный discriminant или отдельный объект стратегии, если поведение действительно различается по типу данных.

Попытка заменить методы на convert(List<?> values) и convert(List<String> values) проблему не устраняет: конфликт после стирания сохраняется.

Ситуация из практики

В библиотеке импорта разработчик хотел добавить отдельную обработку списков идентификаторов и списков числовых значений через две перегрузки importData(List<String>) и importData(List<Long>). Компиляция завершилась конфликтом из-за стирания типов.

Рассматривались три варианта. Разные имена делают API очевидным, но увеличивают его размер. Один метод importData(List<?>) уменьшает API, однако требует общего алгоритма и не позволяет безопасно выполнять операции, специфичные для конкретного типа. Явная стратегия или адаптер сохраняет разные алгоритмы, но добавляет объекты и усложняет вызов.

Выбрали разные имена, поскольку правила импорта для строк и чисел отличались, а различие было важной частью публичного API. В результате исчез конфликт сигнатур, а вызывающий код стал явно отражать выбранный формат данных.

Что кандидаты часто упускают

  1. Можно ли перегрузить методы по List<String> и List<?>?

Нет. Оба параметра после стирания становятся List, поэтому возникает конфликт сигнатур. Различие wildcard и конкретного параметра важно для статической проверки операций, но не для JVM-сигнатуры.

  1. Почему List<String> и List<Integer> нельзя различить по типу аргумента во время выполнения?

Из-за стирания типов параметр String или Integer не сохраняется как часть runtime-типа списка. Объект обычно создаётся как экземпляр одного класса реализации, например ArrayList, независимо от параметра компиляции. Проверки, зависящие от типа элемента, должны выполняться явно или через доступный runtime-класс самого элемента.

  1. Всегда ли параметр типа стирается до Object?

Нет. Если у переменной типа есть bound, стирание происходит до его первого верхнего ограничения. Для <T extends Number & Comparable<T>> стиранием будет Number; дополнительные bounds участвуют в проверке компилятором, но не становятся основным типом параметра в JVM-сигнатуре.