– 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
class Animal
{
public void doAnimalActions();
}class Cat extends Animal
{
public void doCatActions();
}class Tiger extends Cat
{
public void doTigerActions();
}
Mamy tutaj deklaracje trzech klas: Animal, Cat oraz Tiger. Cat dziedziczy Animal. Za to Tiger dziedziczy Cat.
public static void main(String[] args)
{
Tiger tiger = new Tiger();
Cat cat = new Tiger();
Animal animal = new Tiger();
Object obj = new Tiger();
}
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
public static void main(String[] args)
{
Object obj = new Tiger();
Animal animal = (Animal) obj;
Cat cat = (Cat) obj;
Tiger tiger = (Tiger) animal;
Tiger tiger2 = (Tiger) cat;
}
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
public static void main(String[] args)
{
Object obj = new Tiger();
if (obj instanceof Cat)
{
Cat cat = (Cat) obj;
cat.doCatActions();
}}
Użyj lepiej sprawdzenia instanceof
public static void main(String[] args)
{
Animal animal = new Tiger();
doAllAction(animal);

Animal animal2 = new Cat();
doAllAction(animal2);

Animal animal3 = new Pet();
doAllAction(animal3);
}

public static void doAllAction(Animal animal)
{
if (animal instanceof Tiger)
{
Tiger tiger = (Tiger) animal;
tiger.doTigerActions();
}

if (animal instanceof Cat)
{
Cat cat = (Cat) animal;
cat.doCatActions();
}

animal.doAnimalActions();
}
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.

public static void main(String[] args)
{
Cat cat = new Tiger();
Animal animal = cat;
Object obj = cat;
}
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.