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:
1 2 3 4 5 6 7 8 9 10 11 |
package dziedziczenie; public class Nadrzedna { String nazwa = "Pierwsza"; int liczba = 1; public void drukujDane() { System.out.println("nazwa: " + nazwa + "\n" + "liczba: " + liczba); } } |
Zawartość tej klasy nie wymaga komentarza (o ile opanowałeś czytelniku poprzednie lekcje).
Teraz wypełnijmy treścią (na razie niewielką) metodę Dziedziczenie
:
1 2 3 4 5 6 7 8 9 10 11 12 |
package dziedziczenie; public class Dziedziczenie { public static void main(String[] args) { Nadrzedna nad = new Nadrzedna(); nad.drukujDane(); System.out.println("metoda toString(): " + nad.toString()); } } |
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
:
1 2 3 4 5 6 |
package dziedziczenie; public class Pochodna extends 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package dziedziczenie; public class Nadrzedna { String nazwa; int liczba; public Nadrzedna(String nazwa, int liczba){ this.nazwa = nazwa; this.liczba = liczba; } public void drukujDane() { System.out.println("nazwa: " + nazwa + "\n" + "liczba: " + liczba); } } |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package dziedziczenie; public class Pochodna extends Nadrzedna{ boolean wazna; public Pochodna(String nazwa, int liczba) { // Wywołanie konstruktora superklasy super(nazwa, liczba); } public void kimJestes() { System.out.println("Jestem Podrzędna"); } } |
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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package dziedziczenie; public class Nadrzedna { String nazwa; int liczba; int innaLiczba = 3; public Nadrzedna(String nazwa, int liczba){ this.nazwa = nazwa; this.liczba = liczba; } public String toString() { return "\ninstancja klasy: " + getClass().getName() + "\n" + " nazwa: " + nazwa + "\n" + " liczba: " + liczba + "\n"+ " innaLiczba: " + innaLiczba; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package dziedziczenie; public class Pochodna extends Nadrzedna{ boolean wazna; int innaLiczba; public Pochodna(String nazwa, int liczba, boolean wazna) { // Wywołanie konstruktora superklasy super(nazwa, liczba); this.wazna = wazna; innaLiczba = 7; } public String toString() { return super.toString() + "\n" + " ważna: " + wazna + "\n" + " innaLiczba: " + innaLiczba + "\n" + " super.liczba : " + super.liczba + "\n" + " super.innaLiczba: " + super.innaLiczba; } public void kimJestes() { System.out.println("Jestem Podrzędna"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package dziedziczenie; public class Dziedziczenie { public static void main(String[] args) { System.out.println("\n - Nadrzędna -"); Nadrzedna nad = new Nadrzedna("Nadrzedna", 1); System.out.println("metoda toString(): " + nad); System.out.println("\n - Pochodna -"); Pochodna poch = new Pochodna("Pochodna", 2, true); System.out.println("metoda toString(): " + poch); poch.kimJestes(); } } |
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
orazEukariont
dziedziczące cechy po klasieOrganizm
ale także dostosowując je do charakterystyki tych dwóch grup organizmów - Następnie napisz klasy
Zwierze
orazRoslina
, które będą rozszerzać klasęEukariont
- W osobnej klasie napisz kod, który stworzy obiekty typu
Prokariont
,Zwierze
orazRoslina
oraz ich użyje, najlepiej w formie interakcji między tymi obiektami (np. zwierzę „zjada” roślinę albo „walczy” z innym zwierzęciem)
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.