13. hét    Öröklődés


Tartalom:

    13.1. Az öröklődés
    13.2. Adattagok elrejtése
    13.3. Metódusok felülírása és elrejtése
    13.4. A
super() kulcsszó használata
    13.5. Az Object osztály
toString()és equals() metódusa
        13.5.1. A
toString() metódus
        13.5.2. Az
equals() metódus
    13.6. A
compareTo() metódus
    13.7. Végleges osztályok és metódusok
    13.8. Tesztkérdések
    13.9. Feladatok


13.1. Az öröklődés

    A 11. leckében már volt szó az öröklődés elméleti alapjairól. Ezen eszköz segítségével létező ősosztályból származtathatunk tetszőleges számú leszármazott osztályt, annak továbbfejlesztése, kibővítése céljából. Az utódosztály további osztályok őse lehet, így egész osztályhierarchiák építhetők fel. A Java-ban minden osztálynak szigorúan csak egy közvetlen szülőosztálya van. Fontos tudni, hogy egy leszármazott osztály minden olyan üzenetre tud reagálni, amelyre az őse is tudna. Az utódosztály örökli összes ősének minden olyan adattagját és metódusát, amely nem private minősítésű.

    Az öröklődéssel egy - már létező - osztály adattagjait (tulajdonságait) és metódusait (viselkedésmódját) bővíthetjük, módosíthatjuk:

    Alapértelmezésben egy újonnan létrehozott osztálynak az Object nevű osztály lesz az őse, amely a Java osztályhierarchiájának csúcsán áll, és minden Java-beli osztály közös őse. Ezt kihasználva olyan általános programokat írhatunk, amelyek nem kötik ki, hogy milyen osztályú objektumokra van szükségük. Pl. implementálhatunk egy általános felhasználású verem adatszerkezetet, amely bármilyen objektumot tud kezelni. Az Object osztály az egyetlen külön nem importálandó csomag, a java.lang csomag része.

    Ha létrehozandó osztályunk számára nem megfelelő (pl. túl általános) az Object osztály, akkor az extends kulcsszót használva más - specifikusabb - ősosztályból is örököltethetjük az új osztályunkat.

Az öröklődés deklarációja:

[módosítók] class <utódosztály_neve> extends <ősosztály_neve> [implements <interfészek_neve>];

 

13.2. Adattagok elrejtése

    Ha egy osztály adattagja ugyanazt a nevet viseli, mint a szülőosztálya, akkor elrejti azt (még akkor is, ha különböző típusúak). Ekkor a szülőosztály adattagjára nem hivatkozhatunk egyszerűen a nevével, ezért általában nem javasolt az adattagok elrejtése.

 

13.3. Metódusok felülírása és elrejtése

    Ha egy leszármazott osztály metódusának ugyanaz a szignatúrája (azaz neve, paramétereinek száma és típusa), valamint visszatérési értéke, mint az ősosztály metódusának, akkor a leszármazott osztály felülírja az ősosztály metódusát. Ez a képesség lehetővé teszi, hogy egy elég közeli viselkedésű osztályból öröklődve - annak viselkedését szükség szerint megváltoztatva - új, leszármazott osztály jöhessen létre. Például az Object osztály tartalmaz egy toString() nevű metódust, amely visszaadja az objektumpéldány szöveges reprezentációját. Ezt a metódust minden osztály megörökli. Az Object ezen metódusának végrehajtása azonban nem túl hasznos a leszármazott osztályok számára, ezért a metódus felülírása célszerű, hogy használhatóbb információt nyújthasson az objektum saját magáról. Erről bővebben e lecke további részében lesz szó.

    A felülíró metódus láthatósága lehet bővebb, mint a felülírt metódusé, de szűkebb nem. Mivel egy leszármazott osztály objektuma bárhol használható, ahol egy ősosztálybeli objektum, ezért a leszármazott egyik tagjának láthatósága sem szűkülhet, hiszen akkor az ilyen használat lehetetlen lenne.

    Egy leszármazott osztály nem tudja felülírni az olyan metódusokat, amelyek az ősosztályában final (végleges) minősítéssel definiáltak. Azonban felül kell írni azokat a metódusokat, amelyeket a felsőbb osztályban abstract-nak nyilvánítottak, vagy magának a leszármazott osztálynak is absztraktnak kell lennie.

    Ha egy leszármazott osztály egy osztálymetódust ugyanazzal az aláírással (szignatúrával) definiál, mint a felsőbb osztálybeli metódus, akkor a leszármazott osztály metódusa elrejti (elfedi) a szülőosztálybelit. Fontos megkülönböztetni a felülírást az elrejtéstől! Az alábbi példaprogram jól szemlélteti a különbséget:

    Adott egy Állat ősosztály, melynek osztály- és példánymetódusa is van:


    Származtassunk belőle egy Macska utódosztályt, amelyben az
o_elrejt() osztálymetódust elrejtjük, és a p_felülír() példánymetódust pedig felülírjuk.


    Ebben az osztályban a
main metódus létrehoz egy Macska példányt Tóni néven, majd ezt a példányt típuskonverzióval átteszi egy Állat példányba állat (kisbetűvel!) néven. Utóbbi példányra hívjuk meg mindkét metódust! Az eredmény:


    Az
o_elrejt() metódus a szülőosztályból került meghívásra, míg a p_felülír() a leszármazottból.

    Osztálymetódushoz a futtatórendszer azt a metódust hívja meg, amely a hivatkozás szerkesztési idejű típusában van definiálva. A példában az állat példány szerkesztési idejű típusa az Állat. Így a futtatórendszer az Állat-ban definiált, rejtett metódust hívja meg. A példánymetódusnál a futtatórendszer a hivatkozás futásidejű típusában meghatározott metódust hívja meg. A példában az állat példány futásidejű típusa a Macska, vagyis a futtatórendszer a Macska osztályban definiált felülíró metódust hívja meg.

    Fontos még tudni, hogy egy példánymetódus nem tud felülírni egy osztálymetódust, és egy osztálymetódus nem tud elrejteni egy példánymetódust.

A példaprogram forráskódja: Allatok.rar.

 

13.4. A super() kulcsszó használata

    Az öröklődés során csak az adattagok és a metódusok kerülnek át a leszármazott osztályba, a konstruktorok nem. Mégis van lehetőség a leszármazott osztály konstruktorából az ősosztály egy konstruktorát meghívni. Ezt a super() kulcsszóval tehetjük meg. A super() egy hivatkozás az ősosztályra, működése hasonló a this() kulcsszóhoz. Segítségével az ősosztály egy konstruktorára vagy egy felüldefiniált metódusára hivatkozhatunk. Ha nem írunk konstruktort, akkor a fordító automatikusan beilleszt egy paraméter nélkülit, amelynek első sora a super() konstruktorhivatkozás. Ezért ha az ősosztályunk nem rendelkezik paraméter nélküli konstruktorral, hibaüzenetet kapunk. Ilyen esetben írnunk kell egy konstruktort, amelyben meg kell adnunk, hogy melyik őskonstruktort szeretnénk használni, s milyen paraméterekkel.

    Az előző lecke Bankbetét osztályából örököltetjük a KamatosBetét osztályt, majd az új osztályt bővítjük egy új adattaggal, amely a kamatösszeget tárolja:


    Figyeljük meg a
this minősítő használatát is! A sima kamat azonosító a konstruktor lokális változóját jelenti, viszont a this.kamat az új osztály adattagját hivatkozza.

A példaprogram forráskódja: Bankbetet.rar.

 

13.5. Az Object osztály toString()és equals() metódusa

    Van az Object osztálynak két olyan alapvető metódusa, amelyeket a leszármazott osztályok példányain sokszor alkalmazunk. Érdemes és hasznos minden új osztály definíciója során felülírni őket, hogy a toString() megfelelő értékeket reprezentálhasson, ill. a saját igényeinknek megfelelő módon tudjunk az equals() segítségével objektumokat összehasonlítani, hogy egyenlők-e.


13.5.1. A
toString() metódus

    Gyakori eset, hogy tájékoztatást kívánunk kapni egy objektum pillanatnyi állapotáról, amelyet az adattagjai reprezentálnak. Erre a Java-ban a toString() névtelen metódus szolgál. Azonban ez a metódus alapesetben nem azt jeleníti meg, amit várnánk. Nézzük, mi lesz a System.out.println(b1) utasítás eredménye:

    Amit látunk, az nem az objektumpéldány adattagjainak tartalma, hanem az azonosítója (OID - Object ID), amely az alábbi felépítésű: <csomagnév>.<osztálynév>@<objektumazonosító>. Minden objektumpéldánynak külön azonosítója van. Definiáljuk felül a nem statikus toString() metódust úgy, hogy a mi elvárásunknak megfelelő kimenetet produkáljon!

Az Object osztály toString() metódusának felüldefiniálása:

    @Override
    public
String toString() {
        return <objektumjellemzőket leíró karakterlánc>
    }


    Az alábbiakban látható egy lehetséges megoldás a
Bankbetét osztály példányainak szöveges formában való megjelenítésére. Figyeljük meg a System.getProperty("line.separator") soremelő metódust, amely biztosítja az adattagok külön sorba tördelését!


13.5.2. Az
equals() metódus

    Két - azonos osztályhoz tartozó - objektum akkor egyenlő, ha az adattagjaik által reprezentált állapotuk megegyezik. Ezt összehasonlító operátorral nem tudjuk megállapítani, ugyanis a (<objektumnév1> == <objektumnév2>) logikai kifejezés az érintett objektumok mutatóját hasonlítja össze, ami természetesen csak akkor lesz azonos, ha egy objektumot önmagához hasonlítunk. Az equals() metódus viszont képes úgy összehasonlítani két objektumot, hogy az összehasonlítás alapját képező adattagok körét mi határozhatjuk meg a metódus felüldefiniálásával.

Az Object osztály equals() metódusának felüldefiniálása:

    @Override
    public
boolean equals(Object obj) {

        if (obj == null || !(obj instanceof <Osztálynév>)
            return false;
        if (obj == this) return true;

        return <adattag_összehasonlító_logikai_kifejezések>
    }


    Figyeljük meg a metódus törzsének első utasítását, amely azonnal kiszűri az üres és az "osztályidegen" objektumokat, ill. a második utasítás felismeri azt, hogy az objektumot önmagához hasonlítjuk.

    Az alábbi példában azokat a Bankbetét objektumokat tekintjük egyenlőnek, amelyek tulajdonosának neve megegyezik:

 

13.6. A compareTo() metódus

    Ha azt szeretnénk elérni, hogy egy osztály példányai valamely adattagjuk alapján összehasonlíthatók legyenek, akkor a compareTo() metódust kell ehhez az osztályba implementálni. A metódus összehasonlítja az aktuális objektumot az átvett objektummal, és a visszatérési értéke attól függően, hogy az aktuális objektum kisebb, egyenlő, vagy nagyobb az átvettnél, rendre negatív egész, nulla ill. pozitív egész érték lesz.

    A compareTo() metódust akkor érdemes - és kötelezően kell is - implementálni egy osztályba, ha célunk az osztály objektumainak sorba rendezése valamely adattagjuk alapján. Ilyenkor az osztálydefiníció fejrészét az implements Comparable utasítással kell kiegészíteni.

    A következő példában kiegészítjük a KamatBetét osztály definícióját úgy, hogy példányai kamatösszegük alapján összehasonlíthatók legyenek:

 

13.7. Végleges osztályok és metódusok

    A final kulcsszóval deklarált adattagok értéke inicializálásuk után már nem módosíthatók, és ezt nem teheti meg a leszármaztatott osztály sem. Ez egy fontos szempont a rendszer biztonsága és az objektum-orientált tervezés szempontjából. Ugyanez a helyzet az osztály metódusaival is. Ha nem akarjuk, hogy egy származtatott osztályban felüldefiniáljanak egy metódust, "véglegesíteni" kell. Az Object ősosztálynak is vannak final típusú metódusai, de nem mindegyik az, lásd toString(), equals().

    Ha magát az osztályt a final kulcsszóval deklaráljuk, akkor belőle leszármaztatott osztályt egyáltalán nem tudunk létrehozni.

 

13.8. Tesztkérdések

 

13.9. Feladatok

13.9.1. Készítsünk megfelelő osztályhierarchiát a hasáb és a gömb felszínének és térfogatának reprezentálására. Közös ősosztályuk neve legyen Test! A testek jellemző adatai (élek, sugár - cm-ben megadva) egész típusúak legyenek, a számított értékek pedig double típusúak!

Megoldás

 

13.9.2. Írjuk felül a két leszármazott osztály toString() metódusát úgy, hogy a példányaik adattagjai és a két metódusuk eredménye az alábbi formában jelenjenek meg (figyeljünk a Gömb osztálynál az 1 tizedes kerekítésre!):

Hasáb (élei: 5, 10, 15 cm)
 - felszíne  550 cm2
 - térfogata 750 cm3

Gömb (sugara: 10 cm)
 - felszíne  1256,6 cm2
 - térfogata 3141,6 cm3

Megoldás

 

13.9.3. Definiáljuk felül a Hasáb osztály equals() metódusát úgy, hogy két hasáb csak akkor legyen egyenlő, ha éleik hossza - függetlenül azok sorrendjétől - megegyezik!

Megoldás

 

13.9.4. Definiáljuk felül a Gömb osztály equals() metódusát úgy, hogy két gömb csak akkor legyen egyenlő, ha sugaruk megegyezik. Ellenőrizzük a metódus működését! Egészítsük ki ennek az osztálynak a definícióját úgy, hogy példányaik - sugaruk nagysága alapján - összehasonlíthatók legyenek!

Megoldás