1. Nadpisywanie metod
Nadpisywanie (overriding) — to możliwość w podklasie napisania własnej wersji metody, która już istnieje w klasie bazowej. Dzięki temu działa prawdziwy polimorfizm w czasie wykonywania programu (run-time).
Mówiąc prościej:
Jeśli masz klasę bazową z metodą i chcesz, aby podklasa wykonywała tę metodę po swojemu, po prostu deklarujesz w podklasie metodę o tej samej sygnaturze. Przy wywołaniu metody przez referencję typu bazowego zostanie wywołana wersja metody z rzeczywistego (faktycznego) typu obiektu.
Przykład z życia.
Wyobraź sobie, że masz drużynę zwierząt i prosisz każde, by „wydało dźwięk”. Dla wszystkich to komenda makeSound(), ale pies szczeka, kot miauczy, a krowa muczy. W kodzie wygląda to jak wywołanie tej samej metody, lecz rezultat jest różny — oto magia nadpisywania!
Składnia nadpisywania
Podstawowy przykład
class Animal {
void makeSound() {
System.out.println("Zwierzę wydaje jakiś dźwięk");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Hau!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miau!");
}
}
Tutaj w klasie Animal zdefiniowano metodę makeSound(). Podklasy Dog i Cat nadpisują tę metodę, dostarczając własną implementację.
Adnotacja @Override
W Javie zaleca się (to dobra praktyka) oznaczać nadpisywane metody adnotacją @Override:
@Override
void makeSound() { ... }
Nie jest to konieczne do działania kodu, ale:
- Kompilator sprawdzi, czy rzeczywiście nadpisujesz metodę (a nie przypadkowo zrobiłeś literówkę w nazwie lub parametrach).
- Poprawia czytelność kodu — inni programiści od razu widzą, że ta metoda nadpisuje metodę klasy bazowej.
Wywołanie nadpisanej metody
Animal myDog = new Dog();
myDog.makeSound(); // Wypisze: Hau!
Chociaż zmienna jest typu Animal, w rzeczywistości wskazuje na obiekt Dog, i wywoływana jest metoda z klasy Dog. To właśnie dynamiczne (późne) wiązanie.
2. Różnica między nadpisywaniem (overriding) a przeciążaniem (overloading)
Przeciążanie (overloading)
- W jednej klasie (albo w hierarchii, ale zawsze „blisko”).
- Metody mają tę samą nazwę, ale różne parametry (typ, liczba, kolejność).
- Wybór metody następuje podczas kompilacji.
void print(int x) { ... }
void print(String s) { ... }
Nadpisywanie (overriding)
- W różnych klasach: metoda zadeklarowana w nadklasie i nadpisana w podklasie.
- Metody mają tę samą nazwę i sygnaturę (parametry i typ zwracany).
- Wybór metody następuje podczas wykonywania (run-time), na podstawie rzeczywistego typu obiektu.
Tabela porównawcza
| Przeciążanie (overloading) | Nadpisywanie (overriding) | |
|---|---|---|
| Gdzie | W jednej klasie | W nadklasie i podklasie |
| Nazwa metody | Takie samo | Takie samo |
| Parametry | Różne | Identyczne |
| Typ zwracany | Może się różnić | Musi być identyczny lub być podtypem |
| Kiedy wybierany | Kompilacja | Wykonanie (run-time) |
| Adnotacja | Nie jest wymagana | @Override (zalecane) |
3. Zasady nadpisywania metod
Nadpisywanie to potężny mechanizm, ale wiążą się z nim ścisłe zasady. Przyjrzyjmy się im po kolei.
Sygnatura metody musi się zgadzać
- Nazwa metody, typy i kolejność parametrów muszą być identyczne jak w metodzie w nadklasie.
- Typ zwracany musi być zgodny lub kowariantny (czyli podtypem typu zwracanego przez metodę klasy bazowej).
Przykład z kowariantnym typem zwracanym:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK! Cat — podtyp Animal
}
Modyfikator dostępu
Modyfikator dostępu nadpisanej metody nie może być bardziej restrykcyjny niż w metodzie w nadklasie. Jeśli metoda w klasie bazowej jest public, to w podklasie musi pozostać public. Nie można uczynić jej mniej dostępną (protected lub private).
Przykład:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // Błąd! Domyślny modyfikator — package-private, mniej dostępny niż public
@Override
public void greet() { } // OK
}
Wyjątki
- Metoda nadpisana nie może zgłaszać nowych wyjątków checked, które nie są zadeklarowane w metodzie bazowej.
- Można zgłaszać mniej wyjątków albo te same.
Przykład:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK, FileNotFoundException — podtyp IOException
// void doWork() throws SQLException { } // Błąd! SQLException nie jest zadeklarowany w klasie bazowej
}
Metody statyczne nie są nadpisywane
Metody statyczne mogą być ukryte (hidden), ale nie nadpisane. Jeśli zadeklarujesz metodę statyczną o tej samej sygnaturze w podklasie, to nie jest nadpisywanie! To będzie tylko ukrycie metody, a nie polimorfizm.
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
Wywołanie Dog.info() wypisze "Dog", ale jeśli wywołać przez zmienną typu Animal, zostanie wywołana metoda Animal.info(). To nie jest polimorfizm!
final — metod oznaczonych tak nie można nadpisywać
Jeśli metoda w nadklasie jest zadeklarowana jako final, próba jej nadpisania spowoduje błąd kompilacji.
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // Błąd! Nie można nadpisywać metody final
}
4. Praktyczne przykłady
Przyjrzyjmy się w praktyce, jak działa nadpisywanie i czym różni się od przeciążania.
Przykład 1: Klasa Shape i jej potomkowie
class Shape {
void draw() {
System.out.println("Rysujemy figurę");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Rysujemy koło");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Rysujemy prostokąt");
}
}
Użyjmy polimorfizmu:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // Rysujemy koło
s2.draw(); // Rysujemy prostokąt
}
}
Chociaż zmienne zadeklarowano jako Shape, wywoływana jest metoda dokładnie tej klasy, do której faktycznie należy obiekt.
Przykład 2: Różnica względem przeciążania
class Printer {
void print(String s) {
System.out.println("Tekst: " + s);
}
void print(int n) {
System.out.println("Liczba: " + n);
}
}
Tutaj obie metody nazywają się print, ale mają różne parametry — to przeciążanie, a nie nadpisywanie.
5. Nadpisywanie i wywołanie metody klasy bazowej (super)
Czasem w nadpisanej metodzie chcesz najpierw wykonać logikę klasy bazowej, a potem dodać własną. Do tego służy słowo kluczowe super.
class Animal {
void makeSound() {
System.out.println("Zwierzę wydaje dźwięk");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // Wywołanie metody klasy bazowej
System.out.println("Hau!");
}
}
Wywołanie new Dog().makeSound() wypisze:
Zwierzę wydaje dźwięk
Hau!
Jak działa dynamiczne wiązanie (late binding)
Gdy wywołujesz metodę przez referencję typu bazowego, Java w czasie wykonywania sprawdza, do jakiego rzeczywistego obiektu odnosi się ta referencja, i wywołuje właśnie tę wersję metody, która jest zdefiniowana w klasie tego obiektu.
Animal a = new Cat();
a.makeSound(); // Wywoła Cat.makeSound(), a nie Animal.makeSound()
To jest podstawa polimorfizmu w Javie.
6. Jak to się ma do twojej aplikacji
W naszej aplikacji szkoleniowej (np. w systemie ewidencji pracowników) możesz utworzyć klasę bazową Employee z metodą work(), a podklasy Manager i Developer mogą zaimplementować tę metodę po swojemu:
class Employee {
void work() {
System.out.println("Pracownik pracuje");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("Menedżer zarządza");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("Programista pisze kod");
}
}
Teraz możesz przechowywać wszystkich pracowników w jednej tablicy lub liście:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // Dla każdego — własny wynik!
}
7. Typowe błędy przy nadpisywaniu metod
Błąd nr 1: literówka w nazwie metody lub parametrach. Jeśli przypadkowo pomylisz się w nazwie metody lub jej parametrach, nie nadpisujesz metody, lecz tworzysz nową. W rezultacie polimorfizm nie zadziała. Dlatego zawsze używaj adnotacji @Override — kompilator od razu cię poprawi.
Błąd nr 2: bardziej restrykcyjny modyfikator dostępu. Jeśli w klasie bazowej metoda jest public, a w klasie potomnej zadeklarujesz ją jako protected lub bez modyfikatora — otrzymasz błąd kompilacji.
Błąd nr 3: próba nadpisania metody static lub final. Metody statyczne nie są nadpisywane, a metod final w ogóle nie da się nadpisać. Jeśli spróbujesz — kompilator cię zatrzyma.
Błąd nr 4: zmiana typu zwracanego na niezgodny. Jeśli typ zwracany metody w podklasie nie zgadza się z typem w nadklasie (i nie jest jego podtypem), kompilator nie pozwoli nadpisać metody.
Błąd nr 5: dodanie nowych wyjątków checked. Metoda nadpisana nie może zgłaszać nowych wyjątków checked, których nie ma w deklaracji metody bazowej. Jeśli to zrobisz — kompilator zgłosi błąd.
Błąd nr 6: zapomnienie o super. Jeśli w nadpisanej metodzie chcesz zachować część zachowania klasy bazowej, nie zapomnij jawnie wywołać super.methodName(). Java sama tego nie zrobi.
Teraz wiesz, jak działa nadpisywanie metod, czym różni się ono od przeciążania i jak dzięki niemu realizowany jest polimorfizm w Javie. W następnej lekcji omówimy, jak stosować polimorfizm w praktyce — z kolekcjami, tablicami i prawdziwymi zadaniami!
GO TO FULL VERSION