Spis treści
Polimorfizm to bardzo potężny i często wykorzystywany w językach obiektowych mechanizm. Pozwala on na traktowanie obiektów pewnej klasy jako obiektów innej klasy, jeżeli jedna z tych klas dziedziczy po drugiej klasie (pośrednio bądź bezpośrednio).
Polimorfizm w akcji¶
Wracając do przykładu z poprzedniego rozdziału, obiekt klasy Pracownik mógłby zostać potraktowany jako reprezentant klasy Osoba, ponieważ klasa Pracownik rozszerza klasę Osoba – zachodzi więc tutaj zależność Pracownik jest Osobą.
Jak to wygląda w praktyce? W poniższej klasie metoda wejdzDoBudynku w klasie Budynek oczekuje jako argumentu obiektu typu Osoba:
package prostyprzyklad; public class Budynek { public static void main(String[] args) { Osoba osoba = new Osoba(); osoba.imie = "Jan"; osoba.nazwisko = "Kowalski"; Pracownik pracownik = new Pracownik(); pracownik.imie = "Joanna"; pracownik.nazwisko = "Sikorska"; pracownik.numerIdentyfikatora = 1234; wejdzDoBudynku(osoba); // 1 wejdzDoBudynku(pracownik); // 2 } public static void wejdzDoBudynku(Osoba osoba) { // 3 System.out.println("W budynku jest " + osoba); } }
W wyniku uruchomienia tego programu zobaczymy na ekranie:
Zauważmy, że metoda wejdzDoBudynku oczekuje argumentu typu Osoba (3). W metodzie main najpierw wywołujemy ją z argumentem osoba typu Osoba (1), ale w kolejnej linii (2) wywołujemy tę samą metodę z argumentem innego typu – obiektem pracownik typu Pracownik!
Kompilator języka Java nie zgłasza problemów podczas kompilacji, a Maszyna Wirtualna Java nie rzuca wyjątku w trakcie działania programu, ponieważ powyższy kod jest całkowicie legalny i ilustruje polimorfizm w akcji. Kod działa, ponieważ każdy obiekt klasy Pracownik może być traktowany jako obiekt klasy Osoba ze względu na to, że jedna z tych klas dziedziczy (rozszerza) drugą klasę.
Czy moglibyśmy zmienić argument metody wejdzDoBudynku z Osoba na Pracownik? Tak, ale wtedy kod przestałby się kompilować – problemem byłaby linia (1). Wynika to z faktu, że o ile każdy Pracownik to Osoba, to nie każda Osoba to Pracownik. Kompilator wypisałby następujący błąd:
Przykład metody wejdzDoBudynku pokazuje, że argument tej metody raz wskazuje w pamięci na obiekt typu Osoba, a drugi raz – na obiekt typu Pracownik. Oznacza to, że zmienne typu bazowego możemy stosować do wskazywania na obiekty klas pochodnych – spójrzmy na poniższy przykład:
Osoba innaOsoba = new Pracownik(); innaOsoba.imie = "Artur"; innaOsoba.nazwisko = "Strzelecki";
Powyższy fragment kodu jest prawidłowy. Jak już wiemy, każdy Pracownik to Osoba. Korzystamy ze zmiennej innaOsoba, która może przechowywać referencję do obiektu typu Osoba, ale obiekt, jaki faktycznie tworzymy i którego adres przypisujemy do tej zmiennej, jest typu Pracownik. Nie jest to jednak problem, ponieważ klasa Pracownik dziedziczy po klasie Osoba.
Ustawiliśmy powyżej imie i nazwisko – te pola są w klasie Osoba. A gdybyśmy spróbowali ustawić pole numerIdentyfikatora?
// blad! innaOsoba.numerIdentyfikatora = 4321;
Ta linia spowodowałaby następujący błąd kompilatora:
Co prawda tworzymy obiekt typu Pracownik, w której to klasie zdefiniowane jest pole numerIdentyfikatora, ale do odnoszenia się do tego obiektu używamy referencji typu Osoba – a obiekty klasy Osoba pola numerIdentyfikatora nie posiadają.
Jak zobaczymy w kolejnych rozdziałach, jest sposób na ustawienie pola numerIdentyfikatora za pomocą referencji typu Osoba. W tym celu należy skorzystać z mechanizmu rzutowania, który poznaliśmy już w poprzednich rozdziałach – rzutowaliśmy na przykład liczby typu całkowitego na liczby rzeczywiste. Rzutowanie odbywa się poprzez napisanie typu docelowego w nawiasach przed rzutowaną wartością. Zobaczmy, jak moglibyśmy poprawić powyższy przykład, by zadziałał:
((Pracownik) innaOsoba).numerIdentyfikatora = 4321;
Dzięki takiemu zapisowi, kod jest poprawny i jesteśmy w stanie ustawić wartość pola numerIdentyfikatora. Poprzez użycie rzutowania powiedzieliśmy kompilatorowi:
„Wiem, że zmienna innaOsoba jest typu Osoba, ale tak naprawdę wskazuje ona na obiekt klasy pochodnej o nazwie Pracownik, proszę więc o pozwolenie na kompilację i ustawienie pola numerIdentyfikatora na moją odpowiedzialność”.
Dlaczego „na naszą odpowiedzialność”? Ponieważ zmienna innaOsoba mogłaby wskazywać na obiekt klasy Osoba, a nie Pracownik – wtedy rzutowanie byłoby niemożliwe i działanie programu zakończyłoby się błędem. Działanie programu, a nie kompilacja – zauważmy tutaj, że kompilator nie jest w stanie nas uchronić przed próbą nieprawidłowego rzutowania – o potencjalnym problemie dowiemy się dopiero po uruchomieniu programu:
Osoba kolejnaOsoba = new Osoba(); // kompilacja ok, ale blad wykonania! ((Pracownik) kolejnaOsoba).numerIdentyfikatora = 5555;
Kompilacja tego fragmentu kodu zakończy się bez błędów – kompilator w tym przypadku nie uchroni nas przed potencjalnym błędem, który, w tym przypadku, ewidentnie popełniliśmy, co widać na ekranie po uruchomieniu programu:
Problem występuje, ponieważ próbujemy ustawić pole numerIdentyfikator rzutując obiekt kolejnaOsobana na typ Pracownik, jednak jest to niemożliwe – zmienna kolejnaOsoba wskazuje na obiekt typu Osoba, a nie Pracownik.
Przykład method overriding¶
Z polimorfizmem i dziedziczeniem wiąże się także bardzo ważny mechanizm zwany method overriding (nadpisywanie metod), o którym opowiemy sobie więcej w kolejnych rozdziałach, a na razie zobaczymy jeden przykład, który wyjaśni, na czym ten mechanizm polega.
Method overriding to tworzenie w klasie pochodnej takiej samej metody, jaka już znajduje się w klasie nadrzędnej (z możliwością pewnych zmian, które poznamy wkrótce). Gdy metoda ta zostanie wywołana na zmiennej typu bazowego, to wykonana zostanie nie metoda z typu bazowego, lecz typu obiektu, na który ta zmienna faktycznie wskazuje w pamięci.
Dla przykładu, załóżmy, że dodamy do klasy Pracownik metodę toString – ta metoda będzie wypisywała nie tylko imię i nazwisko (jak już to robi metoda toString w klasie Osoba), ale także identyfikator Pracownika:
package prostyprzyklad; public class Pracownik extends Osoba { public int numerIdentyfikatora; public String toString() { return "Pracownik " + imie + " " + nazwisko + ", identyfikator: " + numerIdentyfikatora; } // metoda main zostala pominieta }
Do klasy Pracownik dodaliśmy dedykowaną dla tej klasy metodę toString. Przypomnijmy jeszcze, jak wygląda metoda toString z klasy bazowej Osoba:
package prostyprzyklad; public class Osoba { public String imie; public String nazwisko; public String toString() { return "Osoba " + imie + " " + nazwisko; } }
Gdy teraz utworzymy obiekt klasy Osoba i obiekt klasy Pracownik i wypiszemy ich tekstową reprezentację na ekran, to zobaczymy następujący komunikat:
Osoba pewnaOsoba = new Osoba(); pewnaOsoba.imie = "Jan"; pewnaOsoba.nazwisko = "Kowalski"; System.out.println(pewnaOsoba.toString()); Pracownik pewienPracownik = new Pracownik(); pewienPracownik.imie = "Joanna"; pewienPracownik.nazwisko = "Sikorska"; pewienPracownik.numerIdentyfikatora = 1234; System.out.println(pewienPracownik.toString());
Na razie nie jest to nic nowego – obiekt pewnaOsoba został zamieniony na tekst za pomocą metody toString z klasy Osoba, a obiekt pewienPracownik – nową metodą toString z klasy Pracownik.
Spójrzmy jednak co się stanie, jeżeli do referencji do typu Osoba przypiszemy obiekt typu Pracownik i wtedy użyjemy metody toString:
Osoba innaOsoba = new Pracownik(); innaOsoba.imie = "Adrian"; innaOsoba.nazwisko = "Sochacki"; System.out.println(innaOsoba.toString());
Ten fragment kodu spowoduje wypisanie na ekran komunikatu:
Tutaj zachodzi magia mechanizmu method overriding – pomimo, że typ zmiennej innaOsoba to Osoba, a nie Pracownik, została użyta metoda toString zaimplementowana w klasie Pracownik, ponieważ faktyczny obiekt wskazywany przez zmienną innaOsoba jest właśnie typu Pracownik.
Mechanizm ten działa automatycznie i daje ogromne możliwości. Jest on jedną z podstaw programowania zorientowanego obiektowo. Jest kilka istotnych zasad dotyczących nadpisywania metod (method overriding), które trzeba mieć na uwadze – opowiemy sobie o nich dokładnie w jednym z kolejnych rozdziałów.