PO Dziedziczenie i interfejsy: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Janusz (dyskusja | edycje)
Nie podano opisu zmian
Janusz (dyskusja | edycje)
Nie podano opisu zmian
Linia 3: Linia 3:
== Wprowadzenie ==
== 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.
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.
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.
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).  
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¹.
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.
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¹.
''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 jakiegoœ pojêcia (np. klasa Prostok¹t dziedzicz¹ca po klasie Czworok¹t),
* opisanie specjalizacji jakiegoś pojęcia (np. klasa Prostokąt dziedzicząca po klasie Czworokąt),
* specyfikowanie po¿¹danego interfejsu (klasy abstrakcyjne, interfejsy),
* specyfikowanie pożądanego interfejsu (klasy abstrakcyjne, interfejsy),
<!-- * opisywanie rozszerzanie pojêæ (np. klasa KoloroweOkno dzieidzcz¹ca po klasie CzarnoBia³eOkno), -->
<!-- * opisywanie rozszerzanie pojęć (np. klasa KoloroweOkno dzieidzcząca po klasie CzarnoBiałeOkno), -->


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).
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 wyraziæ w hierarchii klas wyj¹tki (pingwin), mo¿na to  
  Czasami chcemy wyrazić w hierarchii klas wyjątki (pingwin), można to  
         uzyskaæ dziêki {\it przedefiniowywaniu} metod (method overriding).
         uzyskać dzięki {\it przedefiniowywaniu} metod (method overriding).


Przez dziedziczenie na tym wyk³adzie rozumiemy zarówno dziedziczenie po klasie ('''extends''') jak i po interfejsie ('''implements''').
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 pogodziæ ze sob¹ dwie sprzeczne tendencje w  
Dziedziczenie pozwala pogodzić ze sobą dwie sprzeczne tendencje w  
tworzeniu oprogramowania:
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 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.
* 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.
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 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.  
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 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ładniowo oba te sposoby dziedziczenia 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:
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, DoG³askania
   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>
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  
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:
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" +
           "  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" +
           "  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" +
           "  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
   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
   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
   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
   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.
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.
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:
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:
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 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.
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" +
           "  wywołano mnie w obiekcie klasy " + this.getClass().getSimpleName() + "\n" +
           "  i z A="+i);
           "  i z A="+i);
     }
     }
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" +
           "  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" +
           "  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 powy¿szego programu otrzymamy nastêpuj¹ce wyniki:
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
   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
   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
   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
   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 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).
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æ.
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ć.
 
Jak widać z tego przykładu, nawet gdy nazwy atrybutów się pokrywają, dalej obiekt klasy C ma warstwy pochodzące z nadklas, tyle że teraz trudniej dostać się do odziedziczonych atrybutów. Zastosowana notacja z rzutowaniem daje wprawdzie dostęp do każdego atrybutu ale jest bardzo nieelegancka i nie należy jej używać w programach (z różnych powodów, jeszcze o nich więcej powiemy). Do dostępu do atrybutów (i ogólnie składowych) z bezpośredniej nadklasy służy bezpieczna notacja ze słowem kluczowym '''super''' (zasygnalizowana w komentarzu z powyższego przykładu). Tak jak słowo kluczowe '''this''' pozwala się odnieść do bieżącego obiektu (tego, którego metodę obecnie wykonujemy), tak '''super''' również odnosi się do tego obiektu, ale traktowanego jako obiekt nadklasy, to znaczy przestają być widoczne wszystkie te deklaracje, które wprowadziła klasa tego obiektu, odsłaniając w ten sposób przesłonięte deklaracje z nadklasy. Z definicji notacja ''super.składowa'' oznacza tyle samo co ''((Nadklasa) this).składowa''. Z notacji super będziemy bardzo często korzystać w programach pisanych w Javie (na przykład przy przedefiniowywaniu metod lub konstruktorów).
 
Czemu zatem w przykładzie nie użyto tej notacji? Dlatego, że pozwala ona sięgnąć tylko jeden poziom wyżej w hierarchii dziedziczenia, a nam zależało także na dostaniu się z klasy C do atrybutów z A.
 
Ale najważniejsza uwaga dotycząca tego przykładu jest inna: pamiętając o tym, że obiekt podklasy składa się z warstw i zawiera wszystkie atrybuty zadeklarowane w nadklasach, unikajmy sięgania do atrybutów z nadklas! Dlaczego? Bo są one częścią opisu tego ''jak'' działają obiekty nadklasy, a nie tego ''co'' one robią. A zgodnie z regułami kapsułkowania nie chcemy nigdy znać wewnętrznej reprezentacji obiektów. Chcemy być niezależni od jej zmian. Oczywiście sytuacja, w której odwołujemy się do atrybutów zupełnie innego obiektu (absolutnie nigdy nie należy tego robić) jest inna od sytuacji, gdy odwołujemy się do atrybutów własnych, tyle że odziedziczonych po nadklasie. Można sobie wyobrazić sytuację, gdy definiujemy jednocześnie całą hierarchię własnych klas i zależy nam na rozdzieleniu poszczególnych części implementacji między klasy w tworzonej hierarchii i co więcej chcemy, by klasy były świadome tego podziału. Takie rozwiązanie jest akceptowalne, do jego realizacji zwykle stosuje się modyfikator widoczności '''protected''' (choć pamiętajmy, że niestety w Javie daje on zbyt szeroko prawa dostępu - także klasom nie znajdującym się w tworzonej hierarchii, a tylko znajdującym się w tym samym pakiecie). Jeśli jednak przewidujemy, że poszczególne części naszej hierarchii mogą być modyfikowane niezależnie od siebie, to dużo bezpieczniej jest odwoływać się tylko do operacji definiowanych przez klasy, a nie ich wewnętrznej implementacji.
 


== Polimorfizm  ==
== Polimorfizm  ==
Linia 184: Linia 191:
== Klasy abstrakcyjne i interfejsy  ==
== Klasy abstrakcyjne i interfejsy  ==


<!-- Zwykle po to tworzymy klasy by tworzyæ ich egzemplarze. -->
<!-- Zwykle po to tworzymy klasy by tworzyć ich egzemplarze. -->


== Podsumowanie ==
== 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.
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ść 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).
* 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).
* Zgodność interfejsów (osiągana przez tworzenie hierarchii klas dziedziczących po wspólnej nadklasie lub interfejsie).




Problemy:
Problemy:
* Problem jo-jo (nadu¿ywanie dziedziczenia mo¿e uczyniæ czytanie programu bardzo ¿mudnym procesem).
* 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).
* 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.
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 11:09, 5 paź 2006

<<< Powrót

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ć.

Jak widać z tego przykładu, nawet gdy nazwy atrybutów się pokrywają, dalej obiekt klasy C ma warstwy pochodzące z nadklas, tyle że teraz trudniej dostać się do odziedziczonych atrybutów. Zastosowana notacja z rzutowaniem daje wprawdzie dostęp do każdego atrybutu ale jest bardzo nieelegancka i nie należy jej używać w programach (z różnych powodów, jeszcze o nich więcej powiemy). Do dostępu do atrybutów (i ogólnie składowych) z bezpośredniej nadklasy służy bezpieczna notacja ze słowem kluczowym super (zasygnalizowana w komentarzu z powyższego przykładu). Tak jak słowo kluczowe this pozwala się odnieść do bieżącego obiektu (tego, którego metodę obecnie wykonujemy), tak super również odnosi się do tego obiektu, ale traktowanego jako obiekt nadklasy, to znaczy przestają być widoczne wszystkie te deklaracje, które wprowadziła klasa tego obiektu, odsłaniając w ten sposób przesłonięte deklaracje z nadklasy. Z definicji notacja super.składowa oznacza tyle samo co ((Nadklasa) this).składowa. Z notacji super będziemy bardzo często korzystać w programach pisanych w Javie (na przykład przy przedefiniowywaniu metod lub konstruktorów).

Czemu zatem w przykładzie nie użyto tej notacji? Dlatego, że pozwala ona sięgnąć tylko jeden poziom wyżej w hierarchii dziedziczenia, a nam zależało także na dostaniu się z klasy C do atrybutów z A.

Ale najważniejsza uwaga dotycząca tego przykładu jest inna: pamiętając o tym, że obiekt podklasy składa się z warstw i zawiera wszystkie atrybuty zadeklarowane w nadklasach, unikajmy sięgania do atrybutów z nadklas! Dlaczego? Bo są one częścią opisu tego jak działają obiekty nadklasy, a nie tego co one robią. A zgodnie z regułami kapsułkowania nie chcemy nigdy znać wewnętrznej reprezentacji obiektów. Chcemy być niezależni od jej zmian. Oczywiście sytuacja, w której odwołujemy się do atrybutów zupełnie innego obiektu (absolutnie nigdy nie należy tego robić) jest inna od sytuacji, gdy odwołujemy się do atrybutów własnych, tyle że odziedziczonych po nadklasie. Można sobie wyobrazić sytuację, gdy definiujemy jednocześnie całą hierarchię własnych klas i zależy nam na rozdzieleniu poszczególnych części implementacji między klasy w tworzonej hierarchii i co więcej chcemy, by klasy były świadome tego podziału. Takie rozwiązanie jest akceptowalne, do jego realizacji zwykle stosuje się modyfikator widoczności protected (choć pamiętajmy, że niestety w Javie daje on zbyt szeroko prawa dostępu - także klasom nie znajdującym się w tworzonej hierarchii, a tylko znajdującym się w tym samym pakiecie). Jeśli jednak przewidujemy, że poszczególne części naszej hierarchii mogą być modyfikowane niezależnie od siebie, to dużo bezpieczniej jest odwoływać się tylko do operacji definiowanych przez klasy, a nie ich wewnętrznej implementacji.


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/>