PO Dziedziczenie i interfejsy: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 3: | Linia 3: | ||
== Wprowadzenie == | == Wprowadzenie == | ||
Wiemy | Wiemy ju¿, ¿e przy pomocy klas mo¿emy modelowaæ pojêcia z dziedziny obliczeñ naszego programu. Bardzo czêsto podczas takiego modelowania zauwa¿amy, ¿e pewne pojêcia s¹ mocno ze sob¹ zwi¹zane. Podejœcie obiektowe dostarcza mechanizmu umo¿liwiaj¹cego bardzo ³atwe wyra¿anie zwi¹zków pojêæ polegaj¹cych na tym, ¿e jedno pojêcie jest uszczegó³owieniem (lub uogólnieniem) drugiego. Ten mechanizm nazywa siê ''dziedziczeniem''. Stosujemy go zawsze tam, gdzie chcemy wskazaæ, ¿e dwa pojêcia s¹ do siebie podobne. | ||
Zwróæmy uwagê, ¿e zastosowanie dziedziczenia jest zwi¹zane ze znaczeniem klas powi¹zanych tych zwi¹zkiem, a nie z ich implementacj¹. To ¿e dwie klasy maj¹ podobn¹ implementacjê w ¿adnym stopniu nie upowa¿nia do wyra¿enia tej zale¿noœci za pomoc¹ dziedziczenia. Implementacja ma byæ ukryta w klasach (wiêc jej podobieñstwa miêdzy klasami nie powinny byæ eksponowane za pomoc¹ dziedziczenia). Implementacja poszczególnych pojêæ mo¿e siê zmieniaæ, co nie powinno mieæ wp³ywu na relacjê dziedziczenia pomiêdzy klasami w programie. | |||
Dziedzieczenie jest | Dziedzieczenie jest jedn¹ z fundamentalnych w³asnoœci podejœcia obiektowego. Pozwala kojarzyæ klasy obiektów w ''hierarchie klas''. Te hierarchie, w zale¿noœci od u¿ytego jêzyka programowania, mog¹ przyjmowaæ postaæ drzew, lasów drzew, b¹dŸ skierowanych grafów acyklicznych. W Javie hierarchia dziedziczenia dla klas ma postaæ drzewa. Jej korzeniem jest klasa ''Object''. Jak wkrótce siê przekonamy jest nies³ychanie wygodnie mieæ tak¹ jedn¹ wspóln¹ nadklasê dla wszystkich klas wystêpuj¹cych w programie. | ||
Realizacja dziedziczenia polega na tym, | Realizacja dziedziczenia polega na tym, ¿e klasa dziedzicz¹ca dziedziczy po swojej nadklasie wszystkie jej atrybuty i metody (i nie ma znaczenia, czy te atrybuty i metody by³y zadeklarowane bezpoœrednio w tej nadklasie, czy ona te¿ odziedziczy³a je po swojej z kolei nadklasie). | ||
W | W ró¿nych jêzykach programowania mo¿na natkn¹æ siê na ró¿n¹ terminologiê, klasê po której inna klasa dziedziczy nazywa siê ''nadklas¹'' lub ''klas¹ bazow¹'', zaœ klasê dziedzicz¹c¹ ''podklas¹'' lub klas¹ pochodn¹. | ||
Dziedziczenie odzwierciedla | Dziedziczenie odzwierciedla relacjê ''is-a'' (''jest czymœ''). Oznacza to, ¿e ka¿dy obiekt podklasy jest tak¿e obiektem nadklasy. Na przyk³ad hierarchia klas zbudowana z nadklasy ''Owoc'' i dwu podklas ''Jab³ko'' i ''Gruszka'' jest prawid³owo zbudowana, bo ka¿de jab³ko i ka¿da gruszka jest owocem. Niestety czêsto relacja ''is-a'' jest mylona z relacj¹ ''has-a'' (''ma coœ'') dotycz¹c¹ sk³adania obiektów. | ||
''Zasada | ''Zasada podstawialnoœci'': poniewa¿ obiekty podklas s¹ te¿ obiektami nadklas, to oczywiœcie zawsze mo¿na podstawiaæ obiekty podklas w miejsce obiektów nadklas. Na przyk³ad metoda, która ma parametr typu ''Owoc'' prawid³owo zadzia³a z argumentem bêd¹cym jab³kiem albo gruszk¹. | ||
Oto typowe zastosowania dziedziczenia: | Oto typowe zastosowania dziedziczenia: | ||
* opisanie specjalizacji | * opisanie specjalizacji jakiegoœ pojêcia (np. klasa Prostok¹t dziedzicz¹ca po klasie Czworok¹t), | ||
* specyfikowanie | * specyfikowanie po¿¹danego interfejsu (klasy abstrakcyjne, interfejsy), | ||
<!-- * opisywanie rozszerzanie | <!-- * opisywanie rozszerzanie pojêæ (np. klasa KoloroweOkno dzieidzcz¹ca po klasie CzarnoBia³eOkno), --> | ||
Nie | Nie nale¿y natomiast stosowaæ dziedziczenia do wyra¿ania ograniczania (np. klasa Stos dziedzicz¹ca po uniwersalnej implementacji sekwencji wartoœci z operacjami dostêpu do dowolnego elementu sekwencji - tu nale¿y zastosowaæ sk³adanie). | ||
<!-- | <!-- | ||
Czasami chcemy | Czasami chcemy wyraziæ w hierarchii klas wyj¹tki (pingwin), mo¿na to | ||
uzyskaæ dziêki {\it przedefiniowywaniu} metod (method overriding). | |||
Przez dziedziczenie na tym | Przez dziedziczenie na tym wyk³adzie rozumiemy zarówno dziedziczenie po klasie ('''extends''') jak i po interfejsie ('''implements'''). | ||
Brak wielodziedziczenia po klasach. | Brak wielodziedziczenia po klasach. | ||
--> | --> | ||
Dziedziczenie pozwala | Dziedziczenie pozwala pogodziæ ze sob¹ dwie sprzeczne tendencje w | ||
tworzeniu oprogramowania: | tworzeniu oprogramowania: | ||
* chcemy | * chcemy ¿eby stworzone programy by³y zamkniête: gdy program ju¿ kompiluje siê, dzia³a i przeszed³ przez wszystkie testy, chcielibyœmy go zapieczêtowaæ tak, by nikt ju¿ go nie modyfikowa³, bo modyfikacje czêsto sprawiaj¹, ¿e programy przestaj¹ dzia³aæ. | ||
* chcemy | * chcemy ¿eby stworzone programy by³y otwarte: jeœli napisaliœmy dobry program, taki który jest u¿ywany przez u¿ytkowników, to na pewno trzeba go bêdzie modyfikowaæ. Nie dlatego ¿e jest z³y i zawiera b³êdy, tylko w³aœnie dlatego, ¿e jest dobry i u¿ytkownicy chc¹ go u¿ywaæ w ci¹gle zmieniaj¹cym siê œwiecie. Skoro zmienia siê sprzêt, na którym jest uruchamiany program, system operacyjny, upodobania u¿ytkownika, przepisy prawne, to oczywiœcie i sam program musi byæ zmieniany. Losem dobrych programów jest czêste ich modyfikowanie. | ||
Dziedziczenie pozwala wtedy gdy potrzebujemy zmian nie | Dziedziczenie pozwala wtedy gdy potrzebujemy zmian nie modyfikowaæ klas ju¿ istniej¹cych, lecz na ich podstawie (przez dziedziczenie w³aœnie) stworzyæ nowe dostosowane do zmienionych wymagañ. Czyli dotychczasowy kod pozostaje bez zmian, a jednoczeœnie mo¿emy rozwijaæ nasz program. | ||
== Realizacja w Javie == | == Realizacja w Javie == | ||
W Javie | W Javie mo¿na dziedziczyæ zarówno po klasach jak i po interfejsach (o tych drugich bêdziemy jeszcze mówiæ dalej). W ramach tego wyk³adu bêdziemy obie te formy dziedziczenia nazywali po prostu dziedziczeniem (lub bêdziemy wyraŸnie zaznaczaæ, ze w danych miejscu chodzi nam o ''dziedziczenie po klasach'' lub o ''dziedziczenie po interfejsach''). Co wiêcej, ¿eby co chwila nie pisaæ o "nadklasie lub nadinterfejsie" pozwolimy sobie mówiæ tylko o nadklasach i traktowaæ interfejsy jako szczególny, bardzo uproszczony, rodzaj klas. | ||
Sk³adniowo oba te sposoby dziedziczenia s¹ odmiennie wyra¿ane (za pomoc¹ innych s³ów kluczowych). Ponadto dziedziczenie po klasach jest ograniczone do tylko jednej bezpoœredniej nadklasy (nie ma w Javie wielodziedziczenia takiego jak w C++, gdzie z kolei nie ma interfejsów), mo¿na natomiast dziedziczyæ (bezpoœrednio) po dowolnej liczbie interfejsów. | |||
Sk³adnia dziedziczenia jest bardzo prosta, po nazwie klasy mo¿na dopisaæ s³owo kluczowe '''extends''' a po nim podaæ nazwê klasy, po której tworzona klasa ma dziedziczyæ. Dalej mo¿na podaæ s³owo kluczowe '''implements''' z nastêpuj¹cymi po nim nazwami interfejsów, po których tworzona klasa ma dziedziczyæ (u¿ywaj¹c terminologii Javy powiedzielibyœmy, ¿e jest to lista interfejsów, które tworzona klasa ma implementowaæ). Poni¿ej podajemy kilka przyk³adów deklaracji dziedziczenia: | |||
class Pracownik extends Osoba | class Pracownik extends Osoba | ||
// dziedziczenie po klasie | // dziedziczenie po klasie | ||
class Samochód implements Pojazd, Towar | class Samochód implements Pojazd, Towar | ||
// dziedziczenie po kilku interfesjach | // dziedziczenie po kilku interfesjach | ||
class Chomik extends Ssak implements Puchate, | class Chomik extends Ssak implements Puchate, DoG³askania | ||
// dziedziczenie po klasie i kilku interfejsach | // dziedziczenie po klasie i kilku interfejsach | ||
Jeœli klauzula '''extends''' jest pominiêta, to domyœlnie przyjmuje siê, ¿e klasa dziedziczy po klasie ''Object''<ref>Nie dotyczy to samej klasy ''Object'', bo ona nie ma ¿adnej nadklasy, ale akurat tej klasy sami nie mo¿emy zadeklarowaæ (mówi¹c precyzyjniej, nie mo¿emy zadeklarowaæ ''tej'' klasy Object, mo¿emy zadeklarowaæ w³asn¹ klasê, która bêdzie siê nazywa³a Object, ale nie bêdzie ona mia³a oczywiœcie ¿adnych specjalnych w³asnoœci).</ref> | |||
Ju¿ powiedzieliœmy wczeœniej, ¿e klasa dziedzicz¹ca otrzymuje ze swojej nadklasy komplet jej atrybutów i metod. O ile nazwy atrybutów i metod z klasy dziedzicz¹cej i tej po której nastêpuje dziedziczenie s¹ ró¿ne sytuacja jest | |||
doœæ jasna, poni¿szy fragment programu: | |||
class A{ | class A{ | ||
Linia 62: | Linia 62: | ||
System.out.println( | System.out.println( | ||
"Jestem infoA() z klasy A\n"+ | "Jestem infoA() z klasy A\n"+ | ||
" | " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + | ||
" iA="+iA); | " iA="+iA); | ||
} | } | ||
Linia 72: | Linia 72: | ||
System.out.println( | System.out.println( | ||
"Jestem infoB() z klasy B\n"+ | "Jestem infoB() z klasy B\n"+ | ||
" | " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + | ||
" iA="+iA + ", iB=" + iB); | " iA="+iA + ", iB=" + iB); | ||
} | } | ||
Linia 84: | Linia 84: | ||
System.out.println( | System.out.println( | ||
"Jestem infoC() z klasy C\n"+ | "Jestem infoC() z klasy C\n"+ | ||
" | " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName()+ "\n" + | ||
" iA="+iA + ", iB=" + iB + ", iC=" + iC); | " iA="+iA + ", iB=" + iB + ", iC=" + iC); | ||
} | } | ||
Linia 95: | Linia 95: | ||
Jestem infoA() z klasy A | Jestem infoA() z klasy A | ||
wywo³ano mnie w obiekcie klasy C | |||
iA=1 | iA=1 | ||
Jestem infoA() z klasy A | Jestem infoA() z klasy A | ||
wywo³ano mnie w obiekcie klasy C | |||
iA=1 | iA=1 | ||
Jestem infoB() z klasy B | Jestem infoB() z klasy B | ||
wywo³ano mnie w obiekcie klasy C | |||
iA=1, iB=2 | iA=1, iB=2 | ||
Jestem infoC() z klasy C | Jestem infoC() z klasy C | ||
wywo³ano mnie w obiekcie klasy C | |||
iA=1, iB=2, iC=2 | iA=1, iB=2, iC=2 | ||
Wyra¿enie ''this.getClass().getSimpleName()'' powoduje wypisanie nazwy klasy obiektu, w którym to wyra¿enie wyliczamy. | |||
Jak | Jak widaæ z tego przyk³adu, klasa C odziedziczy³a po swoich przodkach wszystkie atrybuty i metody (podobnie klasa B). Mo¿emy wiêc myœleæ o obiektach klasy C jak o kawa³kach tortu sk³adaj¹cych siê z wielu warstw, gdzie ka¿da warstwa odpowiada kolejnej nadklasie. | ||
Na razie nic szczególnego | Na razie nic szczególnego siê nie wydarzy³o, spróbujmy nieco skomplikowaæ nasz przyk³ad. Najpierw skorzystamy z zasady podstawialnoœci i zadeklarujemy obiekt klasy C jako obiekt klasy A (precyzyjniej: na zmienn¹ typu A przypiszemy referencjê do obiektu klasy C). Dopisujemy wiêc na koñcu naszego programu: | ||
A a = new C(); | A a = new C(); | ||
System.out.println("a.iA=" + a.iA + ", c.iA=" + c.iA); | System.out.println("a.iA=" + a.iA + ", c.iA=" + c.iA); | ||
Oczywiœcie na wyjœciu naszego programu dodatkowo pojawi siê poni¿szy wiersz: | |||
a.iA=1, c.iA=1 | a.iA=1, c.iA=1 | ||
A teraz | A teraz z³oœliwie zmienimy nazwy wszystkich atrybutów w naszej hierarchii na takie same. Oczywiœcie tworz¹c hierarchie klas w praktyce nie d¹¿ymy do tego, ¿eby wszystkie wystêpuj¹ce w nich atrybuty nazywaæ tak samo. Problem tkwi jednak w tym, ¿e czêsto dziedzicz¹c po jakiejœ klasie nie znamy jej implementacji i nie wiemy jak nazywaj¹ siê wystêpuj¹ce w niej atrybuty. A tym bardziej jak nazywaj¹ siê atrybuty jej nadklasy. Zobaczmy wiêc co siê wydarzy. | ||
class A{ | class A{ | ||
Linia 127: | Linia 127: | ||
System.out.println( | System.out.println( | ||
"Jestem infoA() z klasy A\n"+ | "Jestem infoA() z klasy A\n"+ | ||
" | " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + | ||
" i z A="+i); | " i z A="+i); | ||
} | } | ||
} | } | ||
class B extends A{ | class B extends A{ | ||
int i=2; | int i=2; | ||
Linia 138: | Linia 138: | ||
System.out.println( | System.out.println( | ||
"Jestem infoB() z klasy B\n"+ | "Jestem infoB() z klasy B\n"+ | ||
" | " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + | ||
" i z A="+((A) this).i + ", i z B=" + i); // albo super.i | " i z A="+((A) this).i + ", i z B=" + i); // albo super.i | ||
} | } | ||
Linia 150: | Linia 150: | ||
System.out.println( | System.out.println( | ||
"Jestem infoC() z klasy C\n"+ | "Jestem infoC() z klasy C\n"+ | ||
" | " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName()+ "\n" + | ||
" i z A="+ ((A) this).i + ", i z B=" + ((B) this).i + ", i z C=" + i); | " i z A="+ ((A) this).i + ", i z B=" + ((B) this).i + ", i z C=" + i); | ||
} | } | ||
Linia 161: | Linia 161: | ||
System.out.println("a.i=" + a.i + ", c.i=" + c.i); | System.out.println("a.i=" + a.i + ", c.i=" + c.i); | ||
Dla | Dla powy¿szego programu otrzymamy nastêpuj¹ce wyniki: | ||
Jestem infoA() z klasy A | Jestem infoA() z klasy A | ||
wywo³ano mnie w obiekcie klasy C | |||
i z A=1 | i z A=1 | ||
Jestem infoA() z klasy A | Jestem infoA() z klasy A | ||
wywo³ano mnie w obiekcie klasy C | |||
i z A=1 | i z A=1 | ||
Jestem infoB() z klasy B | Jestem infoB() z klasy B | ||
wywo³ano mnie w obiekcie klasy C | |||
i z A=1, i z B=2 | i z A=1, i z B=2 | ||
Jestem infoC() z klasy C | Jestem infoC() z klasy C | ||
wywo³ano mnie w obiekcie klasy C | |||
i z A=1, i z B=2, i z C=2 | i z A=1, i z B=2, i z C=2 | ||
a.i=1, c.i=2 | a.i=1, c.i=2 | ||
Po pierwsze | Po pierwsze wyjaœnijmy znaczenie konstrukcji ''((A) this)''. U¿ywamy tu bardzo nieeleganckiego zabiegu, a mianowicie rzutowania typów. Prosimy kompilator, ¿eby uwierzy³, ¿e obiekt '''this''' jest typu A. W tym przypadku jest to prawda (pamiêtamy o zasadzie tworzenia hierarchii klas: ka¿dy obiekt podklasy jest obiektem nadklasy. Tu mamy obiekt klasy C, który jest tak¿e obiektem klasy A). | ||
Rzutowanie typów oznacza, | Rzutowanie typów oznacza, ¿e rezygnujemy z bezpieczeñstwa dawanego przez kompilator i silny system typów Javy. Przechodzimy do œwiata dynamicznego sprawdzania typów - kompilator przy rzutowaniu generuje dodatkowe instrukcje, które sprawdz¹ podczas wykonywania programu, czy rzeczywiœcie '''this''' jest typu A (my to wiemy, ale kompilator nie). Gdyby siê okaza³o, ¿e typy siê nie zgadzaj¹, to podczas dzia³ania programu zosta³by zg³oszony wyj¹tek. Dynamiczne sprawdzanie typów jest oczywiœcie znacznie gorsze od statycznego, nie tylko dlatego, ¿e spowalnia wykonywanie programu, ale przede wszystkim dlatego, ¿e komunikaty o b³êdach wypisuje u¿ytkownikowi (a nie twórcy) programu, który zwykle ich nie rozumie i nie ma jak poprawiæ. | ||
== Polimorfizm == | == Polimorfizm == | ||
Linia 184: | Linia 184: | ||
== Klasy abstrakcyjne i interfejsy == | == Klasy abstrakcyjne i interfejsy == | ||
<!-- Zwykle po to tworzymy klasy by | <!-- Zwykle po to tworzymy klasy by tworzyæ ich egzemplarze. --> | ||
== Podsumowanie == | == Podsumowanie == | ||
Dziedziczenie jest bardzo silnym | Dziedziczenie jest bardzo silnym narzêdziem pozwalaj¹cym lepiej strukturalizowaæ programy. Jest charakterystyczne dla podejœcia obiektowego. Tak jak ka¿de silne narzêdzie ma zarówno zalety jak i wady. Poni¿ej krótko je charakteryzujemy. | ||
Zalety: | Zalety: | ||
* | * Mo¿liwoœæ jawnego zapisywania w programie zwi¹zków ogólniania/uszczegó³awiania pomiêdzy opisywanymi pojêciami. | ||
* | * Mo¿liwoœæ ponownego wykorzystywania (nie trzeba od nowa pisaæ odziedziczonych metod i deklaracji odziedziczonych zmiennych). Ponowne wykorzystywanie zwiêksza niezawodnoœæ (szybciej wykrywa siê b³êdy w czêœciej u¿ywanych fragmentach programów) i pozwala szybciej tworzyæ nowe systemy (budowaæ je z gotowych klocków). | ||
* | * Zgodnoœæ interfejsów (osi¹gana przez tworzenie hierarchii klas dziedzicz¹cych po wspólnej nadklasie lub interfejsie). | ||
Problemy: | Problemy: | ||
* Problem jo-jo ( | * Problem jo-jo (nadu¿ywanie dziedziczenia mo¿e uczyniæ czytanie programu bardzo ¿mudnym procesem). | ||
* Modyfikacje kodu w nadklasach | * Modyfikacje kodu w nadklasach maj¹ wp³yw na podklasy i na odwrót (wirtualnoœæ metod, która jednoczeœnie stanowi o sile dziedziczenia). | ||
Nale¿y na koniec podkreœliæ, ¿e dziedziczenie nie jest jedyn¹ technik¹ wyra¿ania zwi¹zków pomiêdzy klasami. Innym wa¿nym mechanizmem wyra¿ania zwi¹zków jest sk³adanie. Oba te mechanizmy wzajemnie siê uzupe³niaj¹ i nigdy nie nale¿y d¹¿yæ na si³ê do zast¹pienia jednego z nich drugim. | |||
== Przypisy == | == Przypisy == | ||
<references/> | <references/> |
Wersja z 01:36, 5 paź 2006
Wprowadzenie
Wiemy ju¿, ¿e przy pomocy klas mo¿emy modelowaæ pojêcia z dziedziny obliczeñ naszego programu. Bardzo czêsto podczas takiego modelowania zauwa¿amy, ¿e pewne pojêcia s¹ mocno ze sob¹ zwi¹zane. Podejœcie obiektowe dostarcza mechanizmu umo¿liwiaj¹cego bardzo ³atwe wyra¿anie zwi¹zków pojêæ polegaj¹cych na tym, ¿e jedno pojêcie jest uszczegó³owieniem (lub uogólnieniem) drugiego. Ten mechanizm nazywa siê dziedziczeniem. Stosujemy go zawsze tam, gdzie chcemy wskazaæ, ¿e dwa pojêcia s¹ do siebie podobne.
Zwróæmy uwagê, ¿e zastosowanie dziedziczenia jest zwi¹zane ze znaczeniem klas powi¹zanych tych zwi¹zkiem, a nie z ich implementacj¹. To ¿e dwie klasy maj¹ podobn¹ implementacjê w ¿adnym stopniu nie upowa¿nia do wyra¿enia tej zale¿noœci za pomoc¹ dziedziczenia. Implementacja ma byæ ukryta w klasach (wiêc jej podobieñstwa miêdzy klasami nie powinny byæ eksponowane za pomoc¹ dziedziczenia). Implementacja poszczególnych pojêæ mo¿e siê zmieniaæ, co nie powinno mieæ wp³ywu na relacjê dziedziczenia pomiêdzy klasami w programie.
Dziedzieczenie jest jedn¹ z fundamentalnych w³asnoœci podejœcia obiektowego. Pozwala kojarzyæ klasy obiektów w hierarchie klas. Te hierarchie, w zale¿noœci od u¿ytego jêzyka programowania, mog¹ przyjmowaæ postaæ drzew, lasów drzew, b¹dŸ skierowanych grafów acyklicznych. W Javie hierarchia dziedziczenia dla klas ma postaæ drzewa. Jej korzeniem jest klasa Object. Jak wkrótce siê przekonamy jest nies³ychanie wygodnie mieæ tak¹ jedn¹ wspóln¹ nadklasê dla wszystkich klas wystêpuj¹cych w programie.
Realizacja dziedziczenia polega na tym, ¿e klasa dziedzicz¹ca dziedziczy po swojej nadklasie wszystkie jej atrybuty i metody (i nie ma znaczenia, czy te atrybuty i metody by³y zadeklarowane bezpoœrednio w tej nadklasie, czy ona te¿ odziedziczy³a je po swojej z kolei nadklasie).
W ró¿nych jêzykach programowania mo¿na natkn¹æ siê na ró¿n¹ terminologiê, klasê po której inna klasa dziedziczy nazywa siê nadklas¹ lub klas¹ bazow¹, zaœ klasê dziedzicz¹c¹ podklas¹ lub klas¹ pochodn¹.
Dziedziczenie odzwierciedla relacjê is-a (jest czymœ). Oznacza to, ¿e ka¿dy obiekt podklasy jest tak¿e obiektem nadklasy. Na przyk³ad hierarchia klas zbudowana z nadklasy Owoc i dwu podklas Jab³ko i Gruszka jest prawid³owo zbudowana, bo ka¿de jab³ko i ka¿da gruszka jest owocem. Niestety czêsto relacja is-a jest mylona z relacj¹ has-a (ma coœ) dotycz¹c¹ sk³adania obiektów.
Zasada podstawialnoœci: poniewa¿ obiekty podklas s¹ te¿ obiektami nadklas, to oczywiœcie zawsze mo¿na podstawiaæ obiekty podklas w miejsce obiektów nadklas. Na przyk³ad metoda, która ma parametr typu Owoc prawid³owo zadzia³a z argumentem bêd¹cym jab³kiem albo gruszk¹.
Oto typowe zastosowania dziedziczenia:
- opisanie specjalizacji jakiegoœ pojêcia (np. klasa Prostok¹t dziedzicz¹ca po klasie Czworok¹t),
- specyfikowanie po¿¹danego interfejsu (klasy abstrakcyjne, interfejsy),
Nie nale¿y natomiast stosowaæ dziedziczenia do wyra¿ania ograniczania (np. klasa Stos dziedzicz¹ca po uniwersalnej implementacji sekwencji wartoœci z operacjami dostêpu do dowolnego elementu sekwencji - tu nale¿y zastosowaæ sk³adanie).
Dziedziczenie pozwala pogodziæ ze sob¹ dwie sprzeczne tendencje w
tworzeniu oprogramowania:
- chcemy ¿eby stworzone programy by³y zamkniête: gdy program ju¿ kompiluje siê, dzia³a i przeszed³ przez wszystkie testy, chcielibyœmy go zapieczêtowaæ tak, by nikt ju¿ go nie modyfikowa³, bo modyfikacje czêsto sprawiaj¹, ¿e programy przestaj¹ dzia³aæ.
- chcemy ¿eby stworzone programy by³y otwarte: jeœli napisaliœmy dobry program, taki który jest u¿ywany przez u¿ytkowników, to na pewno trzeba go bêdzie modyfikowaæ. Nie dlatego ¿e jest z³y i zawiera b³êdy, tylko w³aœnie dlatego, ¿e jest dobry i u¿ytkownicy chc¹ go u¿ywaæ w ci¹gle zmieniaj¹cym siê œwiecie. Skoro zmienia siê sprzêt, na którym jest uruchamiany program, system operacyjny, upodobania u¿ytkownika, przepisy prawne, to oczywiœcie i sam program musi byæ zmieniany. Losem dobrych programów jest czêste ich modyfikowanie.
Dziedziczenie pozwala wtedy gdy potrzebujemy zmian nie modyfikowaæ klas ju¿ istniej¹cych, lecz na ich podstawie (przez dziedziczenie w³aœnie) stworzyæ nowe dostosowane do zmienionych wymagañ. Czyli dotychczasowy kod pozostaje bez zmian, a jednoczeœnie mo¿emy rozwijaæ nasz program.
Realizacja w Javie
W Javie mo¿na dziedziczyæ zarówno po klasach jak i po interfejsach (o tych drugich bêdziemy jeszcze mówiæ dalej). W ramach tego wyk³adu bêdziemy obie te formy dziedziczenia nazywali po prostu dziedziczeniem (lub bêdziemy wyraŸnie zaznaczaæ, ze w danych miejscu chodzi nam o dziedziczenie po klasach lub o dziedziczenie po interfejsach). Co wiêcej, ¿eby co chwila nie pisaæ o "nadklasie lub nadinterfejsie" pozwolimy sobie mówiæ tylko o nadklasach i traktowaæ interfejsy jako szczególny, bardzo uproszczony, rodzaj klas.
Sk³adniowo oba te sposoby dziedziczenia s¹ odmiennie wyra¿ane (za pomoc¹ innych s³ów kluczowych). Ponadto dziedziczenie po klasach jest ograniczone do tylko jednej bezpoœredniej nadklasy (nie ma w Javie wielodziedziczenia takiego jak w C++, gdzie z kolei nie ma interfejsów), mo¿na natomiast dziedziczyæ (bezpoœrednio) po dowolnej liczbie interfejsów.
Sk³adnia dziedziczenia jest bardzo prosta, po nazwie klasy mo¿na dopisaæ s³owo kluczowe extends a po nim podaæ nazwê klasy, po której tworzona klasa ma dziedziczyæ. Dalej mo¿na podaæ s³owo kluczowe implements z nastêpuj¹cymi po nim nazwami interfejsów, po których tworzona klasa ma dziedziczyæ (u¿ywaj¹c terminologii Javy powiedzielibyœmy, ¿e jest to lista interfejsów, które tworzona klasa ma implementowaæ). Poni¿ej podajemy kilka przyk³adów deklaracji dziedziczenia:
class Pracownik extends Osoba // dziedziczenie po klasie class Samochód implements Pojazd, Towar // dziedziczenie po kilku interfesjach class Chomik extends Ssak implements Puchate, DoG³askania // dziedziczenie po klasie i kilku interfejsach
Jeœli klauzula extends jest pominiêta, to domyœlnie przyjmuje siê, ¿e klasa dziedziczy po klasie Object<ref>Nie dotyczy to samej klasy Object, bo ona nie ma ¿adnej nadklasy, ale akurat tej klasy sami nie mo¿emy zadeklarowaæ (mówi¹c precyzyjniej, nie mo¿emy zadeklarowaæ tej klasy Object, mo¿emy zadeklarowaæ w³asn¹ klasê, która bêdzie siê nazywa³a Object, ale nie bêdzie ona mia³a oczywiœcie ¿adnych specjalnych w³asnoœci).</ref>
Ju¿ powiedzieliœmy wczeœniej, ¿e klasa dziedzicz¹ca otrzymuje ze swojej nadklasy komplet jej atrybutów i metod. O ile nazwy atrybutów i metod z klasy dziedzicz¹cej i tej po której nastêpuje dziedziczenie s¹ ró¿ne sytuacja jest doœæ jasna, poni¿szy fragment programu:
class A{ int iA=1; void infoA(){ System.out.println( "Jestem infoA() z klasy A\n"+ " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + " iA="+iA); } } class B extends A{ int iB=2; void infoB(){ infoA(); System.out.println( "Jestem infoB() z klasy B\n"+ " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + " iA="+iA + ", iB=" + iB); } } class C extends B{ int iC=2; void infoC(){ infoA(); infoB(); System.out.println( "Jestem infoC() z klasy C\n"+ " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName()+ "\n" + " iA="+iA + ", iB=" + iB + ", iC=" + iC); } } C c = new C(); c.infoC();
spowoduje wypisanie:
Jestem infoA() z klasy A wywo³ano mnie w obiekcie klasy C iA=1 Jestem infoA() z klasy A wywo³ano mnie w obiekcie klasy C iA=1 Jestem infoB() z klasy B wywo³ano mnie w obiekcie klasy C iA=1, iB=2 Jestem infoC() z klasy C wywo³ano mnie w obiekcie klasy C iA=1, iB=2, iC=2
Wyra¿enie this.getClass().getSimpleName() powoduje wypisanie nazwy klasy obiektu, w którym to wyra¿enie wyliczamy.
Jak widaæ z tego przyk³adu, klasa C odziedziczy³a po swoich przodkach wszystkie atrybuty i metody (podobnie klasa B). Mo¿emy wiêc myœleæ o obiektach klasy C jak o kawa³kach tortu sk³adaj¹cych siê z wielu warstw, gdzie ka¿da warstwa odpowiada kolejnej nadklasie.
Na razie nic szczególnego siê nie wydarzy³o, spróbujmy nieco skomplikowaæ nasz przyk³ad. Najpierw skorzystamy z zasady podstawialnoœci i zadeklarujemy obiekt klasy C jako obiekt klasy A (precyzyjniej: na zmienn¹ typu A przypiszemy referencjê do obiektu klasy C). Dopisujemy wiêc na koñcu naszego programu:
A a = new C(); System.out.println("a.iA=" + a.iA + ", c.iA=" + c.iA);
Oczywiœcie na wyjœciu naszego programu dodatkowo pojawi siê poni¿szy wiersz:
a.iA=1, c.iA=1
A teraz z³oœliwie zmienimy nazwy wszystkich atrybutów w naszej hierarchii na takie same. Oczywiœcie tworz¹c hierarchie klas w praktyce nie d¹¿ymy do tego, ¿eby wszystkie wystêpuj¹ce w nich atrybuty nazywaæ tak samo. Problem tkwi jednak w tym, ¿e czêsto dziedzicz¹c po jakiejœ klasie nie znamy jej implementacji i nie wiemy jak nazywaj¹ siê wystêpuj¹ce w niej atrybuty. A tym bardziej jak nazywaj¹ siê atrybuty jej nadklasy. Zobaczmy wiêc co siê wydarzy.
class A{ int i=1; void infoA(){ System.out.println( "Jestem infoA() z klasy A\n"+ " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + " i z A="+i); } } class B extends A{ int i=2; void infoB(){ infoA(); System.out.println( "Jestem infoB() z klasy B\n"+ " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" + " i z A="+((A) this).i + ", i z B=" + i); // albo super.i } } class C extends B{ int i=2; void infoC(){ infoA(); infoB(); System.out.println( "Jestem infoC() z klasy C\n"+ " wywo³ano mnie w obiekcie klasy " + this.getClass().getSimpleName()+ "\n" + " i z A="+ ((A) this).i + ", i z B=" + ((B) this).i + ", i z C=" + i); } } C c = new C(); c.infoC(); A a = new C(); System.out.println("a.i=" + a.i + ", c.i=" + c.i);
Dla powy¿szego programu otrzymamy nastêpuj¹ce wyniki:
Jestem infoA() z klasy A wywo³ano mnie w obiekcie klasy C i z A=1 Jestem infoA() z klasy A wywo³ano mnie w obiekcie klasy C i z A=1 Jestem infoB() z klasy B wywo³ano mnie w obiekcie klasy C i z A=1, i z B=2 Jestem infoC() z klasy C wywo³ano mnie w obiekcie klasy C i z A=1, i z B=2, i z C=2 a.i=1, c.i=2
Po pierwsze wyjaœnijmy znaczenie konstrukcji ((A) this). U¿ywamy tu bardzo nieeleganckiego zabiegu, a mianowicie rzutowania typów. Prosimy kompilator, ¿eby uwierzy³, ¿e obiekt this jest typu A. W tym przypadku jest to prawda (pamiêtamy o zasadzie tworzenia hierarchii klas: ka¿dy obiekt podklasy jest obiektem nadklasy. Tu mamy obiekt klasy C, który jest tak¿e obiektem klasy A). Rzutowanie typów oznacza, ¿e rezygnujemy z bezpieczeñstwa dawanego przez kompilator i silny system typów Javy. Przechodzimy do œwiata dynamicznego sprawdzania typów - kompilator przy rzutowaniu generuje dodatkowe instrukcje, które sprawdz¹ podczas wykonywania programu, czy rzeczywiœcie this jest typu A (my to wiemy, ale kompilator nie). Gdyby siê okaza³o, ¿e typy siê nie zgadzaj¹, to podczas dzia³ania programu zosta³by zg³oszony wyj¹tek. Dynamiczne sprawdzanie typów jest oczywiœcie znacznie gorsze od statycznego, nie tylko dlatego, ¿e spowalnia wykonywanie programu, ale przede wszystkim dlatego, ¿e komunikaty o b³êdach wypisuje u¿ytkownikowi (a nie twórcy) programu, który zwykle ich nie rozumie i nie ma jak poprawiæ.
Polimorfizm
Klasy abstrakcyjne i interfejsy
Podsumowanie
Dziedziczenie jest bardzo silnym narzêdziem pozwalaj¹cym lepiej strukturalizowaæ programy. Jest charakterystyczne dla podejœcia obiektowego. Tak jak ka¿de silne narzêdzie ma zarówno zalety jak i wady. Poni¿ej krótko je charakteryzujemy.
Zalety:
- Mo¿liwoœæ jawnego zapisywania w programie zwi¹zków ogólniania/uszczegó³awiania pomiêdzy opisywanymi pojêciami.
- Mo¿liwoœæ ponownego wykorzystywania (nie trzeba od nowa pisaæ odziedziczonych metod i deklaracji odziedziczonych zmiennych). Ponowne wykorzystywanie zwiêksza niezawodnoœæ (szybciej wykrywa siê b³êdy w czêœciej u¿ywanych fragmentach programów) i pozwala szybciej tworzyæ nowe systemy (budowaæ je z gotowych klocków).
- Zgodnoœæ interfejsów (osi¹gana przez tworzenie hierarchii klas dziedzicz¹cych po wspólnej nadklasie lub interfejsie).
Problemy:
- Problem jo-jo (nadu¿ywanie dziedziczenia mo¿e uczyniæ czytanie programu bardzo ¿mudnym procesem).
- Modyfikacje kodu w nadklasach maj¹ wp³yw na podklasy i na odwrót (wirtualnoœæ metod, która jednoczeœnie stanowi o sile dziedziczenia).
Nale¿y na koniec podkreœliæ, ¿e dziedziczenie nie jest jedyn¹ technik¹ wyra¿ania zwi¹zków pomiêdzy klasami. Innym wa¿nym mechanizmem wyra¿ania zwi¹zków jest sk³adanie. Oba te mechanizmy wzajemnie siê uzupe³niaj¹ i nigdy nie nale¿y d¹¿yæ na si³ê do zast¹pienia jednego z nich drugim.
Przypisy
<references/>