Tytuł tego rozdziału brzmi pewnie niepokojąco, ale tak naprawdę za pojęciem dziedziczenia nie kryje się nic niezwykłego.
Stykając się po raz pierwszy z tym terminem w programowaniu, mogą przychodzić nam do głowy skojarzenia z zasłyszanymi stwierdzeniami,
że jakieś cechy wyglądu albo zachowania odziedziczyliśmy po naszych rodzicach, dziadkach itd. Nie zdajemy sobie sprawy z tego, że tak naprawdę
rozumiejąc ten rodzaj dziedziczenia, mamy już podstawy do tego, aby zrozumieć czym jest dziedziczenie w Javie.
Wiemy już, że tworząc program obiektowy tworzymy najpierw klasy, które są szablonem (wzorem) do tworzenia obiektów na ich podstawie.
Klasy te najczęściej reprezentują podmiot faktycznie istniejący w rzeczywistości (dokument, film, osoba...) i mają swoje pola,
w których są przechowywane cechy (własności) tych podmiotów (np. nazwa dokumentu, tytuł filmu...). Mamy też metody, które definiują nam
działania jakie będą wykonywane przez dany obiekt.
Wracając teraz do tematu dziedziczenia musimy wiedzieć, że pola i metody niekoniecznie muszą być definiowane tylko w danej klasie, którą stworzyliśmy.
Możemy je również odziedziczyć z innej klasy. Przejdźmy do przykładu...
Słowo kluczowe extends
Mamy dwie klasy. Pierwsza z nich, to znana już nam klasa
Item (tutaj nieco okrojona). Druga natomiast, to całkiem nowa klasa o nazwie
DocumentItem.
Tak więc pierwsza klasa reprezentuje "jakiś tam item w programie", a druga także przedstawia item, ale będący czymś więcej niż tylko zwykłym, przeciętnym itemem.
Nasz item jest tutaj dokumentem...ale zaraz zaraz...dlaczego właściwie twierdzimy, że
DocumentItem też jest itemem? Czy dlatego, że w nazwie tej klasy zawarte jest słowo
Item?
Otóż nie, oczywiście że nie. Stwierdzamy, że klasa
DocumentItem również jest itemem, ponieważ
dziedziczy z klasy
Item!
Widzimy, że klasa dziedzicząca z innej klasy musi po swojej nazwie mieć wpisane słowo kluczowe
extends, poprzedzające
nazwę klasy, z której dziedziczy.
Inaczej mówiąc klasa
DocumentItem rozszerza klasę
Item, co zresztą jest dosłownym tłumaczeniem słowa
extends.
Tylko po co nam takie dziedziczenie? Jak narazie mamy dwie puste klasy i nic ciekawego się tam nie dzieje. Dodajmy więc teraz do naszego
przykładu trochę kodu i zobaczmy co faktycznie daje nam dziedziczenie.
Dostęp do pól i metod
Mamy wiec klasę
Item, która posiada
jedno pole, dwie metody i bezparametrowy konstruktor domyślny (jeśli nie wiecie o jakim konstruktorze mowa, to polecamy powrót do początku rozdziału
Tworzenie własnych obiektów). Jak już wiecie
z poprzednich rozdziałów, możemy
stworzyć obiekt tej klasy, ustawić wartość pola z nazwą za pomocą metody
setName, a później
pobrać tą nazwę z obiektu
w celu wydrukowania jej na konsoli:
Oczywiście po wykonaniu programu na konsoli zostanie wydrukowany tekst "Appa Item 1" i to będzie zgodne z naszymi oczekiwaniami. W tym momencie przyszedł czas na wykorzystanie
naszej nowej klasy
DocumentItem. Dodamy do niej jedno nowe pole
type oraz metody umożliwiające ustawienie i pobranie wartości
w tym polu. Następnie zmodyfikujemy nieco naszą metodę
main:
Klasa po lewej to zwykła, regularna klasa java, której najbardziej interesującą umiejętnością jest to, że rozszerza klasę
Item.
A skoro ją rozszerza, to przy okazji dziedziczy jej pola i metody. Nie widać tego co prawda tak jawnie w kodzie, ale patrząc na taki zapis
zawsze powinniśmy mieć swiadomość, że
to co znajduje się w klasie zdefiniowanej po słowie extends,
jest również bezpośrednio dostępne w ramach klasy roszerzającej, tak jakby było jawnie wpisane do tej klasy (pomijamy tu kwestię modyfikatorów dostępu, aby nie zaciemniać - niech wszystkie metody mają modyfikatory
public, a pola niech nie mają żadnego modyfikatora, czyli tak jak w naszym przykładzie).
Uzbrojeni w tą wiedzę możemy teraz przystąpić do analizy kodu klasy
Start. Widzimy, że do wcześniej istniejącego kodu dopisaliśmy kilka linijek.
W pierwszej z nich dzieje sie coś ciekawego. Tworzymy tam obiekt klasy
DocumentItem i przypisujemy go do zmiennej typu
DocumentItem.
Czyli jak narazie wszystko wygląda tak, jakbyśmy się tego spodziewali. Następnie ustawiamy w obiekcie
item nazwę za pomocą metody
setName.
I tutaj pojawia się pierwsza niejasność. Wiemy co prawda, że metoda taka jest dostępna w klasie
Item, ale czy możemy jej użyć w tym miejscu jeśli właściwie korzystamy tutaj z obiektu klasy
DocumentItem? Oczywiście, że tak. Mamy dostęp do tej metody, więc możemy jej użyć. Wszystko zgodnie z
zasadą mówiącą o dostępności metod i pól (wytłuszczoną w poprzednim akapicie).
Dodajmy jeszcze, że tak samo jak ustawiliśmy nazwę korzystając z metody klasy
Item, tak samo linijkę niżej ustawiamy wartość pola
type znajdującego się
w klasie
DocumentItem. Wszystkie te dane "lądują" w jednym obiekcie. Dlatego też w ostatnich dwóch linijkach możemy je pobrać i wydrukować.
Rozszerzam Cię więc jestem Tobą
Spójrzmy jeszcze raz na ten kod, ale teraz w delikatnie zmienionej formie. Jedyną rzeczą, na którą teraz chcemy zwrócić uwagę jest
tworzenie obiektu klasy
DocumentItem. Wygląda to podobnie jak poprzednio, ale tym razem tak stworzony obiekt przypisujemy
do zmiennej o typie
Item:
Można teraz zadać pytanie: "co tu się właśnie wydarzyło...?!". Czy to w ogóle jest poprawnie działający kod? Odpowiadamy. Tak to jest poprawnie działający kod i od razu przystępujemy do tłumaczenia.
Skoro nasz klasa
DocumentItem dziedziczy z klasy
Item, to poza tym że dziedziczy niejawnie pola i metody,
to również
obiekt tej klasy staje się obiektem tego samego typu, co klasa z której się wywodzi. Pójdźmy teraz jeszcze o krok dalej i stwórzmy klasę
TextDocumentItem,
która będzie dziedziczyła z klasy
DocumentItem:
Taki zapis mówi nam nie tylko to, że klasa
TextDocumentItem rozszerza
DocumentItem, ale też że
obiekt tej klasy jest typu DocumentItem.
I teraz skoro klasa
DocumentItem rozszerza klasę
Item i jej obiekt jest typu
Item, to obiekt klasy
TextDocumentItem
również staje się obiektem typu
Item.
Mając tak zdefiniowane klasy możemy powiedzieć, że:
-
Każdy obiekt tworzony za pomocą: new Item jest obiektem typu Item.
-
Każdy obiekt tworzony za pomocą: new DocumentItem jest obiektem typu DocumentItem oraz Item.
-
Każdy obiekt tworzony za pomocą: new TextDocumentItem jest obiektem typu TextDocumentItem oraz DocumentItem oraz Item.
Klasa rozszerzająca vs klasa rozszerzana
Bardzo istotne jest to aby zrozumieć, że taka zależność działa tylko w jedną stronę. Czyli nie możemy powiedzieć, że obiekt klasy
Item
jest obiektem typu
DocumentItem. Innymi słowy, zapis:
oznacza, że obiekt jest tylko i wyłącznie obiektem typu
Item i nie mamy tutaj bezpośredniego dostępu do pól i metod klasy
DocumentItem.
Z tego powodu metody
setType i
getType nie są widoczne w obiekcie typu
Item.
Taki kod się nie skompiluje:
Oczywiście my jako programiści doskonale wiemy, że pod obiektem
item kryje się instancja klasy
DocumentItem
i na upartego możemy wykonać tak zwane
rzutowanie na obiekt typu DocumentItem.
Jednak coś takiego nie jest zalecane. Raz, że zmniejsza czytelność kodu, a dwa że uderza bezpośrednio w wykonany przez nas projekt klas. Z jakiegoś powodu
klasa
Item przecież nie posiada dostępu do metod
setType i
getType. Może tak właśnie ma być,
że programista projektując taką klasę nie chciał udostępniać tej metody dla ogólnego typu
Item, tylko jego zamysłem było
użycie w tym celu dedykowanej klasy
DocumentItem.
Taka jest powszechnie stosowana praktyka, że ta część metod, która ma być dostępna dla wszystkich klas dziedziczących
jest wstawiana do klasy nadrzędnej (takiej jak
Item), a te metody które mają być dedykowane tylko konkretnej klasie dziedziczącej są dostępne bezpośrednio
w tej klasie (takiej jak u nas
DocumentItem).
Podsumowując takie rzutowanie jest trochę takim hakiem stosowanym przez programistę, aby złamać zaprojektowany wcześniej schemat.
Klasa Object
Na koniec pytanie. Czy klasa
Item dziedziczy z jakiejś klasy? Patrząc to, że w jej definicji nie ma słowa kluczowego
extends, odpowiedź wydaje się prosta - nie dziedziczy.
Jednak to nie jest całkiem dobra odpowiedź. Wszystko sprowadza się do stwierdzenia jawnie/niejawnie.
Jawnie, czyli z użyciem
extends. Niejawnie, to znaczy, że
z definicji
każda klasa w Javie dziedziczy z klasy o nazwie Object, a skoro mówimy, że każda klasa, to znaczy, że klasa
Item
również.
Co nam to daje?
Po pierwsze możemy być pewni, że obiekt każdej naszej klasy zawsze będzie obiektem typu
Object, co jest dosadnym dowodem stwierdzenia, że w Javie wszystko jest obiektem. Po drugie oznacza to, że wszystkie klasy w Javie dziedziczą możliwości klasy
Object.
W tym przede wszystkim dziedziczą kilka kluczowych metod, z których najbardziej popularne to:
toString,
equals i
hashCode.
Już niebawem poświęcimy im nieco więcej czasu.
Przykład w programie
Teraz zobaczmy jak kod naszego programu wygląda w IDE:
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Czy informacje, które otrzymałeś, były pomocne?
Jeśli tak, zapraszam Cię do podarowania mi kawy.
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.