CodeGym /Kursy /JAVA 25 SELF /Zastosowanie polimorfizmu w praktyce

Zastosowanie polimorfizmu w praktyce

JAVA 25 SELF
Poziom 18, Lekcja 3
Dostępny

1. Polimorfizm w kolekcjach: po co jest potrzebny?

Zacznijmy od pytania: „Po co w ogóle polimorfizm w rzeczywistych programach?”

Wyobraź sobie zoo. Masz klasę bazową Animal oraz mnóstwo klas potomnych: Dog, Cat, Cow, Parrot, a nawet Platypus (dziobak, dla miłośników egzotyki). Każdy z nich potrafi wydawać dźwięk (makeSound()), ale robi to na swój sposób.

Zamiast tworzyć osobne tablice dla każdego zwierzęcia, deklarujesz tablicę lub listę typu Animal i wkładasz tam kogokolwiek:

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

Teraz możesz przejść po tej tablicy i wywołać makeSound() dla każdego:

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

Magia! Każdy obiekt sam „wie”, jaki dźwięk wydać, i nie musisz pisać mnóstwa if-ów ani switch-y.

Przykład przez analogię

To tak, jakbyś wydał komendę „Głos!” grupie zwierząt i każde samo zdecydowało, co zrobić: pies zaszczeka, kot zamiauczy, a krowa — zamuczy. Nie doprecyzowujesz, kto jest kim — po prostu wywołujesz tę samą metodę.

2. Przykład praktyczny: hierarchia pracowników

Zróbmy przykład bliższy życiu (i przyszłej pracy w IT). Załóżmy, że mamy firmę z różnymi pracownikami: menedżerami, programistami, testerami. Każdy ma metodę work(), ale jest ona wykonywana inaczej.

Deklaracja klasy bazowej

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

Podklasy

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

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

public class Tester extends Employee {
    @Override
    public void work() {
        System.out.println("Tester szuka błędów.");
    }
}

Użycie tablicy/listy typu bazowego

public class CompanyDemo {
    public static void main(String[] args) {
        Employee[] team = {
            new Manager(),
            new Developer(),
            new Tester(),
            new Developer()
        };

        for (Employee e : team) {
            e.work(); // Zostanie wywołana "właściwa" wersja metody dla każdego obiektu
        }
    }
}

Wynik wykonania:

Menedżer prowadzi spotkanie.
Programista pisze kod.
Tester szuka błędów.
Programista pisze kod.

Jakie są zalety?

  • Nie piszesz wielu sprawdzeń typu „Jeśli to Developer — zrób to i tamto”.
  • Dodanie nowego pracownika (np. Designer) to po prostu utworzenie nowej klasy i dodanie jej do tablicy.
  • Kod, który korzysta z tablicy pracowników, w ogóle się nie zmienia!

3. Zalety polimorfizmu: elastyczność i rozszerzalność

Załóżmy, że w Twojej firmie pojawił się nowy typ pracownika — Designer. Wszystko, co musisz zrobić, to utworzyć nową klasę:

public class Designer extends Employee {
    @Override
    public void work() {
        System.out.println("Projektant tworzy makiety.");
    }
}

Teraz możesz dodać projektanta do zespołu:

Employee[] team = {
    new Manager(),
    new Developer(),
    new Tester(),
    new Designer()
};

Voilà! Program od razu zaczyna poprawnie działać z nowym typem pracownika, bez zmiany ani jednej linijki w kodzie, który iteruje po tablicy i wywołuje work().

To właśnie rozszerzalność: Twój kod łatwo dostosowuje się do nowych typów obiektów.

4. Ograniczenia polimorfizmu: druga strona medalu

Niestety, każda magia ma swoje ograniczenia (i cenę, jak w każdej RPG).

Dostępne są tylko metody klasy bazowej

Gdy pracujesz ze zmienną typu Employee, możesz wywołać tylko te metody, które są zadeklarowane w klasie Employee. Jeśli w klasie Developer istnieje specjalna metoda writeCode(), nie wywołasz jej bezpośrednio:

Employee e = new Developer();
// e.writeCode(); // Błąd kompilacji: taka metoda nie istnieje w Employee!

Jeśli bardzo chcesz wywołać metodę specyficzną, trzeba wykonać rzutowanie. Ale to ostateczność. Jeżeli często rzutujesz typy, być może warto przeprojektować klasy — klasa bazowa lub interfejs powinny zawierać potrzebną metodę.

if (e instanceof Developer) {
    Developer dev = (Developer) e;
    dev.writeCode();
}

Ale wtedy tracisz uniwersalność i elegancję, dla których to wszystko było robione. Dlatego staraj się projektować klasę bazową tak, aby zawierała tylko te metody, które są naprawdę potrzebne wszystkim klasom potomnym.

5. Praktyka: implementujemy hierarchię pracowników

Połączmy przyjemne z pożytecznym: napiszemy prostą aplikację, w której jest kilka typów pracowników, i użyjemy polimorfizmu do ich obsługi.

Krok 1: Klasa bazowa i podklasy

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

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

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

// Tester.java
public class Tester extends Employee {
    @Override
    public void work() {
        System.out.println("Tester szuka błędów.");
    }
}

Krok 2: Klasa główna

// CompanyDemo.java
public class CompanyDemo {
    public static void main(String[] args) {
        Employee[] team = {
            new Manager(),
            new Developer(),
            new Tester(),
            new Developer()
        };

        for (Employee e : team) {
            e.work();
        }
    }
}

Krok 3: Dodajmy rozszerzalność

Załóżmy, że za miesiąc w firmie pojawia się nowy pracownik — projektant. Wszystko, co trzeba zrobić:

public class Designer extends Employee {
    @Override
    public void work() {
        System.out.println("Projektant tworzy makiety.");
    }
}

Gotowe, teraz można dodać projektanta do zespołu:

Employee[] team = {
    new Manager(),
    new Developer(),
    new Tester(),
    new Designer()
};

Podsumowanie

Cały kod główny (CompanyDemo) pozostał niezmieniony! Na tym polega siła polimorfizmu.

6. Typowe błędy przy używaniu polimorfizmu

Błąd nr 1: Oczekiwanie dostępu do specyficznych metod przez odwołanie typu bazowego.
Bardzo często początkujący próbują wywołać metody specyficzne dla podklasy przez zmienną typu superklasy. Na przykład:

Employee e = new Developer();
// e.writeCode(); // Błąd! Taka metoda nie jest zdefiniowana w Employee.

Aby wywołać metodę specyficzną, trzeba wykonać rzutowanie, ale wtedy traci się uniwersalność.

Błąd nr 2: Nieużywanie adnotacji @Override.
Jeśli zapomnisz o adnotacji, możesz przypadkiem napisać nową metodę zamiast nadpisać istniejącą (np. pomylić się w nazwie). Wtedy polimorfizm po prostu nie zadziała i zostanie wywołana wersja z superklasy.

Błąd nr 3: Brak wspólnego interfejsu.
Jeśli klasa bazowa nie zawiera potrzebnej metody, polimorfizm jest niemożliwy. Na przykład, jeśli w Employee nie ma metody work(), pętla po tablicy pracowników nie będzie mogła wywołać tej metody dla wszystkich.

Błąd nr 4: Naruszenie zasady otwartości/zamkniętości.
Jeśli dodanie nowego typu pracownika wymaga zmiany kodu, który iteruje po tablicy/liście, to znaczy, że nie używasz polimorfizmu we właściwy sposób.

1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 3
Niedostępne
Głosy cyfrowej encyklopedii zwierząt
Głosy cyfrowej encyklopedii zwierząt
1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 3
Niedostępne
Korporacyjna symfonia: Polimorfizm w pracy pracowników
Korporacyjna symfonia: Polimorfizm w pracy pracowników
1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 3
Niedostępne
Rozszerzenie zespołu: Dodanie nowego typu pracownika
Rozszerzenie zespołu: Dodanie nowego typu pracownika
1
Zadanie
JAVA 25 SELF, poziom 18, lekcja 3
Niedostępne
Dynamiczne biuro: Rozszerzalność i uniwersalność polimorfizmu
Dynamiczne biuro: Rozszerzalność i uniwersalność polimorfizmu
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION