Каким образом компилятор сохраняет переопределение обобщённого метода после стирания типов?
Компилятор создаёт в подклассе синтетический bridge-метод — прослойку с сигнатурой, совместимой со стёртой сигнатурой родительского метода. Он принимает и возвращает стёртые типы, а затем делегирует вызов настоящему методу с конкретным типом.
Обобщения появились в Java 5, при этом Java сохранила модель стирания типов. Параметры типов в основном используются компилятором, а в байткоде заменяются границами или Object, что поддерживает совместимость с существующим JVM-кодом и библиотеками до появления generics.
После стирания сигнатуры методов, которые в исходном коде считаются переопределёнными, могут различаться на уровне JVM. Bridge-методы решают эту проблему, не требуя внедрения полностью reified generics в виртуальную машину.
Рассмотрим ситуацию, когда интерфейс параметризован типом T, а реализация выбирает вместо него String. В исходном коде метод интерфейса возвращает T, а метод реализации — String.
После стирания метод интерфейса фактически возвращает Object. Метод реализации с возвращаемым типом String не имеет той же JVM-сигнатуры, поэтому одного только исходного метода недостаточно для корректного полиморфного вызова через ссылку на параметризованный интерфейс.
Без специальной прослойки вызов через интерфейс мог бы не попасть в реализацию подкласса или нарушить ожидаемую совместимость методов.
Компилятор добавляет в класс реализации синтетический метод с той сигнатурой, которую ожидает JVM после стирания. Этот метод обычно помечается флагами ACC_BRIDGE и ACC_SYNTHETIC, получает значение через вызов специализированного метода и при необходимости выполняет приведение типа.
В StringBox логически присутствуют два метода: исходный String get() и сгенерированный bridge-метод примерно с поведением Object get(), который делегирует вызов String get(). Поэтому вызов через Box<String> корректно использует реализацию StringBox.
Bridge-метод не является дополнительной бизнес-логикой и обычно не пишется разработчиком вручную. Он может быть виден через reflection, инструменты анализа байткода, трассировку или фреймворки, перебирающие методы класса.
Важно отличать bridge-метод от перегрузки. Это не самостоятельная перегрузка, созданная исходным кодом, а технический адаптер для сохранения полиморфизма после стирания. Если фреймворк анализирует методы, ему часто нужно учитывать признаки isBridge() и isSynthetic().
Фреймворк сериализации ищет в классе методы-геттеры через reflection. У класса, реализующего обобщённый интерфейс, он обнаруживает и настоящий геттер с конкретным возвращаемым типом, и сгенерированный bridge-метод.
Наивный вариант — обрабатывать все найденные методы одинаково. Его минус в том, что один логический геттер может быть зарегистрирован дважды, а правила выбора типа результата могут стать зависимыми от порядка обхода reflection-методов.
Другой вариант — полностью игнорировать все синтетические методы. Это обычно устраняет дубликаты, но может быть неверным для инструментов, которым нужно анализировать байткод на низком уровне или учитывать технические адаптеры вызовов.
Практичное решение для обычного bean-интроспектора — исключать методы, помеченные как bridge, при построении логической модели API, а затем дополнительно дедуплицировать методы по имени и параметрам. Bridge-методы при этом остаются доступными для JVM-вызовов, но не воспринимаются фреймворком как отдельные пользовательские свойства.
1. Вопрос: Bridge-метод создаётся только при использовании generics?
Ответ: Нет. Компилятор может создавать bridge-методы и в других случаях, когда для сохранения полиморфизма нужны методы с адаптированной сигнатурой, например при ковариантных возвращаемых типах. Generics — один из наиболее известных источников таких методов, но не единственный.
2. Вопрос: Можно ли надёжно определять bridge-метод только по его имени?
Ответ: Нет. Имя bridge-метода обычно совпадает с именем настоящего метода, поэтому по имени их различить нельзя. Для reflection следует проверять признаки isBridge() и при необходимости isSynthetic(), а также учитывать типы параметров и возвращаемый тип.
3. Вопрос: Может ли bridge-метод привести к ClassCastException?
Ответ: Да, если в адаптирующем вызове требуется приведение к конкретному типу и фактический объект ему не соответствует. В корректно типизированном коде компилятор обычно предотвращает такую ситуацию, но она может появиться из-за raw-типов, непроверенных приведений или небезопасного взаимодействия с legacy-кодом. В таком случае исключение возникает в сгенерированной прослойке или при передаче управления в специализированный метод.