Разберите, почему объявление C не компилируется, хотя в нём есть метод с совпадающими именем и параметрами:
interface A {
Number value();
}
interface B {
String value();
}
class C implements A, B {
public String value() {
return "ok";
}
}
C не компилируется, потому что один метод не может одновременно реализовать A.value() с возвращаемым типом Number и B.value() с возвращаемым типом String. При одинаковых имени и параметрах Java требует совместимых возвращаемых типов: тип результата реализации должен быть подтипом каждого требуемого типа. String не является подтипом Number, поэтому общий контракт невозможен.
Интерфейсы позволяют классу реализовывать несколько контрактов, не поддерживая множественное наследование состояния и реализации классов. Поэтому Java должна проверять, можно ли свести требования нескольких интерфейсов к одному корректному методу.
Если методы имеют совместимые возвращаемые типы, один метод класса может удовлетворить обоим контрактам. Если типы несовместимы, компилятор запрещает объявление класса, чтобы вызов через любой из интерфейсных типов не нарушал ожидаемый контракт.
У A.value() вызывающий код имеет право получить Number, а у B.value() — String. Реализация C.value() должна возвращать значение, корректное для обоих случаев.
Нельзя выбрать String: он подходит для B, но не подходит для A. Нельзя выбрать Number: он подходит для A, но нарушает контракт B, который обещает именно String. Разные методы с одинаковыми параметрами и только разными возвращаемыми типами также невозможны: возвращаемый тип не входит в сигнатуру метода Java.
Для методов с одинаковыми именем, параметрами и совместимыми модификаторами Java ищет единую реализацию. Её возвращаемый тип должен быть возвратно совместимым с возвращаемым типом каждого переопределяемого метода; на практике это означает тот же тип или допустимый более конкретный тип.
Например, Integer подходит одновременно для Number и Integer, потому что Integer — подтип Number:
Через ссылку A результат можно использовать как Number, а через ссылку B — как Integer. Фактический тип возвращаемого объекта при этом определяется реализацией метода, а статический тип результата — типом ссылки, через которую выполнен вызов.
В исходном примере у Number и String нет отношения подтип–супертип. Поэтому не существует корректного метода, который удовлетворял бы обоим объявлениям. Приведение результата внутри метода не решило бы проблему: компилятор проверяет совместимость самого объявления метода, а не только фактическое значение, возвращаемое в конкретном запуске.
Представим библиотеку преобразований, где один интерфейс объявляет serialize() как возвращающий CharSequence, а другой — как возвращающий String. Их можно реализовать одним методом с результатом String, потому что String является подтипом CharSequence.
Если второй интерфейс вместо String требует, например, byte[], варианты с одним классом невозможны. Разделение методов по разным именам устраняет конфликт, но меняет API. Использование общего типа результата вроде Object тоже не помогает: оно не удовлетворяет интерфейсу, требующему более конкретный тип.
На практике выбирают совместимый общий контракт заранее либо используют разные имена методов, например serializeText() и serializeBytes(). Это предпочтительнее небезопасных приведений типов и сохраняет проверку корректности на этапе компиляции.
A.value() с типом Integer, если он объявлен как Number?Да. Это ковариантный возвращаемый тип: переопределяющий метод может возвращать более конкретный ссылочный тип. Integer является подтипом Number, поэтому любой результат метода остаётся допустимым Number для вызывающего кода через A.
Number и Integer, но класс объявит Number value()?Такой класс не реализует контракт интерфейса с Integer. Возвращаемый тип Number не является подтипом Integer, поэтому реализация недостаточно конкретна для второго интерфейса. Нужно объявить метод с типом Integer либо изменить контракты интерфейсов.
Нет, это будут уже разные методы, а не реализация одного и того же контракта. Например, value() и value(int format) имеют разные сигнатуры, поэтому второй метод не реализует требование к value() без параметров. Перегрузка может быть частью API класса, но не устраняет несовместимость исходных интерфейсных методов.