CodeGym /Kursy /JAVA 25 SELF /Nadpisywanie metod (overriding): różnice względem przecią...

Nadpisywanie metod (overriding): różnice względem przeciążania

JAVA 25 SELF
Poziom 18, Lekcja 2
Dostępny

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!

1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 2
Niedostępne
Głosy zoo: Nadpisanie metody w klasie pochodnej
Głosy zoo: Nadpisanie metody w klasie pochodnej
1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 2
Niedostępne
Magia drukowania: różnica między nadpisaniem a przeciążeniem
Magia drukowania: różnica między nadpisaniem a przeciążeniem
1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 2
Niedostępne
Sztuka rysowania: Użycie adnotacji @Override
Sztuka rysowania: Użycie adnotacji @Override
1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 2
Niedostępne
Muzyczne przedstawienie zwierzaka: Wywołanie metody nadrzędnej za pomocą super
Muzyczne przedstawienie zwierzaka: Wywołanie metody nadrzędnej za pomocą super
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION