Gyakorlati alapok III.
Öröklődés (inheritance)
Az objektumnév (referencia) statikus és dinamikus működése
A dinamikus összekapcsolás szabályai
Az objektum létrehozásának pillanatában eldől annak típusa és ezen tulajdonsága élete végéig változatlan marad. Ez a típus nyilvánvalóan az az ősosztály, amelyből az objektum keletkezett. Ezt nevezzük az objektumreferencia statikus típusának. Például: Negyzet negyzet = new Negyzet(), ahol a típus Negyzet, a keletkezett objektum pedig Negyzet típusú negyzet.
Maga az objektum felé mutató referencia azonban -amint ezt az Ezt sajnos most kell tisztáznunk: mutató és referencia című fejezetben tisztáztuk-, csak egy szigorú működési feltételek közé szorított mutató (legalábbis a Java nyelvben), amellyel voltaképpen rugalmasabban is bánhatunk. Összefoglaló néven ezt a lehetőséget nevezzük az objektumreferencia dinamikus típusának.
Ez eddig nem teljesen érthető, főként gyakorlati szempontból. Az ügy tisztázásához először nézzük meg az előző fejezetekben ismertetett származtatásos hierarchiánkat:
Object
↓
Sikidom
↓
Kor
↓
Negyzet
↓
Teglalap
↓
Haromszog
Mindegyik osztálynak saját típusa van. Ezen szintén sejtelmes kijelentés mögött az húzódik meg, hogy mindegyik típus rendelkezik egy csakis rá jellemző tulajdonsághalmazzal. Ennek felfedezése viszonylag könnyű, ha tudjuk, hogy mi a különbség egy kör és egy négyzet között. Már most az a kérdés, hogy a objektumreferencia dinamikus felhasználásakor főként értékadás formájában összekapcsolhatjuk-e például a kor és a negyzet objektumot?
A kérdés lényegében érinti az öröklődés alapelveit. Az öröklődés alkalmazásakor egymáshoz alapelvben hasonló, de mégis kissé különböző, sőt tulajdonságaikban egyre kibővültebb osztályok jönnek létre. Másként egyébként nem sok értelme volna az öröklődés alkalmazásának.
A dinamikus összekapcsolás szabályai
1. szabály - Adott típusú referenciával mutathatunk különböző típusú objektumok felé. A szabály ekkor, hogy a referenciának vagy azonos típusúnak, vagy valamelyik ősosztály típusúnak kell lennie. Például az Örököltetés: síkidom - kör - négyzet - téglalap a super() segítségével című fejezetben foglaltak szerint egy Negyzet típusú objektummal mutathatunk Object, Sikidom, Kor, Negyzet objektumreferenciák felé, de Teglalap és Haromszog típusúak felé nem.
A futtatásos ellenőrzéshez először létre kéne hoznunk a kérdéses objektumokat, de emlékezzünk egy további fontos szabályra: abstract osztály nem példányosítható (Abstract osztály - abstract metódus című fejezet), már pedig a Sikidom osztály példánkban éppen ilyen, ezért az alábbi példányosítás csak elméleti:
Object object = new Object();
Sikidom sikidom = new Sikidom();
Kor kor = new Kor();
Negyzet negyzet = new Negyzet();
Teglalap teglalap = new Teglalap();
Haromszog haromszog = new Haromszog();
Az 1. szabály szerint (elméletileg) a következő értékadások működőképesek:
object = negyzet;
sikidom = negyzet;
kor = negyzet;
negyzet = negyzet;
Az 1. szabály szerint a következő értékadások NEM működőképesek:
teglalap = negyzet;
haromszog = negyzet;
A hiba már fordításkor kiderülhet (Type mismatch: cannot convert from...) vagy ClassCastException nevű kivétellel szembesülünk, ami azt jelenti, hogy ütközés lép fel az osztálytípusok között.
2. szabály - Egy objektumra több, különböző típusú referenciával is hivatkozhatunk. A szabály ekkor, hogy a referencia mutathat azonos típus, vagy származtatott osztályú objektum felé. Például az Örököltetés: síkidom - kör - négyzet - téglalap a super() segítségével című fejezetben foglaltak szerint egy Negyzet típusú referenciára mutathatunk Teglalap és Haromszog referenciával, de nem Object, Sikidom, Kor, Negyzet referenciával.
A futtatásos ellenőrzéshez először létre kéne hoznunk a kérdéses objektumokat, de emlékezzünk egy további fontos szabályra: abstract osztály nem példányosítható (Abstract osztály - abstract metódus című fejezet), már pedig a Sikidom osztály példánkban éppen ilyen, ezért az alábbi példányosítás csak elméleti:
Object object = new Object();
Sikidom sikidom = new Sikidom();
Kor kor = new Kor();
Negyzet negyzet = new Negyzet();
Teglalap teglalap = new Teglalap();
Haromszog haromszog = Haromszog();
A 2. szabály szerint a következő értékadások működőképesek:
negyzet =
teglalap;
negyzet = haromszog;
A 2. szabály szerint a következő értékadások NEM működőképesek:
negyzet = object;
negyzet = sikidom;
negyzet = kor;
A hiba már fordításkor kiderülhet (Type mismatch: cannot convert from...) vagy ClassCastException nevű kivétellel szembesülünk, ami azt jelenti, hogy ütközés lép fel az osztálytípusok között.
Ellenőrizzük le a fenti állításokat 1 futtatható Java-kódon:
A kódból természetesen ki lett hagyva az Object és Sikidom osztályok példányosítása, a többi azonban az ismertetett szabályok szerint működik.
A hibák ellenére megpróbálhatjuk a típusokat a típuskényszerítés eszközével (casting, classcasting) egymásra kényszeríteni. Ha ezt az ősosztály(ok) irányába tesszük, a típuskényszerítés felfelé történik (upcasting), ha a származtatott osztály(ok) irányába, a típuskényszerítés lefelé történik (downcasting).
Jobban belegondolva mit is teszünk például a negyzet objektummal, ha bármerre is típuskényszerítjük? Megpróbáljuk megváltoztatni tulajdonságait, (szándék szerint) vagy szűkítve vagy bővítve azt.
Upcasting
Ebben az esetben a negyzet objektumot visszaléptetjük egy ős szintre, ezáltal tehát szűkítjük, butítjuk tulajdonságait.
Downcasting
Ebben az esetben kísérletet teszünk a negyzet objektum tulajdonságainak bővítésére, hiszen a belőle származtatott osztály garantáltan több, de kissé másféle tulajdonságokkal van felruházva. Ez azonban nem fog sikerülni, hiszen ez a mozzanat éppen az örököltetés mechanizmusa és azt nem tudjuk megkerülni. A downcasting-műveletnek tehát önmagában véve nincs értelme, mert bár lehetséges, hogy programozás-technikailag kivitelezhető, ám úgysem leszünk képesek a származtatott osztály új tulajdonságait elérni, csakis a régieket.
A típuskényszerítés anomáliái egy jellemző példán keresztül könnyen megmutathatók. Az alábbi példában a szabályok szerint gond nélkül sikerül az upcasting, hiszen kor objektumban a negyzet objektum tulajdonságait használtuk fel, amely azonban garantáltan rossz végeredményeket fog szolgáltatni:
public class
Main {
public static void main(String[] args) {
Negyzet negyzet = new Negyzet(10);
System.out.println(negyzet.getKerulet());
System.out.println(negyzet.getTerulet());
Kor kor = new Negyzet(10);
System.out.println(kor.getKerulet());
System.out.println(kor.getTerulet());
}
}
Végeredmény:
40.0
100.0
40.0
100.0
Következésképpen nagyon óvatosan bánjunk az osztályok közti típuskényszerítéssel!