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.
GO TO FULL VERSION