Image
22.6.2016 0 Comments

Java pod lupou I. /9.časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

Dedičnosť

Byl pozdní večer, první máj… hm, ako ten čas letí. No, ale naspäť k Jave. V minulej časti sme hovorili o deklarácii tried a vytváraní objektov. Tento mesiac sa budeme zaoberať spájaním tried do hierarchie dedičnosti a princípom fungovania polymorfizmu.

Vzťah dedičnosti

Pri objektovo-orientovanom návrhu analytik často prichádza k zisteniu, že objekt, ktorý práve navrhol, sa do značnej miery podobá inému objektu, dokáže reagovať na rovnaké správy (i keď nie nutne rovnakým spôsobom) a naviac pridáva svoju vlastnú funkčnosť. Bolo by preto dobré, keby sa nový objekt dal vytvoriť na základe pôvodného, s explicitným vymenovaním nových vlastností a s tým, že všetky nezmenené vlastnosti nový objekt automaticky „zdedí“. Tento jav, nazývaný dedičnosť, patrí medzi základné, hoci nie fundamentálne princípy OOP. Je takisto možné sa stretnúť s iným pomenovaním vzťahu dedičnosti – môžeme povedať, že odvodený objekt, tzv. potomok, je špeciálnym prípadom nadradeného objektu, predka a naopak, predok je všeobecnejší prípad potomka. Vzťah medzi oboma objektmi potom nazývame vzťah špecializácie/generalizácie.

V Jave sa dedičnosť realizuje úpravou deklarácie tried. Ak chceme, aby nová trieda bola potomkom inej, existujúcej triedy, vyjadríme to pomocou kľúčového slova extends. Tu je príklad:

// Point.java
public class Point
{
  protected int x, y;
 
  Point()
  { this(0, 0); }
 
  Point(int x, int y)
  { this.x = x; this.y = y; }
 
  public void move(int x, int y)
  { this.x = x; this.y = y; }
 
  public void show()
  { System.out.println("Point at ["
      + x + ", " + y + "]"); }
}
 
// ColorPoint.java
public class ColorPoint extends Point
{
  protected int c;
 
  ColorPoint()
  { this(0, 0, 0); }
 
  ColorPoint(int x, int y, int c)
  { super(0, 0); this.c = c; }
 
  public void show()
  { System.out.println("ColorPoint at ["
      + x + ", " + y + "], color " + c); }
}

(Z dôvodu úspory miesta je výpis mierne „zhustený“. Čitateľovi sa neodporúča tento spôsob zápisu používať vo svojich programoch.)

Zdrojový text triedy Point je prakticky rovnaký ako minule. Nová trieda ColorPoint predstavuje špeciálny prípad všeobecného bodu: farebný bod, ktorý okrem svojich dvoch súradníc obsahuje naviac tretiu zložku – farbu. Pozrime sa na deklaráciu triedy ColorPoint. Za názvom triedy objavíme spomínané kľúčové slovo extends, za ktorým nasleduje názov nadradenej triedy, čo je v našom prípade Point. Nadradená trieda môže byť len jedna, pretože Java na rozdiel od C++ nepodporuje viacnásobnú dedičnosť. V tele triedy sa nachádza len deklarácia polí a metód, ktoré sú „iné“ oproti nadradenej triede. V triede ColorPoint konkrétne pribudol jeden údajový člen c, ktorý obsahuje informáciu o farbe. Členy xy triedy Point sa do novej triedy preberajú automaticky (hovoríme, že sa dedia). Ďalej v triede ColorPoint nájdeme dva konštruktory. Tie musíme doplniť vždy, pretože konštruktor, ako špeciálny typ metódy, sa nikdy nededí. Konečne poslednou metódou, ktorej deklaráciu v triede ColorPoint nájdeme, je metóda show(). Táto metóda by sa za normálnych okolností prebrala z triedy Point, ale my chceme, aby sa farebný bod „zobrazoval“ iným spôsobom ako obyčajný bod. Preto sme telo metódy show() prepísali vlastnou verziou. Príklad vytvorenia a práce s objektom ColorPoint možno nájsť na web stránke PC REVUE v sekcii Programujeme.

Implementácia

Princíp dedenia členov a metód je implementačne veľmi jednoduchý. Objekt potomka v sebe obsahuje akoby kópiu objektu predka, so všetkými jeho členskými premennými. Inštancia našej triedy ColorPoint tak napríklad v sebe obsahuje kompletnú inštanciu triedy Point a naviac jeden „vlastný“ údajový člen c. Členy a metódy objektu môžeme teda rozdeliť na dve skupiny: vlastné (deklarované v danej triede) a zdedené (deklarované v triede či triedach nadradených).

Hierarchia dedičnosti sa neobmedzuje na jednu úroveň. Ak by sme potrebovali vytvoriť špeciálnu verziu farebného bodu, odvodili by sme novú triedu, ktorá by bola potomkom triedy ColorPoint. Takto môžeme zostrojiť celý hierarchický strom tried. Koreňom tohto stromu je v Jave preddefinovaná trieda Object, ktorá je priamym, či nepriamym predkom každej existujúcej javovskej triedy. Ak pri deklarácii triedy neuvedieme jej predka, automaticky sa nová trieda stáva potomkom triedy Object.

Táto superrodičovská trieda obsahuje niekoľko univerzálnych metód, spoločných všetkým triedam a okrem toho umožňuje neobyčajnú flexibilitu pri narábaní s inštanciami objektov. V Jave, podobne ako vo väčšine objektových jazykov, platí dôležité pravidlo: všade tam, kde sa očakáva inštancia nejakej triedy, môžeme vždy použiť inštanciu triedy odvodenej. Nasledujúci kód je teda korektný:

Point[] pts = new Point[3];
pts[0] = new Point(10, 40);
pts[1] = new ColorPoint(30, 5, 12);
pts[2] = new ColorPoint();

Hoci sú jednotlivé prvky poľa pts typu Point, priraďujeme im tiež odkazy na objekty typu ColorPoint. Prekladač ani run-time chybu neohlási, pretože ako sme hovorili vyššie, každý objekt typu ColorPoint v sebe v skutočnosti obsahuje skrytý objekt typu Point. Opačný spôsob použitia v zásade nie je možný a vedie k chybe pri preklade či pri behu programu. Ak potrebujeme takto uložiť do poľa alebo inej údajovej štruktúry nepríliš súvisiace objekty, vždy môžeme použiť ako spoločný typ Object.

Finálne triedy

V minulom pokračovaní seriálu sme hovorili okrem iného o tom, že každá trieda môže byť deklarovaná s modifikátorom final. Takáto trieda sa považuje za „finálnu“ – nie je totiž od nej možné odvodiť žiadneho potomka. Pokus o deklaráciu triedy, ktorá by rozširovala finálnu triedu, skončí chybou pri preklade.

Zmysel existencie finálnych tried vyplýva z práve spomenutého pravidla „potomok môže vždy zastúpiť predka“. Ako finálnu obyčajne definujeme takú triedu, pri ktorej sa potrebujeme spoľahnúť na jej funkčnosť a chceme zabrániť možnosti nahradiť ju inou triedou (odvodenou).

Skrývanie členov

Odvodená trieda môže obsahovať údajový člen s rovnakým menom, ako má niektorý člen v nadradenej triede, rovnakého alebo aj odlišného typu. V takejto situácii hovoríme, že člen potomka skrýva člen predka. Termín skrývanie je na rozdiel od nasledujúceho prípadu presným prekladom pôvodného anglického výrazu „hiding“. Skrytý člen nie je prístupný bežným spôsobom; ak s ním chceme pracovať, musíme ho explicitne sprístupniť pomocou kľúčového slova super. Zoberme si príklad:

class A { protected int x = 1; }
 
class B extends A
{
  protected int x = 2;
  public void print()
  {
    System.out.println("super.x = " + super.x);
    System.out.println("x = " + x);
  }
}

Ak vytvoríme objekt triedy B a zavoláme jeho metódu print(), dostaneme takýto výstup:

super.x = 1
x = 2

Názov x v metóde print() predstavuje celočíselný člen triedy B. K členu x zdedenému z triedy A sa dostaneme pomocou výrazu super.x. Podobne ako this sprístupňuje aktuálnu inštanciu, kľúčové slovo super predstavuje odkaz na nadradenú inštanciu.

V prípade, že sa potrebujeme dostať k zakrytému členu z niektorého nepriameho predka (t. j. viac ako o jednu úroveň smerom „nahor“), logické by bolo použiť zápis super.super.x. To bohužiaľ nefunguje, je nutné explicitne pretypovať this na príslušnú nadtriedu:

public class C extends B
{
  protected boolean x;
  public void anotherPrint()
  { System.out.println(((A)this).x); }
}

K členu x z triedy A sa v metódach triedy C dostaneme pomocou výrazu ((A)this).x. V prípade, že by zakrytý člen bol statický, možno použiť aj výraz A.x, resp. B.x.

Pozrime sa ešte, čo sa stane, ak pristupujeme k objektu potomka pomocou premennej typu predka:

B b = new B();
A a = b;
System.out.println("a.x = " + a.x);
System.out.println("b.x = " + b.x);

Máme dve premenné a a b, jednu typu A, druhú typu B. Obe však odkazujú na tú istú inštanciu triedy B. Horeuvedený kód vypíše na obrazovku toto:

a.x = 1
b.x = 2

Je zrejmé, že výraz a.x sprístupňuje skrytý člen x, ktorý trieda B zdedila od triedy A. Dôvod je jednoduchý – premenná a je typu A a v Jave pri prístupe k údajovým členom triedy nezáleží na skutočnom type objektu, ale len na type výrazu, pomocou ktorého sa na objekt odvolávame. Tento typ je známy už pri preklade. Ak by sme potrebovali pracovať s členom tej triedy, ktorej inštanciou objekt skutočne je, bude nutné použiť iný spôsob, ako uvidíme z nasledujúceho odseku.

Skrývanie a predefinovanie metód

V minulej časti sme hovorili o tom, že dve metódy jednej triedy môžu mať rovnaký názov, ak sa líšia počtom a/alebo typmi svojich argumentov. Tomuto javu sa v angličtine hovorí „method overloading“. Vhodný preklad termínu „overloading“ akosi neexistuje – často používaný kalk „preťažovanie“ má od skutočného významu pomerne ďaleko. Snáď by sa dal použiť výraz prekrývanie metód.

Iná situácia nastane v prípade, že deklarujeme v odvodenej triede metódu s rovnakým názvom, ako má niektorá zdedená metóda. O takejto metóde hovoríme, že predefinuje metódu nadradenej triedy. V angličtine sa používa termín „method overriding“. Metóda v odvodenej triede nesmie mať iný typ návratovej hodnoty ako predefinovaná metóda.

Predefinovanie metód používame vtedy, keď nám reakcia nadradenej triedy na príslušnú správu nevyhovuje a chceme, aby sa odvodená trieda správala ináč. V prípade triedy ColorPoint zo začiatku článku napríklad chceme, aby sa farebný bod „zobrazoval“ iným spôsobom ako obyčajný, bezfarebný bod. Z toho dôvodu v triede ColorPoint predefinujeme metódu show(). Naproti tomu metóda move() predefinovaná nie je; dedí sa bezo zmeny z triedy Point, pretože jej telo nám vyhovuje – farebný bod sa „pohybuje“ tak isto, ako obyčajný bod.

Pri predefinovaní metódy sa môže stať, že potrebujeme predsa len zavolať normálne neprístupnú metódu nadradenej triedy. V takej situácii opäť pomôže kľúčové slovo super, ako vidno aj z nasledujúceho príkladu:

class A
{
  public void foo()
  { System.out.println("Calling A.foo()"); }
}
 
class B extends A
{
  public void foo()
  { System.out.println("Calling B.foo()"); }
 
  public void print()
  {
    foo();
    super.foo();
  }
}

Ak nad objektom triedy B zavoláme metódu print(), dostaneme na obrazovke takýto výpis:

Calling B.foo()
Calling A.foo()

Výraz super.foo() teda vyvolal predefinovanú metódu foo() triedy A.

V prípade, že v odvodenej triede deklarujeme statickú metódu s rovnakou signatúrou ako niektorá metóda nadradenej triedy, nehovoríme o predefinovaní, ale, podobne ako pri údajových členoch, o skrytí metódy. Aj skrytú metódu možno sprístupniť prostredníctvom kľúčového slova super.

Nie je možné statickú metódu predefinovať nestatickou a takisto nie je možné nestatickú metódu skryť prostredníctvom statickej metódy, oboje vedie k chybe pri preklade.

Pri prístupe k metódam potomka prostredníctvom premennej typu predka dochádza oproti údajovým členom k závažnej odlišnosti. Viac ukáže príklad:

B b = new B();
A a = b;
b.foo();
a.foo();

Po prebehnutí tohto úseku programu by sme po predchádzajúcej skúsenosti očakávali, že sa v prvom prípade zavolá metóda triedy B a v druhom prípade metóda triedy A. Dostaneme však nasledovný výstup:

Calling B.foo()
Calling B.foo()

Čo sa stalo? V oboch prípadoch sa zavolala metóda foo() triedy B, hoci premenná a je typu A. V Jave sa totiž (na rozdiel od prístupu k údajovým členom) pri volaní metódy určí skutočný, run-time typ objektu, ktorého metódu voláme a podľa tohto typu sa vyberie správna verzia metódy z príslušnej triedy.

Ak porovnáme Javu s C++, zistíme, že zatiaľ čo v C++ sú členské funkcie implicitne nevirtuálne, so statickou väzbou (early binding – správna funkcia sa vyberá už počas prekladu), v Jave sú naopak metódy automaticky „virtuálne“, dynamicky viazané (late binding – konkrétna metóda sa vyberá až počas behu programu, podľa skutočného typu objektu). Ak v C++ existuje možnosť zmeniť funkciu z nevirtuálnej na virtuálnu, kľúčovým slovom virtual, malo by v Jave byť možné deklarovať metódu ako nevirtuálnu. To sa zabezpečí kľúčovým slovom final. Metóda deklarovaná ako finálna nemôže byť v odvodenej triede predefinovaná a pri jej volaní sa nevyhodnocuje dynamický typ objektu.

Mimochodom, práve dynamickému vyhodnocovaniu metód vďačíme za polymorfné správanie objektov v Jave. Polymorfizmus znamená v preklade mnohotvarosť; rôznym polymorfným objektom môžeme poslať jednu a tú istú správu a ony na ňu zareagujú každý „po svojom“. V Jave sa posielanie správ realizuje volaním metód. Programátorovi teda môže byť úplne jedno, nad akým objektom metódu volá; jej správna verzia sa totiž nájde počas behu programu na základe skutočného typu objektu.

Konštruktory a dedičnosť

Na začiatku článku sme spomínali, že konštruktory sa automaticky nededia. Pre každú odvodenú triedu preto musíme napísať konštruktor nanovo. Môže sa však stať, že konštruktor odvodenej triedy už z princípu bude čiastočne opakovať kód konštruktora nadradenej triedy.

Zoberme si náš farebný bod. Ten zrejme budeme konštruovať rovnako ako obyčajný bod, len naviac do jeho stavu uložíme informáciu o farbe. Bolo by preto nanajvýš efektívne, keby sme mali možnosť povedať, že farebný bod sa má skonštruovať presne tak isto ako jeho predok, obyčajný bod, ale naviac si má ešte zapamätať svoju farbu. Našťastie v Jave túto možnosť máme – ako prvý príkaz konštruktora môžeme uviesť kľúčové slovo super nasledované prípadnými argumentmi v okrúhlych zátvorkách. To spôsobí zavolanie príslušného konštruktora nadradenej triedy. (Ak žiaden s vhodným počtom a typmi argumentov neexistuje, dôjde samozrejme pri preklade k chybe.) Volanie superkonštruktora nie je povinné, ale ak ho neuvedieme, resp. ho neumiestnime ako prvý príkaz konštruktora, doplní sa namiesto neho automaticky volanie bezargumentového konštruktora (t. j. super();). Problém by mohol nastať v prípade, že nadradená trieda nemá konštruktor bez argumentov. Ak však k takejto situácii dôjde, je „niečo prehnité“ v návrhu tried.

Jedinou výnimkou, kedy nie je volanie nadradeného konštruktora nutné a ani sa automaticky nedopĺňa, je prípad, keď z jedného konštruktora voláme iný v rámci tej istej triedy. O tom sme hovorili minule – ako jediný príkaz konštruktora v takomto prípade bude kľúčové slovo this nasledované vhodnými argumentmi v okrúhlych zátvorkách.

Použitie kľúčového slova super pri volaní konštruktora nadradenej triedy vidno v triede ColorPoint. Jej konštruktor s tromi argumentmi prvé dva (to sú súradnice bodu) pekne krásne pošle nadradenému konštruktoru a ani v najmenšom sa nestará, čo sa s nimi bude diať. Zaujíma ho až tretí argument, ktorý si uloží „pre vlastnú potrebu“. Ako vidno, pri takomto prístupe je konštruovanie objektu rozdelené do jednotlivých hierarchických stupňov a každá trieda, resp. jej konštruktor, inicializuje len to, čo sama do objektu pridala. No a ak by sa náhodou stalo, že by bolo nutné zmeniť spôsob inicializácie inštancií triedy Point, trieda ColorPoint zostane bezo zmien.

Riadenie prístupu

Teraz, keď už vieme, ako je to s odvodzovaním nových tried pomocou dedenia, môžeme doplniť informácie z minulej časti seriálu o modifikátoroch prístupu. Ako si čitateľ iste spomenie, deklaráciu každého člena alebo metódy môžeme doplniť jedným z kľúčových slov private, protected alebo public. Pri nastavovaní prístupu nezáleží na tom, či sú deklarované členy alebo metódy statické alebo nestatické.

Členy a metódy deklarované ako private sú pre danú triedu privátne, súkromné. Prístup k nim majú len metódy tejto triedy a nikto iný. Nemožno s nimi pracovať „zvonka“ a nemajú k nim prístup ani triedy odvodené. Privátne členy realizujú princíp zapuzdrenia, skrytia stavu objektu pred jeho okolím. Ešte na vyjasnenie: v originálnej špecifikácii Javy sa hovorí, že privátne členy a metódy sa nededia. To je pravda, ale myslí sa tým, že ich odvodené triedy „nevidia“ a nemôžu k nim nijako pristupovať. Napriek tomu sa však tieto členy v inštanciách odvodených tried fyzicky nachádzajú (inak by to ani nešlo a celý mechanizmus dedičnosti by bol nanič).

Členy a metódy s modifikátorom protected („chránené“) sú takisto zvonka neprístupné, na rozdiel od privátnych členov k nim však majú prístup aj metódy odvodených tried. Pokiaľ teda máme objektovú hierarchiu navrhnutú tak, že odvodené triedy musia pracovať s členmi nadradených tried, je nutné tieto členy deklarovať ako chránené. Príkladom môžu byť triedy PointColorPoint z úvodu článku. Údajové členy xy sú v triede Point deklarované ako protected, vďaka čomu ich metóda show() triedy ColorPoint môže použiť na výpis súradníc bodu. Treba si však uvedomiť, že v zásade je takmer vždy možné mať všetky členy privátne a sprístupňovať ich pomocou metód, takže voľba medzi privateprotected prístupom je viac-menej otázkou požadovanej efektivity implementácie.

Konečne modifikátor public deklaruje členy a metódy ako verejné, takže k nim má prístup ktokoľvek, odkiaľkoľvek. Z pochopiteľných dôvodov nie je veľmi rozumné deklarovať v triede verejné údajové členy – môže sa totiž stať, že ich ktorýsi objekt modifikuje nesprávnym, nekonzistentným spôsobom a už je narušená integrita programu. Verejné členy majú zmysel predovšetkým v triedach, ktoré slúžia ako jednoduché štruktúrované typy (analógia struct v C++).

Aby sme boli presní, existuje ešte jedna úroveň prístupu – ak neuvedieme ani jeden zo spomenutých troch modifikátorov, člen alebo metóda má tzv. implicitnú alebo balíkovú úroveň prístupu. Ale o tom až nabudúce.

Od rozhraní k balíkom

V ďalšom pokračovaní prídu na rad abstraktné triedy, rozhrania a balíky. Dovidenia opäť o mesiac.

 

 


Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Tipy a triky: Ako na snímku obrazovky na akomkoľvek počítači s Windows?

02.12.2016 00:13

Ak snímky obrazovky robíte často apotrebujete napríklad funkcie na posun stránok alebo snímanie zobrazenia pri vyššom rozlíšení displeja, zrejme používate nejakú špecializovanú aplikáciu. Väčšina použ ...

Ako na to 1

Tipy a triky: Ako aplikácii prednastaviť spúšťanie s administrátorskými právami?

30.11.2016 00:10

Väčšina aspoň trochu skúsenejších používateľov vie, že aj keď máte na operačnom systéme Windows vytvorený administrátorský účet, aplikácie pre bezpečnosť nefungujú vždy splnými administrátorskými práv ...

Ako na to 2

Tipy a triky: Ako vypnúť uzamykaciu obrazovku vo Windows 10?

29.11.2016 00:10

Rozčuľuje vás, že pred každým prihlásením doúčtu vášho počítača musíte prejsť uzamykacou obrazovkou? Windows 10 na tejto obrazovke ukazuje čas,dátum anejakú zaujímavú fotografiu zrôznych kútov sveta. ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá