1. Nadpisywanie metody
W życiu często zdarzają się sytuacje, gdy „potomek” zachowuje się w szczególny sposób. Na przykład wszystkie zwierzęta potrafią wydawać dźwięki, ale kot — „miau”, pies — „hau”, a programista — „ojej, znowu bug!”. W programowaniu realizuje się to poprzez nadpisywanie metody (override).
Nadpisywanie metody to sytuacja, gdy podklasa zapewnia własną implementację metody, która została już zadeklarowana w klasie rodzica. Czyli „podmienia” standardowe zachowanie na swoje, bardziej specyficzne.
Analogia. Jeśli wyobrazić sobie klasę nadrzędną jako firmowy przepis na barszcz, to nadpisanie metody to moment, gdy babcia dodaje do niego swój sekretny składnik. Barszcz pozostaje barszczem, ale smak u każdego jest inny.
Aby nadpisać metodę, należy w klasie potomnej zadeklarować metodę z dokładnie taką samą sygnaturą (nazwa, parametry, typ zwracany) jak u rodzica.
Przykład: zwierzęta i ich dźwięki
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
// Nadpisujemy metodę makeSound()
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
// Nadpisujemy metodę makeSound()
void makeSound() {
System.out.println("Meow!");
}
}
Teraz, jeśli utworzysz obiekt Dog i wywołasz makeSound(), usłyszysz "Woof!", a nie "Some generic animal sound".
Demonstracja w kodzie
public class Main {
public static void main(String[] args) {
Animal generic = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
}
}
Ważne: jeśli podklasa nie ma metody o takiej samej sygnaturze, zostanie użyta metoda rodzica.
2. Adnotacja @Override: po co jest i jak jej używać
W Javie przyjęło się oznaczać nadpisane metody specjalną adnotacją @Override. To nie tylko ozdoba kodu, ale przydatne narzędzie:
- Kompilator sprawdza, czy rzeczywiście nadpisujesz metodę rodzica. Jeśli pomylisz się w nazwie, typie parametru lub typie zwracanym — kompilator zgłosi błąd.
- Zwiększa czytelność kodu. Inny programista od razu widzi: „O, ta metoda nadpisuje metodę rodzica”.
Przykład z @Override
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
Jeśli przypadkowo napiszesz void makeSond() (literówka!) w metodzie oznaczonej @Override, kompilator zgłosi błąd: "Method does not override or implement a method from a supertype".
Współczesne standardy. Używanie @Override to dobry styl i standard branżowy. Nawet jeśli kompilator tego nie wymaga, zawsze dodawaj tę adnotację — to ułatwia życie Tobie i kolegom.
3. Jak działa wywołanie nadpisanej metody
Gdy wywołujesz metodę na obiekcie podklasy, zostanie użyta implementacja z podklasy, nawet jeśli zmienna jest zadeklarowana jako typ rodzica.
Przykład: polimorfizm w praktyce
Animal animal = new Dog();
animal.makeSound(); // "Woof!", a nie "Some generic animal sound"
Tutaj zmienna jest typu Animal, ale faktycznie przechowuje obiekt Dog. Java „rozumie”, że należy wywołać nadpisaną metodę z Dog. To właśnie polimorfizm (więcej w kolejnych lekcjach).
4. Ograniczenia i zasady nadpisywania
Sygnatura metody
- Nazwa, typ i kolejność parametrów muszą odpowiadać metodzie w klasie rodzica.
- Typ zwracany musi być taki sam lub kowariantny (podtyp typu zwracanego przez rodzica). Na przykład, jeśli rodzic zwraca Animal, a potomny — Dog, jest to dozwolone.
Modyfikatory dostępu
- Nie można uczynić dostępu bardziej restrykcyjnym niż u rodzica.
- Jeśli metoda rodzica jest public, to nadpisana również musi być public.
- Jeśli metoda rodzica jest protected, to nadpisana może być protected lub public.
Jeśli spróbujesz zrobić odwrotnie, kompilator powie: "Cannot reduce the visibility of the inherited method".
Wyjątki
- Nadpisana metoda nie może zgłaszać nowego wyjątku kontrolowanego (checked), którego nie ma w deklaracji rodzica.
- Można zgłaszać mniej wyjątków niż rodzic lub ich podtypy.
static, final, private
- Nie można nadpisać metod zadeklarowanych jako static lub final, a także prywatnych (private).
- static — to ukrywanie (hiding), a nie nadpisywanie.
- final — nie można nadpisać; Java chroni takie metody.
- private — nie jest widoczna w potomku, nie da się nadpisać (można jedynie zadeklarować nową metodę o tej samej nazwie).
Konstruktory
Konstruktory nie są dziedziczone i nie podlegają nadpisywaniu. Każda klasa ma własne konstruktory.
5. Rozwijamy aplikację edukacyjną „Zoo”
Czas zastosować teorię w praktyce! Kontynuujmy rozwijanie naszej „zoo” aplikacji.
Krok 1. Klasa bazowa Animal
public class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
public void sleep() {
System.out.println("Zzz...");
}
}
Krok 2. Podklasy Dog i Cat
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
// Dodatkowa metoda tylko dla Dog
public void fetch() {
System.out.println("Dog brings the stick!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
// Dodatkowa metoda tylko dla Cat
public void scratch() {
System.out.println("Cat scratches the sofa!");
}
}
Krok 3. Używamy nadpisywania
public class ZooTest {
public static void main(String[] args) {
Animal generic = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
// dog.fetch(); // Błąd! Zmienna typu Animal nie zna metody fetch()
// cat.scratch(); // Analogicznie
// Ale jeśli jawnie wskażesz typ:
if (dog instanceof Dog) {
((Dog) dog).fetch(); // Dog brings the stick!
}
if (cat instanceof Cat) {
((Cat) cat).scratch(); // Cat scratches the sofa!
}
}
}
Komentarz:
Metoda makeSound() działa polimorficznie — wywoływana jest wersja z rzeczywistej klasy obiektu. Natomiast metody specyficzne (fetch, scratch) są dostępne tylko przez jawne rzutowanie typu — to ważne dla zrozumienia, jak działają dziedziczenie i nadpisywanie.
6. Przykład z typem zwracanym (kowariancja)
Czasem chcemy, aby nadpisana metoda zwracała „węższy” typ. Na przykład:
class Animal {
Animal getFriend() {
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog getFriend() { // Typ zwracany — Dog, podtyp Animal
return new Dog();
}
}
To nazywa się kowariancją typu zwracanego i jest dozwolone w Javie (od Java 5).
7. Co się stanie, jeśli nie używać @Override?
Jeśli przypadkowo popełnisz błąd w nazwie metody lub parametrach, Java nie zgłosi problemu, o ile nie ma adnotacji @Override. W rezultacie nie nadpiszesz metody, tylko utworzysz nową, a oczekiwane zachowanie się nie zmieni.
Przykład błędu
class Dog extends Animal {
// Literówka: makeSoud zamiast makeSound
void makeSoud() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.makeSound(); // Wypisze "Some generic animal sound"
}
}
Gdyby było @Override, kompilator zgłosiłby błąd: "Method does not override or implement a method from a supertype".
8. Typowe błędy przy nadpisywaniu metod
Błąd nr 1: brak adnotacji @Override.
Bez niej łatwo o pomyłkę w nazwie metody lub parametrach. W efekcie metoda nie zostanie nadpisana, a program zachowa się inaczej, niż oczekujesz.
Błąd nr 2: próba zawężenia modyfikatora dostępu.
Jeśli metoda rodzica jest public, a Ty napiszesz protected lub private — dostaniesz błąd kompilacji.
Błąd nr 3: niezgodność sygnatury.
Jeśli parametry różnią się choćby typem — to już nie nadpisywanie, lecz przeciążenie (overloading).
Błąd nr 4: próba nadpisania metody final lub static.
Java na to nie pozwoli: final chroni przed nadpisaniem, a metody static w ogóle nie są nadpisywane (jedynie ukrywane).
Błąd nr 5: zmiana typu zwracanego na niekompatybilny.
Można zwracać tylko podtyp typu zwracanego przez rodzica (kowariancja), a nie zupełnie inny typ.
GO TO FULL VERSION