Переопределение методов

Определение

Переопределение (overriding) — новая реализация унаследованного instance-метода с той же сигнатурой. Вызов выбирается по реальному классу объекта:

class Formatter {
    String format(Object value) {
        return value.toString();
    }
}
 
class JsonFormatter extends Formatter {
    @Override
    String format(Object value) {
        return "{\"value\":\"" + value + "\"}";
    }
}
 
Formatter formatter = new JsonFormatter();
formatter.format("ok"); // реализация JsonFormatter

Аннотация @Override заставляет компилятор проверить намерение и защищает от случайной перегрузки.

Правила

Переопределяющий метод:

  • имеет ту же сигнатуру после учёта правил generics/стирания;
  • возвращает тот же тип либо его подтип для ссылочного результата;
  • не сужает доступ;
  • не объявляет более широкие checked-исключения;
  • может убрать checked-исключение или заменить его подтипом;
  • не может переопределить final-метод.

private-метод не виден наследнику и не переопределяется. static-метод скрывается, а не переопределяется. Поля тоже скрываются и разрешаются по статическому типу.

Ковариантный результат

class Document {
    Document copy() {
        return new Document();
    }
}
 
class Report extends Document {
    @Override
    Report copy() {
        return new Report();
    }
}

Возврат Report допустим, потому что это подтип Document. Для примитивного результата ковариантности нет.

Вызов базовой реализации

super.method() позволяет дополнить поведение родителя:

@Override
void validate(Input input) {
    super.validate(input);
    validateFormat(input);
}

Это создаёт зависимость от контракта базовой реализации. Если порядок критичен, Template Method с final-алгоритмом и отдельными hooks часто яснее.

Частая ошибка с equals

class User {
    boolean equals(User other) { // перегрузка, не override
        return false;
    }
}

Контракт Object требует equals(Object). Без @Override ошибка может остаться незамеченной:

@Override
public boolean equals(Object other) {
    // проверка типа и полей
}

Полиморфизм и конструктор

Динамический вызов работает даже из конструктора базового класса. В этот момент поля наследника ещё могут иметь значения по умолчанию, поэтому вызов переопределяемых методов из конструктора опасен.

Ловушки

  • Изменить тип параметра и получить перегрузку вместо override.
  • Скрыть static-метод и ожидать динамический dispatch.
  • Сузить модификатор доступа.
  • Расширить список checked-исключений.
  • Переопределить equals с параметром конкретного класса.
  • Нарушить поведенческий контракт родителя при формально корректной сигнатуре.

Вопросы на собеседовании

  1. По какому типу выбирается переопределённый instance-метод?
  2. Можно ли изменить возвращаемый тип?
  3. Что происходит со static-методами и полями?
  4. Можно ли добавить более общее checked-исключение?
  5. Почему equals(MyClass) не переопределяет Object.equals?
  6. Зачем всегда ставить @Override?

Связанные заметки

Наследование · Полиморфизм · Перегрузка Java · Методы Java · Equals и hashCode · Абстрактные классы