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

Переопределение и полиморфизм

Теперь представьте универсальный пульт с надписью «Животное». На нём одна кнопка — «Голос». Наведите пульт на собаку и нажмите: она залает. Наведите на кошку — она мяукнет. Кнопка одна и та же, а что она делает, решает тот, на кого направлен пульт.

В Java пульт — это переменная, а тот, на кого он направлен, — объект. Но сначала собаку нужно научить своему голосу. У Animal есть метод sound, и собака получает его по наследству, а ей нужен лай:

JAVA
public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.sound(); // ...
        Dog dog = new Dog();
        dog.sound();    // Woof
    }
}

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

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

  1. В Animal метод sound печатает ...: какой голос у животного вообще, неизвестно.
  2. В Dog объявлен метод с тем же именем и тем же (здесь пустым) списком параметров. Пометку @Override перед ним разберём ниже.
  3. animal.sound() у объекта Animal печатает ....
  4. dog.sound() у объекта Dog печатает Woof: версия из Dog заменила версию родителя.

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

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

Пометка @Override

Запись @Override — пометка для компилятора: «этот метод переопределяет метод родителя». Компилятор её проверяет. Допустим, вы ошиблись в имени и написали Sound вместо sound. Без пометки получится новый метод, и собака по-прежнему будет печатать .... С пометкой компилятор сразу ответит:

TEXT
error: method does not override or implement a method from a supertype

Программа работает и без @Override, но тогда опечатку никто не заметит. Поэтому пометку ставят перед каждым переопределением.

toString тоже переопределяют

Вы уже переопределяли метод, когда писали toString для точки. Метод toString объявлен в Object, и Point заменил его своей версией, поэтому перед ним тоже ставят @Override. А public там был обязателен, потому что переопределение не может сузить доступ: в Object метод toString объявлен как public.

Тип переменной и тип объекта

Самое интересное начинается, когда переменная типа Animal ведёт к собаке. Так можно: собака является животным, и ссылку на Dog можно положить в переменную Animal. Что выведет вызов?

JAVA
Animal a = new Dog();
a.sound(); // Woof

Выведет Woof. Переменная объявлена как Animal, но ведёт к собаке, а версию метода выбирает объект. Это и есть полиморфизм: один и тот же вызов a.sound() ведёт себя по-разному в зависимости от того, к какому объекту ведёт a.

Вернёмся к пульту. Тип переменной — это пульт, а объект — устройство, на которое он направлен. Получается так:

  • какие методы можно вызвать, решает тип переменной, и проверяет это компилятор ещё до запуска;
  • какая версия метода выполнится, решает объект, и выбирает её JVM во время работы программы.

В виджете переменная a сначала ведёт к собаке, а потом к новому объекту Animal. Нажимайте «Следующий шаг» и следите за столбцом рамок: какой метод sound в нём появится.

ВживуюЧей метод sound выполнитсяшаг 1 из 7 · рамок: 1

new Dog() строит собаку, а ссылку на неё получает переменная a типа Animal.

Следите, к какому объекту ведёт a в момент вызова

Вызов a.sound() записан одинаково, а выполняются разные методы. Компилятор не знает, к какому объекту будет вести a в момент вызова, поэтому выбор откладывается до запуска. Такой выбор метода во время работы программы называют динамической диспетчеризацией.

Этот вопрос часто задают на собеседовании: что выведет Animal a = new Dog(); a.sound()? Ответ: Woof, потому что переопределённый метод выбирается по объекту, а не по типу переменной.

Здесь картинка с пультом неточна. Настоящий пульт может послать команду, которую устройство не понимает, и ничего не случится. В Java так не бывает: у наследника есть все методы родителя, поэтому любая кнопка пульта Animal у собаки сработает.

Версия родителя: super.sound()

Иногда наследнику нужна не замена, а дополнение: сделать то же, что родитель, и ещё что-то своё. Тогда новая версия вызывает старую через super:

JAVA
public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.sound();
    }
}

class Animal { void sound() { System.out.println("..."); } }

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

Класс Animal здесь тот же, только записан в одну строку. Разберём вызов a.sound():

  1. Объект — собака, поэтому выполняется версия Dog.
  2. super.sound() вызывает версию родителя, и она печатает ....
  3. Затем выполняется своя строка Dog и печатает Woof.

Без super.sound() версия родителя не выполнилась бы вовсе: переопределение заменяет метод, а не дописывает к нему. И не путайте этот вызов с super(…) в конструкторе: там super вызывает конструктор родителя, а здесь — его метод.

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

Через переменную типа родителя вызвали метод, который есть только у наследника. Пусть у Dog есть свой метод bark:

JAVA
Animal pet = new Dog();
pet.bark(); // ✗
TEXT
error: cannot find symbol

Программа не компилируется, хотя объект — собака. Компилятор проверяет вызов по типу переменной, а у Animal метода bark нет. На пульте «Животное» нет кнопки «Лай», на кого его ни наведи. Если нужны собственные методы собаки, храните ссылку в переменной типа Dog: Dog d = new Dog(); d.bark();.

Главное

  • Переопределение — метод наследника с той же сигнатурой, что у родителя; пометка @Override просит компилятор это проверить.
  • В Animal a = new Dog(); a.sound() выполняется версия Dog: переопределённый метод выбирается по объекту во время работы программы.
  • Какие методы можно вызвать, решает тип переменной: a.bark() не скомпилируется, даже если a ведёт к собаке.
  • super.sound() вызывает версию родителя из переопределённого метода.