CodeGym /Kursy /JAVA 25 SELF /Pojęcie polimorfizmu: po co jest potrzebny

Pojęcie polimorfizmu: po co jest potrzebny

JAVA 25 SELF
Poziom 18 , Lekcja 0
Dostępny

1. Wprowadzenie

Polimorfizm – to jedna z trzech filarowych koncepcji programowania obiektowego (obok dziedziczenia i hermetyzacji). Dosłownie pochodzi z greki: „poly” (wiele) i „morph” (forma). W programowaniu oznacza: jeden interfejs – wiele implementacji.

Definicja

Polimorfizm to zdolność obiektów różnych klas do różnej reakcji na te same komunikaty (wywołania metod).

To znaczy: jeśli masz metodę makeSound(), możesz ją wywołać dla dowolnego zwierzęcia, ale kot zamiauczy, pies zaszczeka, a krowa zamuczy. Dla programisty to po prostu wywołanie animal.makeSound(), a to, co faktycznie się stanie, zależy od tego, jaki obiekt kryje się pod tą zmienną.

Analogia z życia

Wyobraź sobie, że masz w domu pilot do telewizora i tym samym pilotem możesz sterować głośnikami, projektorem, a nawet ekspresem do kawy. Naciskasz przycisk „Włącz” – turnOn(), a każde urządzenie reaguje po swojemu. Najważniejsze – wszystkie mają „przycisk” włączania, ale implementacja jest różna.

Przykład w Javie

class Animal {
    void makeSound() {
        System.out.println("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!");
    }
}

class Cow extends Animal {
    @Override
    void makeSound() {
        System.out.println("Muuu!");
    }
}

Teraz możemy zrobić tak:

Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();

animal1.makeSound(); // Hau!
animal2.makeSound(); // Miau!
animal3.makeSound(); // Muuu!

Zwróć uwagę: wszystkie zmienne mają typ Animal, ale wynik wywołania zależy od rzeczywistego typu obiektu.

2. Rodzaje polimorfizmu

W Javie (i w większości języków OOP) wyróżnia się dwa podstawowe rodzaje polimorfizmu:

Polimorfizm kompilacyjny (statyczny) – przeciążanie metod (overloading)

To sytuacja, gdy w jednej klasie istnieje kilka metod o tej samej nazwie, lecz z różnymi parametrami. Kompilator wybiera, którą metodę wywołać, na podstawie przekazanych argumentów.

Przykład (wyprzedzając – więcej w następnym wykładzie):

class Printer {
    void print(int x) {
        System.out.println("Liczba: " + x);
    }

    void print(String s) {
        System.out.println("Napis: " + s);
    }
}

Polimorfizm wykonaniowy (dynamiczny) – nadpisywanie metod (overriding)

To gdy metoda jest zdefiniowana w klasie bazowej, a następnie nadpisana w klasach pochodnych. Która metoda zostanie wywołana, rozstrzygane jest w czasie działania (runtime), w zależności od rzeczywistego typu obiektu.

Przykład – patrz wyżej ze zwierzętami.

3. Po co jest potrzebny polimorfizm?

Polimorfizm to nie tylko ładne słowo na rozmowę kwalifikacyjną. To narzędzie, które czyni twój kod elastycznym, rozszerzalnym i wygodnym w utrzymaniu.

Uniwersalność kodu

Możesz pisać kod, który pracuje z obiektami typu bazowego, nie martwiąc się o szczegóły ich implementacji. Na przykład, jeśli masz listę zwierząt, możesz po niej przejść i wywołać u każdego makeSound(), nie zastanawiając się, czy to kot, czy pies.

Animal[] animals = { new Dog(), new Cat(), new Cow() };

for (Animal animal : animals) {
    animal.makeSound(); // Za każdym razem wywoła się właściwa metoda
}

Łatwość rozszerzania

Jeśli jutro szef powie: „Dodajmy papugę!”, po prostu piszesz nową klasę Parrot extends Animal i dodajesz ją do tablicy. Cała reszta kodu pozostaje bez zmian. To – otwartość na rozszerzenia i zamknięcie na modyfikacje (zasada OCP z SOLID).

Uproszczenie architektury

Możesz budować złożone systemy, w których poszczególne części współdziałają przez abstrakcje (klasy bazowe lub interfejsy), nie martwiąc się o konkretne implementacje. To oszczędza czas, nerwy i kawę.

4. Kluczowe pojęcia: typ referencyjny i rzeczywisty

Typ referencyjny zmiennej
Kiedy piszesz Animal animal = new Dog();, zmienna animal ma typ referencyjny Animal, czyli kompilator „myśli”, że to zwierzę, i pozwala tylko na te metody, które są zadeklarowane w klasie Animal.

Rzeczywisty (faktyczny) typ obiektu
W pamięci faktycznie znajduje się obiekt typu Dog. To on decyduje, która metoda zostanie wywołana przy odwołaniu do makeSound().

Ilustracja

Animal animal = new Dog();
animal.makeSound(); // Wywoła Dog.makeSound(), a nie Animal.makeSound()

Ważne! Przez referencję typu bazowego (Animal) nie wywołasz metod, które istnieją tylko w Dog, jeśli nie są zadeklarowane w klasie bazowej.

Późne (dynamiczne) wiązanie
To „magia”, która dzieje się w czasie wykonywania: kiedy wywołujesz metodę przez referencję typu bazowego, JVM patrzy na rzeczywisty typ obiektu i wywołuje „właściwą” implementację. To właśnie polimorfizm w działaniu.

5. Przykład praktyczny: polimorfizm w aplikacji

Kontynuujmy rozwój naszej aplikacji szkoleniowej. Załóżmy, że piszemy prostą symulację zoo. Mamy klasę bazową Animal i kilku jej potomków. Chcemy, aby wszystkie zwierzęta potrafiły „wydawać dźwięk”, ale nie chcemy za każdym razem pisać osobnego kodu dla każdego typu zwierzęcia.

Krok 1: Klasa bazowa i potomkowie

class Animal {
    void makeSound() {
        System.out.println("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!");
    }
}

Krok 2: Tablica zwierząt

Animal[] zoo = { new Dog(), new Cat(), new Animal() };

Krok 3: Iteracja i wywołanie metody

for (Animal animal : zoo) {
    animal.makeSound();
}

Wynik działania:

Hau!
Miau!
Jakiś dźwięk...

Zauważ: nie wiemy z góry, kto dokładnie znajduje się w tablicy – ale program sam rozstrzyga i wywołuje właściwą metodę.

6. Schematyczne przedstawienie polimorfizmu


.        Animal (makeSound)
           /        \
        Dog        Cat
     (makeSound) (makeSound)

Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()

Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()

7. Jeszcze jeden przykład: polimorfizm w realnym zadaniu

Załóżmy, że piszesz program do zarządzania pracownikami firmy. Masz klasę bazową Employee i dwie klasy pochodne: Manager i Developer. Wszyscy pracownicy potrafią pracować – work() – ale robią to na różne sposoby.

class Employee {
    void work() {
        System.out.println("Pracownik pracuje.");
    }
}

class Manager extends Employee {
    @Override
    void work() {
        System.out.println("Menedżer prowadzi spotkanie.");
    }
}

class Developer extends Employee {
    @Override
    void work() {
        System.out.println("Programista pisze kod.");
    }
}

Teraz możesz zrobić tak:

Employee[] staff = { new Manager(), new Developer(), new Employee() };

for (Employee emp : staff) {
    emp.work();
}

Wynik:

Menedżer prowadzi spotkanie.
Programista pisze kod.
Pracownik pracuje.

8. Kiedy polimorfizm NIE działa

Polimorfizm działa tylko dla metod zadeklarowanych w klasie bazowej. Jeśli w podklasie jest jej unikalna metoda, przez referencję typu bazowego jej nie zobaczysz.

class Dog extends Animal {
    void fetchStick() {
        System.out.println("Pies przynosi kij!");
    }
}

Animal animal = new Dog();
// animal.fetchStick(); // Błąd kompilacji! Taka metoda nie jest widoczna przez Animal

Aby wywołać metodę specyficzną dla klasy, trzeba rzutować zmienną na odpowiedni typ:

if (animal instanceof Dog) {
    ((Dog) animal).fetchStick();
}

Ale to już inna historia – najważniejsze: przez polimorfizm dostępne są tylko metody zadeklarowane w klasie bazowej.

9. Typowe błędy przy pracy z polimorfizmem

Błąd nr 1: Oczekiwanie, że przez referencję typu bazowego będą dostępne wszystkie metody podklasy. W rzeczywistości dostępne są tylko te, które zadeklarowano w klasie bazowej.

Błąd nr 2: Nieużywanie adnotacji @Override przy nadpisywaniu metody. Bez niej można przypadkowo napisać metodę z niepoprawną sygnaturą i wtedy polimorfizm nie zadziała (metoda z klasy bazowej nie zostanie nadpisana).

Błąd nr 3: Próba wywołania metody specyficznej dla podklasy bez rzutowania typu. Kompilator na to nie pozwoli, bo nie wie, co dokładnie kryje się pod referencją typu bazowego.

Błąd nr 4: Pomylenie przeciążania (overloading) z nadpisywaniem (overriding). Przeciążanie to kilka metod o tej samej nazwie i różnych parametrach w jednej klasie. Nadpisywanie to zmiana zachowania metody w podklasie.

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