– Amigo, czy lubisz wieloryby?
– Wieloryby? Nie, nigdy o nich nie słyszałem.
– No wiesz, takie jak krowy, tylko większe i pływają. Nawiasem mówiąc, wieloryby pochodzą od krów. A przynajmniej mają wspólnego przodka. Mniejsza z tym.
– Słuchaj. Chciałbym Ci opowiedzieć o kolejnym, potężnym narzędziu OOP: polimorfizmie. Charakteryzuje się ono czterema cechami.
1) Nadpisywanie metod.
Wyobraź sobie, że napisałeś klasę "Cow" na potrzeby pewnej gry. Ma ona wiele części składowych i metod. Obiekty tej klasy mogą robić przeróżne rzeczy: chodzić, jeść czy spać. Jakby tego było mało, to poruszające się krowy dzwonią jeszcze dzwoneczkami. Załóżmy, że wszystko, co do najmniejszego szczegółu, już w tej klasie zostało zaimplementowane.
Wtedy nagle okazuje się, że klient chce wypuścić kolejny poziom gry, tylko tym razem jej akcja ma toczyć się na morzu, a główny bohater będzie wielorybem.
Zaczynasz zatem projektować klasę Whale i zdajesz sobie sprawę, że tylko nieznacznie różni się ona od klasy Cow. Obie klasy korzystają z bardzo podobnej logiki i decydujesz się użyć dziedziczenia.
Klasa Cow jest idealnie dopasowana do klasy macierzystej: posiada już wszystkie niezbędne zmienne i metody. Jedyne, co musisz zrobić, to dodać wielorybowi umiejętność pływania. I wtedy pojawia się problem: Twój wieloryb ma nogi, rogi i dzwoneczek! Klasa Cow implementuje przecież również i te funkcjonalności. Co możesz zrobić w takiej sytuacji?
Oto na ratunek przybywa nadpisywanie metod. Jeżeli dziedziczymy metodę, która nie robi dokładnie tego, czego potrzebujemy w naszej nowej klasie, to możemy zastąpić ją inną metodą.
Jak to się robi? W naszej klasie pochodnej deklarujemy metodę, którą chcemy zmienić (z taką samą sygnaturą metody jak w klasie macierzystej). A następnie piszemy dla tej metody nowy kod. To wszystko. To tak, jakby dawna metoda klasy macierzystej nie istniała.
Oto, jak to działa:
Kod | Opis |
---|---|
|
Tutaj definiujemy dwie klasy: Cow i Whale . Whale dziedziczy Cow .
Klasa |
|
Kod wyświetla na ekranie «Jestem krową». |
|
Kod wyświetla na ekranie «Jestem wielorybem» |
Następnie dziedziczy Cow
i nadpisuje printName
, więc klasa Whale
zawierać będzie następujące dane i metody:
Kod | Opis |
---|---|
|
Nie mamy żadnych informacji o dawnej metodzie. |
– Szczerze mówiąc, właśnie tego się spodziewałem.
2) Ale to nie wszystko.
– Załóżmy, ze klasa Cow
posiada printAll
, czyli metodę, która wywołuje dwie pozostałe metody. Kod będzie wtedy działał w następujący sposób:
Na ekranie wyświetli się:
Jestem białego koloru
Jestem wielorybem
Kod | Opis |
---|---|
|
|
|
Na ekranie wyświetli się: Jestem białego koloru Jestem wielorybem |
Zauważ, że kiedy metoda printAll () klasy Cow jest wywoływana na obiekcie Whale, to zostaje użyta metoda printName() klasy Whale, a nie klasy Cow.
Liczy się nie klasa, w której metoda została napisana, tylko typ (klasa) obiektu, na którym metoda jest wywoływana.
– Rozumiem.
– Możesz dziedziczyć i nadpisywać tylko metody niestatyczne. Metody statyczne nie są dziedziczone i nie mogą być nadpisywane.
A oto, jak wygląda klasa Whale, kiedy zastosujemy dziedziczenie i nadpiszemy metody:
Kod | Opis |
---|---|
|
A oto, jak wygląda klasa Whale, kiedy zastosujemy dziedziczenie i nadpiszemy metodę. Nie mamy żadnych informacji o dawnej metodzie printName . |
3) Rzutowanie typów.
Jest jeszcze jedna ciekawa kwestia. Ponieważ klasa dziedziczy wszystkie metody i dane jej klasy macierzystej, to obiektem tej klasy mogą być zmienne, do których przypisane są odniesienia do klasy macierzystej (i klasy macierzystej tej klasy macierzystej itd, aż do klasy Object). Przeanalizuj ten przykład:
Kod | Opis |
---|---|
|
Na ekranie wyświetli się: Jestem białego koloru. |
|
Na ekranie wyświetli się: Jestem białego koloru. |
|
Na ekranie wyświetli się: Whale@da435a. Metoda toString() jest dziedziczona po klasie Object. |
– Brzmi dobrze. Ale właściwie, czy to jest nam do czegoś potrzebne?
– To niezwykle przydatna funkcja. Później zrozumiesz, że jest ona naprawdę bardzo, bardzo ważna.
4) Późne wiązanie (dynamiczne przydzielanie).
Oto, jak to wygląda:
Kod | Opis |
---|---|
|
Na ekranie wyświetli się: Jestem wielorybem. |
|
Na ekranie wyświetli się: Jestem wielorybem. |
Zauważ, że to nie typ zmiennej determinuje, którą konkretną metodę printName wywołamy (tę z klasy Cow czy klasy Whale), ale raczej typ obiektu, do którego odnosi się ta zmienna.
Zmienna Cow przechowuje referencję do obiektu Whale, zostanie zatem wywołana metoda printName zdefiniowana w klasie Whale.
– Cóż, to tłumaczenie nie sprawia, że jest to bardziej zrozumiałe.
– Zgadzam się, to wcale nie jest takie oczywiste. Pamiętaj o jednej ważnej zasadzie.
Zestaw metod, które wywołujesz na zmiennej, jest determinowany przez typ zmiennej. Ale to, która konkretna metoda/implementacja zostanie wywołana, jest determinowane przez typ/klasę obiektu, do którego odnosi się zmienna.
– Spróbuję zapamiętać.
– Będziesz się ciągle z tym spotykał, więc szybko to zapamiętasz i zrozumiesz.
5) Rzutowanie typów.
Rzutowanie w odniesieniu do typów referencji działa inaczej, niż do typów prostych. A jednak, konwersji rozszerzającej i zawężającej można używać także w stosunku do typów referencji. Przeanalizuj ten przykład:
Konwersja rozszerzająca | Opis |
---|---|
|
Klasyczna konwersja rozszerzająca. Teraz możesz wywoływać na obiekcie Whale tylko metody zdefiniowane w klasie Cow. Kompilator pozwoli Ci użyć zmiennej cow tylko do wywołania metod zdefiniowanych przez typ Cow. |
Konwersja zawężająca | Opis |
---|---|
|
Klasyczna konwersja zawężająca ze sprawdzeniem typu. Zmienna cow typu Cow przechowuje referencję do obiektu Whale. Sprawdzamy, czy tak właśnie jest, a następnie przeprowadzamy (rozszerzającą) konwersję typu. Nazywa się to także rzutowaniem typu. |
|
Możesz także przeprowadzić konwersję zawężająca typu referencji bez sprawdzania typu danego obiektu. W tym przypadku, jeśli zmienna cow wskazuje na coś innego niż obiekt Whale, zostanie wyrzucony wyjątek (InvalidClassCastException). |
6) A teraz będzie coś fajnego. Wywoływanie pierwotnej metody.
Czasami, podczas nadpisywania dziedziczonej metody, nie chcesz jej zastąpić całkowicie. Czasem chcesz po prostu coś do niej dodać.
W tym przypadku potrzebujesz, aby kod nowej metody wywoływał tę samą metodę, ale na klasie bazowej. I w Javie możesz tak zrobić. Używasz do tego: super.method()
.
Oto kilka przykładów:
Kod | Opis |
---|---|
|
|
|
Na ekranie wyświetli się: Jestem białego koloru To nieprawda: Jestem krową Jestem wielorybem |
– Hmm. To dopiero była lekcja! Moje robocie uszy prawie się stopiły.
– Tak, to nie jest prosty temat. To jedno z najtrudniejszych zagadnień, jakie spotkasz. Profesor zadeklarował, że da Ci linki do materiałów innych autorów, więc jeśli nadal czegoś nie rozumiesz, możesz to nadrobić.
GO TO FULL VERSION