Перегрузка или переопределение
Перегрузка и переопределение звучат похоже, и на собеседовании их различие спрашивают постоянно. Оба механизма решают, какой из нескольких методов с одним именем выполнить. Разница в том, кто это решает и когда.
Вернёмся к пульту. На нём может быть несколько похожих кнопок, и нужную выбирают по надписям на самом пульте, ещё до нажатия. Это перегрузка. А что произойдёт после нажатия, решает устройство, на которое направлен пульт. Это переопределение.
Перегрузку вы уже знаете: компилятор выбирает версию по типам аргументов. Посмотрим, что получится, если аргумент — переменная типа родителя, которая ведёт к наследнику:
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 {}Давайте разберём программу по шагам:
- Метод
greetперегружен: одна версия принимаетAnimal, другая —Dog. Animal a = d;кладёт вaссылку на ту же собаку, что и вd.- У
dтипDog, и дляgreet(d)есть точное совпадение — версия дляDog. Печатаетсяdog. - Объект в
aтот же, но тип переменной —Animal. Дляgreet(a)компилятор выбирает версию дляAnimal, и печатаетсяanimal.
Перегрузку выбирает компилятор
Компилятор выбирает перегрузку, когда читает текст программы, задолго до запуска. Объектов в этот момент ещё нет: есть только объявленные типы. Про a компилятор знает одно — это переменная типа Animal.
По-другому он и не может. Ссылку в a можно положить внутри if, и какой объект там окажется, станет ясно только при запуске. Поэтому версию компилятор ищет по объявленным типам аргументов:
- сначала точное совпадение:
greet(d)с переменной типаDogвызываетgreet(Dog d); - если его нет — подходящую версию: собака является животным, поэтому
Dogподошёл бы и в параметр типаAnimal.
По той же причине в переменную или параметр типа Object подходит любой объект, даже строка: каждый класс — наследник Object.
Переопределение выбирает объект
Теперь добавим классам метод sound и вызовем через одну переменную оба механизма:
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"); }
}Разберём два вызова:
greet(a)печатаетanimal: перегрузку выбрал компилятор по объявленному типуAnimal.a.sound()печатаетWoof: переопределённую версию выбрала JVM по объекту, а объект — собака.
Одна и та же переменная a дала в одной программе два разных ответа. В этом и состоит главное различие двух механизмов.
Сравним
| Перегрузка | Переопределение | |
|---|---|---|
| Где | в одном классе или в наследнике | только в наследнике |
| Параметры | разные | те же |
| Кто выбирает | компилятор | JVM |
| Когда | до запуска | во время работы программы |
| По чему | по объявленным типам аргументов | по объекту |
На собеседовании ответ укладывается в одну фразу. Перегрузку выбирает компилятор по объявленным типам аргументов, а переопределение — JVM по объекту, когда программа уже работает.
Здесь картинка с пультом неточна. На настоящем пульте кнопку выбираете вы и при этом видите устройство. Компилятор устройства не видит: он читает только надписи, то есть объявленные типы.
Частая ошибка
Хотели переопределить метод родителя, а изменили его параметры:
class Dog extends Animal {
@Override void sound(String mood) { // ✗
System.out.println("Woof " + mood);
}
}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это поймает.