– Cześć, Amigo! Tematem dzisiejszej lekcji są rozszerzające i zawężające konwersje typów. O rozszerzaniu i zawężaniu typów prymitywnych dowiedziałeś się już eony temu. Na Poziomie 10. Dzisiaj porozmawiamy o tym, jak to działa w przypadku typów referencyjnych, np. instancji klas.
Tak naprawdę, to wszystko jest całkiem proste. Wyobraźmy sobie łańcuch dziedziczenia klasy: klasa, jej klasa macierzysta, klasa macierzysta klasy macierzystej itd., aż do klasy Object. Ponieważ każda klasa zawiera wszystkie metody należące do klasy, którą sama dziedziczy, to instancja owej klasy może zostać zapisana w zmiennej o typie jej dowolnej klasy macierzystej.
Oto przykład:
Kod | Opis |
---|---|
|
Mamy tutaj deklaracje trzech klas: Animal, Cat oraz Tiger. Cat dziedziczy Animal. Za to Tiger dziedziczy Cat. |
|
Obiekt Tiger może zawsze zostać przypisany do zmiennej, której typ jest typem jednego z jej przodków. W przypadku klasy Tiger są to Cat, Animal i Object. |
Przyjrzyjmy się teraz konwersjom rozszerzającym i zawężającym.
Jeśli operacja przypisania powoduje, że przesuwamy się w górę łańcucha dziedziczenia (w kierunku klasy Object), to mamy do czynienia z konwersją rozszerzającą (znaną również jako rzutowanie w górę, ang. upcasting). Jeśli przesuniemy się w dół łańcucha w kierunku typu obiektu, to jest to konwersja zawężająca (znana również jako rzutowanie w dół, ang. downcasting).
Poruszanie się w górę łańcucha dziedziczenia nazywane jest rozszerzaniem, ponieważ prowadzi do bardziej ogólnego typu. W ten sposób tracimy jednak możliwość wywoływania metod dodanych do klasy poprzez dziedziczenie.
Kod | Opis |
---|---|
|
Przy zawężaniu typu musisz użyć operatora konwersji typu, tzn. wykonać jawną konwersję.
Sprawia to, że maszyna Javy sprawdza, czy dany obiekt rzeczywiście dziedziczy właśnie ten typ, na który chcemy go przekonwertować. Ot ta maciupeńka innowacja pozwoliła na wielokrotne zmniejszenie liczby błędów związanych z rzutowaniem, jednocześnie znacznie zwiększając stabilność oprogramowania pisanego w Javie. |
Kod | Opis |
---|---|
|
Użyj lepiej sprawdzenia instanceof |
|
A oto, dlaczego tak jest. Spójrz na przykład po lewej.
Nie zawsze wiemy (czy też wie o tym nasz kod), z jakim typem obiektu przyszło nam pracować. Może to być obiekt tego samego typu, co zmienna (Animal) lub dowolny typ potomny (Cat, Tiger). Przyjrzyj się metodzie doAllAction. Metoda działa poprawnie niezależnie od typu przekazanego do niej obiektu. Innymi słowy, działa ona poprawnie dla wszystkich trzech typów: Animal, Cat oraz Tiger. |
|
Mamy tu do czynienia z trzema operacjami przypisania. Wszystkie z nich są przykładami konwersji rozszerzających.
Operator rzutowania typu na nic się nie przyda, dlatego że żadne sprawdzanie nie jest tu konieczne. Referencja do obiektu może być zawsze zapisana w zmiennej, której typ odpowiada typowi dowolnego z jej przodków. |
– Och, te przykłady od drugiego do ostatniego wszystko mi wyjaśniły: dlaczego potrzebna jest kontrola i dlaczego konieczne jest rzutowanie typu.
– Mam nadzieję. Zwróć jeszcze uwagę, że...
Żaden z tych czynników nie powoduje jakichkolwiek zmian w obiekcie! Jedyne, co się zmienia, to liczba metod możliwych do wywołania na danej zmiennej referencyjnej.
Na przykład, zmienna Cat pozwala na wywołanie metod doAnimalActions i doCatActions. Zmienna ta nic nie wie o metodzie doTigerActions, nawet jeśli wskazuje ona na obiekt Tiger.
– Tak, rozumiem. To było dużo prostsze, niż się spodziewałem.
GO TO FULL VERSION