Override: перевод и переопределение методов в Java и других языках

Override: перевод и переопределение методов в Java и других языках Полезное

Override (англ. «переопределять, отменять, брать управление на себя») в программировании — это переопределение метода: класс-наследник дает собственную реализацию метода, унаследованного от родительского класса или интерфейса, сохраняя его имя и список параметров. Какая реализация сработает, решается во время выполнения по фактическому типу объекта, а не по типу переменной.

Рядом три похожих понятия, которые часто путают:

Термин Что это Когда решается, какой метод вызвать
Override (переопределение) Наследник заменяет реализацию метода родителя с той же сигнатурой Во время выполнения (динамическая диспетчеризация)
Overload (перегрузка) Несколько методов с одним именем, но разными параметрами Во время компиляции, по типам аргументов
@Override в Java Аннотация-проверка: просит компилятор подтвердить, что метод действительно переопределяет Не влияет на вызов, только на компиляцию
Hiding (сокрытие) Статический метод наследника с той же сигнатурой, что у родителя Во время компиляции, по типу переменной

Перевод слова override

Значения слова: manual override — ручное управление в обход автоматики; override settings — заменить настройки по умолчанию своими; override a decision — отменить решение. Общий смысл — «поставить свое поверх существующего», и в ООП наследник делает ровно это. Поэтому устоявшийся перевод термина — «переопределение», а не «перегрузка» (это overload).

Минимальный пример в Java

Сначала полный рабочий код, потом разбор. Файл Main.java:

class Animal {
    String sound() {
        return "...";
    }

    void speak() {
        System.out.println(getClass().getSimpleName() + ": " + sound());
    }
}

class Dog extends Animal {
    @Override
    String sound() {
        return "Гав";
    }
}

class Cat extends Animal {
    @Override
    String sound() {
        return "Мяу";
    }
}

public class Main {
    public static void main(String[] args) {
        Animal[] animals = { new Animal(), new Dog(), new Cat() };
        for (Animal a : animals) {
            a.speak();
        }
    }
}

Запуск javac Main.java && java Main печатает три строки:

Animal: ...
Dog: Гав
Cat: Мяу

Метод speak() написан один раз в Animal и вызывает sound(). Все объекты лежат в массиве Animal[], но при вызове sound() Java смотрит на реальный класс объекта и берет его версию метода. Это полиморфизм времени выполнения.

Правила переопределения в Java

Метод наследника переопределяет метод родителя, только если выполнены условия:

  1. То же имя и те же типы параметров в том же порядке. Другие параметры — это уже перегрузка.
  2. Тип возврата — тот же или его подтип для ссылочных типов (ковариантный возврат): метод Animal copy() можно переопределить как Dog copy(). Для примитивов — строго тот же тип.
  3. Доступ не уже, чем у родителя. public нельзя переопределить как protected — будет ошибка компиляции.
  4. Проверяемые исключения (checked) — не шире, чем объявлены у родителя. Непроверяемые (RuntimeException и наследники) можно добавлять.
  5. Не переопределяются: final-методы (запрещено явно), private-методы (наследник их не видит, одноименный метод будет новым), методы с доступом по умолчанию из другого пакета, конструкторы. static-методы не переопределяются, а скрываются: вызов выбирается по типу переменной, а не объекта.

Зачем нужна аннотация @Override

@Override не обязательна: переопределение работает и без нее. Ее задача — заставить компилятор проверить намерение. Если метод с этой аннотацией на самом деле ничего не переопределяет, компиляция падает. С Java 6 аннотацию можно ставить и на реализацию метода интерфейса.

Классическая ошибка, которую она ловит, — equals с неверным типом параметра.

Неверно:

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

class Point {
    final int x, y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    // параметр Point, а не Object - это перегрузка, а не переопределение
    public boolean equals(Point other) {
        return other != null && x == other.x && y == other.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

public class Main {
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        System.out.println(set.contains(new Point(1, 2)));
    }
}

Результат: программа компилируется и печатает false. HashSet вызывает equals(Object), а его никто не переопределил, и работает версия из Object, сравнивающая ссылки. equals(Point) — соседний перегруженный метод, коллекция о нем не знает.

С @Override над equals(Point) javac остановит сборку: method does not override or implement a method from a supertype.

Исправление — правильная сигнатура (шаблон в instanceof доступен с Java 16):

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point other)) return false;
        return x == other.x && y == other.y;
    }

Теперь set.contains(new Point(1, 2)) возвращает true. Практическое правило: ставить @Override на каждый переопределяющий метод, IDE и линтеры это подсказывают.

Как вызвать версию родителя: super

Через super.имяМетода() наследник вызывает реализацию родителя и дополняет ее, а не выбрасывает:

class Logger {
    void log(String msg) {
        System.out.println("[LOG] " + msg);
    }
}

class PrefixLogger extends Logger {
    @Override
    void log(String msg) {
        super.log("app: " + msg);
    }
}

public class Main {
    public static void main(String[] args) {
        Logger logger = new PrefixLogger();
        logger.log("старт");
    }
}

Вывод: [LOG] app: старт: метод PrefixLogger добавил префикс и передал строку в метод Logger.

Override и overload: сравнение

Признак Override (переопределение) Overload (перегрузка)
Где В наследнике относительно родителя или интерфейса Обычно в одном классе (можно и в наследнике)
Имя метода То же То же
Параметры Те же типы и порядок Обязательно отличаются
Тип возврата Тот же или подтип Любой, но сам по себе перегрузку не создает
Выбор версии Во время выполнения, по объекту Во время компиляции, по аргументам
Пример Dog.sound() вместо Animal.sound() print(int) и print(String)

Как выбрать на практике: нужно, чтобы разные типы объектов по-разному отвечали на один и тот же вызов — переопределение. Нужно принимать разные входные данные одним именем — перегрузка.

Override в C#, Kotlin и C++

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

Язык Родитель разрешает Наследник пишет Если забыть
Java Все нестатические, не final, не private методы @Override — по желанию Работает, но опечатка не ловится
C# Только virtual, abstract или override override — обязательно Без override метод скрывает родительский (предупреждение, явно — new)
Kotlin Только open, abstract или override (по умолчанию методы закрыты) override — обязательно Ошибка компиляции
C++ Только virtual Спецификатор override (с C++11) — по желанию Опечатка создает новый метод без ошибки

В C# разница между override и new видна на одном вызове:

Animal d = new Dog();
Animal c = new Cat();
Console.WriteLine(d.Sound()); // Гав - переопределение
Console.WriteLine(c.Sound()); // ... - new только скрывает метод

class Animal { public virtual string Sound() => "..."; }
class Dog : Animal { public override string Sound() => "Гав"; }
class Cat : Animal { public new string Sound() => "Мяу"; }

В Kotlin родитель пишет open fun sound(), наследник — override fun sound().

В C++ без override легко ошибиться в сигнатуре. Неверный вариант: у наследника забыт const.

#include <iostream>
#include <memory>
#include <vector>

struct Shape {
    virtual ~Shape() = default;
    virtual double area() const { return 0.0; }
};

struct Rect : Shape {
    double w, h;
    Rect(double w, double h) : w(w), h(h) {}
    double area() const override { return w * h; }
};

struct Circle : Shape {
    double r;
    explicit Circle(double r) : r(r) {}
    double area() { return 3.14159 * r * r; } // нет const - это новый метод
};

int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Rect>(2, 3));
    shapes.push_back(std::make_unique<Circle>(1));
    for (const auto& s : shapes) {
        std::cout << s->area() << "\n";
    }
}

Результат: печатается 6, затем 0. Для круга вызвалась версия Shape: Circle::area() без const не совпадает по сигнатуре и ничего не переопределяет. Ошибки нет, в лучшем случае предупреждение (Clang с -Wall: hides overloaded virtual function).

Исправление: double area() const override { return 3.14159 * r * r; }, вывод 6 и 3.14159. Если забыть const, но оставить override, код не соберется, Clang: non-virtual member function marked 'override' hides virtual member function, у GCC текст другой, но смысл тот же.

Если переопределение не срабатывает

  • Вызывается метод родителя, хотя наследник его «переопределил». Сверьте сигнатуры: типы параметров (Object против Point), const в C++, регистр (tostring вместо toString). @Override/override покажет несовпадение.
  • Ошибка overridden method is final (javac) или is final and cannot be overridden (Kotlin). Метод закрыт автором (для final-класса javac пишет cannot inherit from final). Переопределять нельзя, используйте композицию: оберните объект и делегируйте ему вызовы.
  • «attempting to assign weaker access privileges» в Java. Переопределение сузило доступ, например public стал protected. Верните исходный модификатор или шире.

Выводы

  • Override в ООП — переопределение метода: наследник заменяет реализацию родителя с тем же именем и параметрами, версия выбирается во время выполнения.
  • Перегрузка (overload) — другое понятие: разные параметры при одном имени, выбор при компиляции.
  • В Java @Override не меняет поведение, а включает проверку компилятора; она ловит ошибки вроде equals(Point) вместо equals(Object).
  • В C# и Kotlin переопределение нужно явно разрешить у родителя и явно пометить у наследника; в C++ спецификатор override защищает от тихой ошибки в сигнатуре.

Где применяется / связь с практикой

Переопределение есть в каждом Java-проекте: toString, equals и hashCode в моделях данных, реализация интерфейсов, обработчики фреймворков. Отличать override от overload ждут от кандидата уже на первом собеседовании.

Освойте тему на практике

Системно пройти ООП, коллекции и основы Java с практикой и ревью кода можно на курсе «Java Developer. Basic». Попробовать формат бесплатно можно на открытых уроках Otus.

FAQ

Можно ли переопределить конструктор?
Нет. Конструкторы не наследуются, поэтому переопределять нечего. Наследник объявляет свои конструкторы и вызывает родительский через super(...).

Можно ли запретить переопределение метода?
Да. В Java метод помечают final, в Kotlin — final override (обычные методы и так закрыты, а override-метод открыт, пока его не закрыли), в C# — sealed override, в C++ — спецификатор final.

Есть ли override в Python?
Ключевого слова нет: одноименный метод наследника просто заменяет метод родителя. С Python 3.12 в typing есть декоратор @override, но его проверяют только анализаторы типов (mypy, pyright), интерпретатор — нет.

OTUS Журнал