Kurs java dla początkujących - #7 Dziedziczenie, Polimorfizm, Interfejsy

Kurs Java dla początkujących – #7 Dziedziczenie, Polimorfizm, Interfejsy

W tej części zajmiemy się podstawowymi zagadnieniami programowani obiektowego, takimi jak: dziedziczenie, polimorfizm czy enkapsulacja i tym wszystkim, co się z nimi wiąże. Są to bardzo przydatne elementy, które stosowane z rozwagą mogą znacznie poprawić organizację twojego kodu. Opiszę tutaj także, do czego służą interfejsy i jak organizować swój kod w pakiety.

Z drugiej części kursu dowiedziałeś się co to są klasy i obiekty. Teraz zapoznam Cię z mechanizmami, które pomogą ci wykorzystać wszystkie możliwości jak dają. Zacznijmy od najprostszej rzeczy, czyli od dziedziczenia.

Dziedziczenie

Jest to mechanizm, który pozwala na rozszerzanie funkcjonalności obiektów poprzez hierarchiczne powiązanie ich ze sobą tzn. jeden obiekt dziedziczy zachowania i stan od drugiego.

Dziedziczenie definiujemy na poziomie deklaracji klasy poprzez użycie słowa kluczowego extends. Klasycznym przykładem dziedziczenia są klasy odwzorowujące zwierzęta. Mamy klasę Animal, która jest naszą klasą nadrzędną (nazywaną często również bazową). Klasa ta ma zaimplementowane podstawowe metody takie jak eat(), walk(), sleep():

Po klasie Animal dziedziczą takie klasy jak Dog i Cat:

Obie klasy mogą teraz korzystać z metod zawartych w klasie Animal:

Powyższy program wyświetli:

Na tej zasadzie możesz rozszerzać każdą klasę w Javie, z wyjątkiem tych, które są oznaczone jako final np. String (gdy zajrzysz w źródła klasy String, to zobaczysz jej deklarację public final class String).

Wiele klas może dziedziczyć jedna po drugiej np. class SmallDog extends Dog, ale im bardziej rozbudowana hierarchia klas, tym trudniej jest się zorientować, która metoda jest z jakiej klas. Czasem może to doprowadzić do błędów.

Polimorfizm

Kolejnym zagadnieniem związanym także z dziedziczeniem jest polimorfizm, czyli w skrócie wielopostaciowość. Klasy pochodne mogą dziedziczyć zachowania i stan swoich klas nadrzędnych, ale także mogą zmieniać te zachowanie i stan. I to do tego właśnie w dużej mierze sprowadza się polimorfizm. 

Nadpisywanie metod klasy nadrzędnej 

W każdej klasie dziedziczącej z jakiejś klasy można nadpisać (Override) każdą publiczną (public) i chronioną (protected) metodę. Żeby odpowiednio oznaczyć taką metodę używamy adnotacji @Override.

Dla klasy Dog zmiana zachowania dla metody walk() wygląda w taki sposób:

Metoda, która nadpisuje metodę z klasy nadrzędnej, może wywołać metodę, którą nadpisuje. Używamy do tego słowa kluczowego super:

Jako wynik takiego wywołania otrzymamy:

Czasem takie rozszerzenie funkcjonalności metody nadrzędnej jest bardzo pożądane.

Przysłanianie metod klasy

Nadpisanie (Override) to jeden ze sposobów osiągnięcia polimorfizmu w Javie. Kolejny to przeciążenie (Overload) i jest on związany z metodami. Ta sam metoda może mieć wiele postaci, a wyrażać się to może tym, że przeciążona metoda, może przyjmować inną liczbę parametrów lub tą samą liczbę parametrów, ale innego typu.

Po uruchomieniu:

Otrzymujemy następujący wynik:

Oczywiście klasy pochodne mogą definiować swoje własne metody i pola niezależne od klas nadrzędnych.

Interfejsy

Kolejnym sposobem na osiągnięcie polimorfizmu jest użycie interfejsów. Interfejs można potraktować jak szablon dla klas (jednocześnie dla wielu klas). Mówimy że jakaś klasa implementuje interfejs (używamy słowa kluczowego implements). To zachowanie jest bardzo podobne do dziedziczenia z klasy nadrzędnej (tak jak w przypadku Animal). W interfejsach jednak nie ma możliwości implementacji metod publicznych (abstrakcyjne). Przez to każda klasa implementująca dany interfejs ma te same zachowania (ten sam zestaw metod), ale każda musi je implementować na własny sposób.

W interfejsie Pet mamy zadeklarowane trzy metody. Każda klasa, która implementuje ten interfejs musi mieć zaimplementowane te trzy metody. Musza być one także oznaczone adnotacją @Override.

i kolejna klasa

Po uruchomieniu programu

otrzymamy wynik jak poniżej:

Użycie interfejsów jest bardzo podobne do użycia klas nadrzędnych. Jak praktyka pokazuje, w Javie częściej używane są interfejsy. Chociaż ich metody nie mają implementacji. Jednak właśnie to daje łatwiejsze zrozumienie kodu. Interfejs definiuje jakieś zachowanie np. walk(), a jego implementacja jest po stronie klasy. Dodatkowo interfejsy obsługują dziedziczenie. Podobnie jak w wypadku klas, jeden może rozszerzać drugi poprzez użycie słowa extends.

Natomiast dziedziczenie niewielkich fragmentów kodu bywa pomocne. Jednak często prowadzi do zbyt dużego przyrostu kodu w klasie nadrzędnej i często te klasy stają się tzw. „God class”. Wszystko trzeba robić z głową.

Używanie typów interfejsów i klas nadrzędnych

Jak pewnie zauważyłeś w powyższych przykładach, ale także w części poświęconej kolekcjom, do deklaracji typów zmiennych używam interfejsów lub klas nadrzędnych (Pet myLittleDog czy Animal dog). Ma to swoje zalety, ale także wady. Zaletą jest to, że każda taka zmienna może przyjmować jako wartość obiekt dowolnej klasy, która implementuje dany interfejs lub rozszerza klasę nadrzędną. Zarówno interfejs jak i klasa nadrzędna mają określony zestaw metod publicznych, więc operując na zmiennej np. typu Pet, bez względu na to, czy implementacją jest MyLittleDog czy MyLittleCat, poruszamy się w obrębie tego samego zestawu metod.

Problemy zaczynają się wtedy gdy konkretna implementacja np. MyLittleDog implementuje dodatkowe metody specyficzne tylko dla tego typu. Wtedy musimy posłużyć się rzutowaniem do typu MyLittleDog. Warto także sprawdzić czy dany obiekt jest dokładnie tego typu. Służy do tego instrukcja instanceof:

Rzutowanie – jest to zmiana jednego typu na inny. Jeśli jest poprawne, to stary typ obiektu jest zamieniany na nowy. Jeśli rzutowanie nie jest poprawne, wyrzucany jest wyjątek ClassCastException. Dzieje się tak, ponieważ nie da się zamienić typu Pies na Kot – są to dwa różne typy. Natomiast da się zamienić typ Animal na Pies (jeśli dany obiekt jest faktycznie typu Pies).

W tym wypadku możemy zmienić implementację metody giveMeMyPet() tak, by zwracała obiekt klasy MyLittleCat i cały kod będzie nadal wykonywał się poprawnie.

Enkapsulacja

Inaczej hermetyzacja. Jest to ukrywanie pewnych wartości bądź zachowań przed światem zewnętrznym. Budując klasę nadrzędną nie zawsze chcesz, żeby wszystkie jej szczegóły (metody i pola) były widoczne dla klas, które dziedziczą po nich lub na zewnątrz tych klas. Dlatego w klasach mamy dostępne modyfikatory dostępu takie jak: public, protected i private.

Jeśli chcesz żeby dana metoda była dostępna tylko w klasie Animal to możesz nadać jej modyfikator private. Jeśli jakaś metoda ma być dostępna dla klas potomnych, ale nie powinna być widoczna na zewnątrz tych klas, to możesz nadać jej modyfikator protected. wszystkim metodom, które mają być widoczne w klasach potomnych i na zewnątrz powinieneś nadać modyfikator public. Istotne jest to, że można nadpisywać metody public i protected.

Pakiety

Pakiety służą do grupowania klas. Jako wyznacznik grupowania może nam posłużyć typ, z którego dziedziczą dane klasy lub typ interfejsu, który implementuje dana grupa klas. Pakiety w języku Java są bardzo powszechnie używane. Kryterium, którym się posłużymy do zebrania klas w jeden pakiet może być w zasadzie dowolne. Może to być jakaś funkcjonalność w danej aplikacji np. Zamówienia, Katalog produktów lub bardziej technicznie, mogą to być warstwy aplikacji np. Frontend, Backend lub bardziej klasycznie Widok, Model Kontroler. Poszczególne części nazwy pakietu oddzielone są kropką i każda z tych części odpowiada katalogowi na dysku komputera.

Każda klasa, która znajduje się w danym pakiecie musi mieć w pierwszej linijce zadeklarowaną nazwę pakietu:

Jeśli chcesz teraz użyć tej klasy w innym pakiecie (w innej klasie), musisz ją zaimportować:

Często spotykaną konwencją w nazewnictwie pakietów jest używanie nazwy domeny internetowej pisanej od końca z dopisaną nazwą funkcjonalną pakietu np. pl.nullpointerexception.pet

Podsumowanie

Poznanie technik programowania obiektowego pomaga w lepszej organizacji kodu i uelastycznieniu powiązań pomiędzy różnymi elementami aplikacji. Także w lepszym ustrukturyzowaniu ich. Warto jest ćwiczy różne rozwiązania, tak by się nimi swobodnie posługiwać. Mimo, że niektóre z tych mechanizmów są używane raczej rzadko, to znajomość ich pozwala w bardzo krótkim czasie rozwiązywać napotkane problemy.

 

Źródła:

https://pl.wikipedia.org/wiki/Polimorfizm_%28informatyka%29
https://docs.oracle.com/javase/tutorial/java/concepts/index.html
https://stackoverflow.com/questions/6308178/what-is-the-main-difference-between-inheritance-and-polymorphism/6308416

 

Kurs Java dla początkujących

Spis Treści:

  1. Wprowadzenie
  2. Klasy i Obiekty
  3. Tablice
  4. Kolekcje
  5. Instrukcje warunkowe i pętle
  6. Operacje wejścia i wyjścia
  7. Dziedziczenie, Polimorfizm, Interfejsy
  8. Stream’y i lambdy

Zapisz się na newsletter

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat i zachęcam Cię do lektury mojego bloga

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *