Перейти к содержимому
Теория · Наследование и полиморфизм · урок 5 из 10

Перегрузка или переопределение

Перегрузка и переопределение звучат похоже, и на собеседовании их различие спрашивают постоянно. Оба механизма решают, какой из нескольких методов с одним именем выполнить. Разница в том, кто это решает и когда.

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

Перегрузку вы уже знаете: компилятор выбирает версию по типам аргументов. Посмотрим, что получится, если аргумент — переменная типа родителя, которая ведёт к наследнику:

JAVA
public class Main {
    static void greet(Animal a) { System.out.println("animal"); }
    static void greet(Dog d) { System.out.println("dog"); }

    public static void main(String[] args) {
        Dog d = new Dog();
        Animal a = d;
        greet(d); // dog
        greet(a); // animal
    }
}

class Animal {}
class Dog extends Animal {}

Давайте разберём программу по шагам:

  1. Метод greet перегружен: одна версия принимает Animal, другая — Dog.
  2. Animal a = d; кладёт в a ссылку на ту же собаку, что и в d.
  3. У d тип Dog, и для greet(d) есть точное совпадение — версия для Dog. Печатается dog.
  4. Объект в a тот же, но тип переменной — Animal. Для greet(a) компилятор выбирает версию для Animal, и печатается animal.

Перегрузку выбирает компилятор

Компилятор выбирает перегрузку, когда читает текст программы, задолго до запуска. Объектов в этот момент ещё нет: есть только объявленные типы. Про a компилятор знает одно — это переменная типа Animal.

По-другому он и не может. Ссылку в a можно положить внутри if, и какой объект там окажется, станет ясно только при запуске. Поэтому версию компилятор ищет по объявленным типам аргументов:

  • сначала точное совпадение: greet(d) с переменной типа Dog вызывает greet(Dog d);
  • если его нет — подходящую версию: собака является животным, поэтому Dog подошёл бы и в параметр типа Animal.

По той же причине в переменную или параметр типа Object подходит любой объект, даже строка: каждый класс — наследник Object.

Переопределение выбирает объект

Теперь добавим классам метод sound и вызовем через одну переменную оба механизма:

JAVA
public class Main {
    static void greet(Animal a) { System.out.println("animal"); }
    static void greet(Dog d) { System.out.println("dog"); }

    public static void main(String[] args) {
        Animal a = new Dog();
        greet(a);  // animal
        a.sound(); // Woof
    }
}

class Animal { void sound() { System.out.println("..."); } }
class Dog extends Animal {
    @Override void sound() { System.out.println("Woof"); }
}

Разберём два вызова:

  1. greet(a) печатает animal: перегрузку выбрал компилятор по объявленному типу Animal.
  2. a.sound() печатает Woof: переопределённую версию выбрала JVM по объекту, а объект — собака.

Одна и та же переменная a дала в одной программе два разных ответа. В этом и состоит главное различие двух механизмов.

Сравним

ПерегрузкаПереопределение
Гдев одном классе или в наследникетолько в наследнике
Параметрыразныете же
Кто выбираеткомпиляторJVM
Когдадо запускаво время работы программы
По чемупо объявленным типам аргументовпо объекту

На собеседовании ответ укладывается в одну фразу. Перегрузку выбирает компилятор по объявленным типам аргументов, а переопределение — JVM по объекту, когда программа уже работает.

Здесь картинка с пультом неточна. На настоящем пульте кнопку выбираете вы и при этом видите устройство. Компилятор устройства не видит: он читает только надписи, то есть объявленные типы.

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

Хотели переопределить метод родителя, а изменили его параметры:

JAVA
class Dog extends Animal {
    @Override void sound(String mood) { // ✗
        System.out.println("Woof " + mood);
    }
}
TEXT
error: method does not override or implement a method from a supertype

Программа не компилируется. У sound(String mood) и sound() разные сигнатуры, поэтому новый метод не переопределяет старый, а перегружает его. Без @Override код бы скомпилировался, а a.sound() у собаки печатал бы .... Чтобы переопределить метод, повторите его параметры в точности: void sound().

Главное

  • Перегрузка — методы с одним именем и разными параметрами; версию выбирает компилятор до запуска, по объявленным типам аргументов.
  • Переопределение — метод наследника с той же сигнатурой; версию выбирает JVM во время работы программы, по объекту.
  • Если Animal a = new Dog(), то greet(a) вызовет версию для Animal, а a.sound() — версию Dog.
  • Другой список параметров в наследнике даёт перегрузку, а не переопределение, и @Override это поймает.