Переопределение и полиморфизм
Теперь представьте универсальный пульт с надписью «Животное». На нём одна кнопка — «Голос». Наведите пульт на собаку и нажмите: она залает. Наведите на кошку — она мяукнет. Кнопка одна и та же, а что она делает, решает тот, на кого направлен пульт.
В Java пульт — это переменная, а тот, на кого он направлен, — объект. Но сначала собаку нужно научить своему голосу. У Animal есть метод sound, и собака получает его по наследству, а ей нужен лай:
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"); }
}Давайте разберём программу по шагам:
- В
Animalметодsoundпечатает...: какой голос у животного вообще, неизвестно. - В
Dogобъявлен метод с тем же именем и тем же (здесь пустым) списком параметров. Пометку@Overrideперед ним разберём ниже. animal.sound()у объектаAnimalпечатает....dog.sound()у объектаDogпечатаетWoof: версия изDogзаменила версию родителя.
Переопределение метода
Переопределение — объявление в наследнике метода с той же сигнатурой, что у родителя: с тем же именем и теми же типами параметров. Новая версия заменяет унаследованную у объектов наследника. У объектов самого Animal остаётся прежняя.
Пометка @Override
Запись @Override — пометка для компилятора: «этот метод переопределяет метод родителя». Компилятор её проверяет. Допустим, вы ошиблись в имени и написали Sound вместо sound. Без пометки получится новый метод, и собака по-прежнему будет печатать .... С пометкой компилятор сразу ответит:
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. Что выведет вызов?
Animal a = new Dog();
a.sound(); // WoofВыведет Woof. Переменная объявлена как Animal, но ведёт к собаке, а версию метода выбирает объект. Это и есть полиморфизм: один и тот же вызов a.sound() ведёт себя по-разному в зависимости от того, к какому объекту ведёт a.
Вернёмся к пульту. Тип переменной — это пульт, а объект — устройство, на которое он направлен. Получается так:
- какие методы можно вызвать, решает тип переменной, и проверяет это компилятор ещё до запуска;
- какая версия метода выполнится, решает объект, и выбирает её JVM во время работы программы.
В виджете переменная a сначала ведёт к собаке, а потом к новому объекту Animal. Нажимайте «Следующий шаг» и следите за столбцом рамок: какой метод sound в нём появится.
1public class Main {2 public static void main(String[] args) {3 Animal a = new Dog();4 a.sound();5 a = new Animal();6 a.sound();7 }8}9 10class Animal {11 void sound() {12 System.out.println("...");13 }14}15 16class Dog extends Animal {17 @Override18 void sound() {19 System.out.println("Woof");20 }21}
new Dog() строит собаку, а ссылку на неё получает переменная a типа Animal.
Вызов a.sound() записан одинаково, а выполняются разные методы. Компилятор не знает, к какому объекту будет вести a в момент вызова, поэтому выбор откладывается до запуска. Такой выбор метода во время работы программы называют динамической диспетчеризацией.
Этот вопрос часто задают на собеседовании: что выведет Animal a = new Dog(); a.sound()? Ответ: Woof, потому что переопределённый метод выбирается по объекту, а не по типу переменной.
Здесь картинка с пультом неточна. Настоящий пульт может послать команду, которую устройство не понимает, и ничего не случится. В Java так не бывает: у наследника есть все методы родителя, поэтому любая кнопка пульта Animal у собаки сработает.
Версия родителя: super.sound()
Иногда наследнику нужна не замена, а дополнение: сделать то же, что родитель, и ещё что-то своё. Тогда новая версия вызывает старую через super:
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():
- Объект — собака, поэтому выполняется версия
Dog. super.sound()вызывает версию родителя, и она печатает....- Затем выполняется своя строка
Dogи печатаетWoof.
Без super.sound() версия родителя не выполнилась бы вовсе: переопределение заменяет метод, а не дописывает к нему. И не путайте этот вызов с super(…) в конструкторе: там super вызывает конструктор родителя, а здесь — его метод.
Частая ошибка
Через переменную типа родителя вызвали метод, который есть только у наследника. Пусть у Dog есть свой метод bark:
Animal pet = new Dog();
pet.bark(); // ✗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()вызывает версию родителя из переопределённого метода.