В классе, реализующем два интерфейса с одинаковым абстрактным методом, почему одна реализация может удовлетворить обоим контрактам?
Одна реализация удовлетворяет обоим интерфейсам, если методы имеют совместимые сигнатуры, совместимые возвращаемые типы и метод класса доступен для переопределения. Java рассматривает такую реализацию как единую реализацию одного унаследованного контракта, поэтому вызов через ссылку любого из интерфейсных типов попадёт в тот же метод класса.
Метод Document.id() одновременно является реализацией HasId.id() и Auditable.id(). Отдельные методы с одинаковыми параметрами объявить нельзя только ради различения интерфейсов: возвращаемый тип не входит в сигнатуру метода.
Интерфейсы позволяют классу поддерживать несколько независимых контрактов без множественного наследования реализации. Это решает задачу совместимости объекта с разными подсистемами: один объект может быть, например, идентифицируемым, сериализуемым и валидируемым одновременно.
При совпадении абстрактных методов Java не создаёт искусственный конфликт, если обе декларации описывают совместимый контракт. Иначе классу пришлось бы дублировать логически одну и ту же операцию или выбирать реализацию без достаточных оснований.
Предположим, класс реализует два интерфейса, в каждом из которых объявлен метод с одинаковым именем и списком параметров. Возникает вопрос, нужно ли писать две реализации и как компилятор определит, какая из них используется при вызове через разные интерфейсные ссылки.
Неверное понимание приводит к попыткам создать методы, различающиеся только возвращаемым типом, или к ошибочному выводу, что любой метод с тем же именем автоматически реализует оба интерфейса. В результате код либо не компилируется, либо формально компилируется, но одна операция имеет неподходящую семантику для одного из контрактов.
Java сопоставляет метод класса с методом интерфейса по имени и параметрам с учётом правил сигнатур. Возвращаемый тип не используется для перегрузки, но должен быть совместим с возвращаемым типом интерфейсного метода; обычно он должен совпадать или быть его ковариантным подтипом.
Реализация должна иметь достаточную видимость. Поскольку методы интерфейса являются публичными, метод класса, реализующий их, должен быть объявлен как public; более узкий доступ контракт нарушит.
После компиляции вызов через ссылку HasId или Auditable проверяется по типу этой ссылки, но реализация экземплярного метода выбирается для фактического объекта. В обоих случаях объектом является Document, поэтому вызывается его единственный метод id().
Совпадение деклараций недостаточно, если контракты противоречат друг другу по смыслу. Например, один интерфейс может трактовать id() как постоянный идентификатор, а другой — как временный идентификатор с иными гарантиями. Компилятор не проверяет такие семантические требования, поэтому ответственность за корректность общей реализации лежит на разработчике.
Если возвращаемые типы несовместимы, один метод не сможет удовлетворить оба контракта. То же относится к несовместимым после стирания обобщённым сигнатурам: внешне разные методы могут превратиться в один метод JVM и вызвать ошибку name clash.
В сервисе документ должен использоваться и как объект с идентификатором, и как объект аудита. Оба интерфейса требуют метод id(), возвращающий строку.
Вариант с двумя методами id() невозможен: Java не различает методы только по возвращаемому типу. Переименование одного метода устранило бы конфликт, но потребовало бы менять API одного из интерфейсов. Адаптеры или отдельные обёртки сохранили бы разные семантики, но добавили бы объекты, преобразования и усложнили бы код.
Выбран единый метод в Document, потому что оба контракта действительно требуют один и тот же стабильный идентификатор. Это уменьшает дублирование, сохраняет оба интерфейсных API и гарантирует одинаковый результат независимо от типа ссылки. Если бы требования интерфейсов различались по смыслу, предпочтительнее было бы использовать адаптер или разделить ответственность, а не скрывать различие за одной реализацией.
Если один возвращаемый тип является подтипом другого, реализация с более конкретным возвращаемым типом может удовлетворить оба требования. Например, метод, возвращающий ArrayList, может реализовать требования с возвращаемым типом List и ArrayList, если остальные условия совпадают. Если типы не связаны отношением совместимости, класс не сможет получить одну корректную реализацию.
checked-исключения в общей реализации?Метод класса не может объявлять более широкие checked-исключения, чем разрешены интерфейсными методами. Если два интерфейса допускают разные наборы исключений, реализация должна выбрать исключения, допустимые одновременно для обоих контрактов, либо не объявлять checked-исключения и обработать их внутри метода. Для unchecked-исключений такого ограничения при переопределении нет.
Нужно учитывать стирание типов. Например, методы, которые после удаления параметров типов получают одну и ту же JVM-сигнатуру, могут вызвать конфликт name clash, даже если в исходном коде выглядят различными. В такой ситуации Java не может безопасно представить их как два независимых метода; обычно требуется изменить дизайн интерфейсов, имена методов или параметры типов.