1. Переопределение метода
В жизни часто встречаются ситуации, когда «наследник» ведёт себя по‑особенному. Например, все животные умеют издавать звуки, но у кошки — «мяу», у собаки — «гав», а у программиста — «ой, опять баг!». В программировании это реализуется через переопределение метода (override).
Переопределение метода — это когда подкласс предоставляет свою реализацию метода, который уже объявлен в родительском классе. То есть «подменяет» стандартное поведение на своё, более специфичное.
Аналогия. Если представить родительский класс как фирменный рецепт борща, то переопределение метода — это когда бабушка добавляет туда свой секретный ингредиент. Борщ остаётся борщом, но вкус у каждого свой.
Чтобы переопределить метод, нужно в дочернем классе объявить метод с точно такой же сигнатурой (имя, параметры, возвращаемый тип), как у родителя.
Пример: животные и их звуки
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
// Переопределяем метод makeSound()
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
// Переопределяем метод makeSound()
void makeSound() {
System.out.println("Meow!");
}
}
Теперь, если создать объект Dog и вызвать makeSound(), вы услышите "Woof!", а не "Some generic animal sound".
Демонстрация в коде
public class Main {
public static void main(String[] args) {
Animal generic = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
}
}
Важно: если у подкласса нет метода с такой же сигнатурой, будет использован метод родителя.
2. Аннотация @Override: для чего нужна и как использовать
В Java принято помечать переопределённые методы специальной аннотацией @Override. Это не просто украшение для кода, а полезный инструмент:
- Компилятор проверяет, действительно ли вы переопределяете метод родителя. Если вы ошиблись в имени, типе параметра или возвращаемом типе — компилятор выдаст ошибку.
- Повышает читаемость кода. Другой программист сразу видит: «О, этот метод переопределяет родительский».
Пример с @Override
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
Если случайно написать void makeSond() (опечатка!) в методе, помеченном как @Override, компилятор ругнётся: "Method does not override or implement a method from a supertype".
Современные стандарты. Использовать @Override — это хороший тон и стандарт индустрии. Даже если компилятор не требует, всегда ставьте эту аннотацию — и себе, и коллегам будет проще жить.
3. Как работает вызов переопределённого метода
Когда вы вызываете метод на объекте подкласса, будет использована реализация из подкласса, даже если переменная объявлена как родительский тип.
Пример: полиморфизм в действии
Animal animal = new Dog();
animal.makeSound(); // "Woof!", а не "Some generic animal sound"
Здесь переменная типа Animal, но реально в ней лежит объект Dog. Java «понимает», что нужно вызвать переопределённый метод из Dog. Это и есть полиморфизм (подробнее — в следующих лекциях).
4. Ограничения и правила переопределения
Сигнатура метода
- Имя, тип и порядок параметров должны совпадать с методом в родителе.
- Возвращаемый тип должен совпадать или быть ковариантным (подтипом возвращаемого типа родителя). Например, если родитель возвращает Animal, а дочерний — Dog, это разрешено.
Модификаторы доступа
- Нельзя сделать доступ более строгим, чем у родителя.
- Если родительский метод public, то и переопределённый должен быть public.
- Если родительский — protected, то переопределённый может быть protected или public.
Если попытаться сделать наоборот, компилятор скажет: "Cannot reduce the visibility of the inherited method".
Исключения
- Переопределённый метод не может выбрасывать новое checked‑исключение, которого нет в объявлении родителя.
- Можно выбрасывать меньше исключений, чем родитель, или их подтипы.
static, final, private
- Нельзя переопределить методы, объявленные как static или final, а также приватные (private).
- static — это скрытие (hiding), а не переопределение.
- final — вообще нельзя переопределить, Java защищает такие методы.
- private — не виден в наследнике, переопределить нельзя (можно только объявить новый метод с тем же именем).
Конструкторы
Конструкторы не наследуются и не переопределяются. У каждого класса свои конструкторы.
5. Развиваем учебное приложение «Зоопарк»
Пора применить теорию на практике! Давайте продолжим развивать наше «зоопарковое» приложение.
Шаг 1. Базовый класс Animal
public class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
public void sleep() {
System.out.println("Zzz...");
}
}
Шаг 2. Подклассы Dog и Cat
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
// Дополнительный метод только для Dog
public void fetch() {
System.out.println("Dog brings the stick!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
// Дополнительный метод только для Cat
public void scratch() {
System.out.println("Cat scratches the sofa!");
}
}
Шаг 3. Используем переопределение
public class ZooTest {
public static void main(String[] args) {
Animal generic = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
// dog.fetch(); // Ошибка! Переменная типа Animal не знает про fetch()
// cat.scratch(); // Аналогично
// Но если явно указать тип:
if (dog instanceof Dog) {
((Dog) dog).fetch(); // Dog brings the stick!
}
if (cat instanceof Cat) {
((Cat) cat).scratch(); // Cat scratches the sofa!
}
}
}
Комментарий:
Метод makeSound() работает полиморфно — вызывается версия из реального класса объекта. А вот специфичные методы (fetch, scratch) доступны только через явное приведение типа — это важно для понимания, как работает наследование и переопределение.
6. Пример с возвращаемым типом (ковариантность)
Иногда хочется, чтобы переопределённый метод возвращал более «узкий» тип. Например:
class Animal {
Animal getFriend() {
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog getFriend() { // Возвращаемый тип — Dog, подтип Animal
return new Dog();
}
}
Это называется ковариантность возвращаемого типа и разрешено в Java (начиная с Java 5).
7. Что будет, если не использовать @Override?
Если вы случайно ошиблись в имени метода или параметрах, Java не будет ругаться, если нет аннотации @Override. В результате вы не переопределите, а создадите новый метод, и ожидаемое поведение не изменится.
Пример ошибки
class Dog extends Animal {
// Опечатка: makeSoud вместо makeSound
void makeSoud() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.makeSound(); // Выведет "Some generic animal sound"
}
}
Если бы был @Override, компилятор выдал бы ошибку: "Method does not override or implement a method from a supertype".
8. Типичные ошибки при переопределении методов
Ошибка №1: отсутствие аннотации @Override.
Без неё легко ошибиться в имени метода или параметрах. В результате метод не будет переопределён, и программа будет вести себя не так, как вы ожидали.
Ошибка №2: попытка сузить модификатор доступа.
Если родительский метод public, а вы пишете protected или private — получите ошибку компиляции.
Ошибка №3: несовпадение сигнатуры.
Если параметры отличаются хотя бы на тип — это уже не переопределение, а перегрузка (overloading).
Ошибка №4: попытка переопределить final или static метод.
Java этого не позволит: final защищает от переопределения, а static методы вообще не переопределяются (только скрываются).
Ошибка №5: изменение типа возвращаемого значения на несовместимый.
Можно возвращать только подтип возвращаемого типа родителя (ковариантность), но не совершенно другой тип.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ