При попытке реализовать один и тот же обобщённый интерфейс с разными параметрами типа какой конфликт предотвращает компилятор Java?
Java запрещает классу быть одновременно подтипом одного и того же обобщённого интерфейса с разными параметризациями. Например, нельзя через разные родительские интерфейсы унаследовать одновременно Хранилище<String> и Хранилище<Integer>.
Ограничение нужно для однозначности системы типов и разрешения методов: после стирания типов обе специализации опираются на один и тот же JVM-интерфейс, а методы могут получить одинаковые стёртые сигнатуры.
Обобщения в Java проектировались с учётом совместимости с существующим кодом. Поэтому они реализуются главным образом посредством стирания типов: параметры типа используются компилятором, но обычно не сохраняются в сигнатурах JVM-методов.
Такой подход позволил сохранить совместимость с кодом до появления generics, но создал ограничения. В частности, JVM не может надёжно различать несколько реализаций одного интерфейса, если после стирания они имеют один и тот же тип.
Представим два промежуточных интерфейса: один наследует Хранилище<String>, другой — Хранилище<Integer>. Класс, реализующий оба интерфейса, должен был бы одновременно предоставить методы для строк и для чисел.
После стирания параметров типа оба варианта могут превратиться в один метод вроде положить(Object). Нельзя однозначно определить, какой контракт реализуется, а результат вызова может нарушить типобезопасность.
Компилятор проверяет не только непосредственные implements, но и всю иерархию типов. Если класс получает один и тот же обобщённый интерфейс с несовместимыми аргументами типа, объявление отклоняется ещё на этапе компиляции.
Проблема не сводится только к совпадению исходных текстовых сигнатур. Важен факт наследования разных параметризаций одной generic-декларации. Даже если конкретные методы кажутся совместимыми, Java не позволяет создать такую неоднозначную типовую иерархию.
Bridge-методы это ограничение не устраняют. Они помогают сохранить полиморфизм при одном согласованном наследовании обобщённого метода, например адаптируют реализацию положить(String) к стёртому вызову положить(Object). Bridge-метод не может безопасно представить две разные специализации одного интерфейса в одном классе.
Одинаковая параметризация через несколько промежуточных интерфейсов допустима: если оба интерфейса наследуют Хранилище<String>, класс может реализовать их одновременно. При этом отдельно могут возникнуть другие конфликты, например несовместимые default-методы, но конфликт параметризаций отсутствует.
Практические способы моделировать два разных типа данных — объявить два независимых интерфейса, использовать композицию с двумя полями-делегатами или создать два адаптера над одним сервисом. Попытка применить raw-тип или wildcard обычно лишь скрывает часть типовой информации и не является корректным способом реализовать две разные специализации.
В библиотеке есть интерфейс Реестр<T>. Команда хочет, чтобы один сервис был одновременно реестром документов и реестром бинарных идентификаторов, то есть унаследовал Реестр<Документ> и Реестр<byte[]>.
Вариант с двумя промежуточными интерфейсами выглядит естественно, но компилятор его отклонит: сервис не может иметь две несовместимые специализации Реестр. Обход через raw-тип формально уменьшает количество ошибок компиляции, но переносит проблему в приведения типов и может привести к ClassCastException во время выполнения.
Лучшее решение — разделить контракты на два независимых интерфейса либо оставить Реестр<T> отдельным компонентом и внедрить в сервис два его экземпляра. Разделение сохраняет типобезопасность, делает зависимости явными и не требует неоднозначного разрешения методов; цена решения — немного больше кода и объектов-делегатов.
Можно ли реализовать два промежуточных интерфейса, если они наследуют одну и ту же параметризацию?
Да. Например, если оба интерфейса наследуют Реестр<String>, класс может реализовать их одновременно. В этом случае он получает одну согласованную специализацию общего интерфейса. Однако компилятор всё равно отдельно проверит конфликты методов и default-реализаций.
Почему bridge-методы не позволяют реализовать Реестр<String> и Реестр<Integer> одновременно?
Bridge-метод решает другую задачу: он связывает обобщённую сигнатуру с её стёртым представлением в конкретной цепочке наследования. Например, реализация метода с String может получить синтетический мост для вызова через стёртый тип Object.
Для двух параметризаций потребовались бы два несовместимых поведения для одного стёртого метода. Bridge-методы не создают новые различимые сигнатуры JVM и не устраняют конфликт типовой иерархии.
Достаточно ли заменить один из параметров типа на wildcard, чтобы обойти ограничение?
Нет. Реестр<?> — это использование интерфейса с wildcard, а не отдельная реализация, способная безопасно принимать произвольные значения. Кроме того, правило о несовместимом наследовании параметризаций применяется к структуре типов, а не только к удобству записи параметров.
Если компонент действительно должен работать с разными типами, обычно выбирают параметризацию на уровне метода или класса-обёртки, либо используют композицию нескольких типизированных объектов. Это сохраняет проверку типов вместо её маскировки wildcard или raw-типом.