Dziedziczenie w Javie – polimorfizm
W poprzedniej części szkolenia podjęliśmy temat wprowadzania i wyświetlania danych wejściowych.
Nadszedł czas na powrót do programowania obiektowego, a dokładniej do kluczowej części czyli dziedziczenia. Na początek przypomnijmy, że każda klasa w Javie dziedziczy po klasie bazowej Object. Możliwe jest dziedziczenie tylko po jednej klasie własnej – nie występuje tu mechanizm znany np. z języka C++ czyli dziedziczenia wielokrotnego.
Relacje między klasami
Aby dobrze zrozumieć relacje zachodzące między klasą bazową a klasą dziedziczną należy przypomnieć pojęcia znane z algebry takie jak podzbiór i nadzbiór.
Relacja inkluzji
A ⊂ B ⇔ ∀ x ( x ∈ A ⇒ x ∈ B) – co należy rozumieć, że każdy element zbioru A należy do zbioru B (ale zbiory nie muszą być równe – B może posiadać więcej elementów).
Niech A={1,2,3} i B={1,2,3,4}, wtedy A ⊂ B
Niech A={1,2,3,5} i B={1,2,3,4}, wtedy A ⊄ B, gdyż zawiera element, który nie należy do B.
Inkluzja a dziedziczenie w Javie
Odniesienie relacji inkluzji zbiorów do klas jest następujące – nadklasa jest podzbiorem podklasy. Intuicyjnie wydawało by się, że to klasa nadrzędna zawiera, więcej funkcji i daje więcej możliwości. W rzeczywistości podklasa posiada wszystkie elementy nadklasy oraz dodatkowe cechy wyłącznie do dyspozycji podklasy.
Przy dziedziczeniu należy pamiętać, że klasa dziedziczy wszystkie pola.
Dziedziczenie
Przejdziemy teraz do przykładu już spoza teorii matematyki i przyjrzymy się przykładowi z życia codziennego. Wyobraźmy sobie, że próbujemy zaimplementować produkty produkty spożywcze, które będą częścią aplikacji Dieta.
Założenia koncepcyjne
Zacznijmy od opisania całego problemu. Nasza aplikacja powinna pomagać kontrolować ilość spożywanych kalorii przez użytkownika. Dlatego ogólną cechą, która będzie polem każdego obiektu będzie kaloryczność oraz wartości odżywcze (białka, węglowodany i tłuszcze).
Żywność możemy podzielić na napoje oraz jedzenie. Robimy tak dlatego, że napoje będziemy odmierzać w litrach, natomiast jedzenie w gramach w związku z czym potrzebujemy zupełnie innej jednostki dla żywności odpowiedniego typu. Jednakże kaloryczność oraz makroelementy są wspólne dla obu bardziej szczegółowych typów żywności.
Poziomy abstrakcji
Przechodzenie na kolejne poziomy abstrakcji można kontynuować bardzo długo. Jedzenie możemy dalej podzielić na wegetariańskie i mięsne, napoje na alkoholowe i zwykłe, a te z kolei na gazowane i niegazowane. My jednak zatrzymamy się na bardzo ogólnym podziale czyli jedzenie i napoje gdyż naszym celem jest pokazanie mechanizmów dziedziczenia klas.
Poniżej pokażemy uproszczony model klas.

Załóżmy teraz, że chcemy dodatkowo posłodzić nasz napój i dodać cukru. Czy metoda odziedziczona pokaże nam prawidłową kaloryczność? Niestety musimy napisać dodatkową metodę, która obliczy nam kaloryczność napoju po dodaniu do niego cukru. Do tego napiszemy klasę, która będzie dziedziczyć po napojach
Dlaczego nie dopisać tej metody po prostu do klasy napój? Ponieważ w naszym modelu jest ona już osobnego typu. Przykładem niech będzie sok z owoców – jeśli dodamy do niego cukru lub w jakikolwiek sposób zmienimy jego skład dodając wodę przestaje być sokiem – zostaje np. nektarem. My po prostu zrobimy dodatkową klasę, która będzie posiadała wszystkie cechy Napoju oraz da nam możliwość zmodyfikowania go dodając cukier. Klasę nazwiemy WlasnyNapoj
.
Słowo kluczowe extends
Aby przekazać klasę do dziedziczenia należy użyć słowa kluczowego extends. Składnia wyrażenia wygląda następująco.
1 2 3 4 5 6 |
class Parent { private String dane; public void getDane(){ System.out.println("Dane rodzica: " + dane); } } |
1 2 3 4 5 6 |
class Child extends Parent{ //pola, metody i konstruktory są identyczne jak w klasie Parent public void getDane(){ System.out.println("Dane dziecka: " + dane); } } |
Mechanizm dziedziczenia bardzo ogranicza rozmiar kodu. Wszystko, co znajduje się w klasie wyżej automatycznie „przechodzi” do klasy pochodnej. W przykładzie powyżej w klasie Child
korzystamy z pola, którego nie definiowaliśmy w niej. To pole zostało odziedziczone z klasy Parent
.
Polimorfizm i przesłanianie metod
W przypadku, gdy odziedziczona metoda nie pasuje już w klasie podrzędnej można ją przesłonić (ang. override). Taką czynność nazywamy polimorfizmem. Polega to na napisaniu na nowo metody w klasie pochodnej.
Metoda getDane()
, która wyświetla zawartość pola obiektu, ale najpierw wypisuje stały fragment np. „Dane rodzica: „. Dlatego należy ją przesłonić dzięki czemu metoda zadziała prawidłowo.
Metoda przesłaniana musi zwracać ten sam typ, co metoda pierwotna, jednakże może posiadać różne argumenty.
Słowo kluczowe super
Aby dostać się do metody klasy nadrzędnej należy użyć słowa kluczowego super (w przeciwnym wypadku użyjemy metody, która znajduje się wewnątrz klasy, której jest dany obiekt.
Wywoływanie metod i konstruktorów klas nadrzędnych
Słowo kluczowe super możemy użyć także w przypadku konstruktorów. Poniżej pokażemy przykłady jak wywoływać konstruktor i metody klasy nadrzędnej. Poniżej pokazujemy przykład.
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Parent{ private String dane; public Parent(String d){ dane = d; } public String getDane(){ return dane; } private void ukrytaMetoda(){ System.out.print("prywatna metoda klasy Parent"); } } |
1 2 3 4 5 6 7 8 9 10 |
public class Child extends Parent{ private int dane2; public Child(int d, int d2){ super(d) dane2 = d2; } public String getDane{ return super.getDane() + dane2; //automatyczna konwersja int na String } } |
Spójrzmy na konstruktor klasy Child
. Słowo super powoduje wywołanie konstruktora klasy nadrzędnej w związku z czym w dalszej części pozostaje tylko zainicjować pola specjalne niedostępne w klasie Parent
.
Metoda getDane()
z klasy podrzędnej zwraca sumę obu wartości przez wywołanie metody getDane()
z klasy nadrzędnej i dodanie wartości z klasy Child
.
Modyfikatory dostępu
Wcześniej wspominaliśmy, że dziedziczone są różne elementy z klasy nadrzędnej. W tej sekcji omówimy jak modyfikatory dostępu wpływają na widoczność w klasach potomnych. Natomiast bardziej szczegółowo omawiamy je w artykule o modyfikatorach dostępu.
Modyfikator dostępu public
Najmniej restrykcyjny modyfikator dostępu. Wszystkie metody i pola widoczne są w klasie podrzędnej i można z nich bez problemu korzystać.
Przykładem jest wykorzystanie konstruktora Parent
oraz metody getDane()
przy użyciu słowa kluczowego super.
1 2 3 4 5 6 7 8 9 10 |
//konstruktor public Child(int d, int d2){ super(d) dane2 = d2; } //metoda public String getDane{ return super.getDane() + dane2; } |
Modyfikator dostępu protected
W kategorii dziedziczenia efekty użycia protected będą identyczne jak public. Oznacza to, że metody i pola będą widoczne w klasie potomnej.
Modyfikator dostępu private
Najbardziej restrykcyjny modyfikator, który ukrywa wszystko wewnątrz klasy. Jednakże nie należy mylić tego modyfikatora jako blokowanie dziedziczenia. Pole będzie widoczne w podklasie, a dostęp do niego zapewnia metoda getDane()
.
Inaczej zachowują się metody. Jeśli są prywatne, to nie ma do nich dostępu. Przykładem jest ukrytaMetoda()
w klasie Parent
. Próba wywołania tej metody w klasie Child
lub na jej obiekcie spowoduje poniższy błąd.
Exception in thread „main” java.lang.Error: Unresolved compilation problem:
The method ukrytaMetoda() from the type Parent is not visible
at Child.ukrytaMetoda(Child.java:11)
at Child.main(Parent.java:18)
W kolejnej części omówimy hierarchię dziedziczenia, rzutowanie oraz klasy abstrakcyjne