JavaRush /Курси /JAVA 25 SELF /Помилки зі спадкуванням і перевантаженням методів

Помилки зі спадкуванням і перевантаженням методів

JAVA 25 SELF
Рівень 23 , Лекція 1
Відкрита

1. Помилки під час спадкування

Спадкування — одна з основ ООП, але й тема, у якій розробники-початківці найчастіше наступають на граблі. Розберімо класичні помилки та навчімося їх уникати.

Відсутність виклику конструктора базового класу (super(...))

Коли ви створюєте підклас, важливо памʼятати: базовий клас може вимагати ініціалізації через конструктор. Якщо в базовому класі немає конструктора за замовчуванням (без параметрів), у конструкторі підкласу обовʼязково потрібно явно викликати конструктор базового класу за допомогою super(...).

Приклад помилки:

class Animal {
    private String name;
    public Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    // Помилка! У Animal немає конструктора за замовчуванням
    public Dog() {
        // super(); // компілятор вставляє super() автоматично, але такого конструктора немає!
    }
}

Як виправити:

class Dog extends Animal {
    public Dog(String name) {
        super(name); // Усе гаразд!
    }
}

Коментар:
Якщо в базовому класі є лише конструктор із параметрами, компілятор автоматично не додасть конструктор без параметрів. Це часта причина помилок компіляції.

Спроба успадкувати final-клас або перевизначити final-метод

У Java можна оголосити клас або метод як final. Це означає:

  • Клас не можна успадковувати.
  • Метод не можна перевизначати (override) у нащадках.

Приклад помилки:

final class Cat {}

// Помилка компіляції!
class Tiger extends Cat { 
    // ...
}
class Animal {
    public final void sleep() {
        System.out.println("Zzz...");
    }
}

class Dog extends Animal {
    // Помилка компіляції!
    @Override
    public void sleep() {
        System.out.println("Dog is sleeping...");
    }
}

Коментар:
Якщо бачите помилки «cannot inherit from final», «cannot override final method» — перевірте модифікатори.

Порушення принципу підстановки Лісков (Liskov Substitution Principle)

Звучить серйозно, але на практиці це означає: об’єкт підкласу має поводитися так само, як об’єкт базового класу, не порушуючи логіку програми. Частою помилкою є таке перевизначення методів, за якого підклас перестає відповідати очікуванням щодо базового класу.

Приклад:

class Bird {
    public void fly() {
        System.out.println("Я лечу!");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Пінгвіни не літають!");
    }
}

У чому проблема?
Код, що працює з Bird, очікує, що будь-який птах уміє літати. Але якщо йому передати Penguin, програма може завершитися винятком.

Як краще:
У таких випадках варто переглянути ієрархію або використати інтерфейси чи композицію.

2. Помилки з перевантаженням методів (overloading)

Перевантаження — це ситуація, коли в одному класі є кілька методів з однаковим імʼям, але різними параметрами. Здавалося б, усе просто, та й тут можна потрапити в пастку.

Перевантаження замість перевизначення (помилка в сигнатурі)

Часто новачки хочуть перевизначити (override) метод базового класу, але випадково змінюють його параметри. У підсумку виходить перевантаження, а не перевизначення — і поліморфізм не спрацьовує.

Приклад помилки:

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

class Dog extends Animal {
    // Хотіли перевизначити, а зробили перевантаження!
    public void makeSound(String extra) {
        System.out.println("Bark! " + extra);
    }
}

Проблема:
Виклик dog.makeSound() виконає метод базового класу, а не ваш новий.
Виклик dog.makeSound("loudly") спрацює для перевантаженого методу, але поліморфізму тут немає.

Найкраща практика:
Використовуйте анотацію @Override — якщо ви помилилися в сигнатурі, компілятор одразу про це скаже.

@Override
public void makeSound() { /* ... */ }

Неочевидна поведінка під час перевантаження (автоматичне перетворення типів, неоднозначні виклики)

Java інколи може «обрати» не той метод, на який ви розраховували, якщо аргументи підходять одразу до кількох перевантажених сигнатур.

public class OverloadDemo {
    public void print(int x) {
        System.out.println("int: " + x);
    }
    public void print(double x) {
        System.out.println("double: " + x);
    }
}

OverloadDemo demo = new OverloadDemo();
demo.print(5);     // int: 5
demo.print(5.0);   // double: 5.0
demo.print(5L);    // long -> double: double: 5.0

Проблема:
Якщо викликати demo.print(5L), Java обере print(double x) (оскільки long неявно перетворюється на double).
Якщо є методи з параметрами типів Object, Integer та int, виклик із null може спричинити помилку компіляції: «reference to print is ambiguous».

Використання однакових імен методів із різними типами значення, що повертається (помилка компіляції)

У Java не можна оголосити два методи з однаковим імʼям і списком параметрів, які відрізняються лише типом значення, що повертається.

public class Demo {
    // Помилка компіляції!
    public int foo() { return 1; }
    public String foo() { return "hello"; }
}

Пояснення:
Сигнатура методу для перевантаження — це імʼя + параметри. Тип значення, що повертається, не враховується, тож компілятор не зможе зрозуміти, який метод ви хочете викликати.

3. Найкращі практики

Щоб не наступати на граблі зі спадкуванням і перевантаженням, дотримуйтеся цих рекомендацій:

Завжди використовуйте анотацію @Override для перевизначуваних методів

Це не лише підвищує читабельність коду, а й захищає від помилок у сигнатурі. Якщо ви випадково зміните параметри або імʼя методу, компілятор одразу дасть знати.

@Override
public void makeSound() {
    System.out.println("Гав!");
}

Чітко розрізняйте перевантаження і перевизначення

  • Перевизначення (override): змінюєте поведінку методу базового класу — сигнатура має збігатися.
  • Перевантаження (overload): додаєте новий метод із тим самим імʼям, але з іншими параметрами.

Таблиця для наочності:

Перевантаження (overloading) Перевизначення (overriding)
Де В одному класі/ієрархії У підкласі
Імʼя методу Збігається Збігається
Параметри Різні Збігаються
Тип, що повертається Може відрізнятися Має збігатися/бути сумісним
Анотація Не потрібна @Override рекомендовано

Не зловживайте перевантаженням

Якщо метод має надто багато перевантажених варіантів, код стає нечитаємим і заплутаним. Краще використовувати обʼєкти-параметри або патерн Builder, якщо варіантів надто багато.

4. Приклад: система обліку домашніх тварин

Припустімо, ви створюєте просту систему обліку домашніх тварин. У вас є базовий клас Pet і підкласи Cat і Dog.

public class Pet {
    private String name;
    public Pet(String name) {
        this.name = name;
    }
    public void speak() {
        System.out.println(name + " видає незрозумілий звук.");
    }
}

public class Cat extends Pet {
    public Cat(String name) {
        super(name);
    }
    @Override
    public void speak() {
        System.out.println(getName() + " каже: Няв!");
    }
    // Помилка: у Pet немає методу getName()!
}

Типова помилка:
Намагаючись перевизначити метод, ми звертаємося до методу, якого немає в базовому класі. Краще додати геттер:

public class Pet {
    private String name;
    public Pet(String name) { this.name = name; }
    public String getName() { return name; }
    public void speak() { System.out.println(name + " видає незрозумілий звук."); }
}

Тепер усе працює коректно, і ми можемо використовувати поліморфізм:

Pet myPet = new Cat("Барсик");
myPet.speak(); // Барсик каже: Няв!

5. Типові помилки зі спадкуванням і перевантаженням

Помилка № 1: забули викликати super(...).
Якщо в базовому класі є важлива логіка в конструкторі, але ви її не викликаєте, програма може поводитися неочікувано або навіть не скомпілюватися.

Помилка № 2: перевизначили не той метод.
Хотіли змінити поведінку методу базового класу, а насправді додали новий метод із подібним імʼям або іншими параметрами. Підсумок: старий метод продовжує працювати як раніше, а ваш новий — ніхто не викликає.

Помилка № 3: спробували перевизначити final-метод.
Java цього не дозволить — і це добре. Але якщо ви бачите помилку компіляції — шукайте final.

Помилка № 4: перевантажили метод до невпізнаваності.
Коли у вас десять варіантів методу calculate і ви самі плутаєтеся, який із них викликається, — час зупинитися й подумати про рефакторинг.

Помилка № 5: порушили принцип Лісков.
Якщо ваш підклас змінює сенс поведінки базового класу, уся архітектура може «поїхати». Наприклад, якщо є клас Shape із методом getArea(), а підклас BrokenShape повертає -1, це може призвести до дивних помилок.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ