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.