Метод потомка возвращает более конкретный тип результата, чем метод родителя: считается ли это переопределением?
Да, это считается переопределением, если возвращаемый тип потомка является подтипом ссылочного возвращаемого типа родительского метода. Такое правило называется ковариантностью возвращаемого типа.
Для примитивных типов ковариантность не применяется: нельзя переопределить метод с результатом int методом с результатом long. Также должны сохраняться имя метода, параметры и совместимые правила доступа.
До Java 5 переопределённый метод должен был иметь тот же возвращаемый тип. Это ограничивало полиморфные API: потомок мог возвращать только общий тип родителя, даже если фактически создавал более специализированный объект.
В Java 5 появилась поддержка ковариантных возвращаемых типов. Она позволяет сохранять совместимость с контрактом родителя, одновременно предоставляя клиентам потомка более точный тип результата.
Представим базовый класс, метод которого возвращает общий тип объекта. Потомку может быть известно, что он всегда создаёт объект конкретного подтипа. Возврат общего типа заставляет вызывающий код выполнять приведение или терять информацию о типе результата.
Неверно считать любое изменение возвращаемого типа переопределением. Если новый тип не является подтипом исходного ссылочного типа, компиляция завершится ошибкой. Аналогично, изменение только возвращаемого типа при одинаковых параметрах не создаёт перегрузку.
При проверке переопределения Java сопоставляет имя метода и список параметров. Возвращаемый тип может отличаться только в допустимом направлении: тип результата потомка должен быть тем же самым типом либо его подтипом.
Например, если родитель возвращает Animal, потомок может вернуть Dog, потому что каждый Dog является Animal. Вызывающий код, работающий с переменной типа Animal, всё равно видит результат как Animal, а код, обращающийся к методу через Dog, может использовать более конкретный тип Dog без явного приведения.
Вызов выбирается полиморфно по фактическому объекту: выполняется реализация Dog.copy. При этом статический тип переменной result остаётся Animal, поскольку вызов выполняется через ссылку типа Animal.
Правила доступа также должны быть совместимыми: нельзя при переопределении сделать метод менее доступным. Метод не должен быть static, final или private в смысле обычного переопределения: статические методы скрываются, финальные запрещают переопределение, а приватные методы не входят в наследуемый контракт.
Для ссылочных типов важна именно совместимость типов, а не наличие одинакового имени. Нельзя вернуть несвязанный класс. Для обобщённых типов компилятор при необходимости может создать синтетический bridge-метод, чтобы сохранить полиморфное поведение после стирания типов.
В библиотеке есть базовый класс с методом копирования, возвращающим общий тип. Конкретный класс документа всегда создаёт копию именно своего типа. Если оставить общий возвращаемый тип, клиенту придётся делать приведение; это увеличивает риск ошибки во время выполнения.
Первый вариант — вернуть в потомке более конкретный тип. Он прост, безопасен на уровне компиляции и хорошо подходит, когда иерархия классов стабильна. Минус — при сложной иерархии похожие переопределения могут дублироваться.
Второй вариант — оставить общий тип и заставить клиентов использовать приведение. Он сохраняет единообразную сигнатуру, но ухудшает удобство API и переносит проверку корректности на выполнение.
Третий вариант — спроектировать API с обобщённым типом, например через параметр типа самого класса. Это может уменьшить приведения в большой иерархии, но усложняет сигнатуры и понимание кода.
Обычно выбирают ковариантный возвращаемый тип, если конкретный потомок действительно гарантирует более узкий результат. В итоге контракт родителя сохраняется, а специализированный код получает более точную типизацию без небезопасного приведения.
1. Можно ли заменить возвращаемый ссылочный тип на его родителя?
Нет. Направление ковариантности только одно: потомок может вернуть более узкий тип, но не более общий. Если родитель возвращает Dog, переопределение с результатом Animal нарушает ожидания кода, работающего с родительским контрактом, поэтому Java его не принимает.
2. Влияет ли ковариантный возвращаемый тип на выбор реализации метода во время выполнения?
Нет, он не меняет основное правило виртуального вызова. Реализация экземплярного метода выбирается по фактическому типу объекта, а доступный тип результата определяется статическим типом выражения. Поэтому вызов через ссылку родительского типа может выполнить метод потомка, но результат в исходном выражении будет иметь тип, объявленный в родительском контракте.
3. Почему изменение только возвращаемого типа не создаёт перегрузку?
Перегрузка определяется различиями в имени метода или параметрах, но не только возвращаемым типом. Иначе вызов с одинаковыми аргументами часто был бы неоднозначным: компилятор не мог бы надёжно выбрать метод по одному лишь контексту присваивания. Поэтому два метода с одинаковым именем и параметрами, отличающиеся только результатом, объявить нельзя.