CodeGym /Kursy /JAVA 25 SELF /Błędy związane z dziedziczeniem i przeciążaniem metod

Błędy związane z dziedziczeniem i przeciążaniem metod

JAVA 25 SELF
Poziom 23 , Lekcja 1
Dostępny

1. Błędy przy dziedziczeniu

Dziedziczenie – jedna z podstaw OOP, ale też temat, na którym początkujący programiści najczęściej się potykają. Przyjrzyjmy się klasycznym błędom i nauczmy się ich unikać.

Brak wywołania konstruktora klasy bazowej (super(...))

Gdy tworzysz podklasę, pamiętaj: klasa bazowa może wymagać określonej inicjalizacji przez konstruktor. Jeśli w klasie bazowej nie ma konstruktora domyślnego (bez parametrów), to w konstruktorze klasy potomnej koniecznie trzeba jawnie wywołać konstruktor klasy bazowej za pomocą super(...).

Przykład błędu:

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

class Dog extends Animal {
    // Błąd! Brak konstruktora domyślnego w klasie Animal
    public Dog() {
        // super(); // kompilator wstawia super() automatycznie, ale takiego konstruktora nie ma!
    }
}

Jak naprawić:

class Dog extends Animal {
    public Dog(String name) {
        super(name); // Wszystko w porządku!
    }
}

Komentarz:
Jeśli w klasie bazowej istnieje tylko konstruktor z parametrami, kompilator nie doda automatycznie konstruktora bez parametrów. To częsta przyczyna błędów kompilacji.

Próba dziedziczenia po final-klasie lub nadpisania metody final

W Javie można oznaczyć klasę lub metodę słowem kluczowym final. Oznacza to:

  • Nie można dziedziczyć po takiej klasie.
  • Nie można nadpisywać (override) takiej metody w klasach potomnych.

Przykład błędu:

final class Cat {}

// Błąd kompilacji!
class Tiger extends Cat { 
    // ...
}
class Animal {
    public final void sleep() {
        System.out.println("Zzz...");
    }
}

class Dog extends Animal {
    // Błąd kompilacji!
    @Override
    public void sleep() {
        System.out.println("Dog is sleeping...");
    }
}

Komentarz:
Jeśli widzisz błąd "cannot inherit from final", "cannot override final method" – sprawdź modyfikatory!

Naruszenie zasady podstawienia Liskov (Liskov Substitution Principle)

Brzmi groźnie, ale w praktyce oznacza: obiekt podklasy powinien zachowywać się jak obiekt klasy bazowej, nie psując logiki programu. Częsty błąd – nadpisywać metody tak, że nowa klasa zachowuje się niezgodnie z oczekiwaniami wobec klasy bazowej.

Przykład:

class Bird {
    public void fly() {
        System.out.println("Lecę!");
    }
}

class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Pingwiny nie latają!");
    }
}

Na czym polega problem?
Kod operujący na Bird zakłada, że każdy ptak potrafi latać. Jeśli przekażesz mu Penguin, program może się wyłożyć.

Jak lepiej:
W takich przypadkach warto przeprojektować hierarchię albo użyć interfejsów/kompozycji.

2. Błędy związane z przeciążaniem metod (overloading)

Przeciążanie to sytuacja, gdy w jednej klasie istnieje kilka metod o tej samej nazwie, lecz innym zestawie parametrów. Wydaje się proste, ale i tu można wpaść w pułapkę.

Przeciążenie zamiast nadpisania (błąd w sygnaturze)

Początkujący często chcą nadpisać (override) metodę klasy bazowej, ale przypadkowo zmieniają jej parametry. W efekcie powstaje przeciążenie, a nie nadpisanie – i polimorfizm nie zadziała!

Przykład błędu:

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

class Dog extends Animal {
    // Chcieliśmy nadpisać, a wyszło przeciążenie!
    public void makeSound(String extra) {
        System.out.println("Bark! " + extra);
    }
}

Problem:
Wywołanie dog.makeSound() uruchomi metodę z klasy bazowej, a nie tę nową.
Wywołanie dog.makeSound("loudly") trafi do wersji przeciążonej, ale polimorfizm nie zadziała!

Dobra praktyka:
Używaj adnotacji @Override – jeśli pomylisz się w sygnaturze, kompilator od razu to zgłosi.

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

Nieoczywiste zachowanie przy przeciążaniu (automatyczna konwersja typów, niejednoznaczność wywołania)

Java czasem może „wybrać” nie ten wariant metody, którego się spodziewasz, jeśli argumenty pasują do kilku przeciążeń.

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

Problem:
Jeśli wywołasz demo.print(5L), Java wybierze print(double x) (ponieważ long lepiej konwertuje się do double niż do int).
Jeśli istnieją metody z parametrami typu Object, Integer, int, wywołanie z null może skończyć się błędem kompilacji: „reference to print is ambiguous”.

Użycie tej samej nazwy metody z różnym typem zwracanym (błąd kompilacji)

W Javie nie wolno deklarować dwóch metod o tej samej nazwie i tej samej liście parametrów, które różnią się tylko typem zwracanym!

public class Demo {
    // Błąd kompilacji!
    public int foo() { return 1; }
    public String foo() { return "hello"; }
}

Wyjaśnienie:
Sygnatura metody dla przeciążania to nazwa + parametry. Typ zwracany nie jest brany pod uwagę. Kompilator nie będzie w stanie ustalić, którą metodę chcesz wywołać.

3. Najlepsze praktyki

Aby uniknąć pułapek związanych z dziedziczeniem i przeciążaniem, stosuj poniższe zalecenia:

Zawsze używaj adnotacji @Override dla metod nadpisywanych

To nie tylko poprawia czytelność, ale też chroni przed błędami w sygnaturze. Jeśli przypadkowo zmienisz parametry lub nazwę metody, kompilator natychmiast to zgłosi.

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

Wyraźnie odróżniaj przeciążanie od nadpisywania

  • Nadpisywanie (override): zmieniasz zachowanie metody z klasy bazowej – sygnatura musi być identyczna.
  • Przeciążanie (overload): dodajesz nową metodę o tej samej nazwie, ale z innym zestawem parametrów.

Tabela poglądowa:

Przeciążanie (overloading) Nadpisywanie (overriding)
Gdzie W tej samej klasie/hierarchii W podklasie
Nazwa metody Takie samo Takie samo
Parametry Różne Takie same
Typ zwracany Może się różnić Musi być identyczny/kompatybilny
Adnotacja Nie jest wymagana @Override zalecana

Nie nadużywaj przeciążania

Jeśli metoda ma zbyt wiele wariantów przeciążenia, kod staje się nieczytelny i mylący. Lepiej użyć obiektu parametrów lub wzorca Builder, jeśli wariantów jest zbyt dużo.

4. Przykład: system ewidencji zwierząt domowych

Załóżmy, że tworzysz prosty system ewidencji zwierząt domowych. Masz klasę bazową Pet oraz klasy potomne Cat i Dog.

public class Pet {
    private String name;
    public Pet(String name) {
        this.name = name;
    }
    public void speak() {
        System.out.println(name + " wydaje niezrozumiały dźwięk.");
    }
}

public class Cat extends Pet {
    public Cat(String name) {
        super(name);
    }
    @Override
    public void speak() {
        System.out.println(getName() + " mówi: Miau!");
    }
    // Błąd: w Pet nie ma metody getName()!
}

Typowy błąd:
Próbując nadpisać metodę, odwołujemy się do metody, której nie ma w klasie bazowej. Lepiej dodać getter:

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 + " wydaje niezrozumiały dźwięk."); }
}

Teraz wszystko działa poprawnie i możemy użyć polimorfizmu:

Pet myPet = new Cat("Barsik");
myPet.speak(); // Barsik mówi: Miau!

5. Typowe błędy dotyczące dziedziczenia i przeciążania

Błąd nr 1: zapomniano wywołać super(...).
Jeśli w klasie bazowej jest ważna logika w konstruktorze, ale jej nie wywołujesz, program może zachowywać się nieprzewidywalnie, a nawet się nie skompilować.

Błąd nr 2: nadpisano nie tę metodę.
Chciałeś zmienić zachowanie metody z klasy bazowej, a w rzeczywistości dodałeś nową metodę o podobnej nazwie lub z innymi parametrami. Efekt: stara metoda działa po staremu, a twojej nowej nikt nie wywołuje.

Błąd nr 3: próba nadpisania metody final.
Java ci na to nie pozwoli – i dobrze! Jeśli jednak widzisz błąd kompilacji, poszukaj final.

Błąd nr 4: przeciążono metodę do niepoznaki.
Gdy masz 10 wariantów metody calculate i sam już nie wiesz, która się wywołuje – czas się zatrzymać i pomyśleć o refaktoryzacji.

Błąd nr 5: naruszono zasadę Liskov.
Jeśli twoja podklasa zmienia sens zachowania klasy bazowej, cała architektura może się „rozjechać”. Na przykład jeśli masz klasę Shape z metodą getArea(), a podklasa BrokenShape zwraca -1, może to prowadzić do dziwnych błędów.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION