artykuł logo
Choinka
25 grudnia 2015
Wyjątki w języku Java
31 stycznia 2016
artykuł logo - dziedziczenie

W innych artykułach omawiałem pewne aspekty programowania obiektowego. Wiesz już o interfejsach i dlaczego warto ich używać. Dzisiaj przeczytasz o dziedziczeniu. Bez dziedziczenia nie można mówić o programowaniu obiektowym w Javie. Dowiesz się czym jest Object, dlaczego dziedziczenie jest ważne i kiedy powinniśmy go używać. Przeczytasz o przeciążaniu i nadpisywaniu metod. Poznasz też słowa kluczowe abstract i final. Do kodu!

To jest jeden z artykułów w ramach darmowego kursu programowania w Javie. Proszę zapoznaj się z pozostałymi częściami, mogą one być pomocne w zrozumieniu materiału z tego artykułu.

Dziedziczenie

Na początku postaram się wyjaśnić czym właściwie jest dziedziczenie. Nie jest to nic skomplikowanego.

Niektóre obiekty mogą mieć między sobą dużo wspólnego. Na przykład zarówno samochód osobowy jak i samochód ciężarowy mają silnik, kierownicę, drzwi, światła itd. Co prawda każdy z tych elementów może być różny, jednak bez wątpienia oba te pojazdy mają wiele wspólnego. Oba są pojazdami. Możemy powiedzieć, że samochód ciężarowy rozszerza (ang. extends) funkcjonalność pojazdu.

W naszym przykładzie pojazd możemy uznać, za tak zwaną klasę bazową (lub „nadklasę”). Natomiast samochód osobowy i samochód ciężarowy rozszerzają funkcjonalność pojazdu. Możemy też powiedzieć, że każda z nich jest klasą pochodną (lub „podklasą”). Proszę spójrz na przykład:

Dziedziczenie jest jedną z podstaw programowania obiektowego (nie tylko w języku Java). Dzięki dziedziczeniu możemy ograniczyć ilość zduplikowanego kodu poprzez definiowanie atrybutów, konstruktorów, metod w klasach bazowych.

Dziedziczenie może być wielopoziomowe, jednak w języku Java zawsze bezpośrednio możemy dziedziczyć od jednej klasy.

W przykładzie powyżej SUV dziedziczy po klasie Car. Klasa Car jest podklasą klasy Vehicle. Zatem pośrednio SUV także dziedziczy po klasie Vehicle.

Modyfikatory dostępu

Dzięki dziedziczeniu możemy mieć dostęp do metod, atrybutów, konstruktorów klas po których dziedziczymy. Do określenia czy dany element może być dostępny w ramach podklasy służą modyfikatory dostępu.

Do tej pory poznałeś modyfikatory dostępu takie jak:

  • public – element oznaczony tym modyfikatorem dostępny jest „z zewnątrz” obiektu, stanowi jego interfejs,
  • private – element oznaczony tym modyfikatorem jest dostępna wyłącznie wewnątrz obiektu, także klasy pochodne nie mają do niego dostępu.

W przypadku dziedziczenia znaczenie ma także modyfikator protected. Element poprzedzony tym atrybutem może być dostępny wewnątrz klasy bądź przez każdą inną klasę która po niej dziedziczy1

Przesłonięcie metody

Łatwo sobie wyobrazić sytuację, w której metoda o tej samej sygnaturze występuje zarówno w klasie bazowej jak i klasie pochodnej. W tej sytuacji mówimy o tym, że klasa pochodna przesłania metodę z klasy bazowej (ang. override). Proszę spójrz na przykład poniżej.

W naszym przykładzie wywołanie metody startEngine na obiekcie typu Car zmusi kierowcę do zapięcia pasów (wyświetli się komunikat Force driver to fasten seat belts). Jeśli tą samą metodę wywołamy na instancji obiektu klasy Vehicle wówczas pojawi się komunikat Engine starts. Brum brum brum..

Co jeśli chcielibyśmy nieznacznie zmodyfikować oryginalną metodę? Jest na to sposób. Słowo kluczowe super pozwala na wywołanie nadpisanej metody z klasy bazowej. Rozszerzając przykład powyżej moglibyśmy napisać taki fragment kodu:

W takim przypadku wywołanie metody startEngine na instancji obiektu Car na początku wywoła tą metodę z klasy bazowej (wyświetli się komunikat Engine starts...) następnie pokazany zostanie komunikat Force driver... (zachęcam do eksperymentowania z IDE).

Konstruktory a dziedziczenie

Konstruktory w przypadku dziedziczenia zachowują się tak samo jak metody. Także możemy wywołać konstruktor z klasy bazowej wewnątrz klasy dziedziczącej używając słowa kluczowego super (jeśli pozwala na to modyfikator dostępu).

Klasa pochodna musi mieć możliwość wywołania konstruktora klasy bazowej. Jeśli tego nie robi domyślnie wywoływany jest konstruktor bezparametrowy

W powyższym przykładzie wewnątrz konstruktora klasy Dog wywołujemy konstruktor klasy Animal wywołując super(). Jak napisałem wyżej możemy pominąć to wywołanie, wówczas kompilator zrobi to za nas. Ma to pewne konsekwencje. Jeśli w klasie bazowej zdefiniujemy konstruktor z parametrami wówczas konstruktor bezparametrowy nie zostanie utworzony automatycznie.

W takich przypadkach w konstruktorach klas pochodnych musimy wywołać konstruktor klasy bazowej. Pokazałem to w przykładzie poniżej:

Jak widzisz w przykładzie powyżej klasa Car nie musi definiować konstruktora z taką samą liczbą parametrów jak klasa bazowa, ale musi wywołać konstruktor klasy Vehicle i przekazać dwa parametry. Dzieje się tak, ponieważ w klasie Vehicle jest tylko konstruktor z dwoma parametrami.

Klasy abstrakcyjne

Czasami może wystąpić sytuacja, w której klasa bazowa jest swego rodzaju uogólnieniem, abstrakcją, która nie ma sensu bez konkretnych implementacji. Wówczas możemy mówić o klasie abstrakcyjnej.

Nie ma możliwości stworzenie instancji klasy abstrakcyjnej. W naszym przykładzie klasa Vehicle mogłaby być klasą abstrakcyjną. Klasy abstrakcyjne poprzedzamy słowem kluczowym abstract. Proszę spójrz na przykład poniżej.

Jak widzisz klasa abstrakcyjna może mieć konstruktor, jednak służy on tylko do tego, żeby uniknąć duplikacji kodu w klasach pochodnych. Klasa Car używa konstruktora zdefiniowanego w abstrakcyjnej klasie Vehicle.

Klasy i metody finalne

Możliwość dziedziczenia i nadpisywania metod daje bardzo duże możliwości. Wyobraź sobie następujący kod:

Co stanie się jeśli programista utworzy nową klasę jak w przykładzie poniżej?

Przy takiej implementacji konto docelowe zostałoby zasilone dodatkową kwotą jednak ta kwota nie byłaby pobrana z konta źródłowego. Niedobrze.

W takich przypadkach możemy użyć słowa kluczowego final. Słowo to umieszczone przed klasą oznacza, że nie możemy po danej klasie dziedziczyć. W przypadku metody oznacza, że metoda nie może zostać nadpisana.

Dla przykładu klasy w pakiecie java.lang są finalne. Nie można nadpisać ich implementacji.

Klasa java.lang.Object

Teraz już wiesz czym jest dziedziczenie. A wiesz, że używałeś go od pierwszej lekcji nauki języka Java? 🙂

Z jednego z poprzednich artykułów wiesz o tym, że kompilator dodaje automatycznie konstruktor bezparametrowy jeśli nie zdefiniujesz żadnego w swojej klasie. Podobnie jest z dziedziczeniem, każda klasa domyślnie dziedziczy po klasie java.lang.Object (chyba, że zdefiniujesz inną klasę po której dziedziczysz).

Dzięki tej klasie masz dostęp do zestawu metod, które zdefiniowane są w ciele klasy Object. Na przykład metoda String toString() ma swoją podstawową implementację w klasie Object 2.

Dobre praktyki

Dziedziczenie to bardzo pomocny mechanizm. Jak napisałem wcześniej pozwala nam na uniknięcie duplikowania kodu. Jednak ma też swoje wady. Hierarchie dziedziczenia, które mają dużo poziomów mogą stać się mało czytelne. Tak zagmatwany kod może stać się trudny w utrzymaniu. Powinniśmy unikać takiej sytuacji.

Nie ma tu jasnej reguły, jednak w przypadku gdy w programie występuje wielopoziomowe dziedziczenie starałbym się uprościć taki kod. Bardzo często mówi się o preferowaniu kompozycji nad dziedziczeniem. Kompozycja to nic innego jak zawarcie innego obiektu jako atrybut naszej klasy. Kompozycja w wielu przypadkach potrafi uprościć skomplikowane hierarchie dziedziczenia.

Zadanie

Na koniec mam dla Ciebie zadanie do wykonania, przećwiczysz w nim zagadnienia omówione w tym artykule.

Napisz program, w którym zasymulujesz hierarchię dziedziczenia zwierząt. Stwórz abstrakcyjną klasę Animal, po której będą dziedziczyły klasy Fish i Mammal. Wszystkie te klasy powinny być abstrakcyjne. Następnie stwórz konkretne klasy które dziedziczą po Fish i Mammal. Będą to odpowiednio Goldfish i Human.

Nadpisz metodę toString w każdej z tych klas. Stwórz instancje obu tych klas i wyświetl je na konsoli.

Jeśli miałbyś problemy z zadaniem możesz spojrzeć na przykładowe rozwiązanie.

Dodatkowe materiały

Poniżej przygotowałem dla Ciebie zestaw linków z dodatkowymi materiałami, część z nich jest w języku angielskim.

Podsumowanie

Bardzo się cieszę, że przeczytałeś artykuł do końca. Dzisiaj dowiedziałeś się czegoś więcej o dziedziczeniu. Poznałeś słowa kluczowe abstract i final. Wiesz już czym jest nadpisywanie metod czy klasa bazowa. Innymi słowy poznałeś kolejny zestaw narzędzi niezbędnych dla każdego programisty. Tak trzymaj! 🙂

Na koniec bardzo proszę Cię o podzielenie się artykułem ze swoimi znajomymi, którzy są zainteresowani taką tematyką. Jak zwykle zależy mi na tym, żeby z blogiem i jego zawartością dotrzeć do jak największej liczby czytelników takich jak Ty 🙂 Jeśli jeszcze tego nie zrobiłeś prosiłbym o polubienie strony na facebooku.

Do następnego razu!

Newsletter

  Jeśli chcesz otrzymywać informacje o nowych artykułach na blogu prosto na Twój email, zapisz się 🙂

  1. Istnieje też „brak modyfikatora dostępu” jednak na początek możemy tą sytuację pominąć. Opiszę to w osobnym artykule.
  2. Domyślna implementacja pokazuje nazwę klasy wraz z pakietem oraz jej adres w pamięci np. pl.samouczekprogramisty.kursjava.cars.X@14ae5a5
  • Łukasz

    Czy tu nie powinno być:

    ponieważ w klasie Vehicle mamy konstruktor

    • Marcin Pietraszek

      Nie musi tak być. Zauważ, że wewnątrz konstruktora klasy Car wywołuję super(engine, NUMBER_OF_WHEELS), czyli konstruktor z dwoma parametrami z nadklasy. W Twoim przykładzie parametr numberOfWheels nie jest nawet wykorzystywany.

  • lukasz

    Czyli jak w nadklasie mam konstruktor z dwoma parametrami to w podklasie ten sam konstruktor mogę zdefiniować z jednym parametrem?

    • Marcin Pietraszek

      Proszę, przeczytaj jeszcze raz akapit „Konstruktory a dziedziczenie”.

      Nie możesz powiedzieć „ten sam konstruktor” w odniesieniu do dwóch różnych klas. To są konstruktory dwóch osobnych klas i liczba parametrów w każdym z nich może być zupełnie inna.

  • lukasz

    Czy można ten przykład wyjaśnić dokładniej? Coś tu nie rozumiem. Chodzi o kod dotyczący tego zdania:

    „Przy takiej implementacji konto docelowe zostałoby zasilone dodatkową kwotą jednak ta kwota nie byłaby pobrana z konta źródłowego. Niedobrze.”

    • Marcin Pietraszek

      Porównaj dwie implementacje metody transferMoney w klasach Transfer i FraudTransfer. Czym one się różnią, dlaczego ta druga jest niepoprawna?

      Tutaj pomaga słowo kluczowe final, które nie pozwoli nadpisać tej metody.

  • lukasz

    Hej. Proszę o podpowiedź. Metoda druga jest niepoprawna ponieważ została nadpisana a tego nie chccemy w tym przypadku? Implementacja tej drugiej wydaje sie poprawna

    • Marcin Pietraszek

      Transfer pieniędzy to „usunięcie” środków konta A i „dodanie” środków do konta B. W drugiej metodzie mamy tylko część tej operacji. Musisz dokładniej analizować kod. Podawanie rozwiązania na tacy jak w tym przypadku nie jest dobre i nie przyspiesza nauki.

  • lukasz

    OK. Ja tak myśłałem że oto chodzi ale ja jak to ja zawsze wolałem być na 100% pewny

  • charlesw

    Cześć Marcin,
    Dlaczego przy zmianie metody z toString na inną (u mnie np. w klasie Human występuje metoda public String nazwijSie(){return "jestem spoko kolo";}, konsola wyrzuca mi przy System.out.println(human) adres obiektu Human (Human@7f31245a)?
    W Twoim przykładzie System.out.println przyjmuje wartość wpisaną w ciapki w metodzie toString.
    Próbowałem po human.toString, human.nazwijSie itp. ale nie wychodzi.
    Chodzi o to, że w przypadku dziedziczenia po klasie, która nei zawiera metody, automat podstawia dziedziczenie jedynie z Object i tę metodę możemy nadpisać jedynie swoją własną?
    Jeśli chciałbym prócz nadpisywania metod, utworzyć swoją własną, jak w przykładzie wyżej (metoda nazwijSie ze stringiem w środku), to co musiałbym wpisać w klasie Main, aby system.out.printn wyrzucił mi „jestem spoko loko” w Stringu, a nie adres obiektu?

    • Marcin Pietraszek

      Zobacz jak wygląda ciało metody println (możesz to zrobić w IDE klikając w nazwę metody z wciśniętym Ctrl).

      Wewnątrz tej metody wywoływana jest metoda toString(), więc nie możesz nic „wpisać” w twojej klasie Main żeby to zmienić.

      W trakcie wykonania programu wybrana jest pierwsza implementacja metody zgodnie z hierarchią dziedziczenia, jeśli nie nadpiszesz tej metody pierwszą (i za razem ostatnią) będzie ta domyślna zdefiniowana w klasie Object.

      • charlesw

        Dzięki 🙂