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.

 

Co powinien wiedzieć każdy początkujący programista?

 

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

 

Ź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

 

Mini kurs testy jednostkowe

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat z czego ponad 8 programuję w Javie. Zapraszam Cię do lektury mojego bloga. Możesz przeczytać więcej o mnie >>TUTAJ<<

6 thoughts to “Kurs Java dla początkujących – #7 Dziedziczenie, Polimorfizm, Interfejsy”

  1. Bardzo dobrze się to czyta , krótko zwięźle i na temat , 🙂 Pozdrawiam serdecznie i zachęcam do wrzucania dalszych materiałów.

  2. Mateuszu, nie wiem, czy się zgodzisz, ale myślę, że warto tutaj podkreślić jedną rzecz. Mam wrażenie, że użyte w tekście przykłady dziedziczenia klas i implementacji interfejsów nie do końca obrazują naturę problemu. W obu podanych przykładach pokazujesz przekazywanie funkcjonalności (metody walk(), eat()…). Warto tu chyba podkreślić, że jednak dziedziczenie jest istotne dla odzwierciedlenia struktury obiektowości, wyszczególnienia cech wspólnych – niekoniecznie zachowań (relacja pies jest zwierzęciem), a interfejsy propagują funkcjonalności niezależnie od struktury dziedziczenia.

    1. Cześć Kamil dzięki za komentarz. Trochę zgadzam się z tobą 😉 Ale weź pod uwagę to, że ten kurs jest dla początkujących i starałem się to przedstawić w taki sposób, żeby było zrozumiałe właśnie dla takich osób. Starałem się to przedstawić w miarę prostej formie, żeby nie przytłaczać wiedzą.

      Trudno jest znaleźć dobry przykład, w którym wytłumaczysz osobie początkującej, co to jest dziedziczenie i jednocześnie, żeby ten przykład był sensowny dla osoby z większym doświadczeniem.

      W wielu kursach jest bardzo szczegółowo pokazane, co to jest dziedziczenie (tak mniej więcej jak ty to opisałeś), ale też w tych kursach nie jest opisane, z jakimi problemami się to wiąże. Ludzie czytają te wszystkie kursy i rozumieją je dosłownie. Później stosują tę wiedzę w praktyce: „skoro mogę współdzielić wszystko, to jest to dobre”. No nie jest. Dlatego skupiłem się na dziedziczeniu zachować i wybrałem takie przykłady. Uważam, że takie podejście do dziedziczenia jest w miarę bezpieczne. To, co ja myślę po kilkunastu latach o dziedziczeniu, jest bardzo odległe od tego, jak uczą się tego początkujący programiści. Ale to nie jest wiedza na kurs dla początkujących 😉

      1. Oceniam trochę ten materiał z perspektywy osoby, która ma już jakieś doświadczenie, ale jeszcze dość dobrze pamięta swoje trudne początki i nie jestem w 100% przekonany, czy gdybym od tego wpisu zaczął swoją przygodę, to czy widziałbym wyraźną różnicę pomiędzy interfejsem, a klasą bazową na podstawie tych przykładów, a jak jeszcze dorzucimy do interfejsu metodę defaultową, to już w ogóle :).

        Szukanie dobrych przykładów to największa tajemnica edukowania ludzi, dlatego ja tego nie robię ;).

        Komentarz poczyniłem z premedytacją. Może jakieś nowego adepta dodatkowo zaswędzi pod kopułką i doczyta trochę, poszuka innych przykładów.

        1. „… i nie jestem w 100% przekonany, czy gdybym od tego wpisu zaczął swoją przygodę, to czy widziałbym wyraźną różnicę pomiędzy interfejsem, a klasą bazową na podstawie tych przykładów, a jak jeszcze dorzucimy do interfejsu metodę defaultową, to już w ogóle :).” dzięki Kamil za ten komentarz teraz lepiej rozumiem twój punkt widzenia. Masz rację, można nie zauważyć różnicy. Postaram się zredagować ten artykuł, tak żeby bardziej było to widoczne. Dzięki 😉

Dodaj komentarz

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