Kihagyás

Java IO, kivételkezelés, reflection

Előadásfóliák

Java Input/Output

228: A Java nyelvnek nincs "beépített" IO rendszere, a kimenet/bemenet kezelést osztályokon keresztül oldják meg. Alapvetően kétféle IO rendszert különböztetünk meg. Az egyik a ...Stream osztályok csoportja, ez az adatforrást/-nyelőt byte-ok sorozataként kezeli. A másik a ...Reader/...Writer osztályok csoportja, amelyik az adatforrást/-nyelőt karaktersorozatként kezeli. A kettő között leginkább az a különbség, hogy ha van egy nem ASCII karakterünk egy UTF-8 -as kódolású, mondjuk egy 'á' vagy 'Ő' karakter, amiket a 0xc3 0xa1 illetve 0xa5 0x90 byte-párokkal kódolunk (vagyis egy karakterhez több byte tartozik), azt a stream-ek két bájtként, míg a Reader/Writer osztályok egy karakterként érzékelik. Nagyjából úgy kell elképzelni, hogy alapértelmezésként a Stream-ek felelnek meg a bináris, a Reader/Writer osztályok pedig az UTF-8 kódolású szöveges IO kezelésnek. De ez nem teljes analógia és az IO osztályok által használt kódolás megadható.

229-230: A Java nem csak az IO-hoz, de a fájlrendszer kezeléséhez szükséges osztályokat is tartalmaz. A File objektumok például a háttértáron tárolt fájlokat (és könyvtárakat) reprezentálják.

231-238: Az IO osztályok a fenti bájt/karakter különbségen túl további két dimenzió szerint csoportosíthatók. Az egyik az Input/Output dimenzió, vagyis hogy kimenetet vagy bemenetet valósítanak-e meg.

A másik a feladat dimenzió. Ez utóbbi szerint kétféle osztály van: az egyik az adat helyét határozza meg. Ez IO esetében hagyományosan egy fájl, de valójában bármi lehet: egy String objektum, egy byte tömb, az operációs rendszer által nyújtott "csővezeték" (pipe). A lényeg az, hogy a forrást, mint bájtok/karakterek sorozatát kezeljük, függetlenül a valódi fizikai megvalósulásától, vagyis az objektumon keresztül bájtonként/karakterenként tudjuk írni/olvasni. A másik a dekoráló osztály. Egy ilyen objektum egy másik IO objektumra tud csatlakozni (az előbb említett egyszerű írás/olvasás műveletekkel), és bővített interfészt nyújt annak kezelésére. Vagyis lehetőségünk lesz nem csak egyesével kiolvasni/írni a bájtokat/karakterteket, hanem akár összetett adatokat is egyszerűen kezelni (pl. teljes sorokat beolvasni, vagy egy több-bájtos adatot, pl. float típust binárisan egyszerre kiírni). Ezen dekoráló osztályok a FilterInputStream/FilterOutputStream/FilterReader/FilterWriter osztályokból származnak.

A stream-ek és a Reader/Writer osztályok között stream->Reader/Writer irányban az InputStreamReader és OutputStreamWriter osztályok adnak kapcsolatot. Vagyis egy Reader tud InputStream-ből olvasni, egy Writer pedig OutputStream-re írni.

StringOutputStream és StringWriter nincs, egész egyszerűen nem lenne értelmük: String-et alakítanának át String-é.

239-242: A példában a javszínű dobozokban a standard output, a szürkében pedig a létrehozot fájl tartalma látható.

243: A C-hez hasonlóan a Java is rendelkezik az operációs rendszer által nyújtott 3 standard csatornával; ezek a System osztályon keresztül érhetőek el (annak statikus adattagjaként).

Kivételkezelés Java-ban

245: Régebbi programozási nyelvekben probléma volt, hogy a hibakezelő kód nem különült el a normál logikát tartalmazó kódtól. Ez olvashatatlanná, átláthatatlanná tette a kódot. Ráadásul több szinten kézzel kellett propagálni a hibát, vagyis a hiba keletkezésének helyétől a hiba kezelésének helyéig minden függvényben aktívan figyelni kellett az esetleges hibára. Ezt a Java-ban kivételkezeléssel oldották meg (nem Java találmány, korábbi nyelvekből átvett mechanizmus).

246: Arról van szó, hogy ha futás közben valahol hiba keletkezik, azt egy nyelvileg elkülönült kódrészletben tudjuk kezelni. Amikor a hiba keletkezik, létrejön egy kivétel objektum, ami a hibát reprezentálja, majd a Java "másik módba vált", és a hívási lánc mentén visszafelé megkeresi, hogy ki tudja lekezelni ezt a hibát. Ha talál ilyen hibakezelő kódot, akkor ennek átadja a kivétel objektumot és futtatja a hibakezelést, majd visszavált "normál" módba, és innen folytatódik a program. Ha nem talál, akkor a kivétel előbb utóbb becsapódik a JVM-be, ami a program végét jelenti.

247: Kivételeket mi magunk a throw kulcsszó segítségével dobhatunk, de azok bizonyos esetekben "maguktól" keletkezhetnek. Ha mi csinálunk kivételt, megadhatjuk, hogy mi volt a hiba.

248: Kivételeket a try-catch-finally blokkokban kezelhetünk. A try blokkba jön az a kód, ami futás közben valamilyen hibát dobhat. A catch blokkokban tudjuk megmondani, hogy milyen hibákat tudunk/szeretnénk lehezelni. A finally blokk tartalma mindenképpen végrehajtódik, akár lekezeltük a hibát, akár továbbengedtük.

250-252: Ha egy metódusban lehetőség van arra, hogy olyan hiba keletkezzen (benne, vagy az általa hívott metódusokban), amit nem kezelünk le, azt a függvény deklarációjában deklarálni kell a throws kulcsszó segítségével. Ez alól csak a RuntimeException osztály (és annak alosztályai) a kivételek, ezek ugyanis BÁRHOL keletkezhetnek, nincs értelme külön jelölni őket.

Java reflection

254: A reflection egy olyan mechanizmus Java-ban, aminek segítségével futás közben férhetünk hozzá olyan adatokhoz, mint például az adott objektum típusa, vagy egy adott osztály (igen, osztály) tulajdonságai. Ennek az az alapja, hogy Java-ban minden objektum, még az osztály is! Minden osztálynak van ugyanis egy reprezentáns objektuma, amit a JVM (Java Virtual Machine, a virtuális gép, a Java "környezet) tölt be, amikor szüksége van az adott osztályra, mert mondjuk példányosítani szeretné. Ilyenkor a JVM a fájlrendszeren megkeresi a megfelelő .class kiterjesztésű fájlt (az osztály leírását), és "betölti", azaz létrehoz egy objektumot, ami leírja, hogy hogyan néz ki az adott osztály. Ennek az (osztályt leíró) objektumnak a típusa Class.

255: Egy osztály Class típusú reprezentáns objektumát többféleképpen is elérhetjük. Ami igazán "reflection", az a Class.forName(String) metódus, itt ugyanis egy sztringben tudjuk megadni a keresett osztály nevét. Vagyis nem kell fordításkor ismernünk ezt a típust, elég, ha futás közben rendelkezésre áll.

256: A fentiek alapján a static kulcsszónak tudunk egy második értelmezést adni. Elképzelhetjük úgy, hogy ami static, az nem az osztály példányaihoz, hanem az osztályt reprezentáló objektumhoz tartozik (ez így nem teljesen igaz, de nagyjából stimmelhet). Így értelmet nyer az Alakzat és Haromszog osztályok törzsében található static { ... } kódrészlet: a static nélkül ez ugye az "instance initialization clause" lenne, vagyis az objektum inicializálásához szükséges kódrészletet tartalmazná. A static viszont azt jelenti előtte, hogy nem az adott osztály objektumainak inicializálásához, hanem a reprezentáns objektum inicializálásához kötődik, vagyis az osztály betöltésekor kerül végrehajtásra. Látszik, hogy az AlakzatReflectionPelda1 osztályban semmit sem tudunk a Haromszog osztályról, futás közben mégis be tudjuk tölteni (és így később használhatnánk is).

258: A Haromszog.class visszaadja a Haromszog osztály reprezentáns objektumát, amitől meg tudjuk kérdezni, hogy az o által hivatkozott objektum az ő által reprezentált osztály példánya-e.

259: Hasonló az előzőhöz, de a reprezentáns objektumot nem "égettük be" a kódba, hanem az osztály nevét a felhasználótól várjuk, és a Class.forName(String) segítségével kérjük el az adott nevű osztály reprezentáns objektumát.

260-261: Mire jó még a reflection? A programban le tudjuk például kérdezni egy fordítási időben ismeretlen osztály nevét (na jó, ez azért még nem nagy kunszt, hiszen mi adjuk meg), konstruktorait, metódusait, hogy interfész-e, stb. Mint látható, az egyes metódusokat, konstruktorokat szintén (Constructor és Method típusú) objektumok reprezentálják. És a reflection ennél többre is képes, de abba nem megyünk most bele. (De nem tilos utánanézni!)


Utolsó frissítés: 2022-03-04 08:24:04