PO Dziedziczenie i interfejsy

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania

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

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