Twitter: @grzegg
Kategoria: java, Tagi: - - - - .

Java [25] – Klasy i obiekty cz. 3: dziedziczenie

Ważnym elementem programowania obiektowego w Javie jest dziedziczenie, które pozwala na stworzenie klas, które „dziedziczą” pewne elementy (pola, metody) po innej klasie a jednocześnie pozwalają je zmodyfikować i dodać nowe.

Dziedziczenie – podstawy

Dziedziczenie to ważny mechanizm stosowany w programowaniu obiektowym. Dzięki niemu na podstawie klas istniejących tworzy się nowe klasy, które „dziedziczą” po swoich pierwowzorach dane składowe i metody (niekoniecznie wszystkie). Można więc wyróżnić:

  • klasy nadrzędne (bazowe, superklasy) – na podstawie których tworzy się następne klasy
  • klasy dziedziczące (pochodne, potomne) – tworzone na podstawie klas nadrzędnych, które dziedziczą po nich elementy

Klasa może być jednocześnie klasą bazową dla innych klas i klasą dziedziczącą po innych klasach. Dzięki czemu można tworzyć „łańcuchy” dziedziczących po sobie klas: KlasaA -> KlasaB -> KlasaC -> …. Należy pamiętać, że klasa może dziedziczyć bezpośrednio tylko po jednej klasie, co należy rozumieć w ten sposób, że klasa nie może mieć jednocześnie dwu lub więcej superklas, choć oczywiście klasa wobec niej nadrzędna może dziedziczyć po innej klasie bazowej.

W zasadzie każda klasa w Javie jest klasą potomną klasy Object, po której dziedziczy szereg metod. Można się z nimi zapoznać w dokumentacji. Zaraz zresztą się o tym przekonamy.

Tworzenie klas potomnych.

Klasę potomną tworzymy z użyciem słowa kluczowego extends. Utwórz nowy projekt a w nim trzy klasy:

  • Dziedziczenie – zawierająca metodę main.
  • Nadrzedna
  • Pochodna

Jak łatwo się domyślić, klasa Pochodna będzie dziedziczyć po klasie Nadrzedna. Dlatego zaczniemy od tej drugiej:

Zawartość tej klasy nie wymaga komentarza (o ile opanowałeś czytelniku poprzednie lekcje).

Teraz wypełnijmy treścią (na razie niewielką) metodę Dziedziczenie:

W metodzie main został utworzony obiekt typu Nadrzedna, została na nim wywołana metoda drukujDane() a w kolejnej linijce widzimy polecenie wydrukowania wyniku działania metody toString(). Zaraz, zaraz… przecież nie ma takiej metody w klasie Nadrzedna! Owszem, nie napisaliśmy takiej, ale znajduje się ona w klasie Object o czym można się przekonać wspomnianą powyżej dokumentację klasy Object. Widać na tym przykładzie, że ponieważ każda klasa jest pochodną klasy Object, może używać metod, które wchodzą w jej skład.

Wynik uruchomienia programu powinien być mniej więcej taki:


nazwa: Pierwsza
liczba: 1
metoda toString(): dziedziczenie.Nadrzedna@677327b6

Jak widać metoda toString() zwróciła ciąg znaków, z którego możemy wyczytać nazwę pakietu, klasy oraz tajemniczy ciąg liter i cyfr. Później postaramy się, żeby robiła coś bardziej pożytecznego.

Teraz jednak czas na klasę pochodną względem klasy Nadrzedna. Samo nazwanie jej Pochodna nie sprawi, że stanie się pochodną. W tym celu należy do linii w której deklarujemy klasę dodać słowo kluczowe extends oraz nazwę klasy względem niej bazowej, w tym wypadku będzie to Nadrzedna:

Na razie nie dopisaliśmy do niej żadnych pól ani metod, ale można wykonać następujący kod dopisując go na końcu metody main w klasie Dziedziczenie:


Pochodna poch = new Pochodna();
poch.drukujDane();
System.out.println("metoda toString(): " + poch.toString());

Po uruchomieniu programu otrzymamy:


nazwa: Pierwsza
liczba: 1
metoda toString(): dziedziczenie.Nadrzedna@677327b6
nazwa: Pierwsza
liczba: 1
metoda toString(): dziedziczenie.Pochodna@14ae5a5

Jak widać klasa Pochodna odziedziczyła po klasie Nadrzedna jej pola i metody. Na razie nie widać specjalnie różnicy między obiektami utworzonymi na podstawie obu klas (poza wartością zwracaną przez metodę toString()). Dopiszmy metodę do klasy Pochodna:


public void kimJestes() {
    System.out.println("Jestem Podrzędna");
}

W metodzie main klasy Dziedziczenie dopisujemy:


poch.kimJestes();

w związku z czym po uruchomieniu programu pojawi się dodatkowa linijka wyniku:


Jestem Podrzędna

Metoda kimJestes() jest właściwa klasie Pochodna, klasa nadrzędna nie „widzi” jej. Tak więc klasa Pochodna jest teraz rozszerzeniem klasy Nadrzedna, nie tylko dziedziczy po niej metody i pola, ale także posiada dodatkową funkcjonalność.

Powoli dochodzimy do sensu stosowania dziedziczenia. Jest ono użyteczne wtedy, gdy chcemy stworzyć klasę, która jest w jakiś sposób podobna do klasy już istniejącej. Chcemy na przykład dopisać jakieś metody, czy dane składowe. Istnieje oczywiście możliwość skopiowania kodu jednej klasy do drugiej i uzupełnienie go o dodatkowe elementy, ale przecież nie ma sensu kilkakrotnie umieszczać tego samego kodu w programie, skoro można tego uniknąć

Dodajemy konstruktory

W klasie Nadrzedna a co za tym idzie w klasie Pochodna mamy dwa pola. Mają one na dzień dobry wpisane wartości, ale teraz dodamy konstruktory, które będą ustawiać ich wartości. Zacznijmy od klasy Nadrzedna, która teraz będzie wyglądać tak:

Teraz NetBeans się buntuje, wymagając po pierwsze poprawek w klasie Dziedziczenie w linii w której tworzymy obiekt nad. Trzeba ją uaktualnić tak, aby odpowiadała utworzonemu konstruktorowi:


Nadrzedna nad = new Nadrzedna("Nadrzedna", 1);

Przypominam, że kiedy tworzymy konstruktor, „znika” konstruktor domyślny nie przyjmujący argumentów. Z tego powodu NetBeans zgłasza błąd w klasie Pochodna. Ponieważ nie ma już konstruktora domyślnego w klasie nadrzędnej, klasa podrzędna także go nie posiada. Kiedy istnieje w superklasie konstruktor bezargumentowy, wywoływany jest z klasy dziedziczącej automatycznie, jednak gdy go nie ma, należy wywołać istniejący konstruktor klasy nadrzędnej z konstruktora klasy podrzędnej, dostarczając mu odpowiednich argumentów.

No dobrze, ale jak wywołać konstruktor klasy bazowej? Można to zrobić posługując się poleceniem super(), które wskazuje na element „powyżej” w hierarchii klas, w przypadku gdy wywoływany jest sam (ewentualnie z argumentami) odnosi się do konstruktora klasy bazowej. Komenda ta powinna być pierwszą w konstruktorze klasy pochodnej. Uzbrojeni w tą wiedzę, dopisujemy konstruktor do klasy Pochodna:

Pozostaje jeszcze poprawić linijkę w której tworzymy obiekt poch w klasie Dziedziczenie:

Pochodna poch = new Pochodna("Pochodna", 2);

Program w obecnej wersji wyświetli taki rezultat:


nazwa: Nadrzedna
liczba: 1
metoda toString(): dziedziczenie.Nadrzedna@677327b6
nazwa: Pochodna
liczba: 2
metoda toString(): dziedziczenie.Pochodna@14ae5a5
Jestem Podrzędna

Skoro dodaliśmy pole wazna w klasie Pochodna możemy dostosować konstruktor tak, aby przyjmował i ustawiał wartość tej zmiennej. Zatem konstruktor może wyglądać tak:


public Pochodna(String nazwa, int liczba, boolean wazna) {
    // Wywołanie konstruktora superklasy
    super(nazwa, liczba);
    this.wazna = wazna;
}

W tej sytuacji trzeba też poprawić komendę tworzącą obiekt poch w klasie Dziedziczenie:


Pochodna poch = new Pochodna("Pochodna", 2, true);

Nadpisywanie metod

Skoro klasa pochodna jest zwykle rozszerzeniem czy doprecyzowaniem klasy nadrzędnej to może się okazać, że metoda znajdująca się w klasie znajdującej się wyżej w hierarchii, powinna być rozszerzona albo zmodyfikowana w klasach dziedziczących. Pokażę to na przykładzie metody toString(), która znajduje się w klasie Object i którą wywoływołujemy na obiektach utworzonych w klasie Dziedziczenie w ten sposób:


System.out.println("metoda toString(): " + nad.toString());

Tak naprawdę, jest to pewna nadgorliwość, bowiem można równie dobrze tą linię kodu napisać tak:


System.out.println("metoda toString(): " + nad);

Po prostu metoda println() automatycznie wywołuje metodę toString(), która (jak można przeczytać w dokumentacji) zwraca „reprezentację obiektu w postaci łańcucha znaków”. Na razie to co zwraca ta metoda nie jest dla nas zbyt przydatne. Można jednak sprawić, że będzie na przykład w czytelny sposób informowała nas o tym jakiej klasy obiekt jest instancją (czyli na podstawie jakiej klasy został utworzony) a także wypisywała wartości wszystkich pól. Robimy to w ten sposób, że w klasie potomnej napiszemy własną metodę o nazwie toString(). Nazywamy to nadpisywaniem (ang. overriding). Tak więc w klasie Nadrzedna dopiszmy kod:


public String toString() {
    return "\ninstancja klasy: " + getClass().getName() + "\n" +
         " nazwa:        " + nazwa + "\n" +
         " liczba:     " + liczba;
}

Wyrażenie getClass().getName() pozwala, jak się można domyślić, uzyskać informację na temat klasy której instancją jest obiekt.

Teraz po uruchomieniu programy uzyskamy taki rezultat:


nazwa: Nadrzedna
liczba: 1
metoda toString():
instancja klasy: dziedziczenie.Nadrzedna
nazwa:        Nadrzedna
liczba:     1
nazwa: Pochodna
liczba: 2
metoda toString():
instancja klasy: dziedziczenie.Pochodna
nazwa:        Pochodna
liczba:     2
Jestem Podrzędna

Jak widać, nie tylko obiekt typu Nadrzedna ale także typu Pochodna, który dziedziczy implementację metody toString() pokazują zmienione dane. Jednak w klasie Pochodna dopisaliśmy jedno pole ważna. Należałoby je także uwzględnić w metodzie toString(). Piszemy zatem jej implementację także w klasie Pochodna:


public String toString() {
    return "\ninstancja klasy: " + getClass().getName() + "\n" +
         " nazwa:        " + nazwa + "\n" +
         " liczba:     " + liczba + "\n" +
         " ważna:        " + wazna;
}

W zasadzie teraz wszystko działa jak powinno, ale być może zwróciłeś uwagę, że piszemy dwa razy ten sam kod w obu klasach. Czy nie dałoby się jakoś wywołać z metody Pochodna metody toString() z klasy Nadrzedna, tak jak robiliśmy powyżej to z konstruktorem? Owszem da się, użyjemy do tego słowa kluczowego super, tym razem jednak będzie ono uzupełnione po kropce o nazwę wywoływanej metody:


public String toString() {
    return super.toString() + "\n" +
         " ważna:        " + wazna;
}

W ten sposób można wywołać nadpisane metody z klasy nadrzędnej.

Przy okazji najwyższy czas usunąć z klasy Nadrzedna metodę drukujDane(), która stała się zbędna, oraz odwołania do niej w klasie Dziedziczenie.

Dostęp do elementów klasy

Ogólnie, można odwoływać się do elementów klasy nadrzędnej, zarówno pól jak i metod, w ten sposób: super.element. Należy jednak uważać, bo zwracane wartości mogą zaskoczyć. Zmodyfikuj nieco klasy Nadrzedna i Pochodna, aby wyglądały tak jak poniżej. Przy okazji podaję obecną wersję klasy Dziedziczenie.

Następnie przeanalizuj wynik, który się pokaże. Zwróć uwagę które komendy zwracają jakie wartości, zwłaszcza w przypadku pól liczba i innaLiczba. Zastanów się dlaczego.

Kończąc wątek dostępu do elementów klasy warto wspomnieć o sposobach blokowania do nich dostępu:

  • Jeśli chcemy zablokować innym klasom, także klasom potomnym dostęp do poszczególnych elementów, używamy modyfikatora private.
  • Jeśli chcemy żeby do nich miały dostęp tylko klasy potomne, używamy modyfikatora protected.

Klasy abstrakcyjne

Klasę deklarujemy jako abstrakcyjną używając słowa kluczowego abstract:


abstract class MojaKlasa {
...
}

Klasy abstrakcyjne tworzy się, gdy chcemy stworzyć coś w rodzaju „szkicu” dla klas potomnych. Czyli gdy chcemy opisać jakie pola i metody powinny znaleźć się w klasach potomnych ale na poziomie klasy nadrzędnej niekoniecznie możemy (lub chcemy) je całkowicie zaimplementować. Nie oznacza to, że nie można w klasie abstrakcyjnej stworzyć w pełni funkcjonalnych metod czy pól, można tak zrobić jeśli jest to potrzebne. Nie można natomiast stworzyć obiektu na podstawie klasy abstrakcyjnej, jest to możliwe dopiero dla klas potomnych o ile one też nie są abstrakcyjne.

Te „nie do końca zaimplementowane” metody, znajdujące się w klasie abstrakcyjnej, określamy także jako metody abstrakcyjne używając modyfikatora abstract tak:


abstract zwracanaWartosc nazwaMetody(){
    ...
}

Zadeklarowanie, że metoda jest abstrakcyjna, powoduje, że w klasie potomnej musi być ona nadpisana, o ile klasa pochodna nie jest także abstrakcyjna. Oznacza to, że klasie potomnej, „nieabstakcyjnej” muszą być zaimplementowane wszystkie metody abstrakcyjne klasy nadrzędnej. Należy też pamiętać, że nie tworzy się abstrakcyjnych konstruktorów ani abstrakcyjnych elementów statycznych (static) oraz oczywiście oznaczonych jako final. Ten ostatni modyfikator można natomiast można użyć w sytuacji, gdy chcemy uniemożliwić nadpisanie metody w klasach potomnych. Metoda zadeklarowana w ten sposób:


final void metodaPerfekcyjna(){
    ...
}

nie może już być nadpisana.

To samo kluczowe użyte przy deklaracji klasy, powoduje, że na podstawie tej klasy nie można tworzyć klas potomnych. Oczywiście nie można na raz używać modyfikatorów abstact oraz final.

Nie można tworzyć metod abstrakcyjnych w klasie, która nie jest abstrakcyjna. Jeśli choć jedna z metod jest abstrakcyjna, cała klasa musi też być zadeklarowana jako abstrakcyjna. Można natomiast napisać klasę abstrakcyjną, która nie zawiera żadnych metod abstrakcyjnych. Robi się tak na przykład w przypadkach, gdy kod klasy abstrakcyjnej wymaga dopisania jakiejś dodatkowej funkcjonalności aby być użyteczny.

Tablice obiektów

Jeśli chcemy przechowywać obiekty w tablicy to oczywiście tworzymy tablicę odpowiedniego typu. Przy czym możemy w takiej tablicy przechowywać obiekty będące instancjami klas pochodnych. Na przykład w klasie Dziedziczenie możemy dopisać taki kod:


Nadrzedna[] tablicaNad = new Nadrzedna[2];
tablicaNad[0] = nad;
tablicaNad[1] = poch;

W zasadzie można by stworzyć tablicę typu Object, po której dziedziczą pozostałe klasy:


Object[] tablicaObj = new Object[2];
tablicaObj[0] = nad;
tablicaObj[1] = poch;

W drugą stronę jednak się nie da. Taki kod, dokładnie druga linia, będzie błędny:


Pochodna[] tablicaPoch = new Pochodna[3];
tablicaPoch[0] = nad;
tablicaPoch[1] = poch;

Zadanie

  • Stwórz klasę Organizm zawierającą dowolne pola i metody, które można by przypisać wirtualnej wersji dowolnego organizmu.
  • Następnie stwórz klasy Prokariont oraz Eukariont dziedziczące cechy po klasie Organizm ale także dostosowując je do charakterystyki tych dwóch grup organizmów
  • Następnie napisz klasy Zwierze oraz Roslina, które będą rozszerzać klasę Eukariont
  • W osobnej klasie napisz kod, który stworzy obiekty typu Prokariont, Zwierze oraz Roslina oraz ich użyje, najlepiej w formie interakcji między tymi obiektami (np. zwierzę „zjada” roślinę albo „walczy” z innym zwierzęciem)

1 comment to Java [25] – Klasy i obiekty cz. 3: dziedziczenie

  • Tadeusz Sobiło

    Mam pytanie „Dodaj do elementów klasy:”

    po wykonaniu kodu otrzymuję:
    „Nadrzędna ————-
    Metoda toString():
    Instancja klasy: dziedziczenie.Nadrzędna nazwa: Nadrzędna, liczba: 1 innaLiczba: 3

    Pochodzna ————–
    Metoda toString():
    Instancja klasy: dziedziczenie.Pochodna nazwa: Pochodna, liczba: 2 innaLiczba: 3, ważna true innaLiczba: 7 super.liczba: 2 super.innaLiczba: 3
    Jestem Podrzędną!”

    na moje oko powinno być — Pochodna — 2 — 3 — true — 7 — 1 — 3
    bo mamy super.liczba super.innaLiczba – czyli wartości z klasy nadrzędnej

    Pochodna liczba 2 bo przekazane wartości obiektowi w klasie dziedziczenie natomiast inna liczba 3 tu wartość wzięta z klasy nadrzędnej bo nie przypisaliśmy jeszcze nowej wartości. Od ważne true działa nowy konstruktor ale true i 7 jest dobrze przypisane natomiast super.liczba powinno podać 1 z nadrzędnej a podaje 2 z podrzędnej, zaś super.innaLiczba działa tak jak oczekiwałem podała wartość z Nadrzędnej.

    Pozdrawiam i proszę o odp czy coś nie tak myślę Tadeusz.

Leave a Reply