1. Перевизначення методів
Перевизначення (overriding) — це можливість для підкласу оголосити власну версію методу, який уже є в батьківському класі. Завдяки цьому під час виконання програми (run-time) працює справжній поліморфізм.
Простими словами:
Якщо у вас є базовий клас із методом і ви хочете, щоб підклас виконував цей метод по‑своєму, просто оголошуйте метод із такою самою сигнатурою в підкласі. Під час виклику методу через посилання на базовий тип викликається версія методу з фактичного типу обʼєкта.
Приклад із життя:
Уявіть, що у вас є група тварин, і ви просите кожну «подати звук». Для всіх це команда makeSound(), але собака гавкає, кішка нявчить, а корова мукає. У коді це виглядає як виклик одного й того самого методу, але результат різний — ось вона: магія перевизначення!
Синтаксис перевизначення
Базовий приклад
class Animal {
void makeSound() {
System.out.println("Тварина видає якийсь звук");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Гав!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Няв!");
}
}
Тут у класі Animal визначено метод makeSound(). Підкласи Dog і Cat перевизначають цей метод, надаючи власну реалізацію.
Анотація @Override
У Java рекомендується (і це гарна звичка) позначати перевизначені методи анотацією @Override:
@Override
void makeSound() { ... }
Це не обов’язково для роботи коду, але:
- Компілятор перевірить, чи справді ви перевизначаєте метод (а не випадково припустилися помилки в імені або параметрах).
- Покращує читаність коду — іншим розробникам одразу помітно, що цей метод перевизначає батьківський.
Виклик перевизначеного методу
Animal myDog = new Dog();
myDog.makeSound(); // Виведе: Гав!
Хоча змінна має тип Animal, насправді вона вказує на обʼєкт Dog, тож викликається метод із класу Dog. Це й є динамічне (пізнє) звʼязування.
2. Відмінність перевизначення (overriding) від перевантаження (overloading)
Перевантаження (overloading)
- В одному класі (інколи в межах ієрархії, але фактично «поруч»).
- Методи мають однакову назву, але різні параметри (тип, кількість, порядок).
- Вибір методу відбувається під час компіляції.
void print(int x) { ... }
void print(String s) { ... }
Перевизначення (overriding)
- У різних класах: метод оголошено в суперкласі й перевизначено в підкласі.
- Методи мають однакове імʼя та сигнатуру (параметри й тип, що повертається).
- Вибір методу відбувається під час виконання (run-time), на основі фактичного типу обʼєкта.
Порівняльна таблиця
| Перевантаження (overloading) | Перевизначення (overriding) | |
|---|---|---|
| Де | В одному класі | У суперкласі та підкласі |
| Імʼя методу | Однакове | Однакове |
| Параметри | Різні | Однакові |
| Тип, що повертається | Може відрізнятися | Має збігатися або бути підтипом |
| Коли обирається | Під час компіляції | Під час виконання (run-time) |
| Анотація | Не потрібна | @Override (рекомендується) |
3. Правила перевизначення методів
Перевизначення — потужний механізм, але з ним пов’язані суворі правила. Розгляньмо їх по черзі.
Сигнатура методу має збігатися
- Імʼя методу, типи та порядок параметрів мають бути ідентичними методу в суперкласі.
- Тип, що повертається, має збігатися або бути коваріантним (тобто підтипом типу, що повертається батьківським методом).
Приклад із коваріантним типом, що повертається:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // ОК! Cat — підтип Animal
}
Модифікатор доступу
Модифікатор доступу перевизначеного методу не може бути суворішим, ніж у методу в суперкласі. Якщо метод у батьківському класі public, то в підкласі він має залишитися таким самим public. Зробити його менш доступним (protected або private) не можна.
Приклад:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // Помилка! Модифікатор за замовчуванням — package-private, менш доступний, ніж public
@Override
public void greet() { } // ОК
}
Винятки
- Перевизначений метод не може кидати нові checked‑винятки, які не оголошені в базовому методі.
- Можна кидати менше або ті самі винятки.
Приклад:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // ОК, FileNotFoundException — підтип IOException
// void doWork() throws SQLException { } // Помилка! SQLException не оголошено в батьківському класі
}
Статичні методи не перевизначаються
Статичні методи можуть бути приховані (hidden), але не перевизначені. Якщо ви оголосите статичний метод із такою самою сигнатурою в підкласі, це не перевизначення! Це буде просто приховування методу, а не поліморфізм.
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
Виклик Dog.info() виведе «Dog», але якщо викликати через змінну типу Animal, буде викликано метод Animal.info(). Це не поліморфізм!
final-методи не можна перевизначати
Якщо метод у суперкласі оголошено як final, спроба перевизначити його призведе до помилки компіляції.
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // Помилка! Не можна перевизначати final-метод
}
4. Практичні приклади
Розгляньмо на практиці, як працює перевизначення та чим воно відрізняється від перевантаження.
Приклад 1: Клас Shape і його нащадки
class Shape {
void draw() {
System.out.println("Малюємо фігуру");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Малюємо коло");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Малюємо прямокутник");
}
}
Використовуємо поліморфізм:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // Малюємо коло
s2.draw(); // Малюємо прямокутник
}
}
Хоча змінні оголошено як Shape, викликається метод саме того класу, до якого насправді належить обʼєкт.
Приклад 2: Відмінність від перевантаження
class Printer {
void print(String s) {
System.out.println("Рядок: " + s);
}
void print(int n) {
System.out.println("Число: " + n);
}
}
Тут обидва методи називаються print, але мають різні параметри — це перевантаження, а не перевизначення.
5. Перевизначення та виклик батьківського методу (super)
Іноді в перевизначеному методі хочеться спочатку виконати логіку батьківського класу, а потім додати свою. Для цього використовується ключове слово super.
class Animal {
void makeSound() {
System.out.println("Тварина видає звук");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // Виклик методу батьківського класу
System.out.println("Гав!");
}
}
Виклик new Dog().makeSound() виведе:
Тварина видає звук
Гав!
Як працює динамічне звʼязування (late binding)
Коли ви викликаєте метод через посилання на базовий тип, Java під час виконання дивиться, до якого реального обʼєкта належить це посилання, і викликає саме ту версію методу, яка визначена в класі цього обʼєкта.
Animal a = new Cat();
a.makeSound(); // Викличе Cat.makeSound(), а не Animal.makeSound()
Це і є основа поліморфізму в Java.
6. Як це повʼязано з вашим застосунком
У нашому навчальному застосунку (наприклад, у системі обліку працівників) ви можете створити базовий клас Employee із методом work(), а підкласи Manager і Developer можуть по‑своєму реалізувати цей метод:
class Employee {
void work() {
System.out.println("Співробітник працює");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("Менеджер керує");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("Розробник пише код");
}
}
Тепер ви можете зберігати усіх працівників в одному масиві або списку:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // Для кожного — свій результат!
}
7. Типові помилки під час перевизначення методів
Помилка № 1: описка в імені методу або параметрах. Якщо ви випадково помилилися в імені методу або його параметрах, ви не перевизначаєте метод, а створюєте новий. У результаті поліморфізм не працює. Саме тому завжди використовуйте анотацію @Override — компілятор одразу підкаже про помилку.
Помилка № 2: суворіший модифікатор доступу. Якщо в батьківському класі метод public, а в підкласі ви оголосили його як protected або без модифікатора — отримаєте помилку компіляції.
Помилка № 3: спроба перевизначити static‑ або final‑метод. Статичні методи не перевизначаються, а final‑методи перевизначати не можна. Якщо ви це спробуєте — компілятор вас зупинить.
Помилка № 4: зміна типу, що повертається, на несумісний. Якщо тип, що повертається, у методі підкласу не збігається з типом у суперкласі (і не є його підтипом), компілятор не дозволить перевизначати метод.
Помилка № 5: додавання нових checked‑винятків. Перевизначений метод не може кидати нові checked‑винятки, яких немає в оголошенні базового методу. Якщо це зробити — компілятор видасть помилку.
Помилка № 6: забули про super. Якщо в перевизначеному методі ви хочете зберегти частину поведінки батьківського класу, не забудьте явно викликати super.methodName(). Java сама цього не зробить.
Тепер ви знаєте, як працює перевизначення методів, чим воно відрізняється від перевантаження та як за його допомогою реалізується поліморфізм у Java. У наступній лекції розглянемо, як застосовувати поліморфізм на практиці — з колекціями, масивами та реальними завданнями!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ