Kihagyás

Újrafelhasználhatóság

Az előadás videója elérhető a itt.

Újrafelhasználhatóság

Az objektum orientált programok egyik legvonzóbb tulajdonsága a kód hatékony újrafelhasználásának lehetősége. Azonban fontos, hogy az újrafelhasználás nem egyszerű kód másolás. A procedurális nyelvekben maguk az eljárások által már megjelent a kód újrafelhasználása, hiszen az eljárás paraméterének függvényében más és más módokon tudunk végrehajtani egy-egy algoritmust.

Az objektum orientált programokban a már megírt osztályok segítségével újabb és újabb osztályokat tudunk összerakni. A lényeg, hogy mindezt úgy csináljuk, hogy a meglévő kódot (osztályokat) ne módosítsuk.

Az egyik lehetőség erre a kompozíció, amikor meglevő osztályok összerakásával készítünk új osztályt, és abból készítünk új objektumokat.

A másik lehetőség az öröklődés, amikor új osztályt hozunk létre a meglévő "altípusaként" új funkciók hozzáadásával.

Kompozíció

A kompozíció egy összetétel, aggregáció, rész-egész kapcsolat. Egy osztály egy attribútuma egy másik osztály vagy primitív típusú:

class Szemely {
    private String nev;
    private Datum szul;
    private String cim;
}

Öröklődés

Minden objektum orientált nyelvnek szerves része sz öröklődés. Java-ban minden új osztály implicite örököltetve van az Object-ből (direkt, vagy indirekt módon), így vannak eleve definiált funkciói minden objektumnak.

Amikor származtatunk egy új osztályt, az "olyan, mint a régi", illetve bővítheti, kiegészítheti azt (innen az extends kulcsszó). A származtatott az ős minden adatát és metódusát megkapja, megörökli az őstől. Az ős a gyerek általánosítása, a gyerek az ős speciális változata.

Legyen egy ősosztályunk:

class TisztitoSzer {
    public void tisztits() {}
    public void surolj() {}
}

Ez a TisztitoSzer ős osztályunkrendelkezik néhány interfész metódussal (amik publikusak, bárki által meghívhatóak).

Származtassunk ebből az osztályból egy MosoPor osztályt, ami így megörökli ezt a két metódust, amiből az egyiket (surolj) rögtön felül is definiálja, módosítja, az egész osztály viselkedését pedig kiegészíti egy további metódussal:

public class MosoPor extends TisztitoSzer {
    // metódus módosítása (felüldefiniálás):
    public void surolj() {
        super.surolj(); /* ... */
    }
    // új metódus hozzáadása:
    public void aztass() {}
}

A származtatott osztály objektumában mindhárom metódus elérhető:

MosoPor mp = new Mosopor();
mp.aztass();
mp.tisztits();
mp.surolj();

Ős osztály inicializálása

Öröklődéskor azonban nemcsak az ős osztály interfésze másolódik. Az származtatott objektumnak konkrétan egy kis darabja az ős objektum, amelyet bizony inicializálni is kell. Ezért a származtatott osztály konstruktorában nem szabad elfelejteni, hogy az ős objektumot is inicializálni kell. A legjobb, ha ehhez az ős konstruktorát használjuk, és azt hívjuk a származtatott konstruktorból.

Ezt teszi a származtatott generált default konstruktor is. Előbb inicializálódik az ős rész, utána a gyermek.

Ha nem default az ős konstruktor, hanem vannak argumentumai, akkor kézzel kell meghívni a super konstruktor hívás segítségével. A konstruktor első utasítása kell legyen az ős konstruktorának hívása. Erre a fordító is kényszerít, a super hívást nem előzheti meg semmi.

Vegyünk egy másik példát, amiben egy kész öröklődési hierarchiát építünk fel:

class Jatek {
    Jatek(int i) { /* ... */ }
}

class TablaJatek extends Jatek {
    TablaJatek(int i) {
        super(i); /* ... */
    }
}

public class Sakk extends TablaJatek {
    Sakk() {
        super(64); /* ... */
    }
}

Legyen egy Jatek az ősosztályunk, aminek van egy konstruktora, ami egy paramétert vár. Tegyük fel, hogy ez valamilyen módon inicializálja a játékterület méretét. A Jatek osztályt a TablaJatek osztály specializálja, így annak konstruktorában meg kell hívjuk a Jatek konstuktorát. Ezt tesszük a super(i) hívással, hiszen az ős konstruktora egy egész értéket vár. A Sakk osztály a TablaJatek osztályt specializálja. A Sakknak a mérete adott, így neki elegendő egy default konstruktort írni, azonban mivel az ősének nincs ilyen, a sakkra jellemző konstanssal kell azt meghívni.

Kompozíció és öröklődés

A kompozíció és öröklődés sűrűn használatos együtt. Míg az ős osztály inicializálására a fordító kényszerít, az adattag-objektumokra nekünk kell figyelni!

Mindkettőnél az új osztály részobjektumokat tárol, de más a kettő nagyon. Mi a különbség és mikor melyiket használjuk? Kompozíciót akkor használjunk, ha egy meglévő osztály funkcionalitását szeretnénk felhasználni, de az interfészét nem. A kliens csak az új osztály interfészét látja. Ezért a beágyazott objektum általában private elérésű.

Öröklődés esetén egy meglévő osztály speciális változatát készítjük el (specializálunk).

Kezdetben induljunk ki inkább a kompozícióból, majd ha kiderül, hogy az új osztály mégis egy speciális típusa a másiknak, akkor származtassunk. Származtatásnak elsődlegesen akkor van létjogosultsága, ha ősre való konvertálás is szükséges lesz. A túl sok származtatással azonban vigyázni kell, mert mély osztályhierarchiákhoz vezethet, ami pedig nehezen karbantartható kódot eredményezhet!

Ősre konvertálás

Mivel a származtatott az ős osztály egy "új típusa", logikus hogy mindenhol, ahol az ős használható, ott a származtatott is használható. Ez azt jelenti, hogy ahol ős típusú objektumot vár a program, ott a származtatott egy implicit konverzión megy át az ősbe.

Emlékezzünk vissza a hangszeres példára.

Adott volt a Hangszer, mint ős osztály:

class Hangszer {
    public void szolj() { /*...*/ }
}

Ennek a Zongora egy speciális esete, ami a Hangszerből örökölt metódust a saját módján valósítja meg:

class Zongora extends Hangszer {
    public void szolj() { /*...*/ }
}

A Hangolo osztályban adott hangolj metódus egy Hangszert vár paraméterül:

class Hangolo {
    static void hangolj(Hangszer i) {
        i.szolj();
    }
}

Amelyet meghívhatunk egy Zongora objektummal, amely ilyenkor impliciten ősre konvertálódik:

Zongora z = new Zongora();
Hangolo.hangolj(z); // upcast - beleolvasztás

Végső dolgok

Tervezési megfontolásból, vagy a hatékonyság növelése miatt adatok, metódusok és osztályok előtt használhatjuk a final kulcsszót. A final jelentése, hogy véges, de azért hogy pontosan mit is jelent, az attól függ, hogy milyen elem előtt szerepel.

Végső adatok

Ha adattag elé kerül a final jelző, az azt jelenti, hogy az adott adattag csak és kizárólag egyszer kaphat értéket. Ez történhet rögtön az adattag definiálásakor, de ha ott nem történik meg, akkor legkésőbb a konstruktorban inicializálni kell.

Ha egy adatról tudjuk, hogy annak értéke nem változhat, akkor lehetőség lehet fordítási időben a konstans érték propagálásával hatékonyabbá tenni a kódot. Ha egy primitív adatra használjuk a final jelzőt, akkor az egy konstans érték lesz, a program futása során nem fog megváltozni. Nem primitív típusra használva nem az objektum lesz konstans, hanem a referencia. Ez azt jelenti, hogy másra nem mutathat, de maga az objektum nem lesz konstans, annak állapota megváltozhat.

class FinalData {
    final int i = 9;
    static final int i2 = 99; // csak egy van belőle
    final Value v2 = new Value();
    final int j = (int)(Math.random() * 20); // futás közben!
    final int k; // blank
    FinalData(int i) {
        k = i;
    }
    void f(final int i) {
        i++;
    } // hiba: i nem változhat
}

Metódus paramétere is lehet final, ilyenkor a paraméter csak olvasható.

Végső metódusok

Ha a final módosító metódusok elé kerül, akkor az adott metódust a származtatott osztály nem definiálhatja felül. Ráadásul jelezzük a fordítónak, hogy az ilyen hívások pont ezért akár inline hívásokká is alakíthatóak, hiszen korai kötéssel meghatározható a célpontjuk.

Ha jobban belegondolunk, minden private metódus implicite finalként viselkedik, hiszen nem lehet felüldefiniálni. Ennek ellenére nem ugyanaz, hogy final-t, vagy private-ot írunk a metódus elé. Ha finalt írunk, de megpróbáljuk felüldefiniálni, akkor fordítási hibát kapunk, ha private-ként definiáljuk, akkor viszont egy új metódust.

Végső osztályok

Ha osztály elé kerül a final jelző, akkor abból nem lehet származtatni. Biztonsági vagy hatékonysági megfontolásból használhatjuk, hiszen így az osztály minden metódusa is impliciten final lesz, mert ha nem lehet származtatni, akkor felüldefiniálni sem.


Utolsó frissítés: 2024-04-11 07:54:27