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.
GO TO FULL VERSION