Ú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ú:
Ö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:
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ő:
Ő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:
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:
A Hangolo osztályban adott hangolj metódus egy Hangszert vár paraméterül:
Amelyet meghívhatunk egy Zongora objektummal, amely ilyenkor impliciten ősre konvertálódik:
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.