Dziedziczenie – klasy abstrakcyjne

W ostatniej części szkolenia przyjrzeliśmy się zagadnieniu dziedziczenia – ważnemu elementowi programowania obiektowego. Wiemy już, że obiekty klasy potomnej są bardziej szczegółowe (są nadzbiorami) klas bazowych. Dzisiaj przyjrzymy się całemu łańcuchowi dziedziczenia od samego szczytu.

Hierarchia dziedziczenia

Każda klasa może dziedziczyć tylko po jednej klasie. Natomiast sam łańcuch może być długi i kolejne klasy mogą reprezentować kolejny poziom abstrakcji modelowanej części rzeczywistości. Im dalej spojrzymy na klasy w łańcuchu dziedziczenia tym bardziej są szczegółowe. Metody lub pola, które się w nich znajdują mogą nie mieć racji bytu w klasie na tym samym poziomie zagnieżdżenia. Do pokazania przykładu użyjemy obrazka z poprzedniej części.

dziedziczenie Java
dziedziczenie Java

Klasa Żywność może zawierać tylko te metody i pola, które są wspólne dla obu klas potomnych. Gdyby dodać np. metodę nalejNapój() do klasy żywność mógłby wystąpić problem.

Jeżeli weźmiemy to pod uwagę może okazać się, że klasa Żywność jest zbyt ogólna (zawiera tylko informacje o makroelementach i kaloryczności). W takim przypadku na szczycie tej hierarchii mówimy o klasie abstrakcyjnej.

Klasy abstrakcyjne

W przypadku, gdy klasa jest zbyt ogólna, aby mogła pełnić istotną rolę w programie można ją zadeklarować jako klasę abstrakcyjną. Nie będzie można tworzyć obiektów danej klasy, a będzie raczej czymś w rodzaju szablonu dla klas potomnych (które będą zawierały wszystkie jej składowe elementy).

Jakie niesie to ze sobą korzyści? Dzięki zastosowaniu klasy abstrakcyjnej można napisać metodę abstrakcyjną, która nie ma implementacji, a jedynie wiadomo, że taka metoda przyda się w obiektach klas potomnych.

Ważną cechą klas abstrakcyjnych jest to, że mogą zawierać zarówno metody abstrakcyjne jak i w pełni zaimplementowane. W związku z tym jeśli metoda w pełni jest poprawna w całej hierarchii, to należy ją umieścić możliwie wysoko, nawet w klasie abstrakcyjnej.

Deklaracja klasy abstrakcyjnej

Do deklaracji klas abstrakcyjnych służy słowo kluczowe abstract. Poniżej pokażemy przykładową implementację klasy Zywnosc.

Każde jedzenie posiada nazwę i nie ma wątpliwości jak ją zwrócić. Natomiast już liczba kalorii nie jest taka oczywista. W zależności od jedzenia będziemy ją liczyć w inny sposób.

Programista implementujący klasę pochodną będzie musiał ją odpowiednio opracować (nie ma możliwości pominięcia tego , chyba, że klasa będzie abstrakcyjna).

Klasy finalne

Jeżeli uznamy, że klasa jest już na samym dole łańcucha dziedziczenia i nie powinno się jej rozszerzać o kolejne cechy należy zadeklarować, że klasa jest finalna. Odbywa się to przy pomocy słowa kluczowego final.

Metody finalne

Podobnie jak w przypadku klas możemy uznać, że metoda nie powinna dać się przesłonić w podklasie. Ponownie słowem kluczowym potrzebnym do zdefiniowania takiej metody jest final.

Pola finalne – stałe

W jednej z poprzednich części kursu wspominaliśmy o stałych i słowie kluczowym final. Oznacza to, że raz przypisana wartość nie może zostać zmieniona (podobnie jak np. implementacja metody lub klasy).

Rzutowanie obiektów

Temat rzutowania był już raz poruszany przy omówieniu typów prostych. Jeśli istnieje ryzyko utraty danych należy wykonać jawne rzutowanie. W przeciwnym wypadku, gdy takie ryzyko nie istnieje, to kompilator zrobi to za nas.

Obowiązkowe rzutowanie oznacza, że godzimy się na możliwość utraty danych np. części ułamkowej w przypadku konwersji liczb rzeczywistych na całkowite.

Kontener

W przypadku obiektów rzutowanie będzie potrzebne w przypadku, gdy referencja wskazująca na obiekt w pamięci jest typu nadrzędnego. Kiedy może nastąpić taka sytuacja? Jeśli zaistnieje potrzeba dodania wszystkich napojów (w tym naszych własnych, które reprezentuje klasa WlasnyNapoj) do jednej tablicy lub kolekcji musimy potraktować specjalny typ bardziej ogólnie (wszystkie napoje będą klasy Napoj). Taka zmienna, która może przechowywać obiekty podklas nazywa się kontenerem.

W podanym przykładzie potraktowaliśmy herbatę słodzoną jako napój i jest to poprawne działania. Natomiast nie możemy przeprowadzić odwrotnej konwersji. Możemy to porównać do awansu i degradacji. Dla bezpieczeństwa lepiej nadać mniejsze uprawnienia wszystkim pracownikom niż większe.

Wykorzystanie rzutowania obiektu

Obiekt herbataSlodzona jest traktowany jako Napoj, mimo że w rzeczywistości jest typu WlasnyNapoj. Aby móc skorzystać z metod lub pól dodatkowych należy wykonać rzutowanie.

Powyższa instrukcja jest prawidłowa, gdyż zostało wykonane prawidłowe rzutowanie.

Ryzyko rzutowania – ClassCastException

Co by się jednak stało, gdyby spróbować wykonać rzutowanie faktycznego obiektu typu Napoj? Zostanie wyrzucony wyjątek ClassCastException. W tym momencie nie potrafimy obsłużyć wyjątków (zajmiemy się nimi w przyszłości), w związku z tym działanie programu zostanie przerwane.

Rzutowanie jest możliwe tylko w obrębie klas dziedziczonych. Tylko gdy operacja jest możliwa do wykonania (nie próbujemy awansować obiektów do typu bardziej szczegółowego niż w rzeczywistości jest).

Aby zapobiec rzuceniu wyjątku można użyć instrukcji instaceof.

Jeśli instrukcja jest prawdziwa można wykonać rzutowanie bez błędu.

W kolejnym wpisie przyjrzymy się interfejsom.