Content extract
Java gyorstalpaló Werner Zsolt Mirôl lesz szó ebben a négy órában? Szinte a nulláról indulva, a Java pár alapvetô fontosságú osztálykönyvtárának legfontosabb osztályaiból egy mûködô, többszálú kliens/szerver alkalmazást dolgozunk ki. A gyorstalpaló során nem csak ezen osztályok használatáról lesz szó, hanem általánosságban az objektum-orientált programozásról is, valamint a modern programnyelvekben általában megtalálható alapkönyvtárakról. Ezek logikája általában hasonló, különösen OOP nyelvekben A Java nyelv - felhasználási területek és tanulhatóság Mire is jó a Java? • egy félreértést kell eloszlatnom: a Java, minden felhajtás ellenére, nem a megoldás. Az agyonbonyolított C++-nak, amely használata ráadásul olyan nehezen kezelhetô osztálykönyvtárakhoz kötôdik, mint pl. az MFC, komoly alternatívája, a viszonylag egyszerûen megtanulható C-nek viszont soha nem lesz a vetélytársa hardverközeli, ill. gyors
/ kis memóriaigényû programok fejlesztése esetében Sajnos még most, hogy a Hotspot 1-et a Sun kiadta, sem beszélhetünk a Java és a C++ sebességbeli, ill. memóriaigény-beli versenyérôl - ugyanaz a tipikus alkalmazás C++-ban (MFC) megírva általában még mindig feleannyi memóriát igényel és gyorsabban fut. Persze ez JVM-függô is - nagyon sokan dolgoznak saját fejlesztésû JVM-en és próbálják a Java byte-kódokat mind effektívebben gépi kódra leképezni. Akit érdekel az, hogy milyen JVM-ek vannak, valamint hogy ezek sebessége, ill. skálázhatósága hogy viszonyul egymáshoz, ajánlom a JavaWorld VolanoMark-kal készített összehasonlító tesztjeit (pl. http://www.javaworldcom/javaworld/jw-03-1999/jw-03-volanomarkhtml) A Java sosem lesz Quake-ek vagy kisigényû operációs rendszerek nyelve, de nem is arra tervezték. A Java egyrészt egy nagyszerû tanulónyelv, amely tökéletesen alkalmas arra, hogy a legtöbb programozási paradigmát a
gyakorlatban rajta bemutathassuk (aszinkron, eseményvezérelt programozás; OOP; párhuzamos programozás; socketprogramozás). Ezen felül a Java net feletti alkalmazások készítésére is kitûnô - ahogy ezt látni is fogjuk, ahogy egyre elôrébb jutunk a példánk kidolgozása kapcsán. Valóban, Java-ban egy net-es alkalmazást pillanatok alatt meg lehet írni. Ezen felül a Java egy nagyszerû RAD (Rapid Application Development) tool bármilyen feladatra, ahol eddig csak Visual Basic-kaliberû nyelveket használtunk - ugyanazt a kényelmet és egyszerûséget biztosítja a Java még form designer-ek ill. IDE-k (Integrated Development Environment) nélkül is. Mint majd látni fogjuk, a Java biztosítja számunkra mindazon eszközöket, melyek segítségével akár vi-ban is pillanatok alatt lekódolhatunk viszonylag nagyméretû alkalmazásokat is. • Mégis, miben nagyszerû a nyelv? Elôször is, egyszerûen és könnyen tanulható maga a nyelv és az átala használt OOP
modell - a C-nél vagy a C++-nál sokkal könnyebb a nyelvet megérteni. Természetesen ez az egyszerûség nem vonatkozik az igen extenzív és rengeteg osztályt tartalmazó osztálykönyvtárakra, de legalább azokon is látszik, hogy megpróbálták egységes elvek mentén felépíteni, és néhány baki (általában csak betûzési hibák és név-inkonzisztenciák) ellenére logikus és, amennyiben használatában már egy bizonyos szintet elértünk, könnyen átlátható. Tehát - a Java a tengernyi osztálykönyvtárával véletlenül sem könnyû nyelv, de általában fél-egy év alatt kielégítô mértékben megismerhetô (vö. mennyi idôbe kerül, míg valaki tényleg jól tud majd bármit lekódolni - a nulláról indulva - Visual C++-ban). Irodalom Sajnos a nyelvrôl írt irodalom nem áll helyzete magaslatán. Igazándiból csak néhány mû van, amelyet tiszta szívvel tudok ajánlani, a maradék 95% pedig a futottak még vagy a kifejezetten ártalmas
kategóriájába tartozik (ezen utóbbi jelentése: szakmai hibák tömkelege, melyekhez nincs online 1 helyesbítés). Az érdeklôdôk számára ajálom az InfOpen címû havilapot, melyben könyvkrtiktikáim rendre megjelennek. Ezek természetesen csak rövidített változatai az általában 3-4 ezer szavas, a mûveket teljesen felboncoló írásaimnak - ezekre a www.infopenhu-ról találhatunk linkeket Az OOP Az OOP a manapság talán leginkább használt programozási paradigma, amellyel csak a C elterjedtsége vetekedhet. Ezt a témakört sajnos a Java-s, C++-os stb, bevezetô jellegû könyvek 99%-a használhatatlanul bonyolultan tálalja, ugyanis a kérdést szinte kizárólag mindegyikük a revolúciós, forradalmi oldaláról közelíti meg, úgy, mintha az OOP-nek az égvilágon semmi köze nem lenne a múltban elterjedten használt procedurális programozáshoz. Igaz, hogy az OOP tanításának ilyen megközelítése bizonyos esetekben talán hasznosabb és
érthetôbb lehet, mint az ún. evolúciós, a procedurális paradigmára szervesen épülô megközelítés esetén, de általában – oktatói tapasztalatom szerint – ezen utóbbi megközelítés azonnali, garantált sikert hoz az OOP alapjainak megértetésében, míg az absztrakt, a hallgatók eddigi tudására nem építô megközelítés, éppen absztraktsága és megfoghatatlansága folytán, nagyon nehezen emészthetô. Az – átalam olvasott – egyetlen épkézláb alkotás, amely az evolúciós, a hallgatók eddigi tudására, valamint konkrét memória-térképekre épülô megközelítést alkalmazza, történetesen magyar szerzôk mûve, igaz, C++-ot használva az OOP gyakorlati bemutatására (Kondorosi- László - Szirmay: Objektum-orientált szoftverfejlesztés). Hasonló lehet a Lippman-féle Inside the C++ Object Model, mely a kritikák és ismertetôk szerint hasonlóan magyarázza el az OOP mûködésének lényegét, sôt, még egy mûködô C++ - C
keresztfordítót is prezentál. Az ebben a jegyzetben alkalmazott megközelítés nem tételez fel semmilyen procedurális elôismeretet, ugyanis az OOP bemutatásához már az is elegendô, ha a Hallgatók tudják azt, mik azok az összetett adatszerkezetek, és az ezekre mutató referenciák (pointerek) a memóriában. Ehhez elegendô az, ha megnézzük, mi vezetett ahhoz, hogy nem csak regiszterekben tárolható adattípusokat definiáltak, hanem bonyolultabbakat is. Hogyan mûködnek a számítógépek? Minden számítógép központi ‘lelke’ a mikroprocesszor, ami végrehajtja a memóriában tárolt, gépi kódú utasításokat. Ezek az utasítások a mikroprocesszor regisztereiben, ill a memóriában lévô adatokon hajthatnak végre mûveleteket. Hogy milyen típusú adatok lehetnek ezek? Például számok vagy karakterfüzérek (stringek). Azonnal belátható, hogy karakterfüzérek tárolására nem lenne elegendô az otthoni számítógépek körében általános 3-10
darab, 8.32 bites regiszter, nem csak a számok, stringek mérete miatt (hiszen egy string akár több száz betûbôl is álhat!), hanem azért is, mert a regiszterek száma limitált, és a hosszú idejû tárolást ezekkel nem lehetett volna megoldani. Ezért kellett azt kivitelezni, hogy adatokat a memóriában tárolhassunk Azaz, ha a felhasználó pl. elvégez egy mûveletet a memóriából lekért adatokon, azokat visszaírhatja (processzortól és a rendelkezésre áló ún. címzési módoktól függ, hogy ezt külön meg kell-e tennie, vagy eleve a mûvelet eredménye nem egy belsô processzor-regiszterbe, hanem a memóriába kerül). Ez természetesen implikálja azt is, hogy minden, a memóriában, tehát nem a regiszterekben tárolt adatnak (pl. számnak) van egy konkrét, sosem változó memóriabeli címe, amelyen át az mindig elérhetô. Azzal most még nem foglalkozunk, hogy ezeknek a vátozóknak hol fogalhatunk memóriát, mi az a stack, heap stb – ezekrôl majd
késôbb esik szó. Természetesen nem csak számokat, karakterláncokat tárolhatunk a memóriában, hanem szorosan egymáshoz tartozó rekordokat (struktúrákat) is. Mire is jó az ilyen összetett adatszerkezet? Amennyiben szorosan egymáshoz tartozó adatokat szeretnénk kezelni (pl. egy autó típusa, gyártási éve, rendszáma, tulajdonosának neve pl. egy biztosítótársaság adatbázisában), akkor nem árt arról gondoskodni, hogy ezek egy olyan adatszerkezetbe kerüljenek, amely a szigorú egymáshoz rendelést garantálja. Ez persze már kicsit túlmutat az egyszerû, gépi kód-szintû programozáson, de azért érdemes megnézni, hogyan is érhetnénk el egy ilyen adatszerkezet elemeit egy nagyon egyszerû, assembly-szerû nyelvbôl. (Az assembly nyelv egy az egyben, komolyabb fordító (assembler) közbeiktatása nélkül leképezhetô a már említett gépi kódra; a fordító az assembly tokenekhez konkrét gépi kódot keres egy táblázatból, valamint a
szmibolikus változóneveket konrét memóriabeli címmel helyettesíti, azaz semmi ördöngôsséget nem csinál 2 – egy ilyen fordítót bárki le tud kódolni, hiszen gyakorlatilag csak egy táblázat elérésébôl, valamint néhány plusz, de nagyon egyszerû mûveletbôl áll, amely összehasonlíthatatlanul egyszerûbb, mint egy magasszintû nyelvhez fordítót írni.) Hogyan érjünk el összetett adatszerkezetet egy (fiktív) assembly nyelvbôl? Tegyük fel, egy programot kell lekódolnunk assemblyben (vagy egy nagyon egyszerû nyelvben, amelyben nincsenek összetett adatszerkezetek. Ilyen szinte minden mikrogépes BASIC-implementáció – a Visual basic kivételével) egy biztosítótársaság számára, mely a már fent leírt jellemzôket tárolja el minden egyes kocsiról, amelyre a Társaság szerzôdést kötött. Remélem, már mindenki érti, miért volt arra szükség, hogy az összetartozó adatokat lehetôleg egy helyen, szorosan egymás mellé rendelve
tároljuk, ezáltal is csökkentve a programozó hanyagságából adódó tévedés lehetôségét, ill. lehetôvé téve azt, hogy az adott autóra vonatkozó összes adatot egy generikus, általános célú rutinnal feldolgozhassuk. A generikusság nagyon fontos a programozásban – olyannyira központi jelentôségû, hogy már a legelsô nyelvek is (pl. a FORTRAN) lehetôvé tették az ún szubrutinhívást, amelynek paraméterekben átadhatjuk annak a memóriabeli táterületnek a kezdôcímét, amelybenen a feldolgozni kívánt rekord ún. mezôi sorrendben elhelyezkednek. Az, hogy sorrendben, és kihagyás nélkül, késôbb nagyon fontos lesz Mi történne akkor, ha nem terveznénk úgy rutinunkat, hogy lehetôleg bármilyen autó adatait fel tudja dolgozni? Természetesen nem nehéz elképzelni: az adatainkon mûveletet végzô kódot gyakorlatilag többszöröznünk kell, mindig éppen az aktuális tárbeli címet (vagy az azt helyettesítô szimbolikus változót,
amennyiben assemblerünk van annyira intelligens, hogy a szimbolikus változókkal elboldugoljon – ezt szinte mindegyik assembler tudja, de – természetesen – az alapszintû memória-monitorok és disassemblerek már nem) kellene minden egyes változó-hivatkozásban kicserélni. Tételezzük fel, az autók adatainak tárolására a következô adatszerkezetet használjuk: • autó típusa : 30 karakter allokálva (8-bites karakterek) • gyártási éve: 32-bites szám • rendszáma: 10 karakter allokálva • tulajdonosának neve: 60 karakter allokálva Azaz, egy ilyen rekord a memóriában a következôképp néz ki, annak a rendszer a következô memóriát forglalja le (autó1 az éppen feldolgozott autó-rekord memóriabeli címe): autó1+0 cím: autó típusa autó1 referencia (memóriacím) 30 byte autó1+30 cím: gyártási éve 32 bit autó1+30+4 cím: rendszáma 10 byte autó1+30+4+10 cím: 60 byte tulajdonosnév 1. ábra: egy összetett adatszerkezet
hogy néz ki a memóriában? Az ábrán látható, hogy minden egyes mezôt egy adott offsettel, eltolással is elérhetünk (figyelem, a lefoglalt táterülettel nem arányos a téglalapok függôleges mérete; a rezervált memóriát a téglalapok jobb oldalán tüntettem fel!). Ennek az eltolásnak központi jelentôsége lesz akkor, amikor az OOP típuskompatibilitásáról esik szó. Tegyük fel, egy olyan programot szeretnénk írni, amely mindezen adatokat kiírja a képernyôre. Hogy ez hogyan történik, azzal ne foglalkozzunk; tegyük fel, van egy olyan, print névre hallgató könyvtári függvényünk (azaz alprogramunk, rutinunk), amely a neki paraméterként átadott memóriaterületet az elsô 0 byte-ig kinyomtatja (ezzel tesszük azt lehetôvé, hogy eltérô hosszú karakterláncokat is kinyomtathassunk vele, azaz nem kell külön függvényeket írnunk arra, hogy kiprintelje a 10, a 30 és a 60 karakter hosszú szövegeket). Figyeljük meg, hogy zárójelek közé
írjuk a függvénynek átpasszolandó ún aktuális paramétert 3 (ez persze egyik közismert assembly-ben sem létezik, az átadandó paramétereket máshogy kell a meghívott függvénynek átadni, de ezzel most nem foglalkozunk : feltesszük, pszeudo-nyelvünk valahol az assembly és a komoly procedurális nyelvek között helyezkedik el). Rutinunk a következôképpen nézne ki, amennyiben autó1 az éppen feldolgozott autó-rekord memóriabeli címe, print pedig a fent említett karakterlánc-kiíró, memória-kezdôcímet váró függvény: print(autó1 + 0); // autó típusa print(autó1 + 30); // gyártási éve print(autó1 + 34); // rendszáma print(autó1 + 44); // tulajdonosának neve Természetesen jó, ha ezt a négy print-invokációt egy olyan függvénybe kiemeljük, amely biztosítja azt, hogy ezt a négy hívást mindig az éppen aktuális memóriaterületen és mindig gond nélkül (pl. elfelejtjük kiíratni valamelyik mezôt) megejthessük. Gondoljuk el, mi
történne akkor, ha a biztosítónak van tízezer ügyfele, és nekünk kellene állandóan újrafordítanunk a nyilvántartóprogramot akkor, amikor új ügyfél érkezik vagy egy régi távozik, és magába a kódba kellene ‘bedrótoznunk’ (ezt a kifejezést akkor használjuk, amikor egy dinamikus paraméterként is kezelhetô változót a program szövegébe literálként, azaz egyszerû értékként, beírunk). Ez, amellett, hogy az ôrületbe kergetne bennünket, ráadásul nagyon bizonytalan eljárás is, éppen amiatt, hogy egy adott kliens adataira hivatkozó memóriacímet nem biztos, hogy mind a négy esetben helyesen adjuk meg: lehet, hogy az egyiket elfelejtjük átírni. Ezért ajánlatos – éppen a print() példáján – egy olyan generikus függvényt létrehoznunk, amely gondoskodik arról, hogy mind a négy, és csakis ennek a négy mezônek az értéke ki legyen írva. A generikusságot azzal biztosítjuk, hogy bevezetünk egy ún. formális paramétert, amit
pl elnevezünk általánosAutóMemóriacím-nek (típust még nem adunk itt neki; majd természetesen látjuk, a formális paraméterek típusa kötelezôen megadandó). Ez a formális paraméter értékül megkapja azt a memóriacímet, amely a függvényhívás aktuális paramétereként szerepelt. Azaz, amennyiben függvényünk hívásakor a zárójelben az autó1 szerepel, akkor a függvéybeli formális paraméter értéke ez lesz. A függvény ún törzsét (body) kapcsos zárójelek közé írtam (így kell tenni Javaban, ill. minden C-alapú nyelvben is) printHívóBiztonságosFüggvény(általánosAutóMemóriacím) { print(általánosAutóMemóriacím + 0); // autó típusa print(általánosAutóMemóriacím + 30); // gyártási éve print(általánosAutóMemóriacím + 34); // rendszáma print(általánosAutóMemóriacím + 44); // tulajdonosának neve } Vegyük észre, hogy itt már a tévedés lehetôségét kiküszöböltük azzal, hogy a négy függvényhívás helyett
a programozónak már csak egyet kell lekódolnia. Hogy foglaljunk memóriát egy ilyen szerkezetnek? Nem foglalkoztunk még eddig azzal, hogy egy ilyen rekordnak a memóriából saját helyet hogy foglaljunk. Minden operációs rendszer a programozó részére felkínál olyan operációsrendszer-hívásokat, amelyek ezt elintézik. MS-DOS alatt foglalhatunk dinamikusan memóriát, C-ben foglalhatunk memóriát stb. A C-beli megvalósítás pl a malloc (memory allocation) névre hallgató függvény, melynek paramétereként a lefoglalandó tárterület byte-beli méretét kéri. Jelen autós esetben, ahol a rekordunk tárfoglalása 104 byte, ennek a függvénynek ezt kell átadnunk. A rendszer biztosítja azt, hogy egy olyan tárterületet keres ennek az 104 byte-nak, ami semmi más által nincs használva (! – ez nagyon fontos – az operációs rendszer mindig nyilvántartja, a dinamikus memóriából, azaz a heap-bôl, éppen mi van lefoglalva, és garantálja, mindig szabad
terület kezdôcímét fogja visszaadni), és garantálja azt, hogy nem is lesz, legalábbis amíg normálisan mûködik a programunk, és egy ‘eltévedt’ pointeren át véletlenül felül nem írjuk az adott tárterületet, de ennek esélye csak assemblyben, C-ben és C++-ban van, ahol engedélyezett a mutató-aritmetika, a tetszôleges memóriacím elérése és a tömbindex-túl/alulcsordulás sem ellenôrzött. Mit kellene akkor tennünk, ha az operációs rendszer, ill. az adott, jelen esetben gépközeli nyelv (a C) nem biztosítana számunkra olyan rendszerhívásokat, amelyekkel biztonságosan lefoglalhatnánk memóriát? 4 Kézzel, abszolút memóriacímekre kellene lefoglalni az 104 byte-unkat. El lehet képzelni, ez mekkora többletterhelést jelentene, ui. abszolút memóriacémekkel kellene dolgoznunk, és mindig számon kellene nemcsak azt tartanunk, hogy éppen hol kezdôdik a memóriában a következô lefoglalható 104 byte, hanem azt is, hogy – a
memóriatakarékosság jegyében – az idôközben érvényüket vesztett rekordok 104 byte-ját szabadnak nyilvánítsuk, és oda is helyezzünk el új rekordokat. Típusok, rekord-típus Eddig csak konkrét rekordokról volt szó. Már érezhetjük az eddigiek alapján is, hogy amennyiben sok, azonos típusú és rendeltetésû, de eltérô, mindig az adott kliensre vonatkozó értékû rekordot létrehozunk, akkor jó, ha ezeket úgy kreáljuk, hogy egy (absztrakt, legalábbis olyan értelemben, hogy amennyiben mi kézzel nem példányosítjuk, a memóriából 0 byte-ot foglaló) típusból hozunk létre egy változót (vagy, OOPs terminológiával élve, egy típust példányosítunk). Ennek mi is lehet az elônye? Egyszerû. Ha mi mindig kézzel foglalunk le egy olyan, viszonylag bonyolult adatszerkezetnek memóriát (külön-külön a stringek, ill. a szám helyét a memóriából lefoglalva, majd az értékét feltöltve – itt feltesszük azt, hogy a rendszer garantálja,
hogy sorfolytonosan foglal vátozóinknak dinamikus memóriát – a való életben természetesen nem így van a heap esetében, de ezt most figyelmen kívül hagyhatjuk), mint a jól ismert autó-rekord, akkor fennáll a veszélye annak, hogy valamelyik ilyen mûvelet során esetleg 60 helyett tévedésbôl 50 byte-ot foglalunk le a tulajdonos neve vagy pl. 30 helyett 20 byte-ot foglalunk le az autó típusa számára. Ennek hatását nem kell ecsetelni: mivel az egymás utáni malloc() hívások mindig pont annyi byte-ot foglalnak le a memóriából, amennyit kérünk, akkor, amikor az adatainkat a fenti offset-ekkel, eltolásokkal pl. a generikus print függvényünkbôl (vagy bárhonnan másonnan) el próbálnák érni, garantáltan rossz értékeket kapnánk. Ha már rögtön az elsô string memóriaallokációját elrontottuk, akkor az összes többi, utána következô változónk lekérésekor is rossz értékeket kapunk vissza. Sajnos a konzisztenciát és hibamentességet
nagyon nehéz lenne akkor biztosítani, ha nyelvünk semmivel sem támogatna minket abban, hogy maga a rendszer tudja, hogy egy mezônek hány bájtot kell lefoglalni. Ez is volt az egyik oka annak, hogy absztrakt típusokat vezettek be már a számítógépes nyelvek hajnalán. Miért is absztrakt egy ilyen típus? Azért, mert csak azt deklarálja, hogy hogyan is fognak kinézni az adott típusból készített példányok, ugyanakkor önmagában nem foglal le memóriát, tárat csak akkor foglalunk le egy ilyen absztrakt típus számára, amikor abból egy önálló példány születik. Hogy ez ne legyen annyira absztrakt, bemutatom, hogy a C-ben pl. mi az absztrakt típus (struct) és annak mi a konkrét példánya. Ugyanez vonatkozik Pascalra is: ott is a type szekcióban egy típust deklarálunk, míg a var szekcióban annak a típusnak akár több, egymástól független (mert teljesen más memóriaterületen allokált) példányát, változóját is létrehozhatjuk. struct
általánosAutó { char[30] autó típusa; int gyártási éve; char[10] rendszáma; char[60] tulajdonosának neve; }; Ha ebbôl a típusból késôbb egy konkrét változót akarunk létrehozni (és annak mezôit a pont (dot) operátorral elérni – ez sokkal kényelmesebb, mint az assembly példánál említett offsetek használata), akkor azt egyszerûen megtehetjük: struct általánosAutó autó1, autó2, autó3; Ezek után pl. autó2-nek már elérhetjük pl a gyártási éve mezôjét, mert az a memóriában már valóban létezô, konkrét változó lesz (próbáljuk meg elérni az absztrakt struct bármelyik mezôjét, meglátjuk, hogy valóban fordításidejû hibát kapunk. (Megjegyzés: a C és C++ különbséget tesz struktúrapointerek és struktúraváltozók között, azaz pl. a fenti példányosítás során létrejövô autó1 stb változók nem memóriacímek lesznek, hanem ‘maguk a változók’, de mivel Java-ban minden ilyen összetett típus példányának a
neve automatikusan memóriacím, ezzel nem foglalkozunk). 5 Mi is az az OOP? class = struct, objektum = struktúraváltozó Nos, az OOP gyakorlatilag semmi turpisságot nem vezetett be az eddigiekhez képest, emiatt nem is szabad tôle tartanunk. Ez azt jelenti, hogy minden OOP program (láthatósági kérdések kivételével) áttranszformálható nem OOP alakba, azaz pl. egy C++ program felírható egyszerû C-ben is (igaz, kicsit bonyolultabban). Jegyezzük meg jól, hogy az OOP programozási paradigma emiatt szinte egyáltalán nem különbözik (programozástechnikai szemszögbôl!) a procedúrálistól, egyszerûen csak kényelmesebb, egyszerûbb és biztonságosabb - mint majd látjuk. Kezdô programozóknak mindig azt ajálom, hogy az OOP szerkezeteket mindig próbálják magukban lefordítani procedúrális szerkezetekké, és maguk is megdöbbennek, mennyire egyszerû az egész. Mindenki ismeri a C struktúráit, vagy a Pascal rekordjait. Ezek a fejlett nyelvi eszközök
teszik azt lehetôvé, hogy összetartozó adatokat együtt kezeljünk. Ilyen adatok lehetnek egy alkalmazott neve, fizetése, egy autó rendszáma, gyártási éve stb, tehát amit mindenkép együtt akarunk kezelni. Ezek a struktúrák játssszák az OOP-ben is a központi szerepet. C-ben, Pascal-ban ezen struktúrák lehetnek típusok is. Ez azt jelenti, hogy a struktúradefiníció nem csak egyetlen konkrét változó (nevezhetjük példánynak (instance) is, majd látjuk, ez miért szerencsés elnevezés) megadása. Erre lássunk is egy C nyelvû példát: struct Alkalmazott { int életkor; int fizetés; }; Ha ebbôl a típusból késôbb egy konkrét változót akarunk létrehozni (és annak mezôit a pont (dot) operátorral elérni), akkor azt egyszerûen megtehetjük: struct Alkalmazott béla, marci, ilona; Ez három, Alkalmazott típusú, de egymástól teljesen független struktúrát hoz létre a memóriában. Amikor ezen rekodoknak a mezôire hivatkozunk, biztosak
lehetünk abban, hogy az egyes struktúrákban (nem a típusban, amelyet tulajdonképp template-nek, vagyis absztrakt nyomóformának is nevezhetünk) levô mezôket ha el próbáljuk érni, azok valóban függetlenek lesznek. Azaz, ha béla életkor mezôjéhez valamilyen értéket rendelünk, akkor ez a hozzárendelés a másik két struktúraváltozó (marci, ilona) életkor mezôjét "békén hagyja", azokat nem módosítja. Az OOP is így mûködik. Vannak absztrakt (nem konkrét) típusok, amelyeket az OOP terminológia class-oknak, osztályoknak nevez, és azoknak vannak konkrét ún. példányai (mint ahogy a fenti példában is három példányát, változóját képeztük az Alkalmazott struktúrának). Egyes programozási nyelvekben (konkrétan a C++-ban) a fenti módon is képezhetünk példányokat (amelyek ilyenkor a stack-re kerülnek), míg mások (és ilyen a Java is) kizárólag a heap-en hajlandóak az objektumaikat tárolni. Megjegyzés: aki nem ismeri a
stack és heap közti különbséget, ne ijedjen meg. Java-ban ezzel nem nagyon kell foglalkozni - épp amiatt, mert osztályok példányai nem kerülhetnek a stack-re, ezzel megelôzve a C++-programozók életét megkeserítô hibalehetôséget, amikor egy olyan, stack-en levô változót próbálunk a hívónak visszaadni, amelyet a hívó, ill. bármilyen, velünk közös stack-et használó megszakítás már lehetséges, hogy fölülírt. A Java biztonságossága abból is adódik, hogy ilyet nem enged meg (C++osoknak: a Kondorosi-féle mûben elolvashatjuk, az újabb C++-fordítók ezt hogy orvosolják - 237 o alul) Amikor egy osztálynak egy példányát képezzük, akkor tehát a rendszer annak a memóriából helyet foglal. Amennyiben a fenti példához hasonlóan két darab integer értéket tartalmaz osztályunk, akkor 64 bitet foglalunk le neki (a tárgyalás természetesen idealizált). Amennyiben késôbb több példányt akarunk ugyanebbôl az osztályból képezni, azt
megtehetjük, és ilyenkor természetesen biztosak lehetünk abban, hogy a létrehozott példányok nem ugyanazon memóriacímekre kerültek. 6 Egységbezárás (encapsulation) = nem csak adatok egy rekordban, hanem programkód is - a C++ újítása A legtöbb könyv turbóban elfecsegi (és "természetesen" nem magyarázza el tisztességgel) azt, hogy mi az az egységbezárás (encapsulation). Tulajdonképpen ez visszanyúlik a C++-hoz, amelynek nagy újítása az volt a C-hez képest, hogy a normál, C-beli struktúrákat átvéve, azokban nem csak mezôket, azaz primitív adattípusokat ill. pointereket, hanem futtatható kódot is el tud helyezni Ez volt a fô különbség a C-beli, csak adatokat tartalmazó, ill. a C++-beli, programkódot is tartalmazni képes struct között Ennek miértje nagyon egyszerû: így egymáshoz tudtuk rendelni az adatokat, ill. az azokon mûveletet végzô kódot. Ez nem csak kényelmi, hanem biztonsági funkciókat is elláthat -
errôl majd késôbb Térjünk vissza eredeti példánkhoz - tegyük fel, hogy Alkalmazott struktúránkat (osztályunkat) ennek szellemében szeretnénk egy olyan függvénnyel felvértezni, amely annak függvényében tér vissza egy adott integer értékkel, hogy mekkora az adott alkalmazott példány fizetése, felszorozva életkorának egy bizonyos százalékával. Tegyük fel, ezt a mennyiFizetésJárNeki függvényt, mivel a saját adatain kell hogy dolgozzon, betesszük az általa elérni kívánt struktúrába: class Alkalmazott { int életkor; int fizetés; int mennyiFizetésJárNeki() { return (int)Math.round(fizetés * (1 + életkor/100.0)); } }; Megj.: vegyük észre a pontosvesszôt az osztálydefiníció végén Erre a C, ill C++ esetében kötelezô pontosvesszôre itt már semmi szükség Ennek oka egyszerûen az, hogy míg ott a típusdefincíciót összeköthettük a konkrét, az adott típusú változó (stack-en történô) létrehozásával, azt Java-ban nem
tehetjük meg. Emiatt, amennyiben elhagyjuk a pontosvesszôt az osztálydefiníciót lezáró csúcsos zárójel mögül, nem történik probléma, azaz a záró zárójel mögötti részt a Java nem próbálja meg egy vagy több változó (példány) neveként értelmezni - a következô pontosvesszôig. Ezt vajon hogy képzelhetnék el C-ben? Mivel C-ben nem lehet egy struktúrába futtatható kódot helyezni, azt globális függvényként kell megvalósítanunk, amely paramétereként az éppen elérni kívánt struktúraváltozó címét kapja meg, a következôképpen: struct Alkalmazott { int életkor; int fizetés; }; int mennyiFizetésJárNeki(Alkalmazott *alkPointer) { return (int)round(alkPointer->fizetés * (1 >életkor/100.0)); } + alkPointer- Ezen utóbbi, C nyelvû függvényt pedig a következôképpen kellene meghívnunk egy adott személyre: mennyiFizetésJárNeki(&béla); (Természetesen ne feledkezzünk meg a visszatérési érték
felhasználásáról, hisz jelen függvényünk meghívásának nincs mellékhatása.) 7 Hogy lehetne ugyanezt a függvényhívást egy adott rekordra megcsinálni, úgy, hogy a . operátort használjuk? Evidens - a függvénynek paraméterként átadott cím (&béla) helyett a függvényt úgy próbáljuk elérni, mintha az rendes adattag volna a struktúrában: béla.mennyiFizetésJárNeki(); Ekkor, mivel a béla. referencia már meghatározza azt, hogy konkrétan melyik vátozón kell a mûveletet elvégeznünk, nem kell a függvénynek paraméterként átadni az aktuális példány címét. Látjuk, hogy a fô különbség az, hogy C-ben nem írhatjuk be a különben logikailag szorosan a struktúrához tartozó függvényt a struktúrába, valamint az, hogy át kell adnunk a (globális) függvénynek paraméterként azt, hogy az melyik példányon (struktúraváltozón) végezzen munkát. Majd látjuk, hogy ennek a kihangsúlyozása miért is fontos. A this referencia
Következik egy olyan téma, amelyet egyetlen könyvben sem láttam még tisztességgel leírva: a this. Ebben a szekcióban (egységbezárás) már láttuk, hogy a C++ struct-ja miben több, mint a C-é - azokban nem csak mezôket, hanem futtatható kódot is el tud helyezni. (Megj: a C++ a fejlettebb struct mellett bevezetett egy másik összetett adattípust is, a class-t. A különbség a kettô között minimális (láthatóság) Ennek ellenére minden C++-oktató csak a class használatát ajánlja A Java, mivel a két típus gyakorlatilag megegyezik, és a Java egyszerûsítésre törekvô nyelv, a struct-ot már teljesen elhagyta fegyvertárából.) Amikor egy olyan osztályt példányosítunk, amely tartalmaz ilyen kódot, érdemes elgondolkodnunk azon, hogy muszáj-e magát a futtatható gépi kódú programkódot is lemásolnunk akkor, amikor az adott osztálynak új példányát képezzük. Természetesen a válasz nem - mekkora ostobaság volna csak azért lemásolni az
adott függvények kódját (amely természetesen több tíz kilobyte is lehet!), ha azok életük során úgy sem változnak, és minden egyes példányra ugyanazon mûveleteket végzik el! Emiatt érthetô, miért kell elkerülni azt a helyzetet, hogy egy adott struktúra / osztály példányosításakor az adattagok mellett az ôrajtuk mûveletet végzô függvények kódját is lemásoljuk. Ekkor viszont, mivel akárhány példánya is legyen egy osztálynak, egyetlen kópiája lesz az osztály példányának mezôin mûveleteket végzô függvényeknek. Ezeknek emiatt explicit tudniuk kell, hogy éppen mely példányon kell mûveletet végezniük. Erre szolgál a fordító által automatikusan a függvények paraméterlistájának végére illesztett this referencia (a referencia a megszokott C-s, C++-os pointerek Java-beli neve), amely explicit megmondja az adott függvénynek, mely memóriacímen található az éppen elérni kívánt példány. Amennyiben nem folyamodnánk
ilyen praktikához, akkor sajnos valóban le kellene másolnunk egy osztály példányosításakor a benne levô függvények kódját is - el lehet képzelni, ez mekkora memóriatöbbletet igényelne. A fenti esetben, amikor a két db int mezônk összes memóriaigénye 64 bit, míg még egy ilyen, viszonlyag egyszerû függvény kódja is, amit mutattam, legalább 200-300 byte helyet foglal, látható, mennyit nyerünk, ha bevezetjük a this pointert. Az, hogy ennek a pointernek mi is a típusa, az elôzôek alapján könnyen kitalálható. Mivel ez az adott osztály példányaira mutat, típusának mindenképp pointertípusnak kell lennie - azaz jelen esetben Alkalmazott* -nak. Emiatt a fordítónk a régi osztályunkat a következôképp fogja lefordítani: class Alkalmazott { int életkor; int fizetés; int mennyiFizetésJárNeki(Alkalmazott* this) { return (int)round(this->fizetés * (1 + this->életkor/100.0)); } } Ugye, mennyire hasonlít a C példához, ahol nekünk
kézzel kellett átpasszolni az éppen kezelni kívánt példány címét? Itt ugyanaz történik, csak a fordító automatikusan gondoskodik arról, hogy minden egyes, az osztályban definiált függvény azt megkapja. Még egyszer - az objektumok mögött ne képzeljünk el 8 semmiféle emberfeletti dolgot, gondoljunk úgy rájuk, mint ha egyszerû, a C-bôl jól ismert struktúraváltozók lennének valahol a memóriában, semmi más. Egyéb finomságok - static (osztály) változók és függvények Már láttuk, hogy az OOP alapvetô osztályait, ill. függvényhívási mechanizmusát milyen egyszerû leképezni olyan nyelvekre, mint a C. Hasonlóan nagyon egyszerû az OOP osztályváltozóinak fogalma is Eddig csak olyan változók deklarálásáról volt szó, amelyekbôl a rendszer mindig egy teljesen új, az elôzô példányoktól független halmazt hozott létre. Lehetnek viszont olyan esetek, amikor szükségünk van olyan változóra, amelyek közösek minden egyes
példányra, és amelyeket ezért a rendszer nem másol le az osztály példányosításakor, mert 1. ha az összes példány ugyanazon a vátozón kell hogy osztozzon (azaz az egyik példányból végrehajtott módosítás az összes többi példányban látszódjon), akkor egyrészt memóriapocsékolás lenne minden egyes példányba belevenni ezt a közös változót is, 2. másrészt, amennyiben - pl a fordító/futtató rendszer minél egyszerûbb felépítése érdekében - úgy döntenénk, hogy mégis lemásoljuk az összes ilyen közös változót minden egyes példányba, akkor bizony vért izzadnánk, hogy ezeket szinkronban tartsuk - ez bizony nagyon nehéz lenne, mindenképpen egy közös, atomikus függvényt kellene ahhoz (rendszerszinten!) létrehoznunk, amely garantája, hogy inkonzisztencia sosem lép fel az egyes változók értéke között. Mint látható, ez borzasztó overhead-et jelentene - nem csak memóriaigény, hanem szinkronizációs és update-lési
processzoridô szempontjából is. Ezért vezették be az OOP megálmodói az olyan speciális változókat, amelyeket, bár az osztályokban taláhatóak, a rendszer nem másol le a példányosítás során. Ebbôl logikusan következik az, hogy ezek a változók akkor is léteznek, ha az adott osztálynak egyetlen példánya sincs. Fenti példánkat pl. kibôvíthetjük úgy, hogy tartalmazzon egy olyan adattagot, amely olyan általános, az összes alkalmazott-ra vonatkozó változó, amely megmondja azt, hogy az alkalmazott fizetése mennyi kell legyen legalább - azaz, tartalmazza a minimálbért is. Belátható, hogy ez a minimálbér minden egyes alkalmazottra ugyanaz, hiszen globális érvényû változó; annak, hogy mégis az Alkalmazott osztályban definiáltuk, oka az, hogy igazából csak ennek az osztálynak van rá szüksége - tegyük fel, más adatstruktúra ugyanis nincs, ami az adott vállalatnál az alkalmazottak adatait leírná, és szüksége lenne erre az
értékre. class Alkalmazott { int életkor; int fizetés; static int hivatalosMinimálbér; int mennyiFizetésJárNeki() { if (hivatalosMinimálbér > Math.round(fizetés * (1 + életkor/100.0))) return hivatalosMinimálbér; else return (int)Math.round(fizetés * (1 + életkor/100.0)); } } Kibôvített osztályunk tartalmaz egy osztályszintû hivatalosMinimálbér nevû ún. osztályváltozót (class variable), amelyet az általunk már jól ismert függvényeinkbôl ugyanúgy elérhetünk, mint ha példányváltozók volnának. Azonban mindenkép tudnunk kell, hogy ez a változó csak egy példányban létezik a memóriában, azaz minden változtatás látszódni fog az osztály összes példányában. Az eddigiek megvilágítására lássuk, hogy futási idôben hogy is néz ki osztályunk három példánya a memóriában, valamint, a statikus osztályváltozókra milyen referenciák mutatnak: 9 béla 32 bites referencia marci 32 bites referencia ilona 32 bites
referencia életkor 32 bit fizetés 32 bit életkor 32 bit fizetés 32 bit életkor 32 bit fizetés 32 bit hivatalosMinimálbér 2. ábra: Hogyan helyezkedik el Alkalmazott osztályunk három példánya (és azok példányváltozói) a memóriában, ill. hol van az egyetlen osztályváltozó, hivatalosMinimálbér Jól látható, a szaggatott nyíllal szedett hivatalosMinimálbér-referencia valójában nem igazi referencia, mert mindig a statikus változók tömbjének kezdetére mutat (amelynek most egyetlen eleme van), ami mindhárom példányra természetesen azonos. Az igazi memóriareferencia viszont, amelybôl itt három van, béla, marci és ilona, konkrétan kijelöli a heap-en azt a területet, ahol a normál (OOPterminológiában instance, példány-) változóink tömbje elhelyezkedik. Az ábráról látható - ez a késôbbiekben, a típuskompatibilitás kapcsán nagyon fontos lesz! - hogy a memóriában egy adott osztály példányainak konkrét mezôi
sorfolytonosan és a deklarálás adott sorrendjében helyezkednek el, azaz azokat a rendszer nem pl. láncolt listában tárolja - ez nevetséges is volna, hiszen egy osztályban adott mennyiségû példányvátozó van, azokat minek kellene egymástól elválasztva, akár linkelt listában tárolni? Ami viszont nagyon fontos - mivel a heap-rôl foglalunk helyet a példányainknak, az egymás utáni példányosítás nem garantálja, hogy a példányokat memóriafolytonos helyre helyezi, azaz a példabeli három példány közvetlenül egymás után helyezkedik el - emlékezzünk arra, hogy a C-ben, amikor malloc-kal foglalunk helyet a heap-rôl, az sem lesz sorfolytonos - a stack-kel ellentétben. A következôkben a függvény elnevezés helyett inkább áttérek a Java-ban használt metódus elnevezésre, hogy ne okozzak konfúziót. Az osztályváltozók bevezetésével rokon az ún. osztálymetódusok fogalma Ezek annyiban hasonlóak az osztályváltozókhoz, hogy az osztályhoz
tartoznak, nem a példányhoz - ebbôl kiindulva, ezt a logikát használva, rájöhetünk, hogy akkor is meghívhatók, amikor az adott osztálynak még nincs egyetlen példánya sem. Ezt használja ki az is, ahogy egy Java program elindul Most errôl beszélek egy kicsit bôvebben. Minden, a konzolról indított Java programnak kell legyen egy main() metódusa (pontosabban egy public static void main(String[] args)). Akár az összes osztályunknak lehet ilyen metódusa – sokan élnek is ezzel a lehetôséggel, ui. az adott osztály tesztelése során nagy haszonnal járhat az, hogy az önmagában is futásképes, önmagából tud példányokat létrehozni, metódusait meghívni stb. Sokakban felvetôdhet a kérdés, hogy lehetséges az, hogy a main(), vagy úgy átalában bármelyik (akár példány-)metódus a saját osztályát példányosítja. Nem lehet ebbôl végtelen ciklus? Nem, nagyon egyszerû oknál fogva. Hacsak nincs direkt kapcsolat a konstruktor és a
példányosítást tartalmazó metódus között (pl. nem a visszahívott konstruktorban példányosítjuk újra az osztáyunkat), akkor nem lehet végtelen ciklus sem, hisz egy osztály pédáyosításakor annak csak a konstruktora hívódik meg, semmilyen más metódusa nem. 10 Még egyszer, ne felejtsük el az ökölszabályt: az osztályok nem mágikus, emberfeletti, teljesen absztrakt képzôdmények, hanem kutya közönséges adatszerkezetek, amikbôl egy ‘példány’ ugyanúgy hozható létre, mint ahogy egy C-bel struct típusból is kreálunk egy példányt. Ezen felül, egy osztályban csak egy metódus futhat egyszerre, azaz mindig konkrétan tudni lehet azt, hogy éppen mi hajtódik végre, melyik metódusban (kis különbséget képeznek, mint majd látni fogjuk, a Thread-leszármazott, vagy a Runnable-t implementáló osztályok, melyek önmagukat példányosítják, és run()-juk az önmagukra hívott start() után párhuzamosan fut majd az osztály önmagát
példányosító kifejezését tartalmazó metódusával). Miért nincsenek globális függvények? Miben más megközelítésû az OOP, mint az imperatív/procedurális programozás? A C-ben, C++-ban járatosak megkérdezhetik, hogy miért hangsúlyoztam azt ki, hogy metódusokat csak és kizárólag egy osztályban (lett légyen ez az osztály akár egy teljesen bogus osztály is) deklarálhatunk. Ez fontos különbség a C++-hoz képest, amely még nem teljesen objektum-orientált (az objektum-orientáltság, melynek ‘absztrakt’, OOD/OOA elméletébe itt nem megyek bele, azt is feltételezi, hogy metódusokat kizárólag Miért is döntöttek úgy a nyelv tervezôi, hogy véglegesen eliminálják a C++-ban még koloncként ott maradt globális függvényeket? Egyszerû: egy jól megtervezett, az OOP irányelveit követô program, amennyiben egy adatstruktúrán kell dolgoznia, akkor van rendesen megtervezve (OOD!), amikor az adaton mûveletet végzô metódusok magában az
adatban vannak. Van is egy ilyen OOD-ökölszabály: amennyiben túltengenek programunkban az olyan osztályok, melyekben egyetlen konkrét mûveletvégzô metódus nincs, csak accessor (a – lehetôleg privát – adatokat visszaadó) és mutator (az adattagokat módosító) metódusok, akkor valószínûleg az objektummodellezés dizájnja rossz. A nyelv tervezésének elsô stádiumában ez még nem volt eldöntött tény, csak akkor határozták el a C++-szal való ilyen erôs szakítást, amikor rádöbbentek, hogy globális függvényeket nagyon keveset igényelnek a Java szabványos osztálykönyvtárai – ami globális függvényt pedig egy jól megtervezett program használt, azok is szinte kizárólag csak matematikai függvények voltak (sin, cos stb). Ezért döntöttek úgy, hogy ezeket a függvényeket inkább egy gyûjtôosztáyba (java.langMath) helyezik, és, hogy ne kelljen ôket egy adott példányra hívni (azaz ne kelljen a Math osztályt fölöslegesen
példáyosítani), osztálymetódusokként deklarálták ôket (érdemes megnézni a Math osztály API dokumentációját – az összes, benne definiált metódus static). Egy fontos ökölszabály – logikailag miben különböznek a C (Pascal stb.) globális függvényei a Java példánymetódusaitól? A Java-ra való átállás során nem árt, ha megtudjuk, mi a fundamentális különbség egy globális függvény és egy példánymetódus között a paraméterlista tekintetében. Mint tudjuk, egy példánymetódust az adott példányra hívjuk meg, tehát implicit tudja azt, hogy melyik példány mezôin kell mûveletet végeznie. A C globális függvényeinek ezt, természetesen, külön kell átpasszolni. Egy C függvény, amely pl a fenti Alkalmazott-példában látott generikus struktúraváltozókat kezel, paraméterlistája ezért általában hosszabb, mint a Java-beli társáé – éppen amiatt, hogy neki még át kell passzolni azt a struktúrát is, melynek mezôin
mûveletet kell végeznie. Ezt ne felejtsük el soha, ha a két nyelv hasonlóságait és különbségeit gondoljuk át (pl. amikor az egyik nyelvet már ismerve a másikat tanuljuk). Példaként lásd a fenti C-Java analógiát Ugyanez vonatkozik egyébként minden, összetett adatszerkezeteket támogató, de nem OOP nyelvre – ott is emulálható az OOP, de lényegesen kisebb biztonsággal (nincs adatelrejtés, hisz globális függvényekkel kell az összetett adatszerkezetek mezôit elérnünk, valamint az éppen aktuális struktúraváltozó címét is át kell explicit passzolnunk a függvényünknek) és egyszerûséggel. Adatelrejtés Már többször megemlítettem, hogy az OOP nem (vagy csak nehezen) képezhetô le teljesen egy tradícionális programnyelvre. Ennek oka az, hogy – a már ismert – absztrakt osztály/konkrét példány/példánymetódusok témakörén kívül az OOP tartogat még a tarsolyában egy-két olyan lehetôséget, 11 mely még szebbé,
átláthatóbbá és legfôképpen biztonságosabbá teszik a programozást. Ezek egyike az ún adatelrejtés, amely lehetôvé teszi, hogy esetlegesen összetartozó, egymástól függô adatokat kívülrôl ne változtathassunk meg úgy, hogy inkonzisztencia léphessen fel a két vagy több változó által tárolt értékben. Azaz, ha pl. az egyik változónkban pl a pillanatnyi dátum napját (numerikusan), a másikban pedig a hét adott napját tároljuk, akkor ne léphessen fel amiatt inkonzisztencia, mert pl. egy módosítás során elfelejtjük beállítani mindkét változót. Sokkal kényelmesebb a – különösen az ilyen, egymástól függô – változókat az osztályra vonatkozóan (a Java private kulcsszavával) privátként definiálni, ekkor azok már más osztáyok pédányaiból nem lesznek elérhetôek. (Fontos azt megjegyezni, amit sajnos nagyon sok könyv nem ír meg, hogy amennyiben egy mezôt privátként deklarálunk, az még elérhetô lesz ugyanezen
osztály más példányaiból. Az adatelrejtés kizárólag más osztályok példányaira vonatkozik. Ezt jó szem elôtt tartanunk akkor, ha egy osztályban olyan metódust definiálunk, amely pl. egy másik példány mezôivel komparálja a sajátját – ez akkor is sikerülni fog, ha az adott mezô private.) Hogyan érhetünk el egy valamilyen más osztályból (pontosabban szólva: más osztály példányainak példánymetódusaiból vagy statikus osztálymetódusaiból) private mezôket? Közvetlenül sehogy. Viszont, amennyiben (természetesen nem private elérésû) olyan, ún. accessor metódust definiálunk, amely visszatérési értékeként a lekérni kívánt mezô értékét adja vissza (ez természetesen azt is involválja, hogy ezen metódusnak nem kell semmilyen paramétert átpasszolni), ezt – közvetve – megtehetjük. Hasonlóképp, amennyiben a private mezôinket meg szeretnék változtatni, akkor ún. mutator metódusokat kell ehhez definiálnunk, amelyeknek
paraméterükként az adott, megváltoztatandó változó új értékét kell átpasszolni. Leszármaztatás A legtöbb Java-t tágyaló mû az Alkalmazott - Fônök, vagy a Síkidom - Négyszög Négyzet példát mutatja be, amikor bevezeti az öröklôdés, az OOP egyik központi fogalmának témáját. Sajnos ezen túlságosan is absztrakt példákról, oktatóként, meglehetôsen negatív véleményem van, ezért most egy lényegesen életszagúbb példával illusztrálom, mire is jó egyáltalán az öröklôdés. A java.awt csomagnak egyik fontos osztálya a Canvas Ennek használatáról azt kell tudni, hogy van benne egy (tulajdonképp örökölt) paint() metódus, amely nem csinál semmit. Amennyiben sikerülne ezt a metódust átírni úgy, hogy legyen benne valami, azaz valóban használjuk a Graphics típusú grafikus kontextusunk valamelyik rajzoló metódusát (pl. drawString), akkor valóban sikerülne ezt az osztályt úgy felhasználni, hogy pl. egy Frame-be ágyazva
kiírhassunk valamit egy grafikus felületre, anélkül, hogy Label-eket kellene használnunk. Ezen felül, osztályunkat vértezzük fel egeres rajzolási lehetôséggel, azaz mindig rajzoljon oda pontot, ahol a felhasználó megnyomta az egérgombot. Ez azt jelenti, hogy az alap funkcionalitást nem csak hogy felül kell definiálnunk (konkrétan: a paint(), a szülôosztályban még üres törzsû, metódust tartalommal fel kell töltenünk), hanem olyan plusz metódusokat kell az osztályhoz hozzáadnunk, mint az egérgomb-lenyomás-események processzálását végzô mousePressed metódus. (Megj.: akik ismerik az AWT-t, azt mondhatják, hogy ez rossz példa, ui a Canvasban szinte semmi kód nincs Elkézelhetjük helyette a Window-ot is, amely már ténylegesen komoly kódot tartalmaz.) 1 Minden Java-beli osztály az rt.jar fájlban taláható (a Java 2-ben legalábbis Régebbi verziókban ez a file classes.zip névre hallgatott) Megtehetnénk, hogy a JDK részét képezô
Canvasjava forrásfile-ot átírjuk, hogy valóban legyen benne egy paint(), ami meghívja a drawString -et, újrafordítjuk és kicseréljük az eredeti osztállyal, azaz az rt.jar-ba berakjuk Ez helytelen ötlet - nagyon csúnya dolog kicserélni egy runtime könyvtár osztályát a sajátunkra, még akkor is, ha megvan a forrása és azt szabadon újrafordíthatjuk. 2 Megtehetnénk azt is, hogy egyszerûen átmásoljuk az adott osztály forrását a mi osztályunkba (pl. RajzoloCanvas.java néven), lefordítjuk, és azt hasznájuk Ez viszont amiatt nem jó ötlet, mert egyrészt az átalunk másolt részekrôl sosem tudjuk majd biztonsággal, hogy az éppen aktuális JDK Canvas.class-ával szinkronban van-e - ha elfelejtjük kódunkat mindig újrafordítani a legaktuálisabb JDK esetleg idôközben megváltozott kódját használva, akkor nem kizárt, hogy elôbb-utóbb inkonzisztencia keletkezik a mi RajzoloCanvas osztályunk és a szabványos Canvas osztály között abban, hogy a
mi osztályunk esetleg rossz függvényeket hív stb. Tehát - még akkor se nyúljunk ilyen praktikákhoz, ha - ahogy már említettem szintén rendelkezésre áll az adott osztály forrása (Java-ban ez így van, MFC és más class library-k esetében az osztályok forrásait hiába is keresnénk). 12 3 Használjuk fel az ún. öröklôdés lehetôségét Ez adja a legtisztább és legbiztonságosabb megoldást - egyszerûen öröklünk minden, a szülôosztályban létezô metódust és adattagot, és azokat átírjuk, ill. azokhoz plusz adunk még egy-két mezôt, ill. metódust Ez az egyedüli járható út Majd az AWT, ill. a Java-s I/O kapcsán látjuk, mennyire tiszta, logikus és átlátható kódot tud az eredményezni, hogy az öröklôdést a Java osztálykönyvtárainak megtervezésekor az öröklôdés minden pozitívumát bevetették. Miért is? (Megj: a következô szekcióból ne csodálkozzunk most, ha nem sokat értünk: amikor az AWT alapjait megértettük,
ezzel sem lesz gondunk.) Nagyon egyszerû. Nézzük pl a Component vagy a Container osztályt A Component leszármazottja a Container, így ez utóbbi a Component összes metódusát tartalmazza. Nézzünk meg pár ilyent, és beszéljük meg, miért is éppen abba az osztályba kerültek! Elôször is, a Component-ben ott vannak az alacsonyszintû (nem szemantikus) eseménykezelôket az adott komponenshez rendelô metódusok (addFocus/Key/Mouse/MouseMotionListener stb.) Ezekre szükség van minden egyes komponensben, ill. konténerben? Igen, nagyon sokszor kell ôket használni, lett légyen szó szimpla komponensekrôl, vagy más komponenseket magukban tartalmazni képes konténerekrôl. Láthatjuk, olyan metódusokról van szó, amelyeket már a közvetlen leszármazott Container-nek is tartalmaznia kell, nem is szólva a Component közvetlen leszármazottjairól (pl. a Button-ról) Nézzük most a Container-t: milyen olyan metódusok vannak benne, amely egy részrôl minden
konténerre tekintve általánosak, azaz ôket ki kell emelni, másrészt nem lenne jó, ha azokat a Component is tartalmazná, mivel már jellegzetesen csak a konténerekre jellemzô metódusokról van szó, amelyet nem lenne értelme egy szimpla komponensre meghívni? Persze, vannak: ilyen pl. a setLayout() Azonnal érthetô, hogy a setLayout() miért került a konténerek szülôosztályába, a Container-be: azért, mert minden konténernek rendelkeznie kell egy olyan metódussal, mellyel a programozó az adott konténer pakolási stratégiáját beállíthatja. Típuskompatibilitás Megint egy olyan része az OOP-nek, amelyet semelyik könyv sem magyaráz el tisztességgel, pedig nagyon egyszerû belátni, hogy miért is lehet az OOP-ben típuskompatibilitási reláció szülô- és leszármazott osztályok objektumai között. Az 2 ábrán már láttuk, hogy egy osztály példányváltozói a memóriában sorfolytonosan, és deklarálásuk sorrendjében vannak eltárolva,
akármelyik példányát nézzük is az adott osztálynak. Ezen felül, amennyiben egy leszármazott osztály pótlólagos példányváltozókat definiál a már meglévôk mellé, biztosak lehetünk abban, hogy - mint ahogy a rendszer szigorúan a deklarálási sorrendben foglal helyet a példányváltozóknak a memóriából - ezek a változók a szülôosztály példányváltozói után fognak, szintén sorfolytonosan, elhelyezkedni. Tegyük fel, kiterjesztjük Alkalmazott osztályunkat egy Manager osztállyá, azt pedig egy Nagyfônök osztállyá. Mivel egy manager több alanyi juttatásban részesül (pl céges kocsija van), mint egy mezei alkalmazott, természetes, hogy több adattagot is fog tartalmazni az ôt leíró osztály. A nagyfônöknek pedig még ennél is több példányváltozója is lesz - titkárnô, a Manager osztályból örökölt céges kocsi, villa valahol stb Nézzük meg konkrétan, milyen példányváltozókat tartalmaz ez a három osztály! class
Alkalmazott { int életkor; int fizetés; } class Manager extends Alkalmazott { int autóAzonosító; } class Nagyfônök extends Manager { int titkárnôAzonosító; int villaAzonosító; } 13 A gyerekosztályokban, ahogy már említettem ebben a részben, csak az új adattagokat kell feltüntetni, az örökölteket nem. A három osztály egy tetszôleges objektumának memóriaképe a következô lesz: béla 32 bites referencia tamás (Manager) 32 bites referencia edit (Nagyfônök) 32 bites referencia életkor 32 bit fizetés 32 bit életkor 32 bit fizetés 32 bit autóAzonosító 32 bit életkor 32 bit fizetés 32 bit autóAzonosító 32 bit titkárnôAzonosító 32 bit villaAzonosító 32 bit Mit láthatunk ezekbôl a memóriaképekbôl? Például azt, ha van egy Alkalmazott típusú objektumunk, ahelyett nyugodtan használhatunk Manager, illetve Nagyfônök típusú objektumokat is. Miért? Egyszerû: ha egy objektumot Alkalmazott típusúként
kezelünk fordítási idôben, akkor arra a fordító csak valóban az Alkalmazott osztályban létezô metódusok meghívását, ill. adattagok elérését engedélyezi. Ha az osztályban deklarált két int változó valóban ugyanott és ugyanolyan sorrendben helyezkedik el, mint a leszármazott osztályok példányaiban, akkor semmi gondunk nem származhat abból, ha a szülôosztály (itt: Alkalmazott) példánya helyett igazából annak egy gyerekének a példányán végzünk mûveletet, hisz az nem járhat együtt véletlen memóriafelülírással. Ez miért is igaz? Egy gyerekosztály példánya kötelezôen tartalmazza a szülöosztály(ok) adattagjait, valamint az azokon felül még pluszban definiált példányváltozókat. Amennyiben egy szülôosztálybeli objektumról hinnénk azt, hogy az igazából gyerek típusú, nagy bajban lennénk, ha a memóriában nem létezô példányváltozóit felül próbálnánk írni, ugyanis szükségszerûen felülírnánk az objektum
utáni, ha nincs szerencsénk, valami más által már használt memóriaterületet - nesze neked biztonság! Elôzô ábránkból is látható: egy Alkalmazott típusú objektum csak 64 bitet foglal le a memóriából, míg egy Nagyfônök típusú objektum egyenesen 160 bitet. Ha egy Alkalmazott típusú objektumról a rendszernek azt mondanánk, hogy az igazából egy Nagyfônök típusú objektum, és aztán ezt a plusz 96 bitnyi memóriaterületet emiatt a fordító átal engedélyezetten elérnénk (felülírnák), baj lenne, hiszen nem tudnák, hogy éppen mit rontottunk el a dinamikus memóriában, milyen, az adott objektumból teljeen független objektumba sikerült belerondítanunk. Ezért mondjuk azt, hogy egy gyerekosztálybeli objektumról, hogy azt minden esetben használhatjuk a szülôosztálybeli objektum helyett, de fordítva nem, hisz ez utóbbi esetben véletlen memóriafelülírás veszélye állna fenn. Emiatt igaz az is, hogy egy igazából
gyerekorsztály-típusú objektumot minden további nélkül, explicit cast (típuskonverzió) nélkül hozzárendelhetünk egy szülôosztály típusú referenciához. A legtöbb Java-s könyv példái ezt ki is használják, lássuk pl. a következô osztályt: 14 class FrameAmireRajzolunk extends Frame { public void paint(Graphics g) { g.drawString("Szia világ!", 30,30); } // paint public static void main(String[] args) { Frame f = new FrameAmireRajzolunk(); f.setSize(200,200); f.show(); } // main } // class Mit is csinál itt a Frame f = new FrameAmireRajzolunk(); utasítás? Nagyon egyszerû - a new operátor egy, az újonnan létrehozott FrameAmireRajzolunk példányára mutató referenciával tér vissza. Mivel ezen objektum osztálya a Frame AWT-beli osztály leszármazottja, ezért referenciáját hozzárendelhetjük ahhoz, hiszen a Frame-ben garantáltan ugyanannyi (ez esetben ez teljesül, ui új adattagokat nem definiálunk a gyerekosztályban, így annak
példányainak memóriabeli képe ugyanolyan lesz, mint a szülôosztály objektumainak a lenyomata), vagy kevesebb, a memóriában elôbb véget érô adattag van. Overriding, dinamikus metódushívás, overloading Az OOP-nek még két központi fogalma van: az overriding (kb. felüldefiniálás) és az overloading (ez utóbbit túlterhelésnek is szokta a magyar irodalom nevezni; a következôkben kizárólag az angol neveket használom). Mindkettô nagyon hasznos jellemzôje az OOP-nek, amit igaz, hogy ebben a gyors kurzusban nem fogunk kihasználni, beszélni viszont mindenképp érdemes róla. Az overloading azt teszi lehetôvé, hogy azonos nevû, de küönbözô formális paramétertípusokkal rendelkezô metódusokat definiáljunk – nem csak egy adott osztály szintjén, hanem akár a gyerekeiben is. Az overriding, ezzel ellentétben, lehetôvé teszi a szülôosztályból örökölt metódusnak az adott szinten történô megváltoztatását. Tegyük fel, hogy a fenti
hármas Alkalmazott/ Manager/ Nagyfônök hierarchiában nem csak osztályokat, hanem azokon belül mûveleteket is kívánunk definiálni (még egyszer: amennyiben programunkban sok olyan osztály van, amelyben csak változók, ill. az azokat elérô accessor/mutator metódusok vannak, akkor az valahol elhibázott OO design-ra vall). Használjuk a már jól ismert mennyiFizetésJárNeki()metódusunkat, amely megmondja egy adott alkalmazottról (még pontosabban: Alkalmazott –(leszármazott)példányról), hogy mennyi fizetés jár neki. Tegyük fel, hogy a leszármazott osztályok fizetését máshogy számítjuk, mint a szülôosztáyokát, azaz a Manager, ill. Nagyfônök objektumok többet keresnek, mint az egyszerû Alkalmazott-ak, mégpedig a Manager-ek kétszer, a Nagyfônök pedig négyszer annyit. Menten eszébe is jutna az embernek, mi lenne, ha ezt a változtatást kapásból a fizetést visszaadó mennyiFizetésJárNeki()metódusunkban tennénk meg, azaz a leszármazott
osztályokban levô metódusokban megváltoztatnánk a kódot, tehát fizetés-t megszoroznák kettôvel, ill. néggyel, mielôtt kerekítenénk, majd egész számmá konvertálnánk. Igen ám, de mi biztosítaná azt, hogy valóban a gyerekosztály metódusai hívódjanak meg akkor, ha esetleg ki szeretnénk használni a típuskompatibilitás óriási elônyeit is a heterogén tömbök segítségével. Mivel tudjuk, hogy a Java runtime futtató rendszer számon tartja minden egyes objektum aktuális típusát, megcsináhatnák azt, hogy ugyan minden objektumunkat egy heterogén tömbben tároljuk (ezzel a programkódolás- és adminisztráció idejét jelentôsen lecsökkentve), majd amikor ezeket az objektumokat sorban egymás után kiszedjük a tömbbôl, az instanceof operátor akár többszöri használatával megállapítjuk a konkrét típusukat, és utána hívjuk csak meg rájuk – valahogy – az adott osztály példánymetódusát. Ez két dolog miatt ellenjavallt: 1. egy
ilyen több esetet tesztelô instanceof-szerkezet szükségképpen hibát rejthet magában (mi van, ha egy adott osztályhoz való tartozást elfelejtünk tesztelni), valamint nagyon nehezen bôvíthetô, ha esetleg valaki más plusz osztályokat akar a fônök-beosztott hierarchiához adni, akkor bizony vért fog izzadni, hogy ezeket a típuslekérdezéseket mindig update-lje, ha változik a helyzet. S ez sajnos még rosszabb helyzetet 15 teremt a szülôosztályokban, ugyanis azokban is módosítani kell a típuslekérdezést, ami bizony rendkívül nehezen managelhetô kódhoz vezet 2. arról nem is szólva, hogy a Javaban nincs olyan szerkezet, amely megmondja, hogy az X osztály példánymetódusát hívd meg az Y osztály példányára, ez csak és kizárólag statikus osztálymetódusokkal tehetnék meg, azaz amelyeknek ráadásul (lásd a C-t a Javaval összevetô szekciót) át kellene adnunk az adott objektum referenciáját. Nesze neked adatelrejtés és egyszerûség
Szerencsére nem ilyen rossz a helyzet, mert a Java runtime mindig ellenôrzi az adott objektum dinamikus típusát, és ez alapján hajtja végre az annak megfelelô (akár leszérmazott) osztályban levô metódust. Nézzünk – és próbáljunk ki! – egy ezt, valamint a heterogén tömbök hasznát bemutató programot: class Alkalmazott { int életkor; int fizetés; int mennyiFizetésJárNeki() { return (int)Math.round(fizetés * (1 + életkor/100.0)); } } class Manager extends Alkalmazott { int mennyiFizetésJárNeki() { return (int)Math.round(fizetés * 2 (1 + életkor/100.0)); } } class Nagyfônök extends Manager { int mennyiFizetésJárNeki() { return (int)Math.round(fizetés * 4 (1 + életkor/100.0)); } } class InditoOsztaly { public static void main(String [] s) { Alkalmazott[] arr = new Alkalmazott[3]; arr[0] = new Alkalmazott(); arr[1] = new Manager(); arr[2] = new Nagyfônök(); for (int i = 0; i<3; i++) { arr[i].életkor=300; arr[i]fizetés=100000; } for (int i =
0; i<3; i++) System.outprintln(arr[i]mennyiFizetésJárNeki()); } } Vegyük észre, hogy a fordító nem tudhatja elôre, hogy az arr tömb elemei milyenek (gondoljuk meg: egy futásidejû véletlenszámgenerátor is eldönthetné a tömb elemeinek feltöltésekor, hogy akkor az aktuális elem éppen milyen dinamikus típusú legyen – ekkor már a Java fordítónak tényleg lehetetlen lenne megjósolnia, hogy az aktuális típus mi lesz, így nem is ‘drótozhatja’ be a kódba a metódushívást, azaz a futtató rendszernek futásidôben kell kiderítenie a típust és azt, hogy a memóriában hol kezdôdik az éppen végrehajtani kívánt kód. (Azt, hogy ez a gyakorlatban hogy történik, a Kondorosi-féle könyv remekül elmagyarázza.) Heterogén tömbök Még nem magyaráztam el, mik is az azok a heterogén tömbök. 16 A típuskompatibilitás, ill. a futásidejû, típusfüggô dinamikus metódushívás nagyszerû lehetôséget ad pl. vállalati
nyilvántartó-rendszerek programozóinak kezébe, amely segítségével nagyságrendekkel könnyebb kódot írnunk. Mindjárt látjuk azt, hogy ez mit is jelent Tegyük fel, hogy feladatunk a fentiekben említett, háromféle dolgozó-típust alkalmazó cég bérelszámolóinak kezébe egy olyan programot adni, amely ezeknek a dolgozóknak az adatait kezeli. C-ben nem lenne más dolgunk, mint hogy definiálunk annyi láncolt listát, ahány típusú összetett változónk (struct) van (manager, alkalmazott, fônök stb), és ezeket külön-külön tároljuk, feldolgozzuk stb. Nem nehéz elképzelni, ez milyen pokoli többletterhet helyez a programozó vállára. Mi a helyzet Jávában? Sokkal egyszerûbb az ember élete, ugyanis, éppen a típuskompatibilitás miatt, egy szülôosztály típusú tömbbe berakhatunk gyerek-típusú változókat is, és – a dinamikus metódushívás miatt – mindig garantáltan a gyerekosztályban override-olt metódusok hívódnak akkor meg, ha egy
szülô statikus típusú objektumra meghívunk egy, a gyerekben override-olt metódust. Miért nem szûkíthetjük egy override-olt metódus láthatóságát? Felmerülhet a kérdés, hogy miközben override-olunk egy, a szülôosztályból örökölt metódust, annak láthatóságát meg szabad-e változtatnunk? A válasz nagyon logikus: azt, ha meg is akarjuk változtatni, csak bôvíthetjük. Miért? (Ezt a kérdést, mivel még egyetlen könyvben sem láttam értelmesen elmagyarázva, külön tágyalom, bár ennek nem lesz sok jelentôsége a projectünkben.) Már láttuk, hogy fordítási idôben ellenôrzi a Java az elérési jogosultságokat. Amennyiben élünk a futásidejû típuskompatibilitás adta elônyökkel (pl. mert generikus tömböt kódolunk), akkor fordítási idôben a fordító nem tudja, hogy futás közben ténylegesen a dinamikus metódushívás mely (leszármazott) osztály metódusát fogja meghívni. Amennyiben egy leszármazott osztályban egy
override-olt metódus láthatóságát szûkítenénk (narrow az angol szakirodalomban), akkor a fordító, mivel nem tudja, hogy futási idôben ténylegesen melyik leszármazott-beli metódus hajtódik végre, nem tudhatja, hogy a szülô(k)ben definiált, még elérhetô metódus helyett nem a gyerekben override-olt, már nem elérhetô (mert pl. private) metódus hajtódna-e végre. Mivel ezáltal privát metódusokat vagy adattagokat is publikusként érhetnénk el, nem fordítja le a Java az ilyen metódus override-, ill.változó shadow-olásokat, hisz az az adatelrejtést kijátszó fegyver lehetne. (Igaz, ennek jelentôsége nem lenne túlságosan magas, ui akkor lenne igazán veszélyes az adatelrejtés kijátszásának lehetôsége, ha a végfelhasználó nyúlhatna a készen kapott, könyvtári szülôosztályokban levô privát adattagokhoz, metódusokhoz. Ez esetben viszont az adott osztályhierarchiát tervezô designer hibája lenne az, ha ezt megengedné, és a
gyerekosztálybeli adattagokat érhetnénk így el.) Ugyanezzel a gondolatmenettel belátható, hogy a láthatóság bôvítése az adatelrejtést nem veszélyezteti, hisz egy gyerekosztály statikus típusú objektumban dinamikusan nem lehet egy szülôosztály típusú, azaz egy, a gyerekben bôvített metódushívás nem eredményezheti azt, hogy úgymond ‘felfelé’ lépve (ami természetesen lehetetlen, épp a típuskompatibilitás és a dinamikus metódushívás miatt) a hierarchián, egy, a szülôben még privátként deklarát metódust vagy mezôt érhessünk el. Konstruktorok Eddig a T. Olvasó azt gondolhatta, hogy miután egy objektumot létrehoztunk a memóriában, annak adattagjait kézzel kell inicializálni. Nos, mivel ez vagy nem teljesíthetô (mert pl az adattagok nem láthatóak egy másik osztályból), vagy eleve hibákat indukálhat. Gondoljuk el, mi van akkor, ha egy Alkalmazott objektumot akarunk létrehozni és az adatait feltölteni, de pl. valamelyik
változójának elfelejtünk értéket adni. Persze, ilyenkor egy metódust (pl initialize()) létrehozhatunk, amely egy lépésben inicializálja az adattagokat – de mi történik akkor, ha ezt a metódust is elfelejtjük meghívni? A konstruktorok pont arra valók, hogy az ilyen programozói tévedéseket és melléfogásokat kiküszöböljék. A konstruktorok ugyanúgy néznek ki, mint a rendes függvények, két különbséggel: 1. konstruktoroknak nincs visszatérési értékük Mivel ennek okát a legtöbb Java programozó nem érti, csak bebiflázza (hasonlóan az OOP megannyi részterületéhez), érdemes megtárgyalni, miért. Egyszerû: egy példányosítási kifejezés visszatérési értéke az objektum tárterület-kezdetének referenciája, amit (látszólag) nem a konstruktor, hanem a new operátor ad vissza. Ez alapjhán lehet pillanatok alatt logikusan megtanulni, hogy konstruktornak sosincs visszatérési értéke (megj.: a színfalak mögött természetesen van,
referenciát adnak vissza, de ez nem látszik nyelvi szinten). 17 2. a konstruktorok neve az osztály nevével kell hogy megegyezzen. A konstruktorban intézzünk minden, a példányváltozók inicializálásával kapcsolatos tevékenységet, ui. itt bármilyen utasítás (nem csak értékadás) állhat, ellentétben a minden metóduson kívül történô inicializációval, ahol kizárólag csak deklarációk-inicializációval típusú utasítások állhatnak. Még pár érdekes dologról érdemes szót ejteni: 1. a konstrukotorok láthatóságát mi magunk szabályozhatjuk Ha például az osztály összes konstruktorát private-ként deklarálunk, akkor azt másik osztályból nem példányosíthatjuk, csak olyan ún. beépített factory metódusokon át, amelyek már természetesen látják a private konstruktorokat is. Ezeket a factory metódusokat pl. arra használhatjuk, hogy megszabjuk, az adott osztályból max hány példány hozható létre stb. 2. a konstruktorok
egymást hívhatják a this() kulcsszóval (ez itt nem referencia, hanem konstruktorhívás, ahol a zárójelekben az overload-olt konstruktornak átpasszolni kívánt aktuális paraméterek állhatnak). Mi az a super és mire hivatkozik? Ez is nagyon elhanyagolt területe a Java oktatásának, és – tapasztalatom szerint – senki sem képes megérteni a rendelkezésre álló ‘szak’irodalom alapján, pedig rendkívül egyszerû és logikus dologról van szó (na igen, megint egy újabb hibája a revolúciós megközelítésnek: annyira absztrakt, hogy a tanulók el nem tudják maguk elôtt képzelni azt, hogy a super (vagy éppenséggel a this) hova is mutathat a memóriában). Elôször is, amikor egy osztály példányát létrehozzuk, akkor – természetesen – valemelyik konstruktora végrehajtódik. Ez azonban nem minden! Nem csak az adott szinten levô konstruktor hajtódik végre, hanem az összes szülôosztálybeli konstruktor is! Ennek oka az, hogy minden egyes
osztálynak magának kell gondoskodnia a benne definiált változók helyfoglalásáról és inicializálásáról. Apropó helyfoglalás. Ha emlékezetünkbe idézzük a típuskopatibilitás során tárgyaltakat, beugrik, hogy ‘igen! mindig a ‘legszuperebb’, legôsibb osztály példányváltozói állnak az objektum számára lefoglalt memóriaterület elején, és utána szép sorban következnek a gyerekosztáyok változói – de szigorúan csak a leszármazás sorrendjében, hiszen csak ezzel lehet garantálni a típuskompatibilitást!’. Igen, ebbôl logikusan következik az, hogy valóban a legôsibb osztály konstruktora hívódik meg legelôször, és utána szép sorban a többi. Na igen ám, de a Java hogy biztosítja azt, hogy ez a konstruktorhívás-láncolat garantáltan a legôsibb szülôosztálynál (ami persze a java.langObject, de annak nincsenek változói, csak metódusai) indul, és a leszármazott osztályok konstruktorait csak késôbb hívja meg? Számon
tartja-e a Java runtime azt, hogy mit kell elôször meghívnia (az Object konstruktorát), és utána merre haladjon tovább? Nem, sokkal egyszerûbben mûködik az egész (gondoljuk el, most is tetemes procseeszoridôt elemészt egy objektum létrehozása – két nagyságrenddel több idôt, mint mondjuk egy integer típusú változó elérése). Amikor egy adott osztály konstruktorát meghívjuk, akkor, mint már mondtam, nem az adott konstruktort hajtja végre lehelôször a rendszer, hanem a legôsibb szülôosztályét. Ezt úgy biztosítja a rendszer, hogy a (paraméter nélküli, akár default, akár általunk definiált) konstruktor elsô utasításaként beilleszt egy super() hívást. Ez hívja meg a szülôosztály konstruktorát Hogy miért lehetséges az, hogy a legôsibb szülôosztály példáyváltozóinak foglal a rendszer legelôször fut le? Egyszerû: úgy, hogy csak a super() hívás után hajtódik végre egy adott gyerekosztályban a példányváltozók
helyfoglalása és inicializálása – ezzel is lehet azt magyarázni, hogy a super() hívásnak kötelezôen a konstruktor elsô utasításának kell lennie, akár implicit módon, a fordító által, akár explicit, a programozó által lett beillesztve a programszövegbe. Na igen ám, de mi van akkor, ha csak nem paraméter nélküli konstruktor van a szülôosztályunkban? Honnan tudja azt a fordító, hogy ezeknek paraméterként mit adjon át? Ilyenkor nekünk kell explicit a super() hívást a konstruktor elsô utasításaként a programszövegbe beírnunk, hisz csak mi tudhatjuk, hogy a konstruktor paramétereként mit akarunk megadni. Megj.: néhány, szakmai bakiktól hemzsegô mû (konkrétan a Teach Yourself Java X in 21 Days- sorozatról van szó, amely már négy kiadása és számtalan újranyomása ellenére sem képes az ilyen durva hibákat kiszûrni) olyan megjegyzéseket tesz, mely szerint amennyiben egy gyerekosztály konstruktorának paraméterlistája ugyanaz,
mint egy – default, paraméter nélküli konstruktorral nem rendelkezô – szülôosztályé, akkor az implict super() hívást a fordító automatikusan beilleszti a kódba. Ez természetesen nem igaz 18 A super kulcsszó nem csak konstruktorhívásnál játszhat szerepet, hanem abban az esetben is, amikor elfedett, shadow-olt szülôosztálybeli változót próbálunk elérni. Eddig a shadowing-rôl, azaz az elfedésrôl nem esett szó; lássuk, mi is az! Ha egy gyerekosztályban egy örökölt mezôvel azonos nevû és típusú változót deklarálunk, akkor az elfedi az öröklött változót, és azt ezentúl nem fogjuk látni az adott osztály példányából. Gondolkozzon el a tisztelt Olvasó – ha egy gyerekosztály elfed egy öröklött változót, az eltûnik abból a memóriarészbôl, ahol az azt elôször deklaráló osztály adattagjainak foglal helyet a Java runtime? Persze hogy nem, hiszen akkor eleve nem teljesülhetne a típuskompatibilitás elve – azaz az
adott gyerekosztály elsô (néhány) memóriaterülete nem lenne már kompatibilis a szülôosztályokkal. Ez a megoldás semmiképp nem lenne használható, arról nem is szóla, hogy mekkora többletmunkával járna az, hogy amikor a super() hívások láncolata visszatér a (shadow-oló) gyerekbe, akkor az saját kezûleg újrarendezné az eddig allokált és inicializált memóriaterületeket (az elfedett változó memóriaterületét eltüntetné, az utána következô változókat pedig visszafelé shiftelné a memóriában). Látható, ez a negoldás teljességgel hasznáhatatlan lenne, már a típuskompatibilitás feladása miatt is. A Java ennél sokkal elegánsabb megoldást választott: amennyiben egy gyerekosztály egy szülôosztály bármilyen változóját elfedi, a gyerekosztály új, elfedô változójának helyet foglal a Java runtime a heap-en, azaz gyakorlatilag két, ugyanolyan nevû változó lesz az egy objektum mezôit tartalmazó tárban. Ezen változók
közül a utolsót éri el a Java, amikor a gyerek példánya arra a változóra hivatkozik, viszont (és itt ugrik a majom a vízbe) az elfedett változót akkor, amikor arra super. elôtaggal hivatkozik Ennyi a magyarázata a super kétfajta használatának – látjuk is, milyen egyszerû is ez az egész, ha valaki az OOP-t a gépi kód felôl közelíti meg! A java.awt csomag A grafika programozásának mindenképp külön fejezetet szentelek, mivel azt az ELTE jegyzete nagyon rosszul ismerteti, abból az AWT programozását megtanulni lehetetlen. Minden lelke a java.awtFrame osztály Ez egy úgynevezett tároló, konténer (Container, mellesleg az azonos nevû szülôosztályból leszármaztatva). Ez azt jelenti, hogy képes más widget-eket, azaz felhasználói interfész-komponenseket megjeleníteni, ill. az ún layout managerek, pakolás-managerek segítségével azokat futásidôben, a felhasználó akaratától függôen többé-kevésbé dinamikusan managelni (nagyítani,
kicsinyíteni stb, ha a felhasználó az azokat tartalmazó Frame-et átméretezi). A java.awt csomag meglehetôsen sok GUI widget osztályt tartalmaz Ennek osztályhierarchiáját mindenképp érdemes lesz késôbb megismernünk, hiszen nélküle sokkal nehezebb lesz megértenünk, miben mások például az appletek, mint az applikációk. Ez tanulásunkat és késôbbi munkánkat rendkívüli mértékben megkönnyíti majd. Kapásból a legfontosabbat meg kell említeni az AWT-vel kapcsolatban: a már említett Container osztály a Component osztályból, a legtöbb AWT komponens (kivéve a menükezelést végzô komponensek, a layout manager osztályok, a grafikus kontextust, ill. képek tárolását megvalószító osztályok stb) szülôosztályából származik. Ez azt jelenti, hogy egy konténer önmagában egy egyszerû komponens is, azaz mindazon helyen szerepelhet, ahol a rendszer egy komponenst vár (hisz a konténer a komponens gyereke, így azzal típuskompatibilis -
alulról). Ez teszi lehetôvé azt, hogy akármilyen mélységben egymásba ágyazhassunk konténereket. Persze ezt egy Frame-vel, amely egy operációs rendszer-hívással rakatja ki az ablakozó rendszerrel az ablakát, nem lehet megtenni, ellentétben jópár, nem felsôszintû AWT konténerrel (azaz amelyeket magukban nem jeleníthetünk meg, azaz melyeket mindig kötelezôen egy már meglevô pl. Frame konténerbe kell rakni: ilyen konténer a Panel - ezt fogjuk használni bonyolult, egymásba ágyazott struktúrák esetében, mint ahogy majd látjuk). Az eddigiekben tehát két absztrakt osztályról (a Container és a Component), valamint két konkrét konténerrôl, a Frame-rôl és a Panel-rôl esett szó. Mivel legegyszerûbb tanulóprogramjaink nem használnak Panel-t, elég elôször csak az elsôvel törôdnünk. A konténerek 19 Már megemlítettem, hogy a konténerek megpróbálják a beléjük pakolt komponenseket mindig a konténer méretétôl függôen,
ésszerû határok között, átméretezni, ill. az adott helyen a lehetô legesztétikusabban elosztani. Ez óriási elôrelépés a legtöbb Windows alá írt program megközelítéséhez képest, ahol a komponensek pixelre pontosan mindig ugyanott kezdôdnek, bármekkora is legyen az ôket tartalmazó konténer. Valószínû mindenki talákozott már ennek a hátulütôivel: 640*480-as felbontás esetén a Win95/NT képernyôbeállító dialógusablak alján levô gombok egyszerûen kilógnak a képernyôrôl, és csak találgatni lehet, hogy az ember - tabulátorozva - épp mit nyom meg. Ilyen esetekben nagyon kényelmetlen az, hogy az ilyen dialógusablakokat nem lehet átméretezni. A Java, ha megfelelôen használja a programozó az ún. layout manager-eket (ELTE-féle szóhasználattal pakolási stratégia), akkor sosem fog ilyen esetekkel megörvendeztetni. Igaz, szélsôséges esetben (pl. FlowLayout-olt gombok) eltüntetheti egyes komponenseinket, de legalább viszonylag
jól lekezeli a konténerek méretének felhasználó általi megváltoztatását. Természetesen nem kötelezô ilyen bonyolult layout manager-eket hasznáni, használhatjuk helyette az ún. null layout manager-t, amikor is saját kézzel állíthatjuk azt be, a komponenseink pontosan hol legyenek Mivel azonban ennek a layout manager-nek a hasnálata erôsen ellenjavallt (lásd a fenti Win95-ös példa tanulságait), így ezzel ezen kurzus során nem is foglalkozunk, helyette a három legfontosabb, ugyanakkor nagyon egyszerûen megérthetô layout manager-t, a FlowLayout-ot, a GridLayout-ot és a BorderLayout-ot ismerjük meg. A Frame (még egyszer: minden grafikus Java alkalmazás kötelezô része, felsôszintû ablaka) használatáról azt is tudni kell, hogy miután példányosítottuk (és beleadd()oltuk a kívánt GUI widget-eket), még explicit méretezni is kell, majd egy setVisible(true)/show() hívással láthatóvá tenni. Ha a méretezést elfelejtenénk, futási idôben
csak az ablak (Windows-os, Solaris-os stb) fejléce látszik majd. Megj.: a Java-s könyvek legnagyobb része külön kihangsúlyozza, hogy a show() deprecated - ez igaz, viszont csak a Component osztályban. A Frame-ben nem az, sôt - érdemes azt preferálni a setVisible(true)-val szemben, ui nemcsak hogy megjeleníti, hanem az elôtérbe is hozza az ablakot. Ennek különösen dialógusablakok megjelenítésénél lesz nagy fontossága: ökölszabályként jegyezzük meg elôre azt, hogy amennyiben a rendesen méretezett ablakunk a setVisible(true) után nem látszik, akkor valószínûleg el van takarva. Ilyenkor preferáljuk a show()-t. Az alábbi példa még egy fontos, sokszor használt, a Component-bôl örökölt metódust, (setBackground()) is bemutat. Mindig ezt használjuk, amikor egy komponens (pl TextField stb) háttérszínét beállítjuk. Komplementere a setForeground() Ezen utóbbi használatakor arra vigyázzunk, hogy a TextComponent két leszármazottja, TextField
és TextArea (ugyanez igaz a Swing-es JTextField és JTextArea-ra is!) egyszerre csak egy elôtér-színt tud használni, azaz kiírás közben a rájuk meghívott setForeground()az addig kiírtak színét is megváltoztatja. Ha többszínû szöveget szeretnénk kiírni, akkor csak Swing komponensekkel boldoulhatunk (pl. JTextPane), AWT-ben maximum drawString()-gel rajzolhatunk valóban különbözô színekkel, ami rendkívül rossz, lassú és csúnya megoldás (több, 1.0-s JDK-t használó szabványos IRC applet így oldotta meg a színkódok processzálását) Ha már a drawString()-rôl szó esett, meg kell említeni, hogy a Graphics osztály setColor() metódusa az ezután rajzolt pontok/síkidomok színét határozza meg, emiatt koncepcionális különbség van közte és a setForeground()között (hisz egy, az ablakozó rendszer által biztosított, natív peer komponens - ilyen minden AWT widget - gombok, legördülô listák stb - paint()-jét hiába is override-olnánk, az
a komponens megjelenését nem befolyásolná, azaz a paint(), ha végre is hajtódik, hatástalan. Ez természetesen nem igaz a Frame-re, Panel-re és Canvas-ra.) import java.awt*; class FramePelda { public static void main (String[] s) { Frame f = new Frame("Simple Frame"); f.setBackground(Colorblack); f.setSize(300,300); f.show(); } // main } // class A képernyôkimenete pedig ez: 20 A példában láttuk, hogy egy bármilyen AWT/Swing komponens használatához azt mindenképp példányosítani kell. A Frame-re ezen felül a két, fent tárgyalt metódust is meg kell hívni, hogy ne csak a fejléce látszódjon, ill. látszódjék egyáltalán Következô példánk bemutatja, hogyan írhatunk egy Frame-re, ill. egy Panel-re Ahogy már többször említettem, egy AWT konténer felületére írhatunk (natív komponensére nem, azaz az AWT Button osztályát ne is próbáljuk subclass-olni és a gyerekben a paint()-bôl rajzolni - ez csak konténerekre, ill. Canvas-ra
mûködik), ha azt subclass-oljuk és a public void paint(Graphics) metódusát override-oljuk. Nagyon fontos megértenünk, hogy amennyiben egy konténerbe rajzolunk ilyen override-olt paint()bôl, akkor, amennyiben a konténerbe raktunk szabvány GUI komponenseket is, azok menthetetlenül elfedik a paint()-ból kirajzoltakat. Ha nem akarjuk, hogy ez bekövetkezzék, használjunk egy szubpanelt vagy szubCanvas-t, és rajzoljunk arra Egy példa: import java.awt*; class PaintetElfedikAButtonok extends Frame { public static void main (String[] s) { new PaintetElfedikAButtonok(); } // main PaintetElfedikAButtonok() { setLayout(new FlowLayout()); add(new Button("1")); add(new Button("2")); add(new Button("3")); setSize(300,300); show(); } // constructor public void paint(Graphics g) { g.drawString("Probaljuk atmeretezni az ablakot, ha nem fednek el a gombok ezt!",50,50); } // paint } // class A képernyôn a következôt látjuk (újraméretezés
után): 21 Megj.: mivel a Swing-ben nincs a Canvas-nak megfelelôje, rajzolási célokra is a JPanel-t ajánlja a Sun. Hasonlóan, AWT-ben is csereszabatos a két komponens (azzal a kis különbséggel, hogy Canvas közvetlen leszármazottja Component-nek, ellentétben a Container-leszármazott Panel-tôl, azaz nincsenek add(Component) metódusai - GUI widgeteket nem rakhatunk bele, se layout managert) - érdemesebb ezért AWT-ben is Panel-t használni szimpla rajzolási célokra (majd látjuk, ezen mit is értek). A layout manager-ek Mint már hangsúlyoztam, egy konténernek mindig van egy ún. layout manager-e, mely segítségével dinamikusan, de elôreláthatóan és nagyon kevés, átlátható szabály szerint határozza meg a benne levô widget-ek helyét és méretét. Egy konténerhez bármilyen layout manager-t hozzárendelhetünk a Containerben definiált setLayout() metódussal, és abba a szintén a Container-ben definiált, azaz a konkrét konténereink által onnan
örökölt add() metódussal helyezhetünk új komponenseket. Ezeket a komponenseket minden esetben az installált layout manager fogja kontrollálni, azaz áthelyezni és a méretüket a konténer éppen aktuális méretétôl függôen kiszámítani. A FlowLayout Nagyon egyszerû layout manager, ugyanis az általa kontrollált widgeteket egymás mellé rakja, és ha az adott sorba már nem férnek ki, az túlnyúló komponenseket letördeli a következô sorba. Lássuk erre a következô példát: import java.awt*; class FlowPelda { public static void main (String[] s) { Frame f = new Frame(); f.setLayout(new FlowLayout()); f.add(new Button("1")); f.add(new Button("2")); f.add(new Button("3")); f.add(new Button("4")); f.add(new Button("5")); f.add(new Button("6")); f.setSize(300,300); f.show(); } // main } // class Ennek a képernyôkimenete alapállapotban: 22 Ha vízszintesen minimalizáljuk az ablakot, a layout
manager dinamikusan megváltoztatja a gombok kiosztását, amely most a következô lesz: Végezetül, figyeljük meg, hogy a FlowLayout manager sosem nyúl az általa kontrollált widgetek méretéhez, még akkor sem, ha azok nem férnek ki az adott konténerbe (erre nagyon vigyázzunk fejlesztés közben - ha olyan problémáink vannak, hogy az utolsónak feltett GUI widgetek nem látszanak, a hiba több, mint valószínû, hogy a FlowLayout-ban van): A GridLayout Nagyon egyszerû layout manager, amely sorokba és oszlopokba rendezi az általa managelt komponenseket. A sorok és oszlopok számát konstruktorának kell átadni (ezektôl - amennyiben késôbb, az add()olás során, az elôzôleg megadott sor/oszlopszámtól eltérnénk - a rendszer bizonyos szabályok szerint eltérhet, ui. minden komponenst ki fog rakni a képernyôre, még akkor is, ha az eredeti felbontásba az nem fért bele). A következô layout manager tágyalásakor látunk majd arra példát, hogyan kell
használni A BorderLayout 23 Másik alap layout manager. Elsô látásra nem tûnik túlságosan hatékonynak, hiszen csak öt komponens elhelyezését teszi lehetôvé - viszont ha számításba vesszük azt is, hogy ezek a komponensek beágyazható konténerek is lehetnek (konkrétan: Panel, ellentétben a Frame-mel, amely top-level, nem beágyazható komponens), akkor máris jobb a helyzet. A managernek négy égtája, illetve egy maradék, Center területe van, és minden egyes égtájba 1-1 komponenst képes kirajzolni. Ez a manager, ellentétben az elôzôleg tárgyalt FlowLayout managerrel, átméretezi a komponenseket, amennyiben a konténer mérete megváltozik. A konténerbe a komponenseinket az add() metódus kétparaméteres változatával kell beraknunk - ha ezt elfelednénk, akkor a komponens a Center területre kerül. Ezen felül, ha egy égtájra több komponenst is beraknánk, mindig csak az utolsó fog látszódni. Ez nagyon gyakori konfúzióforrás kezdô
programozók körében, mert pl ha az elôzô programunkhoz hasonlót akarnak írni, viszont kifelejtik az adott konténer (itt: Frame) default layout managerét (ami Frame esetében BorderLayout - a Panel viszont FlowLayout managert használ) megváltoztatni FlowLayout-ra (lásd fent a f.setLayout(new FlowLayout()); utasítást), akkor meglepôdve tapasztalják, hogy az utolsóként felvett gomb az egész Frame-et betölti, és csak az látszik (próbáljuk ki!): A következô program mind az öt égtájra betesz egy-egy, az adott égtájnak megfelelô feliratú gombot: import java.awt*; class BorderPelda1 { public static void main (String[] s) { Frame f = new Frame(); f.add(new Button("South"),BorderLayoutSOUTH); f.add(new Button("North"),BorderLayoutNORTH); f.add(new Button("East"),BorderLayoutEAST); f.add(new Button("West"),BorderLayoutWEST); f.add(new Button("Center"),BorderLayoutCENTER); f.setSize(300,300); f.show(); } // main }
// class Képernyôkimenet: Megj.: nem kötelezô a BorderLayout konstansait használnunk - használhatunk stringeket is helyettük Ezek elsô betûje nagy, a többi kicsi (bizony, a Java-ban a konstansok esetében néha találkozunk névkonvenció-inkonzisztenciákkal - másik közismert, az ajánlással homlokegyenest eltérô példa a Color osztály kisbetûs színkonstansai) - lásd a fenti példa elsô add-ja a következô utasításra cserélendô: f.add(new Button("South"),"South"); 24 Az elôbb már említettem, ne tévesszen meg bennünket az, hogy a BorderLayout csak öt komponenst tud megjeleníteni. Ha egy vagy több komponens helyére szubpaneleket rakunk, rendkívül komplex GUI felületeket is létrehozhatunk. Egy kalkulátor - GUI például nagyon egyszerûen megkreálható egy, a fôFrame North részébe rakott TextField-del, és a Center-be rakott szubpanellel, amin egy 4*4-es GridLayout manager átal kontrollált gombokat rakunk. Ez a
példa jó arra is, hogy megmutassa, ilyen sok gomb felrakását érdemes automatizálni, hogy ne kelljen 16-szor majdnem ugyanazt leírni (hát még amikor az eseménykezelés is bejön a képbe, és minden egyes gombhoz külön-külön kell majd eseménykezelôket rendelni! Akkor már tényleg jobban járunk, ha a gombok kreálását, felrakását és azokhoz eseménykezelô rendelését algoritmizáljuk.): import java.awt*; class BorderPelda2 { public static void main (String[] s) { Frame f = new Frame(); f.add(new TextField("0"),BorderLayoutNORTH); Panel p = new Panel(); p.setLayout(new GridLayout(4,4)); for (int i = 0; i <= 9; i++) gombAdd(p,String.valueOf((char)(0 + i))); gombAdd(p, "-"); gombAdd(p, "+"); gombAdd(p, "/"); gombAdd(p, "*"); gombAdd(p, "CLEAR"); gombAdd(p, "="); f.add(p,BorderLayoutCENTER); f.setSize(300,300); f.show(); } // main static void gombAdd(Container cont, String s) { cont.add(new
Button(s)); } } // class Vegyük észre, hogy mivel egy BorderLayout-égtájnak nem adhatunk meg egynél több widget-et, hogy azokat managelje, azokat egy külön példányosított Panel-re raktuk, amelynek layout managerét külön beállítottuk, még mielôtt elkezdtük volna ráadogatni a widget-jeinket. Képernyôkimenet: 25 A ma már – elavultsága és a Swing tabbed pane komponense miatt - ritkán használt CardLayout-tal nem foglalkozunk. A GridBagLayout-tal, ill a Swing új layout managereivel a tanfolyamon kiosztandó anyag foglalkozik. Eseménykezelés Megint egy sarkalatos, ugyanakkor az átlag Java-könyvek által nem igazán érthetôen magyarázott témakör (az ELTE könyve csak referencia-szinten tágyalja). A következôkben elavultsága miatt nem foglalkozunk a régi, 1.0-s eseménykezeléssel - nem érdemes megtanulnunk, még akkor sem, ha egyes szakírók, mivel lusták régi mûveiket átírni (vagy esetleg nem is értenek hozzá - ez sajnos általános
a Javas irodalomban), még mindig propagálják használatukat. Az eseménykezelés lényege, hogy widget-jeinkben, illetve konténereinkben vannak különféle add<Valami>Listener() metódusok. Ezek egy részét (az ún alacsonyszintû események kezelését végzô osztályokat regisztráló metódusokat - ezek a metódusok minden Component-leszármazottra alkalmazhatóak, mert minden egyes komponensre értelmes az, hogy azon egérklikkelést, - ha van fókusza billentyûlenyomást észleljünk) a Component osztályból öröklik (a három legfontosabb ilyen metódus az addKeyListener(), az addMouseListener() és az addMouseMotionListener()), a másik részét pedig önmaguk definiálják (ugyanis ezekrôl a magasszintû, pl. listaelem-kiválasztáskor generálódó eseményekrôl csak egy adott, az adott esemény kiváltására egyáltalán képes widget-típus esetében értelmes beszélni). Ezen metódusokat, mint már mondtam, azokra a widgetekre, illetve konténerekre kell
(lehet) meghívni, amelyekre figyelni akarunk, azaz az amelyeken történô eseményeket mindenképp el szeretnénk kapni. Ilyen lehet pl egy gomb megnyomása, egy listaelem kiválasztása vagy egy Checkbox kipipálása A metódusok paramétere pedig egy olyan osztály példánya kell legyen (ez azt jelenti, hogy az eseménykezelés csak példányok szintén mûködik, az eseményt kezelô, azaz az adott esemény interfészét implementáló osztálynak mindenképp legyen példánya!), ami a metódushoz tartozó interfészt implementálja. Ez így elsô hallásra elég zavaros lehet, nézzünk is kapásból egy-két példát Ha megnézzük a java.awtButton gomb-widget API doksiját, láthatjuk, hogy - a Component-bôl örökölt három alacsonyszintû, az eseménykezeléssel kapcsolatos metódusa mellett - van egy addActionListener() metódusa is, melynek paramétere egy ActionListener típusú objektum referenciája. Ha ennek az addActionListener() metódusnak átadunk egy, az
ActionListener interfészt implementáló, azaz a benne deklarált actionPerformed()-et kifejtô osztály paraméterét, akkor ez az actionPerformed() meghívódik akkor, ha a gombra klikkelünk. Erre a témára nézzünk most pár program-változatot Tegyük 26 fel, akarunk írni egy nagyon egyszerû programot, ami egy Frame-et nyit, arra kirak egy gombot, és amikor azt megnyomjuk, akkor a frame háttérszínét feketére változtatja. Sajnos, nem lehet ezt olyan egyszerûen megírni, mint ahogy elsô, a Frame-eket bemutató programunkat, ugyanis ott egyetlen saját osztályunknak sem volt példánya, mindent a statikus main()-bôl intéztünk. Most, sajnos, valamilyen osztálynak nem ússzuk meg a példányosítását, ugyanis, mint mondtam, az addActionListener() metódusnak egy, az ActionListener interfészt implementáló objektum referenciáját kell átadnunk. (Hogy más add<Valami>Listener() metódusok esetében milyen interfészt kell implementálni? 1, Nézzük
meg az adott widget API doksijában (ill. a Component-ében), milyen interfészt implementáló objektumreferenciát vár a metódus; 2, általánosságban elmondható, hogy egy add<Valami>Listener() metódus <Valami>Listener típusú interfészt implementáló objektumreferenciát vár, azaz az API böngészése nélkül is megmondhatjuk, hogy a Component addMouseMotionListener()-e MouseMotionListener-t implementáló osztály példányának referenciáját várja.) Mivel az eseményt saját magunk által létrehozott osztályban (helyesebben: anak példányában!) kell lekezelni, így mindenképpen kell majd példányosítanunk. Az ActionListener Elsô programunk használjon egy külön, az ActionListener interfészt implementáló osztályt. Ekkor a b.addActionListener(new GombEsemenyKezelo());utasítás a következôkép dekódolható: példányosítsd a GombEsemenyKezelo osztályt, és a példány referenciáját add át a gombunk addActionListener metódusának.
Természetesen ez a GombEsemenyKezelo -példány nem csak GombEsemenyKezelo, hanem ActionListener típusú is egyben, hiszen azt az interfészt implementálja. Az addActionListener() pontosan egy ilyen ActionListener típusú referenciát vár - és mivel egy szülôosztálybeli objektum helyett használhatunk egy gyerekosztálybeli objektumot (gondoljunk arra, mit mondtam a típuskompatibilitás tárgyaásakor!), elfogadja az általunk átadott referenciát. import java.awt*; import java.awtevent*; class EventPelda1 { static Frame f; public static void main (String[] s) { f = new Frame("Simple Frame"); f.setLayout(new FlowLayout()); Button b=new Button("Fekete hatter"); f.add(b); b.addActionListener(new GombEsemenyKezelo()); f.setSize(300,300); f.show(); } // main } // class class GombEsemenyKezelo implements ActionListener { public void actionPerformed(ActionEvent e) { EventPelda1.fsetBackground(Colorblack); } // actionPerformed } // class Vegyük észre, hogy
fô, EventPelda1 osztályunk a megnyitott Frame referenciáját globális, ráadásul statikus változóként tárolja. Erre azért volt szükség, mert ezt a referenciát nem csak hogy egy másik metódusból (ami már magában indukálja a globális változók használatát!), hanem egyenesen egy másik osztály példányából szeretnék elérni. Mivel egyrészt EventPelda1 osztályunknak - a kényelem jegyében - nincsen példánya, másrészt, ha úgy döntenénk, hogy azt mégis példányosítjuk, és ennek a példánynak a referenciáját átpasszoljuk a GombEsemenyKezelo szintúgy létrehozandó konstruktorának, rengeteg plusz programsort kellene leírni. Most próbáljuk meg ezt úgy átírni, hogy ne kelljen két osztályt használnunk, hanem mindent egyetlen osztályba próbáljunk besüríteni. Mivel ekkor fôosztályunknak implementálnia kell ActionListener -t 27 és annak actionPerformed() metódusát, a metódust és kódját egyszerûen átmásoljuk az elôzô,
EventPelda1 osztályba, valamint az osztálydeklaráció fejlécében jelöljük, hogy implementáljuk ActionListener-t. No igen, de mi lesz az addActionListener()argumentumával? Mit adjunk át neki? Természetesen egy olyan osztály példányát, ami ActionListener-t implementálja. Milyen ilyen osztályok vannak? Egyedül csak a saját EventPelda1 osztályunk. Ennek van példánya akkor, amikor a main() elindul? Nincs. Mit kell tehát csinálnunk? Példányosítani EventPelda1 -et, és ennek a példánynak a referenciáját átpasszolni addActionListener()-nek. Ezt most két példán is megmutatom - az elsôben egy névtelen példányt hozunk létre fô-osztályunkból, és azt passzoljuk át addActionListener()-nek, a másodikban pedig minden eddigi kódot (a Frame példányosítását stb) az osztály újonnan létrehozott konstruktorába másolunk, és az ottani this-t adjuk át addActionListener()-nek: import java.awt*; import java.awtevent*; class EventPelda2 implements
ActionListener { static Frame f; public static void main (String[] s) { f = new Frame(); f.setLayout(new FlowLayout()); Button b=new Button("Fekete hatter"); f.add(b); b.addActionListener(new EventPelda2()); f.setSize(300,300); f.show(); } // main public void actionPerformed(ActionEvent e) { f.setBackground(Colorblack); } // actionPerformed } // class A konstruktoros/this-es változat pedig: import java.awt*; import java.awtevent*; class EventPelda3 implements ActionListener { Frame f; EventPelda3() { f = new Frame("Simple Frame"); f.setLayout(new FlowLayout()); Button b=new Button("Fekete hatter"); f.add(b); b.addActionListener(this); f.setSize(300,300); f.show(); } public static void main (String[] s) { new EventPelda3() } // main public void actionPerformed(ActionEvent e) { f.setBackground(Colorblack); } // actionPerformed } // class Vegyük észre ebben a következôket: 1. f-et nem deklaráltuk itt már statikusnak Ennek oka egyszerû: elôzô
programunkban még a main()bôl értük el, ezért volt kötelezô azt statikusnak deklarálni, itt viszont, mivel (az actionPerformed mellett) kizárólag az osztályunk konstruktorából, tehát egy példánymetódusból, érjük el, minden ilyen elérés példánymetódusból történik 28 2. mivel most ugyanabban az osztályban van a konkrét eseménykezelô metódus, actionPerformed, abból a f globális változót közvetlenül elérhetjük. Persze az egy kezdônél (különösen, akinek Pascal-os múltja van) gyakori hiba, hogy metódusra nézve lokális változót próbál más metódusból elérni. Mindenképp érdemes ezt a példát továbbragozni, mert még nem magyaráztam el, mi az a rejtélyes ActionEvent példány, amit az actionPerformed -nek átpasszol a rendszer. Egy gyors pillantás az API doksiba - máris látjuk, hogy bizony ebben is van egy-két olyan metódus, amit használni lehetne. De mire? Gondoljunk bele egy kicsit: ha lenne pl. tíz gombunk, amelyek
megnyomására különbözôképpen szeretnénk reagálni (pl. a fenti GUI-kalkulátor esetében 16 ilyen gomb van!), ahhoz tíz különbözô osztályt kellene deklarálni és ezeket egy-egy példánnyal példányosítani, hisz csak így tudnánk garantálni, hogy az actionPerformed valóban egyedi, és csak az adott gombra jellemzô, csak azt szolgálja ki. Ekkor, az ilyen OOD tragédia megelôzése érdekében siet segítségünkre a kapott ActionEvent példány, ugyanis ez konkrétan leírja, melyik gomb az adott esemény kiváltója. Ezáltal megtehetjük, hogy egyetlen példányt hozunk létre egyetlen eseménykezelô osztályból (amely, mint az elôbb már láttuk, a saját osztályunk is lehet), és ennek a példánynak a referenciáját adjuk át a addActionListener metódusnak mind a 16 alkalommal, amikor a gombjainkhoz eseménykezelôt rendelünk. Milyen metódusok állnak rendelkezésünkre az ActionEvent-ben? Számunkra a String-et visszaadó getActionCommand(), illetve a
java.utilEventObject-bôl örökölt, Object visszatérésû getSource() a fontos Az elsôt kényelmesebb használni, mert használatához nem kell, hogy eltároljuk azon gombok referenciáját egy-egy globális változóba, amelyekhez eseménykezelôt rendelünk, ui. ez a metódus a gomb feliratát adja vissza (a valóságban kicsit árnyaltabb a kép, ui. - ez különösen többnyelvû alkalmazások esetén fontos! - a setActionCommand() metódus segítségével nyelvfüggetlen String-et rendelhetünk a gombjainkhoz; ugyanezt kell tennünk, amikor ugyanazt az eseménykezelôt akarjuk ugyanolyan feliratú gombokhoz rendelni). A getSource() ezzel ellentétben a gombok referenciáját adja vissza, amit azonnal össze is hasonlíthatunk az eltárolt referenciáinkkal. Lássuk az elôzô kalkulátor-példánkat újraírva úgy, hogy a felsô TextField-be kiírja az éppen lenyomott gomb feliratát! A problémát oldjuk meg többféleképpen is! Elsô verziónk getActionCommand()-ot
használ - ez a feladat ordít az ilyen megoldásért, mert a terebélyes if/else if-szerkezetet megtakarítjuk azzal, hogy a TextField-ünkbe kapásból kiírjuk (setText()) a getActionCommand() által visszaadott stringet. import java.awt*; import java.awtevent*; class EventPelda4 implements ActionListener { static EventPelda4 esemenyKezeloRef; static TextField tf; public static void main (String[] s) { Frame f = new Frame(); f.add(tf = new TextField("0"),BorderLayoutNORTH); Panel p = new Panel(); p.setLayout(new GridLayout(4,4)); esemenyKezeloRef = new EventPelda4(); for (int i = 0; i <= 9; i++) gombAdd(p,String.valueOf((char)(0 + i))); gombAdd(p, "-"); gombAdd(p, "+"); gombAdd(p, "/"); gombAdd(p, "*"); gombAdd(p, "CLEAR"); gombAdd(p, "="); f.add(p,BorderLayoutCENTER); f.setSize(300,300); f.show(); } // main 29 static void gombAdd(Container cont, String s) { Button b; cont.add(b=new Button(s));
b.addActionListener(esemenyKezeloRef); } public void actionPerformed(ActionEvent e) { tf.setText(egetActionCommand()); } // actionPerformed } // class Vegyük észre, hogy gombAdd metódusonkat bôvítettük az b.addActionListener( esemenyKezeloRef); utasítással, ahol esemenyKezeloRef globális vátozó - a saját osztályunknak egy példánya, amelyet a main() elején inicialiálunk. Ezzel azt kerültük el, hogy gombAdd metódusunkban esetleg 16-szor példányosítsuk osztályunkat (vegyük észre, az statikus, mivel a main()-bôl hívjuk példány nélkül, így impicit this-e sincs). Amennyiben mégis ezt tettük volna, semmi gond nem lenne (a plusz memória- és processzoridô-terhelésen kívül, amit a 15 felesleges objektum adminisztrálása jelent). Ugyanezt a példát megmutatom a getSource() használatával is, ugyanis ezt a metódust sokszor leszünk kénytelenek használni: import java.awt*; import java.util*; // Vector-hoz kell! import java.awtevent*; class EventPelda5
implements ActionListener { static EventPelda5 esemenyKezeloRef; static TextField tf; static Vector buttonVector = new Vector(); public static void main (String[] s) { Frame f = new Frame(); f.add(tf = new TextField("0"),BorderLayoutNORTH); Panel p = new Panel(); p.setLayout(new GridLayout(4,4)); esemenyKezeloRef = new EventPelda5(); for (int i = 0; i <= 9; i++) gombAdd(p,String.valueOf((char)(0 + i))); gombAdd(p, "-"); gombAdd(p, "+"); gombAdd(p, "/"); gombAdd(p, "*"); gombAdd(p, "CLEAR"); gombAdd(p, "="); f.add(p,BorderLayoutCENTER); f.setSize(300,300); f.show(); } // main static void gombAdd(Container cont, String s) { Button b; cont.add(b=new Button(s)); b.addActionListener(esemenyKezeloRef); buttonVector.add(b); } public void actionPerformed(ActionEvent e) { if (e.getSource() == buttonVectorelementAt(0)) tfsetText("0"); else if (e.getSource() == buttonVectorelementAt(1)) tfsetText("1");
else if (e.getSource() == buttonVectorelementAt(2)) tfsetText("2"); } // actionPerformed } // class Itt a gombok referenciáit egy Vector-ba tároltam el. Azért választottam ezt, és nem egy szimpla Button tömböt, hogy megtakarítsam az indexváltozó használatát és update-lését a gombAdd() metódusban. Az actionPerformed() metódust nem dolgoztam ki teljesen, mivel így is érti mindenki, hogy mûködik - 30 egyszerûen memória-referenciákat komparál, és a gomb-referenciák egyezése esetén állítja be a TextFieldet. Megj.: a egetSource() == buttonVectorelementAt(int) két, statikusan Object típusú, 32-bites referenciát hasonlít egymáshoz. Ha az egyik referencia (statikusan) Button vagy bármilyen másik Object leszármazott-típusú lenne, akkor sem kapnánk fordítási idejû hibát. Emiatt nem is kellett a fenti példában az Object referenciák Button-ra való típuskonverziójával piszmognunk. Viszont egymással rokonságban nem levô
osztályok esetében maguknak a (szigorúan típusos!) referenciáknak a komparálása is fordítási idejû hibát eredményez. Példa ilyenre: class A {} class B {} {A a = new A(); B b = new B(); if (a == b) } Természetesen ha valamelyik osztály kiterjeszti a másikat (azaz rokonsági relációba lépnek), nem ad hibát a fordító, hiszen az egyenlôségvizsgálati relációban szereplô egyik referencia statikus típusa szülôje a másiknak, azaz a típuskompatibilitás biztosított, és futásidôben valóban egymással kompatibilis objektumok referenciáinak egyezôségét vizsgáljuk. Egymással alapvetôen nem kompatibilis objektumokra eleve értelmetlenség lenne biztosítani a referenciák egyenlôségének vizsgálatát. A MouseListener és a MouseMotionListener Szintén nagyon gyakran használt listener interfészek. Minden, az egérkezeléssel kapcsolatos callback metódust tartalmaznak: az egérgombot lenyomták, felengedték, lenyomták és felengedték, az egy
komponens területére bement, onnan kijött, ill. elmozdult [*] A következô példa egy, a Web oldalakon nagyon elterjedt menüzôt valósít meg: azt a menüpontot, amelyik felett az egérkurzor tartózkodik, bekeretezi. (Késôbb, amikor az applet-ekkel is megismerkedünk, érdemes lesz a példát úgy, generikusan átírni, hogy a HTML fájlból a getParameter()-rel beolvassa egyrészt a stringek számát, majd a stringeket, és végül esetleg azokat a linkeket, amiket ezek a stringek showDocument()-tel a böngészô egy másik frame-ében megmutatnak. Ha pl egy link-menüzô programot akarunk írni, úgy, hogy a végfelhasználónak ne kelljen egyáltalán foglalkozni a stringek és linkek Java forrásba való bedrótozásával, hanem azokat egyszerûen csak a HTML file <PARAM> tag-jeibe írja bele ekkor ráadásul elég csak a .class lefordított állományt közkinccsé tennünk, nem kell a forrást is kiadnunk a kezeink közül.) Nagyon fontos megfigyelnünk, hogy az
erôforrásokkal csínján bánik, ugyanis nem ellenôrzi állandóan az egérkurzor pillanantnyi koordinátáit, hanem a MouseListener mouseEntered() metódusát implementálja csak nem-üres törzzsel. Ez a metódus pedig csak akkor hívódik meg egy adott widgetre, ha az egérkurzor éppen belépett annak területére. Amikor bent mozog, ill kilép, nem hívódik meg még egyszer - ez csak akkor következik be, amikor újra belép. Ebbôl is látható, mekkora gépidônyereségünk van azzal, hogy a MouseListener ilyen metódusokat is deklarál Ezen felül még azt is érdemes megfigyelni, hogy a String-jeinket nem drawString-gel rajzoltuk ki, mert akkor tényleg nem használhattunk volna ilyen szofisztikált, a gépet gyakorlatilag egyáltalán nem leterhelô eseménykezelést, hanem mindent a kirajzolt n String-et tartalmazó konténerhez adott MouseMotionListener példány mouseMoved() metódusában kellett volna intézni, ami ugyebár mindig egy mouseMoved() metódushívást és
egy komplex, az átadott MouseEvent példány getX()-ét is meghívó kifejezés kiértékelését jelenti minden egyes egérkurzor-mozdításkor. Egyébként ezért is választották szét a két interfészt az 1.1-es verzió tervezésekor A nyelv designerei rájöttek, hogy a programozók nem minden esetben akarnak az egérkurzor minden egyes rezdülésére reagálni. Amennyiben mégiscsak egyetlen interfészt bocsátottak volna rendelkezésünkre, akkor a rendszer mindig meghívná az interfészt implementáló osztály mouseMoved() metódusát - még akkor is, ha arra nincs szükségünk, azaz üres törzzsel implementáltuk. Ezért vezettek be két külön listenert - az egyik inkább magasabb, komponensszintû események kezelésében segít (komponens területére belépett a kurzor, onnan kilépett, valamelyik egérgombot megnyomták / azt elengedték, ill. megnyomták majd elengedték (clicked) 31 azt), míg a másik metódusai egyszerû egérkurzor-mozgatásra (lenyomott
egérgombokkal is: mouseDragged) hívódnak meg. Ne feledjük, mindkét esetben metódusaink egy MouseEvent példányt kapnak, azaz nincs MouseMotionEvent! Menüzô programunk a következô: import java.awt*; import java.awtevent*; class Menuzo extends Frame implements MouseListener { static final int NUMBER OF ELEMENTS = 9; Label[] labelarray = new Label[NUMBER OF ELEMENTS]; String[] labels = {"1","2","3","1","2","3","1","2","3"}; int startX; public static void main(String[] args) { new Menuzo(); } Menuzo() { setLayout(null); for (int i=0; i<NUMBER OF ELEMENTS; i++) { add(labelarray[i] = new Label(labels[i])); labelarray[i].setBounds(20,(i+2)*20+5,40,10); // applet-ben i+2-t irjuk at i-re! labelarray[i].addMouseListener(this); } // for setSize(300,300); show(); } // constr public void paint(Graphics g) { g.drawRect(5,startX,160,20); } public public public public void void void void
mouseClicked(MouseEvent e) {} mouseExited(MouseEvent e) {} mousePressed(MouseEvent e) {} mouseReleased (MouseEvent e) {} public void mouseEntered(MouseEvent e) { for (int i=0; i<NUMBER OF ELEMENTS; i++) if (e.getSource() == labelarray[i]) { startX=i*20+40; break; } // applet-ben a +40-et toroljuk! repaint(); } // mouseEntered } // class Mindenképp érdemes azt a programban megfigyelni, hogy a használt java.awtLabel-ek magassága (setBounds() paraméterei) csak 10, míg azok távolsága 20. Ez azt jelenti, hogy mivel natív komponens (mint a Label is) felületére nem rajzolhatunk, a menüpontok között marad 10-10 pixelnyi, natív widget által le nem fedett rész, hogy oda is rajzolhassunk drawRect()-tel a paint()-bôl, amit természetesen - közvetetten - a mouseEntered -bôl hívtunk. Ez természetesen azt is igényli, hogy a használt konténerünk, melybe a Label-eket berakjuk, a könyvtári konténerek leszármazottja legyen, hogy a paint()-et valóban overrideolhassuk.
Ezért származtattam fôosztályunkat a könyvtári Frame-bôl Nagyon fontos, hogy a MouseEvent alacsonyszintû, és nincs az ActionEvent-hez hasonló getActionCommand() metódusa. Ez azt jelenti, csak az EventObject-bôl örökölt getSource()-t használhatjuk arra, hogy eldöntsük, konkrétan melyik widgeten vagy konténeren belül történt az adott esemény. Még egy nagyon szép példáját látjuk ezen két osztály használatának a fejezetet záró példában. Az ItemListener Az AWT-ben több olyan komponens is van, amelyekhez az absztraktabb, tehát a programozó válláról szinte minden terhet levevô eseménykezelô interfészek (azok az interfészek, amelyeket nem a Component implementál, azaz az ActionListener és ItemListener) közül csak ezt az egy interfészt használhatjuk. Ez kicsit illogikus, és ezt a hibát a Swing komponensei kiküszöbölték. Konkrétan a Choice-rôl van szó, amelyhez AWT-ben kizárólag csak ItemListener-t rendelhetünk, ill. a
Checkbox-okról (szintúgy) A Swing JCheckBox -ához (figyeljük meg az immár jó kapitalizációt! Ez egyike a Java illogikus neveinek (instanceof, arraycopy két másik ilyen), az amerikai angol és angol különbségei miatti inkonzisztenciáknak (cancelled egyszer, máskor pedig canceled) vagy betûhibáinak (olyan konstansok, hogy 32 VK SEPARATER)) már rendelhetünk ActionListener-t is. AWT-ben viszont be kell érnünk az ItemListener-rel, amit egy kicsit kényelmetlenebb kezelni, mint az ActionListener-t. A legfontosabb: az ItemEvent getItem()-ével visszakapott referencia egy olyan (Object típusú!) objektumra mutat, amelyben úgy van felülírva a toString(), hogy az az eseményt kiváltott Checkbox feliratát, a Choice adott elemét stb. jelentse le Viszont ezt a referenciát, sajnos, nem lehet még az egyszerû (pl. Checkbox) komponensek esetében sem összehasonlítani az adott komponens eredeti referenciájával - a fordító ezt ugyan elfogadja, de mivel az
getItem() által visszaadott objektum sosem azonos típusú az eredetivel. Még egyszer: az ember azt várná, hogy ez a getSource()-hoz hasonlóan legalább a Checkbox-ra az eredeti példány referenciájával tér vissza, de nem. Ezért, amikor elegendô azt tudnunk, melyik Checkbox váltotta ki a kivételt, valamint tudjuk annak az eredeti referenciáját, használjuk inkább a getSource()-t, mert akkor az egyenlôségvizsgálat valóban igaz eredményt szolgátat, ha az adott widget volt az esemény forrása. Még egy nagyon fontos: se Checkbox-ra, se a Choice String-jeire nem hívhatunk setActionCommand()ot, azaz, amennyiben internacionalizálni szeretnénk programunkat, elég nehéz dolgunk lesz ezen widgetek eseménykezelôinek az egyes nyelvekhez igazításával. Mint ahogy az elôzôekbôl is kiderült, a getItem() által visszaadott objektummal nem sok értelmeset lehet kezdeni, ha a getSource()-hoz hasonlóan akarjuk felhasználni. Ez az objektum viszont, bár típusa
Object, egy az egyben equals-ozható az adott Choice menüpont, ill. Checkbox stb feliratával (az equals()-t úgy override-olták benne, hogy csak a stringeket komparálja, az objektum más adattagjait nem, és ha a stringek megegyeznek, akkor true értékkel tér vissza). Azaz, ilyenkor még csak toString()-et sem kell rájuk hívni a komparálás elôtt. Figyelem! Az EventObject-bôl örökölt getSource() nem adja vissza konkrétan azt az elemet (pl. egy Choice-ban az egyik String-et), amit a felhasználó választott, kizárólag az eseményt kiváltó widget referenciáját! Ez, amennyiben az adott widget nem konténer típusú (például, nem Choice, hanem pl. Checkbox), nem jelent különösebb problémát. Ekkor természetesen ismernünk kell a használt widget-jeink referenciáját (amelyeket tehát nem használhatunk név nélkül). Nem nehéz megérteni, milyen esetben nincsenek további teendôink, ha egy ItemEvent keletkezik, és egy getItem().equals("valamelyik
checbox/choice listaelem felirata") metódushívással eldöntjük, az kitôl is származott, amennyiben azt egy rádiógomb, illetve egy Choice menüpont váltotta ki - ezek ugyanis csak akkor váltanak ki ItemEvent-et, ha ôket választottuk ki (ezért jó, hogy a Swing már a rádiógombokra is engedélyez ActionListener-t). Viszont egy kétállapotú Checkbox akkor is kivált egy ItemEvent-et, amikor inaktivizáljuk. Ezért, mivel nem tudhatjuk, hogy az adott Checkbox most ki- vagy bekapcsolva van, kell használnunk az ItemEvent getStateChange() metódusát. Ennek int a visszatérése, és az ItemEvent osztályban definiált SELECTED, illetve DESELECTED konstanssal érdemes összehasonlítani. A listener használatára a fejezet végén szereplô program remek példa, ugyanis mindent használ, amirôl eddig beszéltem. A példában szereplô rádiógombok között, mivel csak akkor váltanak ki ItemEvent-et, ha éppen ôket nyomták be, és nem egy másik rádiógomb miatt
kerültek nem-kiválasztott állapotba, egyszerû getItem().equals()-szel szelektálunk A példabeli Choice elem-stringjeivel is ugyanezen az alapon komparáltuk a getItem() által visszaadott objektumot. Az egyetlen igazi Checkbox vizsgálatakor viszont, mivel akkor is kiválthat eseményt, amikor inaktivizáljuk, meghívtuk getStateChange()-t is. Végezetül, elnézést ezen listener ilyen hosszú magyarázatáért, de mûködése kicsit illogikus, a Swingben pedig változtattak pár, vele kapcsolatos dolgon, ezért szántam rá ennyi helyet. A leggyakrabban használt AWT komponensek Button 33 Erre már láttunk jópár példát, így nem ismétlem önmagam. ActionListener-t lehet hozzá rendelni (az alacsonyszintû, fizikai listenereken kívül, természetesen). Checkbox (Figyeljünk a kis b-re; a Swing JCheckBox-ában már kijavították ezt a kis hibát!) Errôl a komponensrôl sem kell sokat mondani - az ItemEvent tágyalásánál már elég sokat beszéltem róla, az
alább levô példa pedig szépen bemutatja használatát. CheckboxGroup Mivel az AWT-ben még nincsenek egymást reteszelô rádiógomb - widgetek, így abban az esetben kell használni ezt az osztályt, melynek példányát átpasszoljuk a Checkbox két háromparaméteres konstruktora valamelyikének (a két konstruktor megegyezik, csak az egyikben a 2. paraméter ez a példány, míg a másikban a 3., míg a kezdeti bekapcsoltságot megadó boolean paraméter 3, ill 2 Ez jó példa arra, hogy kell egy osztálykönyvtárat úgy létrehozni, hogy minél kevesebb lexikális tudással kelljen a programozónak rendelkeznie - nem kell megjegyeznie, hogy "3. paraméter lehet csak" stb, mert mindkét paramétersorrendet elfogadja az overload-olt konstruktor. Az elsô paraméter mindkét esetben a rádiógomb String felirata.), amikor Checkbox-ainkat így össze szeretnénk rendelni A Swing-ben már külön osztályt kaptak a rádiógombok: JRadioButton, valamint természetesen
már használhatunk ActionListenert is velük. Mivel az alsó példa az AWT-s rádiógombok kezelését is bemutatja (hogy használjuk a Checkbox-okat a CheckboxGroup példánnyal stb), valamint az eseménykezelésrôl is már beszéltem fent, ill. látunk rá még példát, itt rájuk nem pazarlok több helyet. Choice Legördülô választómenü, így természetesen egyszerre csak egy elemet választhatunk belôle. ItemListener-rel használható csak; érdekessége, hogy nincs JChoice a Swing-ben (helyette van JComboBox). Miután egy Choice példányt létrehoztunk, abba annak add() metódusával adhatunk új stringeket. Használatára késôbb látunk majd példát Canvas Olyan komponens, amelyre rajzolhatunk. Ennyiben közös az AWT konténereivel - abban nem, hogy nem a Container leszármazottja, azaz rá nem rakhatunk más widget-eket. Természetesen a könyvtári java.awtCanvas-t szubklasszolni kell, hogy a paint()-jét override-olhassuk Default mérete 0*0, így amennyiben
olyan layout managerhez adnánk, amely nem erôltetne rá egy default méretet (ilyen erôltetôs manager pl. a GridLayout, ott ui minden komponens azonos nagyságú), mindenképp override-olnunk kell a java.awtComponent-bôl örökölt public Dimension getPreferredSize() metódust Erre látunk majd példát fejezetzáró applikációnkban, mivel ott a Canvas-unkat egy BorderLayout manager manageli, ami nem kényszerít a Canvas-ra egy default, nem-nulla méretet. Az említett példa igen érthetô, ezért itt külön nem mutatom meg a Canvas-t használat közben. Megj.: Swing-ben nincs már a Canvas különválasztva a Panel-tôl, azaz nincs JCanvas, csak JPanel Megj.: Swing-ben már nem csak osztályszint, valamint kötelezôen bôvítéssel együtt járó preferált-méret-megadás lehetséges, hanem példányszintû is. Ez azt jelenti, hogy nem kell leszármaztatnunk olyan osztályokat, melyek preferált méretét be kívánjuk állítani, elég arra egy, a JComponent-ben
definiált void setPreferredSize(Dimension preferredSize)-ot hívnunk. Ez azt jelenti, hogy nem kell leszármaztatnunk Swing osztályokat akkor, amikor csak a preferált méretet akarjuk állítani. 34 Label Szöveges komponens, amely egy String-et tud kirakni, valamint a fizikai esemény-interfészeket implementálni képes. Figyelem, nem csak statikus, mert setText() metódusával feliratát bármikor megváltoztathatjuk, valamint, természetesen, a szöveg színét/hátterét a Component-bôl örökölt setForeground/ setBackground hívásával beállíthatjuk. A PopupMenu tárgyalásakor bemutatott program erre, valamint az eseménykezelésre szintén mutat példát. TextField Egysoros komponens, amely a felhasználótól szöveges inputot képes fogadni. A magasszintû listenerek közül ActionListener-t és TextListener-t képes használni Az ActionListener-t akkor érdemes implementálni, ha a felhasználó ENTER-nyomására akarunk valamit csinálni (ezt a másik
TextComponentleszármazott, TextArea, nem tudja, ugyanis ott az ENTER a következô sorba lép, azaz ott nem is használhatunk ActionListener-t). Persze nem bízhatunk meg teljesen az ActionListener-ben, mert nem garantált, hogy a felhasználó pont egy a TextField-ünkben kiadott ENTER-rel küldi el pl. az ûrlapot, hanem pl. egérkurzorral vagy TAB-bal más komponensre lép Ezért nem árthat a FocusListener használata sem, amely metódusait a rendszer akkor hívja vissza, ha az éppen aktuális widget megváltozik, azaz a fókusz továbblép. Az osztály, a TextComponent-bôl (a TextArea-hoz hasonlóan) örökli a getText() és a setText() metódusokat, amelyek segítségével kérdezhetjük le, ill. állíthatjuk be egy TextField/TextArea tartalmát Ezen felül lehetôség van szövegkijelölésre, cserére stb. TextArea Hasonlít a TextField-re amiatt, hogy mindketten a TextComponent-bôl származnak. Az elôzô pontban már vázoltam a fôbb különbségeket. Még egy
megemlítendô plusz az, hogy itt van append() (kicsit illogikus, miért nem TextComponent-ben definiálták ezt a metódust) - a TextField esetében azt csak emulálhatjuk egy getText-setText párossal: tf.setText(tfgetText() + "ezt fûzzük hozzá az eredeti tartalomhoz."); List Olyan, nem legördülô lista, amelybôl - éppen emiatt! - egyszerre több elemet is kiválaszthatunk. Viszont itt - ellentétben a Choice esetével, ahol természetes volt, hogy a maximum egy kiválasztható String tartalmával tér vissza - a getItem() nem adja vissza a kiválasztott String-ek tömbjét. Ekkor, teljesen kifogyván az ItemEvent használható metódusaiból, a List osztályban található metódusokhoz kell fordulnunk. Ami bennünket ezek közül érdekel, az a String[] getSelectedItems() és az int[] getSelectedIndexes(). Mint a következô megjegyzésbôl látni fogjuk, ezen metódusok neve logikus, ha az ember a Choice-beli megfelelôjükhöz hasonlítja ôket. Biztonságosan
használhatjuk ezen felül a int getSelectedIndex()-et, ill. a String getSelectedItem()-et is, mert az elsô -1-et, a második pedig null-t ad akkor vissza, ha a felhasználó több listaelemet választott ki egyszerre (vigyázat, az 1.2-es API ez utóbbi esetet nem jelzi!). Megj.: az eddig ismertetett osztályokban (Checkbox: boolean getState(); Choice: int getSelectedIndex() és String getSelectedItem()) is van már arra lehetôségünk, hogy ne az ItemEvent paraméterbôl, hanem közvetlenül az objektumhoz visszanyúlva nézzük meg, az adott checkbox ki van-e jelölve, ill. hogy az adott Choice melyik elemére kattintott a felhasználó Ez természetesen azt jelenti, hogy ezen objektumok referenciáit el kell mentenünk és globálissá tennünk - eddig ezért nem is használtuk ôket. Persze eljárhatunk úgy is, amennyiben nem vagyunk arra konkrétan kíváncsiak, hogy melyik widget váltotta ki az eseméyt, hogy a getSource() Object típusú visszatérését lecast-oljuk a
megfelelô osztályra (itt pl. List-re), és arra hívjuk meg pl a getSelectedItems()-t A List esetében is ugyanígy járhatunk el - amennyiben pl. tudjuk, hogy eseménykezelônk csak egy List-et kezel (és mindenféle más 35 típusú widget-et), akkor egy instanceof-os típusellenôrzés után lekasztolva meghívhatjuk a nem globális referenciára is a List lekérdezô metódusait. Természetesen amennyiben pl több List-ünk is van egy eseménykezelôre, akkor tudomásul kell vennünk, hogy az osztálynak példányát sajnos már nem hozhatjuk létre anélkül, hogy eltárolnánk referenciáját. Amennyiben nincs szükségünk a többszörös választás lehetôségére (ez a default, ill. false a konstrutkornak az alábbi példában), akkor használhatunk esetleg ActionListenert-t is, amely akkor lép be a képbe, ha valamelyik tömbelemen kettôt klikkeltünk gyors egymásutánban. Természetesen az actionPerformed()-ben is, mint bárhol (persze az AWT aszinkron voltának
figyelembe vételével!), a fent említett metódusokat meghívhatjuk, hogy ne csak az ActionEvent/getActionCommand-dal átadott aktuális String-et, hanem az összes, addig kijelöltet is megkaphassuk. Figyelem! A Swing JList komponensét lényegesen nehezebb kezelni! Az alábbi példa mindkét interfész használatát megengedi. Legalább ezt az egy programot érdemes téylegesen lefuttatni, hogy lássuk, mikor, melyik metódus mivel tér vissza, valamint az itemStateChanged(), ill. az actionPerformed() mikor hívódik meg import java.awt*; import java.awtevent*; class ListPelda extends Frame implements ItemListener, ActionListener { List l; ListPelda () { // constructor l = new List(3,true); // megengedjuk a tobbszoros valasztast l.add("1"); l.add("2"); l.add("3"); l.add("4"); l.add("5"); l.add("6"); l.addItemListener(this); // List - ItemListener l.addActionListener(this); // ActionListenert is bemutatjuk add(l); pack();
show(); } public void itemStateChanged(ItemEvent e) { String s; String[] sArr; if ((s=l.getSelectedItem()) == null) Systemoutprintln(" 0 vagy 1-nel tobb van kivalasztva!"); sArr=l.getSelectedItems(); for(int i=0; i<sArr.length;i++) Systemoutprintln("Az " + i +" kivalasztott elem: "+sArr[i]); } public void actionPerformed(ActionEvent e) { System.outprintln(egetActionCommand()); } public static void main(String a[]) { new ListPelda(); } // main } // class Menu Aki a különbözô ablakos-form designer-es IDE-k menüszerkesztôihez szokott, az bizony kicsit megrendül, amikor látja, hogy alapszintû menükezeléshez is három osztály, valamint a Frame kettô (abból egyet, setMenubar()-t, mindig használnunk is kell) metódusának ismerete és használata szükséges. Mint azonban látni fogjuk, ez a viszonylagos bonyolultság nem megy az érthetôség rovására (az osztályok nevei valóban logikusak, ha magyarul megpróbálunk rájuk asszociálni,
akkor mindig beugrik, éppen melyiket kell használnunk), és rá valóban szükség volt. 36 A MenuBar az, amit egy Frame fejlécében látszik - ha magyarról akarnánk visszaemlékezni rá, akkor a mensüsor szót hajtogassuk magunk elôtt. Ebbôl csak egy példányt kell csinálni, és azt a Frame-hez nem a megszokott add()-del kell (sajnos) hozzáadni, hanem a setMenubar()-ral. Amikor megcsináltuk a MenuBar példányunkat és referenciáját eltároltuk valahova, ahhoz elkezdhetünk Menu objektumokat adogatni. Ez az osztály a fômenüket testesíti meg A hallgatóimnak mindig azt ajánlom, hogy nevét, ill. hogy hova kell rakni, ez alapján jegyezzék meg - egy menüsorba kerülnek a fômenük. Ahány fômenüre szükségünk van, annyi példányát hozzunk létre ezen Menu osztálynak, konstruktorának átadva a fômenünek a menüsorban látható feliratát, ill. ezeket a MenuBar példányunkhoz add()-olva. Természetesen ezen Menu objektumok referenciáját tároljuk
majd el legalábbis egy újra felhaszált referenciába, hogy azokhoz aztán a menüpontokat (MenuItem, lásd: köv. bekezdés) feladogathassuk. A következô program kirak egy Fajl, egy Szerkesztes és egy Segitseg fômenüt a Frame-re: import java.awt*; class MenuPelda1 extends Frame { MenuPelda1() { // constructor MenuBar mb = new MenuBar(); mb.add(new Menu("Fajl")); mb.add(new Menu("Szerkesztes")); Menu helpMenu; mb.add(helpMenu = new Menu("Segitseg")); mb.setHelpMenu(helpMenu); setMenuBar(mb); setSize(200,200); show(); } public static void main(String a[]) { new MenuPelda1(); } // main } // class Képernyôkimenete: Programunkban csak a harmadikként felrakott Segitseg fômenü referenciáját tároltuk el, hogy késôbb a MenuBar.setHelpMenu(Menu) metódust meghíva azt - platformfüggôen, Win95/NT alatt ui nem mûködik - kihúzza jobb oldalra. Persze egy igazi programban, ahol a fômenüjeinkhez konrét menüpontokat is adunk, a referenciák
akár idôleges tárolását nem úszhatjuk meg. Amikor a menürendszerünk ezen második szintje megvan, azaz a menüsorba felraktuk a fômenüket, következik ezen fômenük megtöltése tartalommal, azaz a konkrét menüpontok (MenuItem) felvitele. Ez a Menu.add(MenuItem) metódussal történik, mely paraméterekét egy MenuItem objektumot kell 37 átpasszolnunk. Ennek az objektumnak a konstruálásakor kell a konstruktorának megadnunk azt a String-et, amit használni akarunk majd a menüpont neveként. A Menu osztályban nem csak ez az add() metódus van jelen, hanem az addSeparator() is, amivel vízszintes, természetesen nem kattintható menüpont-elválasztót is rakhatunk az adott fômenübe. Minden, kezelni kívánt MenuItem-hez rendeljünk ActionListenert: az ActionEvent getActionCommand() metódusával lekérdezhetjük, melyik menüpontra kattintott a felhasználó - ekkor nem kell a MenuItem-ek referenciáit eltárolni. Még akkor sem kell ezen referenciákat
elmentenünk, amennyiben internacionalizálási törekvéseink vannak vagy egyes eldugott menüpontok nevei ütné egymást, hisz használhatjuk a már megismert setActionCommand() metódust, így soha nem kell a getSource()-hoz nyúlnunk. A következô program ezt demonstrálja: import java.awt*; import java.awtevent*; class MenuPelda2 extends Frame implements ActionListener { MenuPelda2() { // constructor MenuBar mb = new MenuBar(); Menu tempMenuReference; MenuItem tempMenuItemReference; mb.add(tempMenuReference = new Menu("Fajl")); tempMenuReference.add(tempMenuItemReference = new tempMenuItemReference.addActionListener(this); tempMenuReference.addSeparator(); tempMenuReference.add(tempMenuItemReference = new tempMenuItemReference.addActionListener(this); tempMenuReference.add(tempMenuItemReference = new tempMenuItemReference.addActionListener(this); tempMenuReference.addSeparator(); tempMenuReference.add(tempMenuItemReference = new
tempMenuItemReference.addActionListener(this); MenuItem("New")); MenuItem("Load")); MenuItem("Save")); MenuItem("Quit")); mb.add(new Menu("Szerkesztes")); Menu helpMenu; mb.add(helpMenu = new Menu("Segitseg")); mb.setHelpMenu(helpMenu); setMenuBar(mb); setSize(200,200); show(); } public void actionPerformed(ActionEvent e) { System.outprintln("A " + egetActionCommand() + " menupontot valasztottuk"); } public static void main(String a[]) { new MenuPelda2(); } // main } // class 38 Amit még érdemes megemlíteni, az a CheckboxMenuItem, amellyel pipás menüpontokat is rakhatunk fômenüjeinkbe. Ilyenkor természetesen az ItemListener-rel kell annak megváltozását figyelnünk, ActionListener nem használható (azaz használható, mert az addActionListener-t a MenuItem-bôl örökli, de az actionPerformed() nem hívódik meg, ha CheckboxMenuItem-re kattintunk - érdemes kipróbálni!). Az is nagyon
fontos, hogy mivel a Menu a MenuItem leszármazottja, mindazon helyen állhat, ahol MenuItem. Ez teszi lehetôvé az almenük használatát, melyre egy példa (ami egyben CheckboxMenuItem-et is bemutatja): import java.awt*; import java.awtevent*; class MenuPelda3 extends Frame implements ActionListener, ItemListener { MenuPelda3() { // constructor MenuBar mb = new MenuBar(); Menu tempMenuReference; mb.add(tempMenuReference = new Menu("Fajl")); MenuItem tempMenuItemReference; tempMenuReference.add(tempMenuItemReference = new Menu("Almenu")); tempMenuReference = (Menu) tempMenuItemReference; // mivel az almenu menupontjait inkabb adjuk egy mar a Menu osztalyra utalo referenciahoz, mint a MenuItem-re utalohoz tempMenuReference.add(tempMenuItemReference = new MenuItem("Almenupont 1")); tempMenuItemReference.addActionListener(this); tempMenuReference.add(tempMenuItemReference = new MenuItem("Almenupont 2"));
tempMenuItemReference.addActionListener(this); tempMenuReference.addSeparator(); tempMenuReference.add(tempMenuItemReference = new CheckboxMenuItem("Igen/nem kapcsolo")); ((CheckboxMenuItem)tempMenuItemReference).addItemListener(this); mb.add(new Menu("Szerkesztes")); Menu helpMenu; mb.add(helpMenu = new Menu("Segitseg")); mb.setHelpMenu(helpMenu); setMenuBar(mb); setSize(200,200); show(); } public void actionPerformed(ActionEvent e) { System.outprintln("A " + egetActionCommand() + " menupontot valasztottuk"); } public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEventSELECTED) System.outprintln("Checkbox be"); if (e.getStateChange() == ItemEventDESELECTED) System.outprintln("Checkbox ki"); } 39 public static void main(String a[]) { new MenuPelda3(); } // main } // class Mindenképp érdemes megfigyelni, hogy két típuskonverzióra is sor került a programban, mivel egyrészt egy Menu
típusú (a MenuItem leszármazottja!) referenciához a szülô, MenuItem statikus típusú referenciát rendeltünk, mely természetesen fordítási idôben, a statikus típusok inkompabilitása miatt hiba (ezért kellett ide a cast: tempMenuReference = (Menu) tempMenuItemReference;); másrészt, nem akartam külön referenciavátozót létrehozni a CheckboxMenuItem -nek: ((CheckboxMenuItem)tempMenuItemReference).addItemListener(this); Mivel MenuItem-ben nincs definiált új metódust hívtuk). addItemListener(), ezért kellett itt lekasztolnunk (a gyerekosztályban Megj.: Swing-ben már nem csak a JFrame konténerhez rendelhetünk menüt, tehát ez a limitáció megszûnt PopupMenu Az 1.1-es JDK az új AWT widgeteket érintô egyik újítása (Sajnos, a JDK11 nem sok új komponenst hozott, ui. még mindig a peer modellre épül, talán egyedül csak a radikálisan új és hatékonyabb eseménykezelésben jobb.) Bármilyen komponensnek (widget-nek) lehet ilyen popup menüje,
ugyanis a Component osztály definiálja az add(PopupMenu) metódust. A következô példában látjuk majd, hogy ezt hogy lehet effektíven felhasználni, amikor valóban egy kattintott Label mellé akarjuk kirakni a menünket. Eseménykezelés szempontjából ugyanúgy a MenuItem-ekhez rendelt ActionListener objektumokat kell használnunk, mint a normál menüs esetben, és ebbe is ugyanúgy tehetünk almenüket, mint idôsebb testvérébe, hisz itt is ugyanazokat a MenuItem, ill. Menu (almenükhöz) osztályokat kell használnunk, mint ott. Természetesen itt nincs statikus MenuBar Ahhoz, hogy ilyen menüt megjeleníthessünk, meg kell rá hívnunk a show() metódust. Ezt természetesen általában egy eseménykezelôbôl tesszük, hisz a menü alapból nem tudja (honnan is tudná?), hogy milyen komponensen, milyen esemény hatására kell láthatóvá válnia. Ezen felül, a menünket explicit hozzá kell adnunk ahhoz (azokhoz) a komponensekhez, amin használni akarjuk, azaz nem
elég annak a komponensnek pl. az actionPerformed()-jébôl egy show()-t hívnunk Az alábbi programban is ezért van ennyi, látszólag fölösleges add(PopupMenu) hívás. 40 Az alábbi program textuális információt jelenít meg egy Frame-ben, egymástól független oszlopokra és sorokra bontva. A példa egy valódi távközlési projectünk GUI vázát mutatja - bemutatja, hogyan lehet akár egyszerre több PopupMenu-t is egymástól függetlenül kezelni (ráadásul még a FileDialog-ok, ill. a különbözô egérgombok használatára is példát hoz), és mi a legjobb módszer arra, ha String adatokhoz szeretnénk külön-külön listenert (itt: popup menüt) rendelni. A fenti, menüzô példámban már elmagyaráztam, miért erendendôen jobb mindent komponensekkel csinálni drawString() és társai helyett ezt itt csak megismételhetem. Az, hogy nem használunk drawString()-et, hanem helyette Label-eket, az eseménykezelô kódját rendkívül leegyszerûsíti, mivel
konkrétan tudjuk, melyik Label-en kattintott a felhasználó. Mivel minden egyes elemét a többiekétôl független színnel kell ábrázolni, valamint minden egyes elemhez külön-külön popup menüt kell rendelni, nem használhattunk egyszerû (természetesen setEditable(false)-ozott) Frame + TextArea-t. Még a drawString() kézben tartható lett volna, mert ott végül is mi magunk írunk ki stringeket adott koordinátákra, tehát egy adott string kezdetét és méretét mindig meg tudjuk határozni, TextArea esetében viszont ez nem megy - más platformon, esetleg eltérô karakterkészlet / méret mellett, biztos hogy rosszul mûködne programunk. Fôleg akkor, ha a felhasználó átméretezi a TextArea-t Ráadásul egy olyan esetben a TextArea-hez kellett volna rendelni a MouseListener-t, ami aztán a show()-t hívná - mindenki el tudja képzelni, mennyire reménytelen dolog kiszámolni azt, hogy a MouseEvent által átadott koordinátapár tulajdonképp melyik oszlop/sor
elem fölötti kattintást jelképez. Ha mindezt különálló, független Label-ekkel (amik természtesen futás közben módosíthatók - a színük is) csináljuk, akkor sokkal könnyebb dolgunk lesz az eseménykezelés során. Az alapprogram hálózati adatforgalmat mér, bármilyen port felett (persze csak a GUI felületet hagytam meg a példában). Egyszerre több mérôegységgel képes kommunikálni, melyek max számát a NUMBER OF MACHINES tartalmazza, így az új, megfigyelt gépek hozzáadása is egyszerû. A mérôegységek egyszerre 16, egymástól független csatornán képesek méréseket végezni; ezeknek a csatornáknak a felprogramozását, ill. be/kikapcsolását távolról, a Java applikáción keresztül végezzük Természetesen a platformfüggetlenség édekében választottuk a megvalósítás nyelvének a Java-t. Amennyiben a felhasználó bal egérgombbal kattint a sorra, azt update-eljük, amennyiben jobbal, akkor egy pop-up menüt jelenítünk meg, attól
függôen, hogy az adott gép elsô négy, vagy utolsó 16 Label-én kattintottunk-e (azaz, az elsô 4 Label-hez teljesen más (pm4) popupmenü példányt rendelünk, mint a második 16-hoz (pm16). Ezek a példányokat minden egyes gépre külön-külön kreáljuk (lásd a OneMachine oszályt, amelyben ezen menük referenciáján kívül mind a 4+16 Label feliratát, ill. aktuális értékét is eltároltuk)) Minden egyes Label-hez MouseListener-t rendelünk (még egyszer: az alacsonyszintû addMouseListener() minden komponens, így a Label is, számára rendelkezésére áll, mivel a Component osztályban definiálták). Amennyiben a felhasználó a 20 Label bármelyikén kattint, a rendszer visszahívja a mouseClicked() metódust. Ezt kényelmi okokból - egy, a MouseAdapter-t kiterjesztô osztályba raktuk, hogy ne kelljen a MouseListenerben deklarált összes metódust - akár üres törzzsel is - implementálnunk Az osztálynak van saját konstruktora, hogy egyrészt a
fôosztályunk példányváltozóit el tudjuk érni, ill. hogy tudjuk, az adott példány éppen melyik géphez (OneMachine-referencia) tartozik. A mouseClicked() metódusban azt nézzük, hogy az OneMachine-referencia által címzett példányban konkrétan melyik Label-referenciával egyezik a MouseEvent-ben (getSource) kapottal - hanyadikLabelenClickeltek ezt az indexet fogja tartalmazni. Ezután egyszerûen csak megjelenítjük (show()) a megfelelô popup menüt (referenciáját szintén az aktuális OneMachine-példányból vesszük), melynek kezeléséért az ugyanebben az osztályban (amely, természetesen, az ActionListener-t implementálja) definiált actionPerformed() felel. Azért valósítottam meg azonos osztályban a Label-clicket, ill. popupmenü-választást feldolgozó kódot, hogy az actionPerformed() a saját osztályának példányváltozójából kapásból megtudhassa, az adott gép melyik Label-én kattintott a felhasználó, ugyanis az ActionEvent.getSource()
most a popupmenü referenciáját adná vissza, és nem annak a Label-nek a referenciáját, amelyre azt megjelenítettük. import java.io*; import java.awt*; import java.awtevent*; class PopupMenuExample extends Frame { public static final int NUMBER OF MACHINES = 4; PopupMenuExample() { 41 // a Frame 4+1 sorbol es 2 oszlopbol all. A leftPanel kerul baloldalra, a rightPanel pedig jobb oldalra setLayout(new GridLayout(5,2)); // Frame-e OneMachine machineBeingSetUp; Panel leftPanel = new Panel(); // leftPanel baloldalt 4 fejlec-labelt tartalmaz leftPanel.setLayout(new GridLayout(1,4)); leftPanel.add(new Label("IP")); leftPanel.add(new Label("Up?")); leftPanel.add(new Label("In/Out bytes")); leftPanel.add(new Label("NICs")); // van egy temporaris panelunk is a jobboldali fejlec kirakasara - ez 2 elemet fog tartalmazni. Panel tmpPanel = new Panel(); tmpPanel.add(new Label("Measurement Slots")); tmpPanel.add(new Label("(Probe
status)")); // es a fejlecet felrakjuk a 2*5 elso soraba: add(leftPanel); // Frame-re add(tmpPanel); for (int mach = 0; mach < NUMBER OF MACHINES; mach++) // peldanyositunk belul { // a paneljeinket minden egyes gepre ujra letrehozzuk, hisz azok Grid-esek leftPanel = new Panel(); Panel rightPanel = new Panel(); // leftPanel baloldalt 4 labelt tartalmaz, a rightPanel pedig 16-ot jobboldalt leftPanel.setLayout(new GridLayout(1,4)); rightPanel.setLayout(new GridLayout(1,16)); machineBeingSetUp = new OneMachine(); // jobboldali // // // // elobb // // egy darab mouse click handlerunk van gepenkent. Ezt adjuk hozza mind a bal-, mind a Label-tomb minden elemehez. A mouseClicked() felelos azert, hogy a handler peldany index-peldanyvaltozojaba megkeresse az aktualis indexet. Amikor az megvan, attol fuggoen, hogy bal, ill. jobboldalt tortent a click, rakja ki a gep leirojanak pm4, ill pm16 PopupMenu-jet. A ket PopupMenu-bol is eleg egy gephez egy-egy, ui a mouseClicked() az mar
kikereste, melyik Labelen tortent a click. Ezt persze a handlerben globalis valtozoba kell eltarolnunk. MouseHandler mouseHandler = new MouseHandler(machineBeingSetUp,this); // feltoltjuk a bal, ill. jobboldali menuket: machineBeingSetUp.pm16 = new PopupMenu(); machineBeingSetUp.pm16add(new MenuItem("Activate")); machineBeingSetUp.pm16add(new MenuItem("Deactivate")); Menu submenu = new Menu("Set mode to"); submenu.add(new MenuItem("Traffic data / VPI (Task#1)")); submenu.add(new MenuItem("Traffic data / IP protocol (Task#2)")); machineBeingSetUp.pm16add(submenu); machineBeingSetUp.pm16addSeparator(); machineBeingSetUp.pm16add(new MenuItem("Query Task #1-5 results")); machineBeingSetUp.pm4 = new PopupMenu(); machineBeingSetUp.pm4add(new MenuItem("Query Probes Status")); machineBeingSetUp.pm4add(new MenuItem("Reboot Probe Now!")); // ehhez a 2 menuhoz ugyanazt a MouseListener/ActionListener peldanyt
rendeljuk. Ez mindketto egyszerre, mert mindketto interfeszt implementalja add(machineBeingSetUp.pm16); add(machineBeingSetUp.pm4); machineBeingSetUp.pm16addActionListener(mouseHandler); machineBeingSetUp.pm4addActionListener(mouseHandler); // eloszor a baloldali 4, ill. jobboldali 16 label-t generaljuk (ill kitoltjuk); ezekhez a MouseHandler-unk peldanyait rendeljuk for (int i=0;i<4;i++) { leftPanel.add(machineBeingSetUpmachArrayLabels[i] = new Label("111222")); machineBeingSetUp.machArrayLabels[i]setForeground(Colorgray); machineBeingSetUp.machArrayLabels[i]addMouseListener(mouseHandler); } // for - bal 42 for (int i=0;i<16;i++) { rightPanel.add(machineBeingSetUpregsArrayLabels[i] = new Label("?")); machineBeingSetUp.regsArrayLabels[i]addMouseListener(mouseHandler); } // for - jobb add(leftPanel); add(rightPanel); } // for - minden gepre megcsinaljuk az addot pack(); setVisible(true); } public static void main(String[] args) { new PopupMenuExample();
} // main } // azert egyszerre MouseListener es ActionListener is egyben, hogy a Label-click utan kirakott menu a label-clickben kikeresett globalis indexet konnyen elerhesse class MouseHandler extends MouseAdapter implements ActionListener { OneMachine melyikGep; // MouseAdapter-ekbol 1 peldany van minden kulon gephez, ezert kell azt kulon eltarolni PopupMenuExample pointerBack; int hanyadikLabelenClickeltek; MouseHandler(OneMachine gep, { melyikGep=gep; pointerBack= pointer; } // const PopupMenuExample pointer) public void mouseClicked(MouseEvent e) { // eloszor azt csekkeljuk, bal- vagy jobb egergombbal clickeltek. Ha bal, akkor updateljuk az adott gepet (melyikGep), ha jobb, megnezzuk, melyik popup menut kell kirakni. if ((e.getModifiers() & InputEventBUTTON1 MASK) != 0) { updateRow(melyikGep); return; } hanyadikLabelenClickeltek=0; // melyik label? A while-ban ha nem talaljuk meg a labelt, az azt jelenti, hogy az elso reszre clickeltek - ilyenkor break. Az elso reszre nem
csinaltunk kulon handlert while (e.getSource() != (++hanyadikLabelenClickeltek==16) break; // ilyenkor menut is if { melyikGep.regsArrayLabels[hanyadikLabelenClickeltek]) if ha nem volt bent a jobb oldali tombben a Label, akkor a bal oldaliban keressuk meg. Persze a machArrayLabels[]-sel komparaljuk az eventet kivalto source-ot. Kapasbol az ide tartozo kiirjuk. (hanyadikLabelenClickeltek==16) hanyadikLabelenClickeltek=-1; while (e.getSource() != melyikGepmachArrayLabels[++hanyadikLabelenClickeltek]); melyikGep.pm4show(melyikGepmachArrayLabels[hanyadikLabelenClickeltek],3,3); } // if else melyikGep.pm16show(melyikGepregsArrayLabels[hanyadikLabelenClickeltek],3,3); } // mouseClicked public void actionPerformed(ActionEvent e) { // ez mar a popup menu esemenyeit erzekeli. Csak nehany menupontot dolgoztam ki az egyszeruseg kedveert. if (e.getActionCommand()equals("Activate")) { melyikGep.regsArrayLabels[hanyadikLabelenClickeltek]setForeground(Colorred);
melyikGep.regsArrayLabels[hanyadikLabelenClickeltek]setText(""+melyikGepregsArrayValue[ hanyadikLabelenClickeltek]); } if (e.getActionCommand()equals("Deactivate")) { melyikGep.regsArrayLabels[hanyadikLabelenClickeltek]setForeground(Colorgreen); melyikGep.regsArrayLabels[hanyadikLabelenClickeltek]setText(""+melyikGepregsArrayValue[ hanyadikLabelenClickeltek]); } if (e.getActionCommand()equals("Query Task #1-5 results")) { FileDialog fd = new FileDialog(pointerBack); fd.setMode(FileDialogSAVE); fd.show(); File file = new File(fd.getFile()); try { PrintWriter pw = new PrintWriter(new FileWriter(file)); pw.println("Proba!"); 43 pw.flush(); pwclose(); } catch (IOException ee) {} } // if } // actionPerformed void updateRow(OneMachine method, kidolgozatlan } // class MouseHandler melyikGep) { System.outprintln("teljes sor update"); } // helper class OneMachine { // kulon osztalyban vannak az egy gep regisztereit leiro
komponensek, hogy azokbol tobbet konnyen lehessen hasznalni // most az osztalyban csak adatmezok vannak // a 16 regiszter jobboldalt: Label referenciajuk es ertekuk int[] regsArrayValue = new int[16]; Label[] regsArrayLabels = new Label[16]; // a 4 regiszter baloldalt: Label referenciajuk es ertekuk int[] machArrayValue = new int[4]; Label[] machArrayLabels = new Label[4]; PopupMenu pm16; // 1 popupmenu jobbra PopupMenu pm4; // 1 popupmenu jobbra } // OneMachine Megj.: Swing-ben nagyon fontos különbség az, hogy ott már nem csak Egy komplex alkalmazás Végsô, fejezetzáró projectünk egy komplett rajzoló applikáció megvalósítása. Ez a legtöbb, eddig tanult, AWT-vel kapcsolatos tudnivalót alkalmazza, bár jópár osztályt nem használ - nem menüz, nincs benne List stb, viszont gyönyörûen bemutatja, hogy viszonylag komplex programokat is milyen egyszerûen fejleszthetünk Java-ban. A program alapvetô rajzolási funkciókat valósít meg; ehhez a Graphics osztály
két, négyszögeket, ill. oválisokat rajzoló metódusait használja (ill. ezek helyett azok a síkidomot kitöltô változatukat hívja meg, a felhasználó választásától (Checkbox) függôen). A program dinamikusan követi a kurzormozgatást, azaz mindig kirajzolja, éppen hogy nézne ki a síkidom, ha az adott pontban engedné el a felhasználó az egérgombot - ezzel bemutatja, mire is jók a MouseMotionListener callback metódusai. A felhasználó ezen felül megadhatja a következôkben kirajzolandó síkidomok színét is egymást reteszelô rádiógombok segítségével; ezek kezelésére ItemListener-t használunk. Egyik GUI választó-komponens referenciáját sem tároljuk el globálisan, hiszen, mint mondottam, egyedül a List-bôl nem lehet az ItemListener-nek passzolt ItemEvent-példány metódusainak segítségével az összes kijelölt listaelemet megkapni. A program a rajzolásra használt Canvas-leszármazotthoz rendeli a MouseMotionListener / MouseListener
példányokat, amely önmaga. Azzal takarítottuk meg a több különálló osztály használatát, hogy a fôosztály egyszerre legyen a Canvas leszármazottja (mely, mint tudjuk, kötelezôen létrehozandó, hisz máshogy nem override-olhatnák a paint()-jét), ill. implementálja a MouseMotionListener / MouseListener interfészeket, azaz az eseménykezelést se bízzuk más osztályra, mert akkor még pótlólagosan azzal is foglalkoznunk kell, hogy az egyes osztályainkból hogy érjük el a többi osztály példányait, azaz vagy még plusz konstruktorokat kell kreálnunk, aminek átpasszoljuk a saját referenciánkat, vagy static-ként deklaráljuk elérendô változóinkat, hogy azokra az osztálynéven keresztül is hivatkozhassunk. Ezen második variáció egy esetben sajnos nem mûködne: ha lenne, tegyük fel, négy osztályunk (elsô a fôosztály, mely a GUI-t felépíti, a második Canvas-leszármazottat példányosítja, beteszi a GUI-ba és hozzárendeli a
MouseMotionListener / MouseListener-példányokat), akkor az eseméykezelô osztályok a Canvas (szub)osztályunk paint()-jét kellene hogy hívják a Canvas újrarajzoltatása érdekében, ami viszont példánymetódus. Tehát, a két eseménykezelô osztálynak mindenképp kellene egy olyan kontruktorát kreálnunk, amely segítségével a fôprogramból megkaphatják, majd eltárolhatják az aktuális Canvas (szub)osztály-példányunk referenciáját. Arról nem is szólva, hogy csak az elérhetôség szempontjából static-izált változók használata nem vall szép OO dizájnra. Amikor a GUI-nkat tartalmazó Frame-ünket megjelenítjük, elindul az aszinkron eseményfigyelés. Amennyiben a GUI widget-jeinket állítgatnák (a Choice-ból választanánk valamit stb), akkor meghívódik 44 az itemStateChanged. Itt - kizárólag a vett ItemEvent-bôl, azaz nem globális referenciákon át! - meghatározzuk az esemény forrását, ill. amennyiben kizárólag az alapjá nem
lehet eldönteni, hogy az adott widget (konrétan: a Checkbox) éppen milyen állapotban van, akkor meghívjuk az ItemEvent getStateChange()metódusát, és visszatérési értékét az ItemEvent két konstansához hasonlítjuk. Programunk során biztosak lehetük abban, hogy a Java szemétgyûjtôje a nem referált GUI objektumokat nem törli. Ez ökölszabály a Java-ban, hasonlóan ahhoz, hogy GUI-t használó program nem lép ki azonnal, amikor a szinkron, a GUI-t felépítô program véget ér. Láthatjuk összes grafikus programunkban, hogy az utolsó metódushívás, amit végrehajtanak, a show() hívása, de ezek után addig a képernyôn maradnak, amíg le nem lôjük a konzolképernyôt. Ha a felhasználó bármikor lenyomja az egérgombot (és lenyomva tartja), akkor meghívódik a mousePressed(). Ez a globális firstX és firstY változókba eltárolja az aktuális egérkurzor-koordinátákat Ezek a változók tárolják a rajzolás kezdôpontját. Ha most az egérkurzor
elmozdul, akkor a MouseMotionListener mouseDragged() metódusa hívódik vissza, viszont, mivel az egérkorzor pillanatnyi pozíciója a rajzolásnak már nem az elsô koordinátapárját jelenti, innen egyrészt a koordinátákat beírjuk a newX és newY globális változókba, majd repaint()-et hívunk, hogy azonnal, dinamikusan kirajzoljuk az egérkurzor felengedésekor véglegesítôdô síkidomot. Az persze csak akkor következik be (addig nagyon sokszor meghívódhat a mouseDragged()), amikor a felhasználó valóban befejezi a rajzolást - a mouseReleased() a mouseDragged()-hez hasonlóan tölti fel a végkoordináták globális párját. Ne feldjük, hogy a globális firstX és firstY változókat kizárólag csak a rajzolás elején hívódó mousePressed() írja felül, így akár percek is eltelhetnek aközött, hogy ôk, ill. a newX és newY globális változók megkapják végleges értéküket. A paint() egyszerû: meghatározza a megfelelô koordináták
különbségének abszolút értékét (a Graphics legtöbb rajzoló metódusa, így a mi általunk használt négy is, második paraméterpárjaként szélességet, ill. magasságot, és nem abszolút oordinátákat vár!), eldönti, hogy az új x nagyobb-e a réginél (ugyanezt y-ra is megteszi), és egy if-es szerekezetben vált a négszög/ovális, ill. kitöltött/ kitöltetlen rajzolása között Feladat: írjuk át programunkat úgy, hogy ne kelljen még az ItemListener-t sem használnunk, azaz a paint()-ben magunk kérdezzük le az egyes GUI widgetek állapotát, és aszerint állítsuk be a használt színt stb! Ehhez persze a widget-jeink referenciáját globálissá kell tenni (vagy – amennyiben megfelel – a getSource()-re hívni a lekérdezô metódusokat), viszont az egész program egyszerû lesz, mert nem kell már az itemStateChanged és a paint() között globális változókkal (még ha azokból csak három is van: két boolean (isFilled: kitöltött/nem
kitöltött ill. isRect: négyszög/nem négyszög) és egy Color típusú változó, melybe eltároljuk a rádiógombokkal beállított rajzolási színt. Ezt a három globális változót használja fel a paint(), amikor a repaint()-tel utasítjuk az ablakozó rendszert, hogy az elsô adandó alkalommal - az update() metóduson keresztül - a paint()-et meghívja. Megj.: vissza-visszatérô kérdés az, hogy a Color osztályban definiált konstansokkal egyezô nevû/kapitalizációjú String-ekbôl (pl. "white") hogyan készíthetünk egy konkrét Color objektumot: sehogy Nincs sem ilyen konstruktor, sem ilyent elfogadó statikus Color metódus. Ez azt jelenti, hogy ha pl getParameter()-rel akarunk a HTML webmaster-tôl színinformációkat lekérni, akkor bizony extenzíven if/else if-eznünk és equals[IgnoreCase]()-elnünk kell. Megj.: nem tárgyaltam külön a repaint() / update() / paint() különbségeket Ez mindenkép hálás téma, ugyanis a Java-könyvek legnagyobb
része hibásan ismerteti. Az elsô a Component példánymetódusa, mely arra kéri az ablakozó rendszert, hogy az amikor képes rá, hívja meg az update() metódust, amelyet szintén a Component definiál. Az update(Graphics) metódus egyszerûen törli a komponens egész területét, majd meghívja annak paint()-jét. A paint(Graphics)-et pedig már ismerjük Vegyük észre ezen utóbbi két metódus esetében, hogy az ablakozó rendszer egy Graphics típusú objektumot ad át nekik. Ha Windows programozóknak azt mondom, grafikus kontextus, menten leesik nekik a tantusz, mirôl is van szó. Ezt az objektumot az ablakozó rendszertôl a két említett metóduson kívül is lekérhetjük a Graphics getGraphics() metódus segítségével, és ekkor más metódusból is rajozolhatunk a felületünkre, de ezzel bánjunk csínján, ui. ha elfelejtjük a rajzolás után a kapott Graphics objektumunkra meghívni a dispose()-t, néhány op. rendszeren bajba kerülhetünk (a WinNT-nek a
több milliónyi takarítatlan grafikus kontextus sem okoz gondot kipróbáltam) Olyan eseteben, amikor nagyon fontos, hogy ne az ablakozó rendszer döntse el, mikor hívja a paint()-et, mert pl egy ciklusból rajzolunk (pl. csillagokat scrollozunk keresztül a felületünkön), akkor valóban érdemes közvetlenül rajzolni, hogy egyenletes legyen a scroll. Ilyenkor természetesen számûzzük a getGraphics()/ dispose()- hívásokat a ciklusból - horribile dictu, elég a getGraphics()-ot egyszer meghívnunk, és az általa visszaadott grafikus kontextus referenciáját egy globális változóban tárolnunk, hogy aztán csak programunk végén szabadítsuk azt föl. Erre álljék itt egy példa: //<applet code=StarScroller.class width=400 height=400></applet> import java.appletApplet; 45 import java.awt*; public class StarScroller extends Applet implements Runnable { StarData starData[]; Graphics g; Thread gfxThread; // kötelezô - alább látjuk, miért public void
run() { while (true) { for (int i = 0;i < starData.length; i++) { // a mostani koordinátákat háttérszínnel kirajzoljuk (azaz letöröljük) drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]eraseColor); // minden vízszintes koordinátából kivonjuk a sebességet starData[i].xCoordNow = starData[i]xCoordNow - starData[i]speed; // és ellenôrizzük, hogy ez nem kisebb-e nullánál - ha igen, akkor hozzáadjuk (azaz effektíven kivonjuk) a maximális x irányú méretbôl (nem egyszerûen azt töltjük ide, hisz akkor minden újrarajzolt pont onnan indulna) if (starData[i].xCoordNow < 0 ) starData[i]xCoordNow = starData[i]xCoordNow + starData[i]rightMargin; // kirajzoljuk az új pontban rajzolási színnel drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]paintColor); } // for // az összes pont (most 80 db) kirajzolása közben néha ütemenként várunk egyet try { Thread.sleep(20); } catch (Throwable e) {} } } public void init() { starData
= new StarData[80]; setBackground(Color.black); for (int i = 0; i< starData.length; i++) { starData[i] = new StarData(); } for (int i = 0; i < starData.length; i++) { starData[i].xCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].yCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].speed = (int)((Mathrandom() * 4) + 1); } } // init public void start() { g = getGraphics(); gfxThread = new Thread(this); gfxThread.start(); } public void stop() { gfxThread.stop(); g.dispose(); } public void drawPoint(int x, int y, Color c) { g.setColor(c); g.drawLine(x, y, x, y); } // drawPoint } class StarData { Color paintColor = Color.white; int speed; int xCoordNow, yCoordNow; Color eraseColor= Color.black; static int rightMargin= 400; } // class A program nagyon egyszerûen mûködik: az egyes, vízszintesen scrollozott csillagokat egy többen tárolja. Az init()-ben három szabad, random paramétert állítunk be: a kezdô x, y koordinátát és a
sebességet (azaz hogy az egyes ciklussok során az x koordinátából mennyit vonjunk le az új koordináták számolásakor). Az applet a Runnable-t is implementálja, hogy a böngészô által adott szál ne terhelôdjön le (ennek következményeit máshol is taglalom - lásd a kliens-szerver chatter applet-esítésével kapcsolatban mondottak. Egy normál applikációban egyáltalán nem lenne szükség thread-elésre) Próbáljuk ki, mi történik, ha a programot egyrészt threadelés nélküli applikációra, másrészt applet-re átírjuk (próbáljuk ki - látjuk majd, a GUI nem updatelôdik!)! Mivel applet -> applikáció konverzióra az Applet-es fejezetben nem mutatok példát, ez elôbbit most részletesebben kifejtem (igaz, itt pl. az eltérô default layout manager miatti lépés nincs, ui. GUI widgeteket egyáltalán nem hasznáunk, csak rajzolunk a gr kontextusunkra): //<applet code=StarScroller.class width=400 height=400></applet> // import
java.appletApplet; <- erre már nincs szükség import java.awt*; //public class StarScroller extends Applet implements Runnable { class StarScrollerApp extends Frame { Graphics g; // már nincs szükség thread-re, így annak használatát (és persze publikus referenciáját is) elimináltuk public static void main(String arg[]) { new StarScrollerApp(); } 46 // példányosítjuk saját osztályunkat. Erre nem volt különösebb szükség (rakhattunk volna mindent a main-be), mint ahogy arra sem, hogy azt Frame-bôl származtassuk (egy független Frame-et is nyithattunk volna, hisz override-olni most nem kellett Frame-et, ui. a grafikus kontextus eléréséhez és az azon történô mûveletvégzéshez itt nem az override-olt paint()-et használtuk StarScrollerApp() { // init-bol paste: kezdet StarData starData[]= new StarData[80]; // már csak 1 metódusból érjük el, így lehet lokális is setBackground(Color.black); for (int i = 0; i< starData.length; i++) {
starData[i] = new StarData(); } for (int i = 0; i < starData.length; i++) { starData[i].xCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].yCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].speed = (int)((Mathrandom() * 4) + 1); } // init-bol paste: veg // Frame-ünket explicit méretezni kell és megjeleníteni; ezután már elérhetjük grafikus kontextusát is setSize(400,400); show(); // ez fontos: addig semmilyen komponensnek nem érhetjük el a grafikus kontextusát, amíg azt ki nem raktuk a képernyôre. Mivel az appletnek mindig létezik saját grafikus felülete, ott az init() elejére is kerülhetett volna annak lekérése (vigyázat, az init() elé ne rakjuk, mert ezt is a browser adja, akkor, amikor az applet stub-ot inicializálja! Azaz a globális g deklarációja nem lehet egyben inicializációja is), itt viszont a show() utánra kellett helyezni. Próbáljuk a show() elé rakni! g = getGraphics(); // run-bol paste: kezdet
while (true) { for (int i = 0;i < starData.length; i++) { drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]eraseColor); starData[i].xCoordNow = starData[i]xCoordNow - starData[i]speed; if (starData[i].xCoordNow < 0 ) starData[i]xCoordNow = starData[i]xCoordNow + starData[i]rightMargin; drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]paintColor); } // for try { Thread.sleep(20); } catch (Throwable e) {} } // while // run-bol paste: veg } // const public void drawPoint(int x, int y, Color c) { g.setColor(c); g.drawLine(x, y, x, y); } // drawPoint } // class Feladat: ezen programot írjuk át úgy, hogy megbizonyosodhassunk arról, hogy valóban elérhetjük és használhatjuk bármilyen komponensnek a grafikus kontextusát úgy, hogy azt nem szubklasszoljuk! Megoldás: class StarScrollerApp2 { // nincs extends Frame static Graphics g; public static void main(String arg[]) { Frame f = new Frame(); StarData starData[]= new StarData[80];
f.setBackground(Colorblack); for (int i = 0; i< starData.length; i++) { starData[i] = new StarData(); } for (int i = 0; i < starData.length; i++) { starData[i].xCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].yCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].speed = (int)((Mathrandom() * 4) + 1); } f.setSize(400,400); f.show(); g = f.getGraphics(); // run-bol paste: kezdet while (true) { for (int i = 0;i < starData.length; i++) { drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]eraseColor); starData[i].xCoordNow = starData[i]xCoordNow - starData[i]speed; if (starData[i].xCoordNow < 0 ) starData[i]xCoordNow = starData[i]xCoordNow + starData[i]rightMargin; drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]paintColor); } // for try { Thread.sleep(20); } catch (Throwable e) {} } // while } // main public static void drawPoint(int x, int y, Color c) { g.setColor(c); g.drawLine(x, y, x, y); } // drawPoint
} // class 47 Vigyázat! Mint látjuk, a repaint()-tel kért paint()-hívás lényegesen kevesebbszer következik be, mint a brute force-módszer, azaz a grafikus kontextus lekérése és az azon át történô rajzolás. Ez utóbbinak (az esetlegesen fel nem szabadított, és egyes operációs rendszerek nyakán koloncként maradó, limitált számú grafikus kontextusok veszélye mellett) még az is gyengéje, hogy amennyiben a rendszer véli úgy, hogy az adott komponenst újra kell rajzolnia (pl. átméreteztük, kitakartuk stb), akkor, mivel a paint()-et nem override-oldtuk, az üres képernyôt rajzol. Ez nem gond a fenti esetben, amikor egy ciklusból állandóan újrarajzoltuk a felületet, olyan esetben viszont, amikor ilyen ciklus nincs, mert pl. egy képet rakunk ki drawImage()-dzsel, komoly bajunk lehet - egy átméretezés után a teljes kép eltûnhet. Ezért is bánjunk csínján a getGraphics()-szel! Megj.: a Swing JComponent-ének paintImmediately() metódusáról
a mostani Swing-es irodalom nem mondja meg expliciten, hogy az – nevével ellentétben – is ugyanolyan lassú, mint a repaint(), ugyanis ez is várakozási sorba rakja a kiszolgálási kéréseket. Nagy különbség viszont a repaint()-hez képest, hogy ez az egymás utáni repaint()-eket nem egyesíti, hanem azokat mind végrehajtja. Próbáljuk meg programunkat átírni úgy, hogy ezt valóban tesztelhessük! Látni fogjuk, hogy valóban minden köztes pontot is kirajzol a rendszer, azaz valóban nem gyûjti össze azokat egyszeri rajzolásra. Mivel csak ígéret szintjén létezik normál AWT-beli Component.paintImmediately(), ezért sajnos (?) Swing-et kell használnunk A program konverziója során ráadásul abba a problémába is beleütközünk, hogy a Swing-ben a JApplet nem leszármazottja a JComponent–nek, emiatt nincs is paintImmediately() metódusa. Emiatt kötelezôen JPanel-t kell használnunk //<applet code=StarScrollerSwingPaintImmediately.class width=400
height=400></applet> import java.appletApplet; import java.awt*; import javax.swing*; public class StarScrollerSwingPaintImmediately extends JApplet implements Runnable { // StarData starData[]; Thread gfxThread; // kötelezô - alább látjuk, miért Graphics g; JPanel p = new SajatPanel(); public void run() { while (true) { for (int i = 0;i < starData.length; i++) { drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]eraseColor); starData[i].xCoordNow = starData[i]xCoordNow - starData[i]speed; if (starData[i].xCoordNow < 0 ) starData[i]xCoordNow = starData[i]xCoordNow + starData[i]rightMargin; drawPoint(starData[i].xCoordNow, starData[i]yCoordNow, starData[i]paintColor); } // for // try { Thread.sleep(20); } catch (Throwable e) {} } } public void init() { starData = new StarData[80]; setBackground(Color.black); for (int i = 0; i< starData.length; i++) { starData[i] = new StarData(); } for (int i = 0; i < starData.length; i++) {
starData[i].xCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].yCoordNow = (int)(Mathrandom() * starData[1].rightMargin); starData[i].speed = (int)((Mathrandom() * 4) + 1); } getContentPane().add(p); } // init // // public void start() { g = getGraphics(); gfxThread = new Thread(this); gfxThread.start(); } public void stop() { gfxThread.stop(); g.dispose(); } static int x,y; static Color c; // hogy SajatPanel-bôl minél egyszerûbben elérhessük ezeket, static public void drawPoint(int x, int y, Color c) { this.x= x; this.y= y; this.c= c; p.paintImmediately(0,0,200,200); } // drawPoint } class SajatPanel extends JPanel { public void paint(Graphics g) // a volt drawPoint két metódushívása { g.setColor(StarScrollerSwingPaintImmediatelyc); g.drawLine(StarScrollerSwingPaintImmediatelyx, StarScrollerSwingPaintImmediatelyy, StarScrollerSwingPaintImmediately.x, StarScrollerSwingPaintImmediatelyy); } } 48 Ha már itt tartunk, még egy fontos Swing-es, ide
tartozó dolgot kell megemlíteni: azt, hogy JComponent default double buffered. Ez ugyanakkor azt is jelenti, hogy a JApplet nem az, mivel, mint már mondottam, nem a JComponent-bôl származik. Még egy fontos dolog: Swing-ben az update() egyszerûen csak egy paint()-et hív. Ezt már mindenkinek illik saját maga megírnia – látni fogjuk, valóban ez is különbség a két ablakozó library között! (A Swing verziójából hiányzó törlésre pl. használjunk fillRect()et) Nem lesz sok tennivalónk, ugyanis annyit kell csak változtatnunk, hogy az Applet helyett a javaxswingJApplet osztályból származtatjuk fôosztályunkat, és megnézzük, mivel jár az, ha az update()-et override-oljuk, ill. ha nem Vissza a fejezetzáró projecthez: Megj.: az AWT update()-e (ellentétben a Swing-gel), mint mondottam, törli a komponens felületét, és közvetlenül meghívja a paint()-et. Ezért, ha azt a következôképpen override-oljuk a paint()-et is tartalmazó osztályunkban:
public void update(Graphics g) { paint(g); }, akkor nem fogja a felületünket törölni, hanem meghagyja az elôzô állapotot (próbáljuk ki programunkban!). Természetesen, az el fog tûnni, amikor az ablakot kitakarjuk vagy minimalizált állapotból visszaállítjuk eredeti méretébe, ugyanis - legalábbis a Windows - nem menti el az eltakart képernyôfelületet, annak visszaállítását egyedül az alkalmazói programokra bízza. import java.awt*; import java.awtevent*; class KomplexRajzolo extends Canvas implements ItemListener, MouseListener, MouseMotionListener { int firstX, firstY, newX, newY; // a rajzolas kezdo (mousePressed)- es a // pillanatnyi (mouseDragged)/veg (mouseReleased) koordinataja boolean isRect; // negyszog vagy ellipszis? itemStateChanged-bol allitjuk boolean isFilled; // kitoltott vagy nem? itemStateChanged-bol allitjuk Color color; // milyen szinben rajzoljunk? itemStateChanged-bol allitjuk public Dimension getPreferredSize() { return new
Dimension(200,200); } // Canvas + BorderLayout! public public public public void void void void mouseEntered(MouseEvent e){} // MouseListener nem hasznalt metodusai mouseExited(MouseEvent e){} mouseClicked(MouseEvent e){} mouseMoved(MouseEvent e){} // MouseMotionListener nem hasznalt metodusai KomplexRajzolo() { // constructor Frame f = new Frame(); Panel p = new Panel(); Choice c=new Choice(); c.add("circle"); c.add("Rect"); c.addItemListener(this); p.add(c); // Choice - ItemListener Checkbox cb; cb=new Checkbox("filled",false); cb.addItemListener(this); // Checkbox - ItemListener p.add(cb); CheckboxGroup cbg =new CheckboxGroup(); cb=new Checkbox("blue",false,cbg); cb.addItemListener(this); p.add(cb); cb=new Checkbox("red",false,cbg); cb.addItemListener(this); p.add(cb); cb=new Checkbox("yellow",true,cbg); cb.addItemListener(this); p.add(cb); addMouseListener(this); // Canvas - statikus mukodes
addMouseMotionListener(this); // Canvas - dinamikus mukodes f.add(p,"South"); f.add(this,"Center"); // Canvas Frame-be f.pack(); f.show(); } public void itemStateChanged(ItemEvent e) { // radiogombok - itt nem kell a be/kikapcsolt allapotot megnezni getStateChange()-dzsel, mert // csak bekapcsolas eseten johet ilyen esemeny if (e.getItem()equals("yellow")) color = Coloryellow; if (e.getItem()equals("blue")) color = Colorblue; if (e.getItem()equals("red")) color = Colorred; // Choice - ez is inkabb ActionEvent-hez hasonlo, azaz valasztas eseten elezodik if (e.getItem()equals("circle")) isRect = false; if (e.getItem()equals("Rect")) isRect = true; // standard checkbox - itt mar valoban szukseg van getStateChange()-re is if (e.getItem()equals("filled") && egetStateChange() == ItemEventSELECTED) isFilled=true; if (e.getItem()equals("filled") && egetStateChange() == ItemEventDESELECTED)
isFilled=false; } public void paint(Graphics g) { g.setColor(color); 49 int xSize=Math.abs(firstX-newX);// Graphics rajzolo metodusai altalaban x/y meretet kernek vegpontkoordinatak helyett int ySize=Math.abs(firstY-newY); int rajzolasiKezdoX = firstX; int rajzolasiKezdoY = firstY; // // if if ha valamelyik uj koordinata alacsonyabb, mint a kezdo, akkor a rajzolashoz onnan indulunk (mikozben az eredeti, globalis firstX/firstY-hoz nem nyulunk) (firstX>newX) rajzolasiKezdoX = newX; (firstY>newY) rajzolasiKezdoY = newY; if (isFilled == true) { if (isRect == true) g.fillRect(rajzolasiKezdoX,rajzolasiKezdoY,xSize,ySize); else g.fillOval(rajzolasiKezdoX,rajzolasiKezdoY,xSize,ySize); } else { if (isRect == true) g.drawRect(rajzolasiKezdoX,rajzolasiKezdoY,xSize,ySize); else g.drawOval(rajzolasiKezdoX,rajzolasiKezdoY,xSize,ySize); } } public void mousePressed(MouseEvent e){ firstX=e.getX(); firstY=e.getY(); } public void mouseReleased(MouseEvent e){ newX=e.getX(); newY=e.getY();
repaint(); } public void mouseDragged(MouseEvent e){ newX=e.getX(); newY=e.getY(); repaint(); } public static void main(String a[]) { new KomplexRajzolo(); } // main } // class A program képernyôje (az update() már említett override-olása után): Az Applet osztály A Java megjelenésekor mindenki táncoló betôket és fényújságokat akart a honlapjára rakni, ezért is foglalkozott a Java-könyvek túlnyomó többsége a Java ún. applet-eivel Ezt a legtöbb könyv az érthetôség és a didaktikusság rovására teszi, ui. véleményem szerint akkor bevezetni az applet-eket, amikor még a hallgatónak az override-ról és az AWT-rôl fogalma sincs (így az applet-eket nem tudja sehova se helyezni a Java osztályhierarchiáján belül) vétkes felelôtlenség. Az olyan kijelentések pedig, hogy A nyelvet úgy kell 50 tanítani, hogy már az elején bemutatjuk a GUI programozását, hogy ne unja el magát a hallgató szerintem alapvetôen rosszak - az AWT-t csak úgy
szabad elmagyarázni, hogy a hallgató tisztában van az OOP-vel, mert különben semmit sem fog érteni az eseménykezelésbôl, overriding-ból és az AWT osztályhierarchiából. A Java programok HTML oldalakba való beágyazására a java.applet csomag Applet osztályát kell használnunk. Ez az osztály egyrészt egy HTML oldalbeli konténert reprezentál, amely azért viselkedik konténerként, mert közvetetten a Container leszármazottja (közvetlenül pedig a jól ismert Panel-é), másrészt, olyan kitüntetett osztály, melynek kódját a Web-böngészôk azonnal letöltik a lokáis gépre, azt példányosítják, hozzá egy applet kontextust, applet-környezetet rendelnek és -többek között - meghívják az init() metódusukat. Ez (is) az applet konstruktora Megj.: ezt sajnos nagyon sok könyv hibásan magyarázza el, így mindenképp ki kell hangsúlyozni, hogy az appletek (szigorúan paraméter nélküli; amennyiben csak paraméterest hozunk létre, le sem fordítja a
rendszer) konstruktora akkor hívódik meg, amikor a már fent említett applet kontextusnak még nincs hozzárendelt példánya. Ebbôl következik az, hogy a tradícionális konstruktorunkban semmi olyan mûveletet nem végezhetünk, amely ennek az applet kontextusnak a metódusait hívná - pl. nem tölthetünk le egy képet a getImage() metódussal stb Erre nagyon vigyázzunk - legjobb minden inicializációt a public void init() metódusunkba rakni. Természetesen, a HTML fájlunkban explicit hivatkoznunk kell a lefordított, java.appletApplet-et kötelezôen bôvítô osztályunkat. Erre szolgál a Netscape 2-ben bevezetett <APPLET> tag, amely azt mondja meg a browser JVM-jének, konkrétan melyik osztályt töltse le a szerverrôl és futtassa. Ennek kötelezôen három al-tag-e van: code: az osztály nevét adja meg (pl. Menuzoclass), width: az applet ablaka milyen széles legyen (pixelben), height: az applet ablaka milyen magas legyen (pixelben). Megj. az <APPLET>
tag-et sose felejtsük el komplementerével (</APPLET> tag ) lezárni - ennek elmulasztása esetén appletünket a böngészô nem jeleníti meg (sajnos, ez is hiányzik a legtöbb Java-könyvbôl). Megj.: az appletek fejlesztése során a ma még mindennapos JVM-inkompatibilitások (errôl lásd a Java 2 Plug-in installálását és használatát taglaló szekciót!), ill. a kommerciális böngészôk óriási memóriaigénye miatt használjuk a JDK appletviewer nevû programját. Ez gyorsan betöltôdik stb Amennyiben applet-írásra adnánk a fejünket, természetesen nem maradhat el a NS/IE alatti tesztelés sem. A wwwafucom-on található Java FAQ-ban nézzünk utána, ezek a browserek a megváltozott class file-okat hogy töltik újra (Shift-Reload stb) Mit kell tehát tudnunk? 1. az Applet osztály kutya közönséges AWT osztály Egyedüli jellegzetessége az, hogy az ôt bôvítô szubklasszok által reprezentált grafikus konténert a Java-képes
(Java-compliant/enabled) böngészôk képesek megjeleníteni - a HTML-szövegbe ágyazva. 2. az Applet osztály, mivel az AWT osztályhierarchia része, akként is tekintendô Mindig tartsuk azt a szemünk elôtt, hogy tulajdonképpen csak egy speciális konténer, amelyet GUI felület létrehozására, ill. rajzolásra ugyanúgy használhatunk, mint egy bármilyen másik AWT konténert (pl. a Frame-et vagy a Panel-t). 3. az Applet osztálynak mindig van egy default példánya, ebben különbözvén az applikációinktól, ahol ilyet a rendszer nem csinál, mielôtt a main()-ünket meghívná. Ez a példány ráadásul kitüntetett, csak erre hívhatjuk az java.awtApplet-ben definiált egyes metódusokat, más, az osztályból kézzel létrehozott példányokra nem - erre majd látunk példát. (Ezentúl nagybetûvel kezdem és italic-kal szedem az Applet-et, amikor a java.awtApplet osztályra, és nem csak úgy általában a felhasználói appletekre gondolok) 4. az appleteknek az
init()-en és pár speciális társán kívül nincs kitüntetett metódusa Erre mindenképp gondoljunk akkor, amikor szinkron, nem eseményvezérelt mûködést szeretnénk benne végrehajtani - pl. egy ciklust, ami valamit scrolloz a képernyôn Rendes applikációs prografejlesztôi múlttal a hátunk mögött rávágnák, hogy ekkor a legjobb módszer arra, hogy ez valóban fusson is, hogy pl. a start()-ba rakunk egy while(true) ciklust (ami pl. a getGraphics()-szel megszerzett grafikus kontextsura ír), amelyben azért néha várakozunk egy kicsit (Thread.sleep), hogy a böngészô JVM-jét ne terheljük le nagyon. Persze ebben van egy kis buktató - amennyiben azt szeretnénk, hogy ez a ciklus ne fusson tovább, amikor a böngészô egy másik lapra átmegy, akkor az applet stop() metódusában átállított globális változót figyelve állítsa le saját magát - pl. break-eljen ki a végtelen ciklusból, hogy a vezérlés elhagyhassa a start()- 51 ot. Amikor a
böngészô visszatér az adott oldalra, a start() úgyis meghívódik újra, lehet elölrôl kezdeni a ciklust. Ezzel elérhetjük, hogy a processzoridôt nem terheljük addig, amíg az applet nem látható Vigyázat! Mint majd látni fogjuk, az appletek nem egészen úgy mûködnek, mint az applikációk threadelés szempontjából. Amennyiben az appletünk fô szálában (pl. az init()-ben) egy végtelen ciklust kreálunk, akkor az - eltérôen a rendes alkalmazásoktól! annyira leterheli a böngészô által az applet számára adott thread-et, hogy semmi másra nem marad ideje - a GUI kirakására sem Ekkor explicit sleep()-ek sem segítenek majd. Azaz, a legegyszerûbb végtelen ciklusokat vagy blokkoló mûveleteket is rakjuk külön thread-be! 5. Természetesen, amennyiben nem akarunk valamit animálni (amihez szinkron mûködés - egy while ciklus - kell), hanem az aszinkron eseménykezelés (mint fent pl. a Rajzolo-példánk) elegendô, osztályunkat ugyanúgy építsük
föl, mint a rendes applikációban. Sajnos, mivel abban az esetben fôosztályunk a Canvas-t bôvítette, viszont itt osztályunknak kötelezôen az Applet-et kell bôvítenie, nem ússzuk meg két osztály használata nékül. Ez ráadásul kommunikációs bonyodalmakat is jelent - el kell döntenünk, mit valósítsunk meg az Applet-leszármazott fôosztályban, és mint a Canvas leszármazottjában. A szeparáció még viszonylag logikus: az Applet-be a GUI-nkat felépítô programrészek kerülnek, míg a Canvas-leszármazott gyakorlatilag ugyanaz, mint volt, azzal a különbséggel, hogy onnan töröljük a GUI létrehozását. Nem árt azt sem pontokba szedve átnézni, hogyan kell egy applet kódját rendes applikációvá konvertálni, és viszont (sajnos, mivel a legtöbb könyv kapásból koncepcionálisan rosszul vezeti be az appleteket, szó sem esik arról, hogy ez a konverzió a gyakorlatban hogyan történik - épp ellenkezôleg, szerencsétlen olvasó egész idô alatt
meg lesz arról gyôzôdve, hogy az appletek és az applikációk két, teljesen különbözô világ): applet -> applikáció konverzió • döntsük el, melyek azok az applet kontextustól függô dolgok, amelyeket egy applikációban meg lehet csinálni, és melyek azok, amiket nem. Egy getImage()-t pl applikációban is hívhatunk (némi különbséggel persze: a Toolkit osztály getDefaultToolkit() statikus metódusa által visszaadott példányra kell azt meghívni), viszont egy showDocument()-et nem, hisz hiányzik a browser, ami képes lenne egy adott frame-ébe küldött HTML fájlt megjeleníteni. Hasonlóan, a hangkezelés (getAudioClip()) csak appletekben mûködött a JDK1.2 elôtt (most már a newAudioClip() applikációkban is használható) • döntsük el, appletünk kimenete min jelenjen meg. Amennyiben az csak egy egyszerû számítást stb. végzett, még csak nem is kell neki egy GUI-t kreálnunk, hisz az eredményt a standard outputon is megtekinthetjük.
Minden más esetben viszont, amikor az appletünk extenzíven használja a GUI-t, kreáljunk egy explicit Frame-t az applikációnkból (amely fôosztálya akár maga is kiterjesztheti Frame-et, hogy konstruktorába egyszerûen átmásolhassuk az esetlegesen sok widget-et a felületre rakó applet-kódot, hogy ne kelljen mindegyik add() elé beszúrnunk a Frame referenciát). Ez lesz a megfelelôje az applet natív, HTML-oldalba ágyazott ablakának. A Frame-ünk esetében természetesen sose feledjük, hogy annak default layout managere BorderLayout - ezért egy explicit setLayout(new FlowLayout()); hívást mindenképp ki kell adnunk, hogy a Panel (és egyben az Applet) default layout managerét installájuk a Frame-ünkbe. (Megj: ennek Swing-ben már nincs jelentôsége, ott ui. a JApplet és a JFrame conent pane-je ugyanazt a BorderLayout managert használja!) • mindig gondoljunk arra, hogy egy applet összes metódusában (az init()-ben is, eltérôen pl. a main()-tôl!) van
this, mert egy applet osztályát a böngészô mindig példányosítja. Ezért vigyázva konvertáljuk kódunkat - ha a legkevesebb plusz kódolást akarjuk végezni, akkor a legegyszerûbb a Framebôl számaztatni osztályunkat (különösen akkor, ha a paint()-et is override-olta az applet, azaz a felületére nem csak GUI widgeteket helyezett, hanem rajzolt is) és azt a main()-bôl kapásból példányosítani, az init() tartalmát pedig kizárólag az osztályunk konstruktorába másolni (amiben természetesen még - minden, volt init()-es utasítás elôtt - egy setLayout(new FlowLayout()) és a GUI felépítése után egy méretezés (ide egyszerûen csak másoljuk át a HTML APPLET tag width és height mezôinek értékét - a magasságot lehet, hogy kicsit meg kell növelnünk - kb. 40-nel -, ui egy Frame-en belül az y koordinátákat a Frame fizikai bal felsô sarkától számítja a Java, azaz y=20-nál is még vígan takarni fogja a Frame fejléce az oda kirajzolt
dolgokat. A fenti Menuzo osztályban is ezért adtam hozzá 40-et minden y koordinátához), és egy show() hívás. 52 applikáció -> applet konverzió • Amennyiben applikációnk bármilyen GUI felületet használ, akkor a GUI felület felépítését végzô kódot másoljuk át az init()-be (semmiképp sem a paint()-be, mert semmi szükség minden egyes paint() híváskor azt újra kirajzoltatni). Vigyázzunk arra, hogy mivel a Frame default layout managere BorderLayout, egy explicit setLayout(new BorderLayout()); hívást mindenképp ki kell adnunk az Applet this-ére, hogy az applikáció default layout managerét installáljuk az appletünkbe. • Ha applikációnk ezen felül valamilyen olyan AWT osztályt kiterjesztett, amelynek paint()-jét is override-olta, akkor - mivel applet-leszármazottunk kapásból kiterjeszti az Applet osztályt és emiatt az Applet paint()-jét is override-olhatjuk - az applikációs kódot egyszerûen átmásolhatjuk az itteni
paint()-be. • Természetesen, a biztonsági korlátozások miatt, próbáljuk úgy áttervezni programunkat, hogy ne próbáljon elérni lokális fájlokat, ill. a hoszt szerveren kívüli hosztokat Ha képtelenek vagyunk átalakítani úgy programunkat, hogy ezeket ne használja, nézzük át a Java aláírt applet-ekkel kapcsolatos irodalmát. • Applet osztályunk bármilyen interfészt implementáhat, akár többet is, és belôle akárhány példányt képezhetünk, hogy esetleg szálakként, eseménykezelôkként használjuk ôket. Ezeknek a példányoknak az init(), paint() stb metódusai viszont már nem lesznek kitüntetett metódusok, azaz azokat a rendszer nem fogja meghívni (és, ahogy már említettem, ezekbôl az új példányokból az Applet jónéhány metódusát már nem leszünk képesek elérni). Természetesen - csakúgy, mint egy szálat reprezentáló osztályban akármennyi plusz metódust definiálhatunk az osztályon belül Ha pl szálként akarjuk
felhasználni, akkor implementáljuk a Runnable-t és definiáljuk (override-oljuk) a run() metódust; ha eseményeket akarunk az osztály példányaival végeztetni, akkor az adott eseménykezelô interfészeket és az azokban deklarált metódusokat implementáljuk stb • Természetesen ha applikációnk plusz Frame-eket is nyitott az elsôn kívül, akkor - mivel az elsô Frame-nek az applet natív, beágyazott ablakát feleltettük meg - azokat itt is nyugodtan megnyithatjuk. Ilyenkor viszont készüljünk fel arra, hogy ezeket a Frame-eket a rendszer figylemeztetô üzenettel ékesíti, hogy ôket a gyanútlan felhasználó ne nézhesse a lokális programok által kirakott ablakoknak, és ne írjon beléjük pl. jelszavakat, bankszámlaszámokat stb, amiket aztán az appletek simán, egy Socket-en keresztül, visszaküldhetnének egy szerveroldali programnak, amely azokat eltárolná. • Érdemes <APPLET> tag-jeinket kapásból - legalábbis a tesztelés idején - a .java
file-ba közvetlenül beírni. Így egyrészt mindig tudni fogjuk (anékül, hogy a hozzá tartozó HTML file-t meg kellene nyitni), hogy az adott applet natív ablaka mekkora, másrészt, egy Java forrásfile-ot a tesztelésre kiváló appletviewer programnak át lehet adni, az ugyanis kizárólag csak appleteket hajlandó futtatni, semmilyen más szöveget nem ír ki (így a standard HTML tag-eket sem veszi figyelembe). Természetesen, amikor már nagyjából végleges az applet-ünk, azt nem árt más JVM-ek (M$, Netscape), ill. böngészôk alatt sem kipróbálni (persze, amikor ezt a jegyzetet olvassa a T. Olvasó, valószínû már minden böngészô a Sun saját JVM-jét fogja plug-in formájában használni, így nem lesz gond a JVM-inkompatibilitással). • Még egyszer: ne feledjük, hogy egy (akár grafikus is) applikációban ha pl. végtelen ciklust használunk, azt egy grafikus appletben mindenképp egy szálba kell raknunk, hogy a browser egyáltalán megjelenítse az
applet GUI-ját, ill. grafikus kontextusát Példák a konverzióra Elôször konvertájuk elôzô Menuzo alkalmazásunkat appletté. Applikációnk egyetlen, a Frame-et bôvítô, ill. a MouseListener-t implementáló osztályból áll Mivel az Applet (és emiatt minden gyerekosztálya) önmagában grafikus komponens, így nem kell a grafikus felület külön létrehozásáról gondoskodnunk, sôt, a paint() automatikus override-olása (hisz az Applet gyerekosztályában vagyunk!) miatt sem kell olyan konténer-, ill. Canvas- gyereket lérehozni, amelybe rajzolhatunk. Az applikáció által implementált interfészekhez tartozó összes metódust, ill az osztály minden változóját szintén változtatás nélkül átvesszük. Mivel eredeti programunknak override-olt paint()je, azt egy az egyben az appletünk paint()-jébe húzzuk át Az átírt programban bold-dal szedtem a változtatásokat; általában kikommenteztem az applikáció kódjából elhagyottakat, és az új kódot
szintén bold-dal alájuk írva: // <applet code=Menuzo.class width=300 height=300></applet> import java.awt*; 53 import java.awtevent*; import java.appletApplet; public class Menuzo extends Applet implements MouseListener { //static final int NUMBER OF ELEMENTS = 9; final int NUMBER OF ELEMENTS = 9; Label[] labelarray = new Label[NUMBER OF ELEMENTS]; String[] labels = {"1","2","3","1","2","3","1","2","3"}; int startX; // public static void main(String[] args) { new Menuzo(); } // Menuzo() { public void init() { setLayout(null); for (int i=0; i<NUMBER OF ELEMENTS; i++) { add(labelarray[i] = new Label(labels[i])); labelarray[i].setBounds(20,(i+2)*20+5,40,10); // applet-ben i+2-t irjuk at i-re! labelarray[i].addMouseListener(this); } // for // setSize(300,300); // show(); } // init public void paint(Graphics g) { g.drawRect(5,startX,160,20); } public public public public void
void void void mouseClicked(MouseEvent e) {} mouseExited(MouseEvent e) {} mousePressed(MouseEvent e) {} mouseReleased (MouseEvent e) {} public void mouseEntered(MouseEvent e) { for (int i=0; i<NUMBER OF ELEMENTS; i++) if (e.getSource() == labelarray[i]) { startX=i*20+40; break; } // applet-ben a +40-et toroljuk! repaint(); } // mouseEntered } // class Természetesen ez, ahogy van, nem elég általános. Próbáljuk meg a getParameter() segítségével úgy átírni, hogy az APPLET tag-on belül a PARAM tag-ben átadhassunk információkat. A PARAM nevének (name mezô) adjunk könnyen algoritmizálható névkonvenciót, hogy egy for-ból, ill. Stringkonkatenációból tudjuk azokat olvasni: fieldX legyen az (ahol ha X>9, akkor decimális számként ábrázoljuk: 10, 11 stb). Legyen egy nrOfEntries paraméter is, amely megadja, hány ilyen stringünk lesz Ezt töltjük a NUMBER OF ELEMENTS-be, amely természetesen mostantól nem final. Mivel a programban az eseménykezelés,
ill. a paint() nem változik (azokat már generikusként írtam meg), azokat kihagytam a kódból. Láthatjuk, hogy nem csak stringeket, hanem számokat is átadhatunk a PARAM tag-gel - ezeket természetesen konvertálnunk kell String típusról az adott alaptípusra. Erre szolgál az 11-es JDK-ig az Integer wrapper osztály parseInt() metódusa (sajnos csak ez), 1.2-es JDK-tól pedig minden numerikus wrapper osztály parse<típus>() metódusa. Most csak azokat a kódrészleteket szedtem bold-dal, amelyek változtatások az elôzô kódhoz képest. Láthatjuk, hogy mivel az applet valódi konstruktora (amely többek között eddig a minden metóduson kívül esô inicializátor kifejezéseket is lefuttatta) az init() meghívódása elôtt fut le, a Label-jeink feliratáit, ill. referenciáit tároló tömböket csak az init()-ben, a nrOfEntries beolvasása után inicializáltuk. Megj.: nem kell leterhelnünk a webmastert akkor, ha elhagyjuk a nrOfEntries paramétert Ekkor a
paraméterek számát egy sima for ciklussal határozzuk meg; amíg a getParameter() nem null-t ad vissza, addig vannak PARAM paraméterek. // <applet code=Menuzo2.class width=300 height=300> //<param name=nrOfEntries value=2> //<param name=field0 value="Ez egy String"> //<param name=field1 value="Ez egy masik String"> //</applet> import java.awt*; import java.awtevent*; import java.appletApplet; public class Menuzo2 extends Applet implements MouseListener { //static final int NUMBER OF ELEMENTS = 9; int NUMBER OF ELEMENTS; //Label[] labelarray = new Label[NUMBER OF ELEMENTS]; Label[] labelarray; 54 //String[] labels = {"1","2","3","1","2","3","1","2","3"}; String[] labels; int startX; public void init() { setLayout(null); NUMBER OF ELEMENTS = Integer.parseInt(getParameter("nrOfEntries")); labelarray = new Label[NUMBER OF ELEMENTS];
labels= new String[NUMBER OF ELEMENTS]; for (int i=0; i<NUMBER OF ELEMENTS; i++) { labels[i] = getParameter("field"+i); add(labelarray[i] = new Label(labels[i])); labelarray[i].setBounds(20,i*20+5,140,10); labelarray[i].addMouseListener(this); } // for } // init [] } // class Látható, milyen pofonegyszerûen generalizálható úgy az applet-ünk, hogy az az adatait a felhasználótól kapja. Lássunk egy példát arra, hogyan kell az AWT fejezet utolsó példáját appletté alakítani. Már az elôbb említettem, hogy mivel az a Canvas osztályt már kiterjeszti, ráadásul nem az a default felület, amire minden GUI komponenst rak (az elôbb ui. azért volt olyan könnyû konvertálnunk a Frame-et kiterjesztô alkalmazást appletté, mert a Frame-et ott felsô szintû komponensként használtuk, azaz amibe minden más kerül. A példában persze csak rajzoltunk rá), ezért sajnos két osztályt kell használnunk Minden eseménykezelést és rajzolást hagyunk a
Canvas-leszármazottban (CanvasLeszarmazott), hogy ne kelljen az osztályok példányai közötti kommunikációval veszôdnünk, az Applet-leszármazott csak a saját appletablakát tölti fel a használni GUI-elemekkel (többek között a CanvasLeszarmazott példánnyal is). A CanvasLeszarmazott konstruktora üres, mert a listenerek hozzáadását is átraktuk az Applet-leszármazottba. A CanvasLeszarmazott osztálynak nem kell elérnie az ôt példányosító Applet-leszármazott példányát, mivel mindent magában intéz. // <applet code= KomplexRajzoloApplet.class width=400 height=400></applet> import java.awt*; import java.awtevent*; import java.appletApplet; public class KomplexRajzoloApplet extends Applet { public void init() { // a fôosztály konstuktorából (a két Listener-addon kívül) mindent idemásoltunk // Frame f = new Frame(); setLayout(new BorderLayout()); CanvasLeszarmazott can=new CanvasLeszarmazott(); Panel p = new Panel(); Choice c=new Choice();
c.add("circle"); c.add("Rect"); c.addItemListener(can); p.add(c); // Choice - ItemListener Checkbox cb; cb=new Checkbox("filled",false); cb.addItemListener(can); // Checkbox - ItemListener p.add(cb); CheckboxGroup cbg =new CheckboxGroup(); cb=new Checkbox("blue",false,cbg); cb.addItemListener(can); p.add(cb); cb=new Checkbox("red",false,cbg); cb.addItemListener(can); p.add(cb); cb=new Checkbox("yellow",true,cbg); cb.addItemListener(can); p.add(cb); can.addMouseListener(can); // Canvas - statikus mukodes can.addMouseMotionListener(can); // Canvas - dinamikus mukodes // f.add(p,"South"); add(p,"South"); f.add(this,"Center"); // Canvas Frame-be add(can,"Center"); // Canvas kozepre // f.pack(); // f.show(); } // init } // class // //class KomplexRajzolo extends Canvas implements ItemListener, MouseListener, MouseMotionListener class CanvasLeszarmazott extends Canvas implements
ItemListener, MouseListener, MouseMotionListener 55 { int firstX, firstY, newX, newY; // a rajzolas kezdo (mousePressed)- es a // pillanatnyi (mouseDragged)/veg (mouseReleased) koordinataja boolean isRect; // negyszog vagy ellipszis? itemStateChanged-bol allitjuk boolean isFilled; // kitoltott vagy nem? itemStateChanged-bol allitjuk Color color; // milyen szinben rajzoljunk? itemStateChanged-bol allitjuk // public Dimension getPreferredSize() { return new Dimension(200,200); } // Canvas + BorderLayout! public public public public void void void void mouseEntered(MouseEvent e){} // MouseListener nem hasznalt metodusai mouseExited(MouseEvent e){} mouseClicked(MouseEvent e){} mouseMoved(MouseEvent e){} // MouseMotionListener nem hasznalt metodusai // innentôl minden ugyanaz, ami volt (itemStateChanged, paint, mouse(motion)listener-metódusok stb [] // public static void main(String a[]) // { // new KomplexRajzolo(); // } // main } // class Végezetül, konvertáljuk át a
net-kezelô fejezet végén található kliens/szerver chattert úgy, hogy appletekként futhassanak! Gyakorlatilag nem kell rajta semmit változtatni (azaz az alapvetô kommunikáció ugyanúgy zajlik majd, mivel a normál verzióban sem egymással kommunikának közvetlenül a kliensek, hanem a szerveren át - a grafikai felület alapjainak átírása pedig nagyon rövid idô alatt megvan). Egyedül a kliens kódját kell megváltoztatni (természetesen szem elôtt tartva a kötelezô thread-használatot blokkoló mûveletek esetén): // <applet code=ClientApplet.class width=400 height=400></applet> import import import import java.awt*; java.awtevent*; java.io*; java.net*; import java.appletApplet; public class ClientApplet extends Applet implements ActionListener, Runnable TextArea ta = new TextArea(); TextField tf = new TextField(); PrintStream os; // println()-hoz; public void run() { try { // mivel run() nem override-olható több dobott kivétel
deklarálásával Socket soc = new Socket(getCodeBase().getHost(),8888); DataInputStream is = new DataInputStream(soc.getInputStream()); os = new PrintStream(soc.getOutputStream()); while(true) { ta.append(isreadLine() + " "); } // while } catch (Throwable e) {System.outprintln("error" + e);} } // Client() throws Throwable { public void init() { // Frame f = new Frame(); setLayout(new BorderLayout()); // f.add(ta,"Center"); add(ta,"Center"); // f.add(tf,"South"); add(tf,"South"); // f.pack(); // f.show(); tf.addActionListener(this); new Thread(this).start(); } // init public void actionPerformed(ActionEvent e) { os.println(tfgetText()); tf.setText(""); } // actionPerformed // public static void main (String[] s) throws Throwable // { // new Client(); 56 { // } // main } // class Két fontos dolgora fel kell hívnom a figyelmet. 1. Sajnos programunkat át kellett úgy írnunk, hogy a blokkoló socket
stream-várakozást egy külön szálban futtassuk - a netkezelést tágyaló fejezetben külön kihangsúlyoztam, hogy ezt alapesetben miért nem kell megtenni. Érdemes kipróbálni, mi történik akkor, ha a GUI kirakása után - esetleges várakozást érdemes betoldani, hogy lássuk, az se segít semmit! - azonnal elkezdünk várakozni a szerverrôl jövô input streamre - az applet GUI felülete még csak ki sem rajzolódik! Mindent megpróbálhatunk (az alap sleep()-en kívül, hogy ezzel segítsük az AV-t kirajzolni a GUI-t) - helper osztály metódusának meghívását sleep() után, hogy az várakozzon - eredmény nékül. Kizárólag a fenti, többszálú megoldás mûködik 2. Amennyiben applet-osztályunk ugyanannak az osztálynak még egy példányát létrehozza, ebben az új példányban az Applet olyan metódusait, mint az itt is használt getCodeBase(), már annak a példánynak a referenciájára nem hívhatjuk meg (lásd: NullPointerException). Ez azt jelenti,
ezeket a böngészô már nem inicializája tisztességgel Az nem jelent semmit, hogy az adott példány a java.awtApplet közvetlen leszármazottja - a böngészô csak annak a konkrét Applet-példánynak engedélyezi az ilyen java.awtApplet-beli metódusok meghívását, amelyet konkrétan (a HTML APPLET tag megtalálása után) ô inicializált Természetesen, az új példányokból az eredeti, a böngészô által inicializált Applet példányra meghívhatjuk a szóban forgó metódusokat (pl. tároljuk annak referenciáját egy statikus változóba - az init()-bôl a this-t kiírjuk például) Annak, hogy a pédánkban látszólag nem ez történik, oka az, hogy a Thread konstruktorának a this-t, tehát a saját, a browser által inicializált referenciánkat adtuk át. Próbáljuk a Thread-et példányosító és elindító utasítást átírni new Thread(new ClientApplet()).start();-ra: mindjárt látni fogjuk, hogy az új példányon át valóban nem lehet meghívni az
Applet fenti metódusait, azaz NullPointerException -t kapunk, ha azt megpróbáljuk. A Java 2 Plug-in Mint ahogy már említettem, a Java-t legtöbben talán éppen az inkompatibilis Netscape (továbbiakban: NS) / Microsoft Internet Explorer (továbbiakban: IE) JVM-ek miatt kritizálták – joggal. Az elmúlot két évben a két említett cég gyakran évekkel késve (!) követte az áppen aktuális JVM-et, már ha egyáltalán követni akarta. A Microsoft JVM-je pl eleve nem teljes értékû, ui kihagyták belôle a javarmi csomagot, hogy RMI-s alkalmazások ne jelentsenek az ActiveX-nek konkurenciát. Szó se róla, ettôl függetlenül a ‘régi’ browserek (4-es sorozatú, elvileg JDK 1.1-kompatibilis böngészôkrôl van szó – NS esetében 405 fölött) között a Microsoft JVM-je egyértelmûen jobb és gyorsabb. Ez persze nem jelenti azt, hogy a legegyszerûbb 1.1-es applet el fog indulni, amennyiben bármiféle 11-es eseménykezelést tartalmaz – ez jelenti a szûk
keresztmetszetet ezeknél a régi broswereknél. Szerencsére újabban változott a helyzet a a Java 2 Plug-in megjelenésével. Ez egy externális DLL (NS)/ ActiveX komponens (IE), amely, amennyiben azt a HTML file explicit elôírja, a böngészôbe épített, nem-éppen-szabványos NS/MS JVM helyett indul. Amennyiben a plug-in nincs installálva, a böngészô azt automatikusan megkeresi és megkérdezi, installálhatja-e - tehát nekünk, applet-íróknak, nem kell a szerveroldalra raknunk semmit ahhoz, hogy a felhasználó valóban el tudja majd indítani az appletünket. Sajnos, ezt a legtöbb java-s alkotás rosszul magyarázza, ui. a mûvek legnagyobb része azt mondja, a Swing-osztályokat rakjuk fel az appletünk mellé (no igen, akkor a hiányzó Swing osztályokat valóban letölti a browser osztálybetöltôje, de ez az alapjaiban rossz NS/MS JVM-et még nem javítja fel). Szerencsére a plug-in installálása végtelenül egyszerû. Miután letöltöttük a
http://java.suncom/products/plugin/ URL-rôl, az InstallShield mindent elintéz, kézzel nem kell semmit konfigurálgatnunk. Egyedül arra kell figyelnünk, hogy amennyiben kifejezetten a Java 2 új csomagjait használjuk egy appletben (pl. Swing), akkor mielôtt az appletet (pontosabban szólva az azt hivatkozó HTML fájlt) feltöltenénk a szerverünkre, a HTML file-ot vessük alá a http://java.suncom/products/plugin/12/featureshtml URL-rôl letölthetô konverternek Ez konvertálja át úgy, böngészôfüggetlen módon az APPLET tag-ünket, hogy az az adott böngészôbe betöltse a plug-in-t, ill. ha az nincs installálva, akkor letöltesse azt a felhasználóval. A konverter használata igen egyszerû: kicsomagolás után a HTMLConverter.class-t indítsuk el, paraméterként megadva neki a konvertálni kívánt HTML fájlt. Figyeljünk arra, hogy a ZIP fôkönyvtárában található property fájl a CLASSPATH-ban legyen! A konverter ezen felül még másfajta template-ekkel is
rendelkezik, amelyekkel csak NS ill. IEformátumra is lehet konvertálni az APPLET tag-eket - olvassuk el a dokumentációját A szálak használata - multiprogramozás Java-ban 57 A Java abban is elôremutató nyelv, hogy végre nyelvi szinten támogatja a párhuzamos programozást, ami bizony nagyon hiányzik a szabványos C++-ból. Az, hogy olyan, nem éppen széles körben használt nyelvek, mint az Ada vagy a Modula-2, támogatják a multiprogramozást, sajnos nem jelent gyógyírt olyan esetben, amikor egy multithreadelt, platformfüggetlen (!) szerver alkalmazást szeretnék pillanatok alatt összehozni (azaz számunkra a gyors fejlesztés fontosabb, mint az esetlegesen alacsonyabb sebesség egy C/C++-beli, lényegesen nehezebben fejleszthetô alkalmazáshoz képest). A szálak használata pofonegyszerû a nyelvben. A kétféle módszer, melyekkel felhasználásuk lehetôvé válik (a Runnable interfész implementálása, ill. a Thread osztály kiterjesztése)
gyakorlatilag ugyanazon tennivalókat teszi szükségessé, így - szerencsére - a szálak területén is ugyanaz a helyzet, mint ami pl. az appletek és normál applikációk területén - ha egyszer megértjük, mi is az alapvetô különbség közöttük, pillanatok alatt tudunk konvertálni a kétféle reprezentáció között. Kezdjünk is egy olyan példaprogrammal, amely bemutatja, hogyan kell szálakat létrehoznunk Java alatt. Ez nagyon egyszerû: példányosítjuk a Java szabványos Thread osztályát kiterjesztô saját osztályunkat, és meghívjuk a példányra a Thread start() metódusát. Ez gondoskodik arról, hogy az osztályban override-olt public void run() metódust a rendszer immár párhuzamosan, a háttérben meghívja, miközben a többi szál, ill. a fôprogram vezérlése tovább fut Megj.: kezdôk által elkövetett típushiba az, hogy kézzel hívják meg a run()-t Ez természetesen engedélyezett (nincsen olyan metódus, amit ne hívhatnánk meg explicit,
kézzel), tesztcélokra is jó (hogy aktuális thread példány nélkül lássuk, mûködik-e az adott run()-törzs), de sose feledjük: ha ezt tesszük, a run() addig nem tér vissza, amíg nem végzett feladatával. Ha viszont a Threadstart()ra hagyjuk a run() konkurens hívását, akkor a start() után a vezérlést azonnal visszakapjuk, miközben a run() is elindul class ExtendingThreadCounterExampleWithHelperClass { public static void main (String[] s) { int i=0; new HelperClass().start(); while (i++<100) System.outprintln("Visszakaptuk a vezérlést a start() hívás után!"); } // main } // ExtendingThreadCounterExampleWithHelperClass class HelperClass extends Thread { int i; public void run() { while (i<100) { System.outprintln(i++); } } // run() } // HelperClass A programban négy dolgot figyeljünk meg: 1. new HelperClass()start(); - példányosítjuk a Thread-et bôvítô osztályunkat, majd a new által visszaadott Thread (szülô)típusú objektum
start()-ját meghívjuk. Azt fontos kihangsúlyozni, hogy bár a new precedenciája alacsonyabb a metódushívásénál, ez esetben mégsem kellett new HelperClass()-t zárójelezni, ugyanis HelperClass()önmagában értelmetlen, ui. - próbáljuk ki feltétlen! - egy konstruktort metódusként explicit nem hívhatunk meg! 2. class HelperClass extends Thread: az elôbb láttuk, hogy ennek az osztálynak a példányát hozzuk létre. Az osztály, mivel bôvíti Thread -et, szálként is viselkedhet, azaz a Java garantálja, hogy amennyiben példányosítjuk, és a Thread-bôl örökölt start()-ját meghívjuk, akkor a szintén a Thread-bôl örökölt (a szülôosztályban üres törzsû, azaz semmit sem csináló - vö. AWT komponensek, konténerek üres törzsô, override-olható paint()-e) public void run()-ját override-olva egy olyan metódust definiálhatunk, amit a futtató rendszer párhuzamosan hajt végre. Természetesen ezen metódus mellett más metódusokat is
elhelyezhetünk osztályunkban - pl. magát a main()-t is, amivel önmagunkat példányosítjuk, hogy ne kelljen két osztályt használnunk. A net-kezeléses fejezetben látunk erre is egy példát. 3. public void run(): a szülôtôl örökölt, ott üres törzsû, azaz semmit sem csináló metódust (ugyanezzel a szignatúrával!) override-oljuk. 58 4. nem rendeltük hozzá a new által visszadott thread referenciát semmihez A legtöbbekben önkéntelenül felvetôdik a kérdés: nem fogja a GC ezt a thread-et emiatt azonnal leállítani és megsemmisíteni? A válasz: nem, a JVM-nek mindig van egy belsô referenciája a szálakhoz. Egy szálat emiatt nem is állíthatunk le úgy, hogy a new által visszadott thread referenciát hozzárendeljük elôször egy referencia típusú változóhoz, majd ezt kinullázzuk. Sajnos, ma is sok könyv elköveti azt a hibát (pl a Waite Java 1.2 How-To-ja), hogy ennek az ellenkezôjét állítja Ennek a programnak egy jellegzetes kimenete
a következô: 26 27 28 29 30 31 32 Visszakaptuk 33 Visszakaptuk 34 Visszakaptuk 35 Visszakaptuk 36 Visszakaptuk 37 Visszakaptuk 38 Visszakaptuk 39 Visszakaptuk Visszakaptuk Visszakaptuk a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! a vezérlést a start() hívás után! Minden futtatás során más-más eredményt látunk majd, éppen annak függvényében, egy szuszra, azaz egy idôszelet alatt hány ciklust tud a rendszer végrehajtani. A fenti képernyôkimenet viszonylag ideális állapotot tükröz, amikor a rendszer szinte ciklusonkét ide-oda váltogat a háttérben futó szál és a fôprogram végrehajtása között. Mivel programjainkban általában érdemesebb a
Runnable interfész implementálását preferálnunk (mindjárt sort kerítek arra, hogy elmagyarázzam a különbséget a kettô között!), érdemes a fenti példát annak használatával is lekódolni: class ImplementingRunnableCounterExampleWithHelperClass { public static void main (String[] s) { int i=0; new Thread(new HelperClass()).start(); while (i++<100) System.outprintln("Visszakaptuk a vezérlést a start() hívás után!"); } // main } // ImplementingRunnableCounterExampleWithHelperClass class HelperClass implements Runnable { int i; public void run() { while (i<100) { System.outprintln(i++); } } // run() } // HelperClass A döntô különbség az elsô példához képest az, hogy itt nem a Thread-et bôvítjük, hanem a Runnable interfészt implementáljuk. Az azt implementáló osztály példányát (ami itt névtelen példány, mert nem rendeltük semmihez) viszont kötelezôen át kell adnunk a Thread konstruktorának. Ennek az az oka, hogy a Runnable
interfész csak egyetlen metódust deklarál, run()-t, és a Java nem is tudja, hogy minden olyan 59 osztály, ami ezt az interfészt implementálja, szálként viselkedik (az elôbb már említettem, hogy nincsenek a Java-ban olyan értelemben foglalt nevek, amiket pl. a rendszer nem enged kézzel meghívni - ilyen pl a public void run()). Viszont a Thread osztály, ami szintúgy implementálja ezt az interfészt, konkrétan tartalmazza azt a kódot a start() metódusában (mindenképp nézzük meg az osztály forrását!), ami az operációs rendszert megkéri egy új szál indítására. Ezért kötelezô az, hogy a Runnable interfészt implementáló osztály példányát kötelezôen át kell adnunk a Thread konstruktorának. (A Thread osztálynak a start() mellett rengeteg más metódusa is van - pl. a sleep() metódusát sokszor használni fogjuk) Ez a magyarázata a new Thread(new HelperClass()).start(); utasításnak Lássunk egy-két fejlettebb példát is. Mindenképp
ki kell azt hangsúlyozni, hogy egy Threadleszármazott (ill Runnable - implementáló) osztálynak akárhány pédányát képezhetjük, és minden egyes példány run()-ja, ill. példányváltozói egymástól teljesen függetlenek lesznek Erre példaként lássuk a következô programot: import java.awt*; class FourTextFieldsWithStaticAccess { static TextField[] tf = new TextField[4]; public static void main (String[] s) { Frame f = new Frame(); f.setLayout(new FlowLayout()); tf[0] = new TextField("0",10); tf[1] = new TextField("0",10); tf[2] = new TextField("0",10); tf[3] = new TextField("0",10); for (int i=0;i<4;i++) f.add(tf[i]); f.pack(); f.show(); Thread tempThread; (tempThread = new ThreadClass(0)).start(); tempThread.setPriority(1); (tempThread = new ThreadClass(1)).start(); tempThread.setPriority(3); (tempThread = new ThreadClass(2)).start(); tempThread.setPriority(7); (tempThread = new ThreadClass(3)).start();
tempThread.setPriority(9); } // main } // class class ThreadClass extends Thread { int indexToAccess; ThreadClass(int index) { indexToAccess = index; } public void run() { while (true) // végtelen ciklus - lásd: run() csak egyszer hívódik { FourTextFieldsWithStaticAccess.tf[indexToAccess]setText ( "" + ( Integer.parseInt ( FourTextFieldsWithStaticAccess.tf[indexToAccess]getText() ) // Integer.parseInt; belül String, int-et ad vissza +1 // az Integer.parseInt átal visszaadott int-et megnöveljük 1-gyel ) // int->String konverzió (üres stringhez konkatenálva). String.valueOf(int) ); // setText //try { Thread.sleep(20); } catch (InterruptedException e) {} } } } // while // run() // class 60 Lásd még: String A program egy Frame-et nyit, és abba berak négy TextField-et (tizes oszlopszámmal, hogy az ezrestízezres nagyságrendû számok is láthatóak legyenek), mindegyik szövegét a 0 stringre inicializálva. Ezután létrehoz négy példányt a
ThreadClass osztályból. Most a new operátor átal visszadott referenciát nem dobjuk el azonnal, hanem (a start() meghívása után) arra még egy Thread.setPriority()-t is hívunk, amellyel beállítjuk az adott szál prioritását (1:minimum, 10: maximum), hogy lássuk, az explicit prioritási szint mennyiben befolyásolja a (egymáshoz képest) kapott idôszeletek mennyiségét. A ThreadClass osztályból csak egy darab van, viszont négy különbözô TextField-et kell elérnie a négy példányának. Hogyan lehetne tudtára adni ezeknek a példányoknak, hogy konkrétan melyik TextField-et kell update-elniük? Azt kapásból el is felejthetjük, hogy a négy TextField-nek külön-külön létrehozunk egy-egy külön osztályt, és azokat külön-külön példányosítjuk. Ez volna látszólag a legegyszerûbb megoldás, viszont nagyon nehezen bôvíthetô - mi lenne akkor, ha nem négy, hanem tíz thread-et szeretnénk egymás mellett futtatni, és tíz TextField-ben nyomon
követni a nekik juttatott processzoridôt? Ez azzal járna, hogy kézzel kellene egyrészt új textField1, textField2 textFieldn referenciákat adnunk a fô osztályba, másrészt, a helper osztály kódját kézzel mindig le kellene másolni kis változtatásokkal (a neve: ThreadClass1, ThreadClass2, ThreadClassn). A megoldás: a fôosztályban hasznájunk egy TextField-tömböt, a helper osztály konstruktorának pedig passzoljuk át az elérni kívánt TextField tömbön belüli indexét. Ez példányosítás során, kapásból a konstruktorhívás során történik, így el sem felejthetük: (tempThread (tempThread (tempThread (tempThread = = = = new new new new ThreadClass(0)).start(); ThreadClass(1)).start(); ThreadClass(2)).start(); ThreadClass(3)).start(); Jól látható, hogy a ThreadClass konstruktorának egy int paramétert adunk át - az elsô példánynak a 0. indexet, a másodiknak az 1.-t és így tovább Ezzel a huszáros vágással el is intéztük a lehetô
legjobb bôvíthetôséget. Még azt a kérdést kell a tervezés során meggondolnunk, hogy a ThreadClass-ból hogyan érjük el az ôt példányosított osztály TextField-jeit. Mivel a fôosztálynak egyetlen példánya sincs (a ThreadClass példányosítása stb a main()-ben történik), így this sincs, amit a ThreadClass konstruktorának esetleg átpasszolhatnánk. Marad az osztálynéven át történô elérés, azaz, a tf tömböt statikusként deklaráljuk Megj.: OOP-ben általában fôbenjáró bûn egy újrafelhasználható, ill komoly library-ban levô osztályban amiatt használni statikus változókat, hogy az osztály által példányosított másik osztály példányainak konstruktorának ne kelljen átpasszolni az ôket példányosító példány referenciáját – mi történik ugyanis, ha valaki esetleg példányosítja a fôosztályunkat, ezáltal nehezen átlátható dependenciákat létrehozva (éppen a statikus osztályváltozók által)? Jelen esetben ezt nem
olyan komoly baj, de amikor komoly programrendszereket tervezünk, erre feltétlen legyünk figyelemmel, még ha a szép módszer kicsit kényelmetlenebb és több kódolást igénylôbb is! Ennyit a fô osztály mûködésérôl. Következik a ThreadClass Konstruktora, mint már említettem, átveszi az elérni kívánt TextField tf-beli indexét, és azt egy példányváltozóba eltárolja. A run(), természetesen, egy végtelen ciklust tartalmaz, melyben egy viszonylag bonyolult kifejezés található: FourTextFieldsWithStaticAccess.tf[indexToAccess]setText ( "" + ( Integer.parseInt ( FourTextFieldsWithStaticAccess.tf[indexToAccess]getText() ) // Integer.parseInt; belül String, int-et ad vissza +1 // az Integer.parseInt átal visszaadott int-et megnöveljük 1-gyel ) // int->String konverzió (üres stringhez konkatenálva). String.valueOf(int) ); // setText Lásd még: String Ez hogy mûködik? Lekérdezzük az adott indexû TextField-bôl getText()-tel az
elôzô ciklusban beállított String-et, s azt, mivel string-hez nehéz 1-et adni, átkonvertáljuk az Integer wrapper osztály parseInt() metódusával. Az ezen metódus átal visszaadott int-et már minden további nélkül 61 inkrementálhatjuk (kérdés: miért nem használhatjuk a ++ operátort itt?). Az inkrementált értéket visszakonvertáljuk String-gé - itt az erre az egy esetre felüldefiniált + operátorral adtuk azt hozzá egy üres stringhez, de általában preferáljuk inkább - hatékonysági okokból! - a String osztály valamelyik valueOf() metódusát, az ui. nem gyárt egy nagy csomó temporális objektumot, ellentétben a string konkatenációt végzô + operátorral - és setText-tel visszaírjuk a TextField-be. Megj.: a thread-ek, amennyiben azok önzôek (azaz a fenti forrásban kikommentezett Threadsleep()-et nem hívják meg pl minden egyes ciklusban), géptôl (CPU teljesítmény stb) és platformtól függôen terhelhetik le a JVM-et. Gyengébb
gépen a fenti program lehet, hogy odáig sem jut el, hogy a Frame-et megjelenítse, annyira leterhelik a futó szálak. Amennyiben ilyen furcsa, megmagyarázhatatlan hibákat észlelünk, mindig gondoljunk arra, hogy lehetséges, hogy a program látszólagos lefagyásában egy önzô, nem sleep/yield-elô szál is ludas lehet, különösen szerényebb gépeken! Próbáljuk ki a fenti programban felemelni a szálak számát 10re, és nézzük meg, mi történik - lehetséges, hogy a Frame meg sem jelenik! Talán érdemes röviden áttekinteni, mit lehet a Thread bôvítése, ill. a Runnable interfész implementálása mellett, ill. ellenében elmondani A Thread bôvítése kicsit kényelmesebb - elôször is, egy, a Thread-et bôvítô osztály példánya már magában is Thread, amire start()-ot, ill. az összes, a Thread osztályban definiált metódust kapásból hívhatjuk is. Emellett, mivel az aktuális példány Thread típusú, nem kell longhand-eket írnunk: egy
Thread.currentThread()join() vagy egy Threadsleep() hívás helyett elég a join(), ill a sleep() Ezen felül az osztály példányát nem kell a Thread konstruktorának átadnunk, ellentétben a Runnable interfész implementálásának esetével. Persze a Java-beli többszörös öröklôdés-hiány megbosszulja magát A Runnable interfész implementálása minden olyan esetben hasznos, amikor az objektummodellünk olyan, hogy szeretnék minél kevesebb osztályt használni, viszont ez ellentétben van a Java egyszeres öröklôdésével. Szinkronizáció Nagyon érdekes témát veszünk elô a továbbiakban: a szinkronizáció kérdését. Ezt a témakör nagyon kevés könyv dolgozza fel érthetôen (sajnos, az ELTE könyve sem tartozik ezek közé). Tegyük fel, akarunk egy saját, bár meglehetôsen primitív (nincs bounds checking, azaz indextúl- és alulcsordulás-ellenôrzés, ill. korlátos a tömb, amit használunk, de ez a jelenség lényegének megvilágítását tekintve
irreveláns) stack osztályt (hasonlóan a java.utilStack-hez) csinálni, amely belsôleg egy tömbben tárolja le a neki adott elemeket, ill. abból adja vissza a kért számokat A tömböt egy indexszel indexeljük, amit kézzel kezelünk (feltesszük, hogy ez az index mindig a következô üres index indexét tartalmazza, azaz pop() esetén az elérés elôtt elôzetesen csökkentenünk kell az indexet, hogy az utolsó valid indexre mutasson, betétel esetén pedig a push() után kell inkrementálnunk). A stack osztályunkban (csakúgy, mint a Java dinamikus tároló osztályaiban - hisz a java.utilStack-nak része mind a push(), mind a pop()!) implementáljuk mind a push(), mind a pop()metódusokat, és feltesszük, hogy ezeket egymástól független szálak hívják - ahogy az valós életben is valószínû, egy termelô-fogyasztó felállásban, azaz amikor az adatok forrása azok nyelôjétôl viszonylag függetlenül (akár más sebességgel is!) rakja be a stack-re az új
elemeket. A Stack osztályunk tehát így nézne ki: class SajatStack { int index = 0; int[] adattomb = new int[100]; int pop() { index= index-1; return adattomb[index]; }// pop void push(int i) { adattomb[index] = i; 62 index= index+1; }// push } // SajatStack A két, fent mutatott metódust pedig a két, egymástól teljesen független thread hívogatja - ugyanarra az egy SajatStack példányra. A data racing jelensége a következô esetben következhet be: tételezzük fel, hogy az x thread adogatja az új int-eket, míg az y thread azokat pop-olja. Tegyük fel, x éppen elpush-olta az új karaktert, de még nem ért az index inkrementálásához, mert egy másik szál közben preemptálta. Ekkor, mivel nem tudta x megnövelni az indexet, az adatstruktúránk inkonzisztens állapotban van (ui. az index az utolsóként betett elemre mutat): 1 2 3 4 index = 3 Amikor az x thread feléled, nem biztos, hogy ugyanazt az állapotot találja, ahogy a tömböt hagyta
lehetséges, hogy közben az y, pop-oló thread elindult, és kiszedte a 3-mas értéket, majd eggyel visszaállította az indexet: 1 2 3 4 index = 2 Ez azt jelenti, hogy a termelô átal a verembe rakott 4-est a fogyasztó sosem látja majd. Most, hogy az x (pusholó) thread visszakapta a vezérlést, az index mostani értékét megnöveli eggyel: 1 2 3 4 index = 3 Ez azt jelenti, hogy az olvasó (y) thread a 3-mas számot kétszer kapja majd meg, a négyest viszont sohasem. A következô program élôben, mindenféle turpisságok nélkül (a legtöbb könyv, amely ilyen példákat mutat, eléggé el nem ítélhetô módon sleep() hívásokkal besegít a context switch-be, azaz thread-váltásba sajnos ezek a példák emiatt alapvetôen rosszak) bemutatja, hogy valóban, mivel jár, ha hanyagul konstruáljuk meg adatszerkezeteinket. Figyelem! A JIT-et defaultból használó JDK-kban lényegesen kisebb lesz az esélyünk, hogy valóban inkonzisztenciát tapasztaljunk -
ilyenkor vagy próbáljuk futtatni a programot egy régebbi JDK alatt, vagy kapcsoljuk ki a JIT-et a java.exe -Djavacompiler=NONE kapcsolójával. 63 class UjStack { int index = 0; long[] belsoVector = new long[200000]; long pop() { if (index>0) { long i = belsoVector[index-1]; index--; return i; } else return 0; } void push(long i) { belsoVector[index]=i; index++; } } // class class Termelo extends Thread{ static long termSumma; static boolean vege=false; UjStack s; Termelo(UjStack s) { this.s = s; } public void run() { for (long i=0; i<100000L; i++) { s.push(i); termSumma +=i; if (i%1000==0) System.outprintln(i); // debug - lassuk epp hol tart } // for vege=true; } // run } // Termelo class Fogyaszto extends Thread { UjStack s; Fogyaszto(UjStack s) { this.s = s; } public void run() { long summa =0; while(true) { long tempLong =s.pop(); summa +=tempLong; if (Termelo.vege && sindex==0) // teszteljuk indexet is, mert amikor a Termelo veget jelez, meg ki kell olvasni a
maradekot a stack-rol { System.outprintln("Fogyasztoban latott osszeg: "+ summa); System.outprintln("Termelobol tenylegesen kiirt osszeg: "+ TermelotermSumma); break; } // if } // while } // run } // Fogyaszto class HibasStack { public static void main(String args[]) { UjStack s = new UjStack(); new Termelo(s).start(); new Fogyaszto(s).start(); } } A program 1-tôl 100000-ig egyesével elszámol, az egyik thread átal hívogatott push() metódusban ezeket berakja a stack objektumunkra, a pop() pedig kiveszi. Ezen utóbbi metódusról mindenképp meg kell jegyezni, hogy 0-val tér vissza akkor, ha éppen üres a stack, azaz index alulcsordulás van. Egy igazi adatszerkezetben természetesen pl. ArrayIndexOutOfBoundsException-t illik dobni ilyen esetekben Itt csak azért választottam ezt a megoldást, hogy valamivel egyszerûbbé tegyem a kódot (és mivel a 0 visszatérési érték úgy sem változtatja meg az algoritmus helyességét bizonyító összeget, amit a
Fogyaszto osztályban számolunk). A Termelo osztály szintén kiszámolja az általa elpush()olt számok összegét; ezt a long változót azért választottam statikusnak, hogy a Fogyaszto osztályból egyszerûen el tudjuk érni, amikor egymás alá kiírjuk a képernyôre a Termelo, illetve a Fogyaszto osztályban számolt szummákat. Ideális esetben ennek egyenlônek kell lennie. 64 Még egyszer: ha nem volna konkurencia, akkor ilyen hiba sosem léphetne fel, hiszen ha pl. felváltva, vagy random gyakorisággal hívogatnák a pop()-ot, ill. a push()-t, azok garantáltan mindig végigfutnának, és csak akkor lennék egyáltalán képes új metódust indítani, amikor visszatértek. Párhuzamos környezetben viszont, amikor két szál hívogatja ezeket a metódusokat, ez nem áll fenn - ilyenkor lép fel a data racing jelensége. Mivel a Fogyaszto osztály nem tudja, hányszor kell meghívnia a pop()-ot (hiszen a hívásainak egy része eredménytelen a
tömbindex-alulcsordulás miatt), ezért egy boolean változóval jelezzük feléje, ha a Termelo osztály már végzett. Ekkor már csupán ki kell üríteni a stack-et (ezért tesztelünk két feltételt a kiírás/kilépés feltételében), és kiírni a végeredményt, amely, mint látni fogjuk, JIT-es compilerek esetében átalában pontos lesz (szenvedtem is a példával eleget a példa fejlesztése közben, mert rejtélyes módon kifogástalanul viselkedett. Aztán átgondoltam a JIT mûködését, és abból jöttem rá, hogy ez a JIT miatt van), ugyanezen compilerek JIT nélkül az esetek úgy felében, a régi, JIT elôtti JDK-k pedig az esetek túlnyomó többségében hibásan futtatják a programot. Mi a megoldás? A szinkronizáció. Ez hogy mûködik? Nagyon egyszerûen: az ún synchronized kódbolokkok a synchronized után zárójelben megadott (bármilyen) objektumra szinkronizálnak. Ez azt jelenti, hogy ha a végrehajtás bárhol (pl. egy szál által konkurensen
hívott metódusban) egy ilyen szinkronizált kódblokkba kerül, akkor ez a kódblokk az adott (a synchronized zárójelei közötti) objektum úgyenevezett lock-ját, objektumzárját megszerzi, és addig nem engedi el, míg a végrehajtás ki nem kerül ebbôl a kódblokkból. Amíg egy bármilyen szinkronizált kódblokk birtokolja egy adott objektum zárját, addig semelyik másik szinkronizált kódblokk nem kezdheti meg a futást - a vezérlés mindaddig várni fog, amíg az objektumzár nem szabadul fel. Figyelem: az ELTE könyve meg sem említi, hogy nem csak metódus lehet szinkronizált, hanem kisebb kódblokkok is, amikor ráadásul bármilyen osztály bármilyen példányára történhet a szinkronizálható, nem kötelezôen a metódust tartalmazó osztály aktuális példányára! Nézzük meg elôzô példánkat. Egyetlen UjStack példányt hoztunk létre (UjStack s = new UjStack() utasítás), és ennek a referenciáját passzoltuk a Termelo, ill. a Fogyaszto
thread-ek konstruktorának Mivel, ahogy az elôbb mondtam, egy adott objektumra lehet csak szinkronizálni, hisz a Java csak objektumzárakat ismer, ez az UjStack példány pont jó lesz arra, hogy rá szinkronizáljunk - más objektum nincs a rendszerben, amire ezt megtehetnénk. A Termelo példányba nem rakhatnák a push() hívás köré egy synchronized(this)-t, mivel itt a this egy Termelo példányra mutat, ami pedig egy szimpla Thread példány, amit ráadásul a Fogyasztó példánya nem is ismer (azaz a fenti példában a referenciájuk eltárolása nélkül hoztuk létre a két szál-példányt). Viszont, ha ugyanott synchronized(s)-t használnánk (s az egyetlen UjStack példány, amelynek egy-egy metódusát a két szál külön-külön hívogatja), és ugyanezt a Fogyaszto pop()hívásával is megtennénk (azt is egy synchronized(s) kódblokkba ágyaznánk), akkor már a thread osztályok átírásával sikerülne megoldani a problémát. Persze ez csak egy a sokfajta
kombináció közül, amelyekre mindjárt ki is térek. Addig is, módosítsuk a fenti programot úgy, hogy a két metódushívás, a spush(i);, illetve a long tempLong =s.pop(); ilyen kódblokkokba kerüljenek Azaz, az elsô utasítást módosítsuk a következôképpen: synchronized(s) { s.push(i); }, a másodikat pedig ilyeténképp: synchronized(s) { long tempLong =s.pop(); }: class UjStack { int index = 0; long[] belsoVector = new long[200000]; long pop() { if (index>0) { long i = belsoVector[index-1]; index--; return i; } else return 0; } void push(long l) { belsoVector[index]=l; index++; } } // class 65 class Termelo extends Thread{ static long termSumma; static boolean vege=false; UjStack s; Termelo(UjStack s) { this.s = s; } public void run() { for (long i=0; i<100000L; i++) { synchronized(s) { s.push(i); } termSumma +=i; if (i%1000==0) System.outprintln(i); // debug - lassuk epp hol tart } // for vege=true; } // run } // Termelo class Fogyaszto extends Thread {
UjStack s; Fogyaszto(UjStack s) { this.s = s; } public void run() { long summa =0; while(true) { long tempLong=0L; synchronized(s) { tempLong =s.pop(); } summa +=tempLong; if (Termelo.vege && sindex==0) // teszteljuk indexet is { System.outprintln("Fogyasztoban latott osszeg: "+ summa); System.outprintln("Termelobol tenylegesen kiirt osszeg: "+ TermelotermSumma); break; } // if } // while } // run } // Fogyaszto class JoStack1 { public static void main(String args[]) { UjStack s = new UjStack(); new Termelo(s).start(); new Fogyaszto(s).start(); } } (Természetesen, mivel tempLong lokális vátozó, amelyet pont a pop() hívás helyén deklarátunk, a deklarációt a synchronized használata miatt a while-ra nézve lokálissá váló blokkból ki kellett hozni, ezért változott meg egy kicsit a programszöveg - lásd a félkövér kiemelést.) Megj.: ez a példa abból rossz, hogy a szálak itt tulajdonképp csak a forrás, ill nyelô folyamatokat
illusztrálják (amik bármik lehetnek), amelyek természetesen teljesen függetlenek a stack-et megvalósító programtól, így általában nem is állnak rendelkezésre forrásnyelvi szinten. Viszont gyönyörûen bemutatja a példa, hogy milyen lehetôségeink vannak, amikor esetleg más felállásban kell szinkronizálnunk. Gondoljuk át, milyen lehetôségeink vannak még a szinkronizációra. Az elsô, amikor magában a két szál run()-jában szinkronizáltunk rá egy közös objektumra, már láttuk. Ugyanezt megtehetjük a közös UjStack objektumon belül is - ráadásul ott már elég lesz this-re, azaz a közös, egyetlen példányra szinkronizálni. Ezekkel a változtatásokkal a program így néz majd ki: class UjStack { int index = 0; long[] belsoVector = new long[200000]; long pop() { synchronized(this) { if (index>0) { long i = belsoVector[index-1]; index--; return i; } else return 0; } } 66 void push(long l) { synchronized(this) { belsoVector[index]=l;
index++; } } } // class class Termelo extends Thread{ static long termSumma; static boolean vege=false; UjStack s; Termelo(UjStack s) { this.s = s; } public void run() { for (long i=0; i<100000L; i++) { s.push(i); termSumma +=i; if (i%1000==0) System.outprintln(i); // debug - lassuk epp hol tart } // for vege=true; } // run } // Termelo class Fogyaszto extends Thread { UjStack s; Fogyaszto(UjStack s) { this.s = s; } public void run() { long summa =0; while(true) { long tempLong =s.pop(); summa +=tempLong; if (Termelo.vege && sindex==0) // teszteljuk indexet is, mert amikor a Termelo veget jelez, meg ki kell olvasni a maradekot a stack-rol { System.outprintln("Fogyasztoban latott osszeg: "+ summa); System.outprintln("Termelobol tenylegesen kiirt osszeg: "+ TermelotermSumma); break; } // if } // while } // run } // Fogyaszto class JoStack2 { public static void main(String args[]) { UjStack s = new UjStack(); new Termelo(s).start(); new
Fogyaszto(s).start(); } } Természetesen a példát tovább ragozhatnánk, pl. bevezetve egy helper, semmilyen adattaggal nem rendelkezô osztályt, hogy abból egy példányt kreálva arra szinkronizálhassunk, de ezt fölöslegesnek tartom, mert remélhetôleg az eddigiekbôl mindenki megértett azt, hogy mire is jó a szinkronizáció. Még azt szeretném kihangsúlyozni, hogy nem csak kódblokkokat szinkronizálhatunk, hanem teljes metódusokat is: ekkor a synchronized kulcsszót írjuk a metódus visszatérési értéke elé. Ekkor viszont kizárólag az aktuális példányra szinkronizálhatunk, ui. nem adhatjuk meg ilyenkor, hogy melyik objektumra történjék a szinkronizáció. Ezzel a kis változtatással az UjStack osztályunk (a többi osztályt, amely az elôzô példához képest változatlan, a hely kímélése érdekében nem másoltam ide) a következôképp fog kinézni: class UjStack { int index = 0; long[] belsoVector = new long[200000]; synchronized long pop() {
if (index>0) { long i = belsoVector[index-1]; index--; return i; } else return 0; } 67 synchronized void push(long l) { belsoVector[index]=l; index++; } } // class Miért deprecated a suspend/resume? Lépjünk tovább: az elôbb már említettem, hogy egy szinkronizált kódblokk, ill. metódus akkor ereszti el az adott objektum objektumzárját, ha a kódblokkból, ill. metódusból a vezérlés kilép Ezen felül még akkor is eleresztjük az objektumzárat, amikor egy kivételt dobunk egy szinkronizált kódblokkból/metódusból, vagy (kódblokk esetén) break-elünk egyet. Viszont van egy hatalmas baklövés a nyelvben, amelyet szerencsére a Java2 már orvosolt azáltal, hogy a suspend()-et (és párját, a resume()-t) elavultként deklarálta. A probléma abban áll, hogy egy suspend()-elt szál nem engedi el az esetlegesen általa birtokolt objektumzárat, és emiatt komoly deacdlock-szituációval nézhetünk farkasszemet a következô esetben: tegyük fel, hogy van
két szálunk, thread1 és thread2. Tegyük fel, mindkettô azonos objektumon operál, így a lockjuk is közös (az elôbbi stack-példa erre remek iskolapélda lehet). Tételezzük fel, hogy thread2 megszerzi az adott objektum zárját, és amíg magánál tartja, thread1 suspend()-eli ôt. Ekkor persze thread2 nem engedi el a lockot (nagyon komoly hiba! Ha elengedné, nem lenne ilyen hibalehetôség a nyelvben!). Ha viszont ezután a thread2-t felfüggesztô thread1-nek bármikor is szüksége lesz az adott objektum zárjára, végtelen idôkig fog várni (hacsak közben nem resume()-olta thread2-t, hogy az azután kilépve a szinkronizált bolkkjából/metódusából, a zárat elengedje). Emiatt ne használjuk ezt a két metódust (arról nem is szólva, hogy a Netscape 3 elszállhat a suspend()-tôl) - használjunk helyette wait()/notify()-t egy közös (szinkronizált) objektumon, ugyanis a wait() elengedi az objektumzárat, amikor felfüggeszti önmagát. Miért deprecated a
stop? Ha már a suspend()/resume()-párról szó esett, hadd essék szó errôl a témáról is, ugyanis ezt sem tárgyalja a legtöbb mû (az ELTE-s könyv, lévén 1.1-es, természetesen meg sem említi a problémát), ill rengeteg téveszme kering róla, egyesek ördögöt festenek a falra hozzá nem értô kijelentéseikkel, közben pedig az igazság (az, hogy az eseteknek csupán kis százalékában okozhat hibát a stop() direkt hívása) teljesen más. Problémájának lényege teljesen más, mint a suspend()-é: az, hogy egy kívülrôl le-stop()-olt szál azonnal befejezi futását, és még azt sem várja meg, hogy egy esetleges szinkronizált blokkot végigcsináljon. Ez az azonnali megszakadás azt jelenti, hogy ha pl egy thread egy adatbázist min két lépésben update-ol, és a 2. lépés elôtt következik be az adott szálra való stop() hívás, akkor a 2 lépés sosem hajtódik végre, akár szinkronizált blokkban tartózkodott akkor a szál, akár nem. A nyelv
tervezése során erre mindenképp figyelni kellett volna, és legalábbis a szinkronizált blokkokra biztosítani azt, hogy azokat a kívülrôl lestop()olt szál még végigcsinálja. Hogyan helyettesíthetô? Egyszerû - egy globális boolean (pl. isStopped) változóval, amit a run()-ban periódikusan ellenôrzünk, és ha látjuk, hogy a szál környezete azt le szeretné állítani, akkor takarítunk, elvégezzük az adatok konzisztenciájához kellô dolgokat, és simán csak a run() végére lépünk (pl. break-kel a szokásos while(true) végtelen ciklusból). Amikor egy szál run()-ja a végére ér, akkor a szál meghal, többé nem lesz újra elstart()olható - újra kell majd kreálni ahhoz, hogy újra elindíthassuk. Wait/notify és a polling / busy waiting elkerülése Szálprogramozós fejezetünk végéhez közeledünk, már csak a wait/notify tágyalása van csak hátra. Emlékszünk még fenti UjStack-es példánkra? Ott bizony a Fogyaszto thread nagyon szokszor
hívogatja pop()-ot feleslegesen - azaz olyankor, amikor a belsô tömbünkben semmilyen elem nincsen. A 68 pop() ugyanis ellenörzi, hogy van-e bármilyen elem is a tömbben, és ha egyetlen egy sincs, simán kilép (még egyszer: tisztességes megvalósításban pl. NoSuchElementException-t vagy ArrayIndexOutOfBoundsException-t dob). Hogy lehetne elkerülni az ilyen fölösleges metódushívásokat? Hasonlóan, hogy lehetne azt biztosítani, hogy ne kelljen a stack-en átpasszolt maximális elemszámnál azt nagyobbra választani (arra a partikluáris esetre felkészülve, hogy a termelô thread annyira kisajátítja magának a virtuális gépet, hogy a fogyasztó semmilyen processzoridôt nem kap, amíg a termelô ki nem írta minden adatát; a fenti példában ezért választottam 200 000-res elemszámúra a tömböt, ugyanis abba csak 100 000 elemet tölthet maximum a push()), hanem a push()-ban azt is figyelni, hogy egy viszonylag kisméretû belsô tömböt használva se
kelljen index-túlcsordulástól tartanunk - azaz a push() addig várjon, amíg a belsô tömb végén minimum egy üres tömbelem nem keletkezik. Természetesen ezt elsô hallásra nagyon könnyû lekódolni - hiszen már láttuk, a pop() hogy kezeli a 0nál alacsonyabb tömbindexek problémáját, oldjuk most úgy meg a problémát, hogy egy while()-ban figyeljük azt, hogy éppen mekkora a tömbindex, és attól függôen lépünk tovább a while cikluson, hogy az 0-nál magasabb (pop() számára zöld út), ill. a tömb méreténél alacsonyabb-e (push() ekkor illeszthet csak új elemeket a tömb végére). Elsô ötletünk az lenne, hogy a fenti pop, ill push metódusokat ennek megfelelôen ilyen foglalt várakozással (busy waiting) szereljük fel (hogy miért nem a szinkronizált metódusokba/kódblokkokba raktuk a busy waiting-et, mindjárt elmagyarázom!): class UjStack { static int MAX ELEMSZAM = 1000; int index = 0; long[] belsoVector = new long[MAX ELEMSZAM]; long pop() {
while(index==0) ; // használhatnánk persze <=-t is synchronized(this) { long i = belsoVector[index-1]; index--; return i; } } void push(long l) { while(index==MAX ELEMSZAM) ; // használhatnánk persze >=-t is synchronized(this) { belsoVector[index]=l; index++; } } } // class Vegyük észre, hogy a két while törzse egyetlen pontosvesszô, azaz semmit sem csinál - egészen addig vár, amíg kivehetô elem nem érkezik a verembe (pop()), ill. hely nem szabadul rajta fel (push()) Mindenképp vegyük észre, hogy ez a szinkronizált kódblokkon kívül van - ha a while() ciklusokat azokba beraknánk, nem nehéz megjósolni, mi történik: igen, deadlock. Gondoljunk bele: ha pl pop() észreveszi, hogy index értéke 0-ra csökkent, akkor belekezd egy ilyen foglalt várakozásba. Igen ám, de közben nem engedi el a lockot, így push()-nak már esélye sincsen, hogy valaha is végrehajtódjon, így a pop() továbbjutási feltétele, amit egyedül a push() lenne képes
biztosítani, sosem teljesül - deadlock van. Viszont ha úgy kezdünk el várakozni, ahogy a példában is látjuk, azaz anélkül, hogy magunknál tartanánk az objektumzárat, nem lehet baj, mert a másik thread vígan végrehajthatja a maga metódusát, ezzel elôbbutóbb zöld utat adva a mi továbbhaladásunknak is. Természetesen láthatjuk, hogy az ilyenfajta busy waiting, foglalt várakozás nagyon leterheli a virtuális gépet - ez a program nagyságrenddel lassabban fut, mint az elôzô. Ezért nem árt megismerni, mire is jó a nyelv wait/notify mechanizmusa. Egy tômondatban összefoglalva: amennyiben egy thread egy wait()-et hív egy közös objektumra (ezt megteheti, ugyanis a wait() az Object osztályban van definiálva, tehát minden osztálynak létezik wait()metódusa - és ugyanez áll a notify()-ra is!), az a szál leáll, és egészen addig várakozik, míg egy másik szál ugyanezen az objektumra meg nem hívja a notify()-t. Ahhoz, hogy ezt használhassuk,
mindenképp rendelkeznünk kell az adott (amire wait/notify-olunk!) objektum zárjával. Ez az utolsó mondatom így, összehasonlítva az elôzô, busy waiting-elô programmal, sokakat hidegzuhanyként érhet - ott azt hangsúlyoztam, hogy a while-os várakozást nem szabad szinkronizált 69 kódblokkba rakni, mert addig is nálunk lesz az objektumzár, amíg várakozunk - és így szépen deadlockszituációt kapunk. Viszont a wait()-nek van egy eddig még nem kihangsúlyozott tulajdonsága: elengedi az objektumzárat, amikor az adott objektumra meghívjuk, és ezért lehet szinkronizált kódblokkban. Ezt, valamint a fenti egymondatos leírásomat figyelembe véve azonnal meg is írhatjuk programunk nem busy waiting-elô, viszont kis bufferméret esetén is garantáltan hibátlanul mûködô verzióját (megint csak az UjStack osztályt mutatom meg, mivel a többi változatlan): class UjStack { static int MAX ELEMSZAM = 1000; int index = 0; long[] belsoVector = new long[MAX
ELEMSZAM]; long pop() { synchronized(this) { while(index==0) { try { wait(); } catch (InterruptedException e) {} } long i = belsoVector[index-1]; index--; notify(); return i; } // sync } // pop void push(long l) { synchronized(this) { while(index==MAX ELEMSZAM) { try { wait(); } catch (InterruptedException e) {} } belsoVector[index]=l; index++; notify(); } } } // class Próbáljuk ki - látjuk, valóban sokkal gyorsabb, mint a busy waiting-elô elôzô változat. Mûködése is pofonegyszerû: ha pop() nem tud semmit sem kivenni a stackbôl, mert nincs benne egyetlen elem sem (a köv. elem indexe 0), akkor elkezd addig várni, amíg lesz elem Amikor kiadja az aktuális példányra a wait()-et, akkor annak lockját - hiába van szinkronizált kódblokkban! - elengedi, így a push()-ba az ôt hívogató thread garantáltan be tud lépni - és annak a feltételével sem lesz garantáltan gondja, mivel a pop() és push() elején levô feltételek egymást kizárják: ha a pop() nem mehet,
akkor push() mehet (hisz 0 az index), amikor pedig a push() kényszerül várakozni (a köv. elem indexe már túlmutat a tömbön), akkor a pop() futhat garantáltan, hiszen 0-nál index lényegesen magasabb. Mivel garantált az, hogy amennyiben az egyik thread blokkolódik, a másik thread garantáltan meg tudja szerezni az objektumzárat és elôbb-utóbb meg tudja hívni a notify()-t, így továbbengedve az addig wait()-elt szálat, garantált a hibátlan mûködés. A szálkezelés további területeit az ELTE jegyzete szépen ismerteti, így azokat itt nem veszem elô. Stream (folyam)-kezelés A Java file-kezelése eredendôen folyam-alapú, ami azt jelenti, hogy az adatokhoz sorosan juthatunk hozzá, és nem seek-elhetünk szabadon, azaz a fájlmutatót nem változtathatjuk. Nem kell megijedni, egyáltalán nem nehéz a Java fájlkezelése, és, ha megértjük a Java stream-jeinek logikáját, egyátalán nem találjuk majd nehéznek azt, hogy bármilyen átviteli csatorna
(socket és file - a két legfontosabb közülük) felett stream-mûveleteket végezzünk. A stream-ekrôl azt kell tudni, hogy vannak külön input (bemenô) és output (kimenô) folyamok. Ez szigorú típusosságot jelent abban az értelemben, hogy már fordítási idôben észreveszi a rendszer, ha egy bemeneti folyamra ki akarunk írni valamit, és viszont. 70 Nemcsak ilyen felosztása létezik a stream-eknek, hanem az, hogy azok fizikai folyamok-e, vagy wrapperek (Java-s szóhasználatban filtered-nek is nevezzük), azaz amelyek kötelezôen elvárnak példányosításkor egy már létezô fizikai stream példányának referenciáját. Az elôbbiekre példa a FileInputStream, amely egy konkrét filerendszerbeli fájlból tud (kizárólag) bájtokat olvasni, az utóbbira pedig a DataInputStream, amely a konstruktorának átadott fizikai folyamból (pl. file-ból) bármilyen bonyolult alaptípust be tud olvasni, ill. a PrintStream, amely ASCII alakra alakít át bármilyen
típusú bemenô adatot (hasonlóan a C printf-éhez). Mint már említettem, a fizikai adatfolyamok kizárólag byte-ok olvasására, ill. írására használhatóak, s emiatt használati értékük kétes. Ezen felül nem is buffereltek, azaz hatékonyságuk nagyon alacsony Ezért fontos azt mindenképp megértenünk, hogyan használhatjuk a wrapper osztályokat. Ezen felül fontos megismerkednünk két alapvetô fontosságú interfésszel, az InputStream-mel és az OutputStream-mel. Ezekrôl azért kell tudnunk, mert jónéhány, általunk is használt metódusnak ez a visszatérési étéke - konkrétan a Socket osztály getInputStream() és getOutputStream() metódusairól van szó. Ezen metódusok visszatérését kapásból bármilyen folyamtípus konstruktorának átadhatjuk, azaz a következô kódrészlet helyes: DataInputStream is = new DataInputStream(socketInstance.getInputStream()); PrintStream ps = new PrintStream(socketInstance.getOutputStream()); Látszik, hogy a két
wrapper stream konstruktorának átadtuk a getInputStream() és a getOutputStream()visszatérési értékét. A fizikai stream-ek, mint említettem, kizárólag byte-okat tudnak írni, ill. olvasni Erre szolgál a InputStream, ill. OutputStream write(), ill read() meródusa Lássunk is arra egy példát, hogy hogyan is írhatunk ki, ill. olvashatunk vissza byte-okat: import java.io*; class ByteIrasEsOlvasas { public static void main(String[] aa) throws Exception { byte a=9; FileOutputStream fo = new FileOutputStream("data.bin"); fo.write(a); fo.close(); FileInputStream fi = new FileInputStream("data.bin"); int i; System.outprintln(i = firead()); System.outprintln(i = firead()); } // main } // class Ez a program létrehoz egy FileOutputStream objektumot, amely a data.bin konkrét file-ba ír ki egyetlen byte-ot, 9-et. Ezután lezárja a stream-et, és egy FileInputStream objektumot hoz létre, amely ugyanebbôl a file-ból fog olvasni. Vegyük észre, hogy annak
bemutatására, hogy az InputStream read()-je -1-gyel tér vissza, ha már nincs több bemenô adat, kétszer hívtam a read()-et. A hibakezelést elhanyagoltam - annak egyedüli tanulsága az lett volna, hogy a read() nem EOFException-t dob, mint a DataInputStream read<Típus>() metódusai, hanem -1-gyel tér vissza (még fontos kihangsúlyozni azt, hogy van egy harmadik lehetôsége az EOF jelzésének - a DataInputStream, ill. a BufferedReader readline() metódusa null-t ad vissza akkor, ha már elfogytak a bemeneti adatok). Megj.: nagyon illogikus, hogy bár a Java a C/C++ majd’ minden negatívumával szakított, a – kétségkívül egy kezdô számára nehezen emészthetô, ui. szinte minden második kérdés arra vonatkozik, hogy miért int a read() visszatérési típusa, és miért nem byte – -1-es EOF-jelzést változatlanul átvette Az EOFException konzekvens használata sokkal jobb ötlet lett volna. Még nagyon fontos azt is észrevennünk, hogy a read()
int-tel tér vissza. Ennek oka az elôbb már kihangsúlyozott -1-es EOF-jelzés. Próbáljunk más-más adatot kiírni (azaz a-t más-más értékekre 71 inicializálni), látni fogjuk majd, hogy az eredetileg -127128 értékkészletû byte-okat visszaolvasva 0255 értékû int-eket kapunk - erre feltétlen vigyázzunk! -, -1-et pedig valóban kizárólag csak akkor, ha file vége van. Viszont ez nem jelnti azt, hogy egy int-ként olvasott 0255 számot új kiírás elôtt (write()) le kellene konvertálnunk a -128127 tartományba - errôl szó sincs. Ezt maga a write() elintézi, tehát pl egy egyszerû file másoló applikációként a következôt is írhatjuk: import java.io*; class FileCopy { public static void main (String[] s) throws Throwable { BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.bin")); BufferedOutputStream bos = new BufferedOutputStream (new FileOutputStream ("target.bin")); int justRead = 0; while(justRead !=
-1) { justRead = bis.read(); if (justRead!=-1) boswrite(justRead); } bos.flush(); bos.close(); } // main } // class Mivel ezek a fizikai stream-ek, mint az az elôbbiekbôl is kiderült, nagyon primitívek, byte-on kívül más adattípusokat nem tudnak kezelni, nem tudnak ENTER-rel lezárt sorokat (String-eket) a billentyûzetrôl olvasni, objektumok példányait nem tudják sem olvasni, sem írni, ráadásul nem is buffereltek - az idô 99%ában a wrapper osztályokkal együtt leszünk ezeket kénytelen használni. A számunkra legfontosabb wrapper osztályok a következôk: DataInputStream: minden alaptípust képest beolvasni, ill. kiírni Ezen metódusok nevét logikusan képezi a rendszer. Ezen felül van egy nagyon hasznos metódusa, a readLine(), amely segítségével a beágyazott stream-ból tudunk String-eket tudunk beolvasni. A kliens-szerver programunk szintén ennek a segítségével fog kommunikálni. PrintStream: van egy print(), ill. println() metódusa, melyek
segítségével alaptípusok formázott ASCII kiírását kérhetjük. Mindenképp jegyezzük meg, hogy bár több paramétert lehet neki átadni, azokat nem a C-féle printf()-hez hasonlóan kell megadnunk: nincs formátumstring, valamint a vesszôvel elválasztott argomentumok helyett a + operátort kell használnunk. Természetesen itt is (mint mindenhol) hívhatunk bármilyen metódust, sôt, aritmetikai kifejezések is állhatnak - megfelelôen zárójelezve. (Megj: néhány könyv (pl. Java: An IS Perspective) ennek az ellenkezôjét állítja – azt, hogy alapmûveletek nem állhatnak a println() argumentiumában, még zárójelezve sem. Ez természetesen nem igaz) BufferedInputStream / BufferedOutputStream: ennek a két stream osztálynak nincsenek nem byte-okat is kiíró/visszaolvasó metódusai, viszont automatikusan elintézi az általa wrappelt stream bufferelését, ezáltal nagyon meggyorsítva a stream-mûveleteket. Amennyiben nem csak byte-okat szeretnénk
olvasni/írni, akkor ezt a stream osztályt érdemes egy másik, szintén wrapper streambe beágyazni - pédául DataOutputStream-be, ha integer-eket stb. is ki akarunk írni, mégpedig gyorsan Hogy mennyire gyorsítja meg pl. a fájlmûveleteket egy ilyen buffer osztálynak a használata, lássunk egy példát, egy benchmark programot, amely kiír 100 000 long értéket egy fájlba, majd azokat visszaolvassa, mindezt bufferelve, illetve anélkül. Minden egyes mûvelet ms-okban számított idejét a System.currentTimeMillis() metódus segítségével számoljuk ki A példa arra is jó, hogy lássuk, hogyan is kell a stream-eket egymás köré wrappelni, ill. hogy általános esetben hova kell a bufferelt stream-eket wrappelnünk, ha az aztán a köré wrappelt filter stream metódusait úgy szeretnék elérni, hogy azok már automatikusan buffereltek legyenek. import java.io*; class FileBufferBenchmark { public static void main(String[] args) throws Exception { long elozoTime=
System.currentTimeMillis(); DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin")); for (int i =0; i<100000; i++) dos.writeLong(LongMAX VALUE); dos.close(); System.outprintln("Kiiras buffer nelkul: " + (SystemcurrentTimeMillis() - elozoTime) + " ms"); elozoTime= System.currentTimeMillis(); dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("data.bin"))); 72 for (int i =0; i<100000; i++) dos.writeLong(LongMAX VALUE); dos.close(); System.outprintln("Kiiras bufferrel: " + (SystemcurrentTimeMillis() - elozoTime) + " ms"); elozoTime= System.currentTimeMillis(); DataInputStream dis = new DataInputStream(new FileInputStream("data.bin")); for (int i =0; i<100000; i++) dis.readLong(); dis.close(); System.outprintln("Beolvasas buffer nelkul: " + (SystemcurrentTimeMillis() - elozoTime) + " ms."); elozoTime= System.currentTimeMillis(); dis = new
DataInputStream(new BufferedInputStream(new FileInputStream("data.bin"))); for (int i =0; i<100000; i++) dis.readLong(); dis.close(); System.outprintln("Beolvasas bufferrel: " + (SystemcurrentTimeMillis() - elozoTime) + " ms"); } } // main // class A program példa-kimenete: c:code>java.exe FileBufferBenchmark Kiiras buffer nelkul: 5397 ms. Kiiras bufferrel: 401 ms. Beolvasas buffer nelkul: 5628 ms. Beolvasas bufferrel: 421 ms. A programban ezen felül arra is látunk példát, hogy nem összeadás-jellegû aritmetikai mûveletet hogy végezhetünk el a println() argomentumában. A Reader/Writer osztályok Szöveges inputra, ill. outputra most már az ún Unicode karakterstream osztályokat (Reader és Writer leszármazottjai) ajánlja a Sun. Ezekre általában csak akkor van szükségünk, ha valóban internacionalizált alkalmazásokat akarunk készíteni, amit akár Japánban is használhatnak. Amennyiben alkalmazásainkat csak indogermán
nyelvterületre szánjuk, akkor nem lehet abból baj, hogy nem használjuk ezeket az új osztályokat, még akkor sem, ha tudjuk a régi, 1.0-s Stream osztályok egyes metódusairól, hogy azok használata ellenjavalt. A Sun két fô verziószám-váltásig garantálja a deprecated, elavult metódusok (mint amilyen például a DataInputStream readLine()-ja) nyelvben maradását. Ha választanunk kell egyszerû tanulhatóság/ taníthatóság és az ajánlásokhoz ragaszkodó osztályok, metódusok használata között, akkor megfontolhatjuk, belemélyedjünk-e komolyabban a 16-bites karakter stream-ekbe. Sajnos, egyes mûvek odáig mennek a régi stream-ek temetésében, hogy nem is foglalkoznak velük - lásd pl. Krüger: Go To Java 2. Viszont még sem árthat megismernünk ezeket az osztályokat, hiszen most már nagyon sokan használják ôket. Kezdjük a legfontosabbakkal: BufferedReader: abban hasonlít a régi BufferedInputStream-re, hogy buffereli az inputot. Ezen felül viszont
(a hiányzó DataReader helyett) tartalmaz egy readLine() metódust is, amelyet (a DataInputStreamhez hasonlóan) ha megtanulunk használni, gyakorlatilag a gyakorlati esetek 90%-ában meg tudjuk találni a megoldást tárolási vagy kommunikációs problémáinkra. PrintWriter: a PrintStream megfelelôje, azaz a paraméterét Stringgé konvertálja, amely string természetesen a numerikus wrapper osztályok parse<típus>() metódusaival visszakonvertálható az eredeti, bináris típusra. Ha valaki ez alapjá sem érti, mi a különbség a PrintWriter / PrintStream és a bináris adatokat író DataOutputStream által kiírtak formátuma között, akkor olvassa el a következô szekcióban szereplô figyelmeztetésemet arról, hogy szöveges adatokat a DataInputStream read<típus>() metódusaival miért nem lehet olvasni. Egy DataOutputStream pl a 601234f float változót négy bájtba (melyek értéke 40 c0 65 17) írja ki: import java.io*; class BinarisWriteFloat 73
{ public static void main(String args[]) throws Exception { DataOutputStream dis = new DataOutputStream(new FileOutputStream("target.bin")); dis.writeFloat(601234f); disflush(); disclose(); } } Egy PrintStream viszont természetesen 6.01234-et ír ki (a literál végén a float típust mutató f-et nem!) InputStreamReader: fô felhasználási területe az eredendôen 8-bites stream-ekhez 16-bites streamekkel való kapcsolódás támogatása. A következô szakaszban kifejtem, hogy a Systemin által referált példány milyen típusú - nos, InputStream, azaz 8 bites. Egy InputStream-et egy Reader közvetlenül nem tud olvasni - azaz az éppen aktuális 8 bitbôl 16 bitet nem tud csinálni, ugyanis nem tudja, hogy a felsô nyolc bittel mit kezdjen - nullázza ki ôket? Az InputStreamReader konstruktorának átadható, hogy a 8 -> 16 bit konverziót milyen karakterkódolási szabváy szerint csinálja. Windows alatt pl a default enkódolási szabvány az ISO 8859-1 (vagy
ahogy még ismerik: ISO Latin-1), ha a konstruktornak második paramétert nem adunk meg, akkor eszerint konvertál. Amúgy minden komoly enkódolási szabványt ismer a Java OutputStreamWriter: ugyanaz vonatkozik rá, mint az InputStreamReader-re, persze nem fogjuk annyit használni. Vegyük észre, hogy nincsenek sem alaptípusok, sem objektumok (bináris) kiírását, ill. beolvasását végzô Writer, ill. Reader osztályok Tulajdonképpen a Reader/Writer osztályokra csak akkor van szükség, ha komolyan vesszük a Unicode-ot, ill. esetleg internacionalizáni akarjuk alkalmazásunkat Az UTF Nagyon sokan nem értik, mi az az UTF, és egyáltalán mi szüksé van rá. Ökölszabályként elfogadható, hogy ne használjuk, mert a normál 8, ill. 16-bites PrintWriter / PrintStream, ill DataInputStream/ BufferedReader println(), ill. readLine() metódusa sokkal kézbentarthatóbb szöveges adatok írásánál, ill olvasásánál, azaz ezzel a formátummal jobb, ha nem is foglalkozunk.
Akit mégis érdekel, mire jó az UTF: Unicode karakterek tárolására kifejlesztett adatábrázolási formátum, amely abban különbözik a szabvány 16-bites Unicode ábrázolástól, hogy a max. 127 karakterkódú (tehát, amelyek legfelsô bite nulla, azaz a szabvány ASCII karakterkészletben benne levô) karaktereket változtatás nélkül, 1 bájton ír ki; a magasabb karakterkódúakat pedig 2, ill. 3 bájton (ezek tárolásánál azt hasznája ki, hogy az ábrázolási formátumból, egy adott bájtról mindig egyértelmûen eldönthetô, hogy az micsoda: ha 10-val kezdôdik, akkor az egy 3-bájtos karakter alsó két, ill. egy 2-bájtos karakter alsó bájtja; ha 110, akkor egy 2bájtos karakter felsô bájtja; ha pedig 1110, akkor egy 3-bájtos karakter legfelsô bájtja) Ezzel egy csapásra megoldja azt, hogy a zömében ASCII karaktereket tartalmazó szöveg tárolása a lehetô legkisebb háttérkapacitást igényelje (mivel az ASCII karaktereket nem 16, és még csak
véletlenül sem 24 biten tárolja; a néhol feltûnô, 127-nél magasabb karakterkódú Unicode karaktereket pedig egyértelmûen lekódolja 2, ill. 3 bájton), viszont teljesen Unicode-kompatibilis, azaz minden Unicode karaktert képes lekódolni Gyakorlatban nem használják, viszont tele van vele a DataInputStream/ DataOutputStream, ezzel is elrémítve a kezdôket. Használjunk helyette tisztán 8, ill. 16-bites stream/karakterosztályokat Lokális, külföldi piacra nem szánt terméknél legjobb választás az elôzô, mert tárolása feleakkora memóriát igényel. A konzolos input kezelése Sajnos, a Java konzoltámogatása nagyon szegényes. Az rendszer semmi olyan beépített metódust nem bocsájt rendelkezésünkre, amellyel nem-bájtjellegû (InputStream) információt közvetlenül olvasni lehetne. Ez, ha azt tekintjük, hogy a Java grafikus könyvtárai milyen fejlettek és milyen egyszerûen programozhatók, nem is tûnik olyan komoly hibának. Ellenben nem árt
megismerkednünk a konzolkezeléssel, mivel sokan olyan konzolkezelô osztályokat tanítanak / mutatnak be, amelyeket sokkal jobban is meg lehet írni. Miért is? Abból, hogy a Java egyedül egy elôre elkészített (!) InputStream objektumot bocsájt rendelkezésünkre, azonnal következik, hogy egyrészt annak van egy read() metódusa, amivel byte-okat olvashatunk a billentyûzetrôl, másrészt, hogy eköré az InputStream objektum köré konkrét, filtered stream-eket is fûzhetünk. Nézzük végig sorban, hogy ez mit jelent! 74 Ha valaki jobban belegondol, megérti, miért is használhatjuk a System.in referenciát a DataInputStream vagy az InputStreamReader konstruktorában. Olyan konstruktora ennek a két osztálynak nincs, amely System.in típusú referenciát fogadna Nézzük meg az API doksiban, mi is tulajdonképpen ez a System.in! Ez egy referencia egy elôre, a JVM által a rendelkezésünkre bocsájtott InputStream típusú példány referenciája (ráadásul ez
az in nevû mezô a System osztály statikus mezôje, hogy az osztály példányosítása nélkül is elérhessük), amely, mivel típusa InputStream, közvetlenül szerepelhet bármilyen wrapper input stream konstruktorában. Hogy mi ezek közül a DataInputStream-et, ill - áttételesen, az InputStreamReader-en át - a BufferedReader-t használjuk, azért van, mert ezzel a két osztállyal lehet a felhasználótól (ill. általánosabban: a bemeneti folyamból) sorokat beolvasni A read() metódus használata sokaknak nem tetszhet, hiszen akkor bájtonként kellene a bemenetet olvasni. Ennek viszont van egy hatalmas elônye a késôbbiekben ismertetendô readLine() + parse<típus>()-os módszerhez képest - nem igénylik egy új stream-példány létrehozását. Ekkor persze egy kicsit körülményesebb lesz bájtonként beolvasni az inputot, azt egy StringBuffer-be eltárolni (append()-del mindig hozzáfûzve az új bájtokat), és amikor a felhasználó végzett (azaz ENTER-t
ütött), azt parszálni a fent említett metódusokkal. Nagyon fontos, hogy amennyiben így dolgozunk, akkor használjunk StringBuffer-t, mert különben semmi értelme a gyötrôdésünknek, hisz egy temporális Stringhez való állandó karakter-fûzés temérdek átmeneti String példány létrehozásával jár, ami összehasonlíthatatlanul erôforrásigényesebb, mint a readLine()-ot tartalmazó DataInputStream vagy BufferedReader egyszeri példányosítása (azt is belekalkulálva, hogy ez utóbbi esetben még egy InputStreamReader-példányosítást is meg kell ejtenünk). Egyszerûbb és gyorsabb módszer, ha valóban használunk egy DataInputStream vagy egy BufferedReader (+ InputStreamReader) példányt, és ezen két osztály readLine() metódusával olvassuk be a String-et, amit a felhasználó beír, majd ezt a megfelelô numerikus wrapper osztály parse<típus>() metódusával konvertáljuk az adott alaptípusra. Ez a legtisztább és legegyszerûbb megoldás
Figyelem: sokan rávághatják, hogy ha már úgy is van egy DataInputStream-ünk mindenféle olyan metódussal, amelyek alaptípusokat olvasnak, nosza, használjuk ezeket (persze miután a DataInputStream-példányunkkal körülwrappeltük a rendszer InputStream objektumát): import java.io*; class HibasLoad { public static void main(String args[]) throws Exception { DataInputStream dis = new DataInputStream(System.in); System.outprintln(disreadFloat()); } } Ez sajnos nem fog mûködni (próbáljuk ki!). Miért? Egyszerû A billentyûzetrôl ASCII karakterek (amelyeket int-be kasztol a rendszer) érkeznek. A float 4 bájtos - beolvas a rendszer 4 bájtot (int-et, hisz a read() azt ad vissza, hogy tudja jelezni a stream végét 1-gyel), és ezeket tekinti a float-nak Azaz, nem próbálja a bemenetet parsálni, és a stringként beadott karakteres értéket (pl "6.23456") konvertálni Pl amennyiben ezt az értéket próbáljuk meg így, bájtról-bájtra a rendszernek átadni,
akkor a következôt kapjuk : 2.595724E-6 Próbáljuk ki, mi történik, ha csak az elsô négy karaktert írjuk be, és utána nyomunk egy ENTER-t - ugyanezt kapjuk. Miért? Mert csak az elsô négy bájtot dolgozta fel a readFloat(). Kipróbálhatjuk ugyanezt int-re, double-ra stb, látjuk majd, hogy valóban bináris bájtokként próbálja a rendszer értelmezni a karakteres (ASCII) inputunkat! Sajnos, erre a nagyon is valós hibalehetôségre gyakorlatilag egyetlen könyv sem hívja fel a figyelmet. A hálózat elérése Nagyon egyszerû a Java hálózati könyvtára is, amennyiben kliens/szerver alkalmazást szeretnénk készíteni. Jelen fejezetben kizárólag a socket-alapú, TCP kommunikációt tekintjük át, mivel az internetes kommunikáció legnagyobb része ilyen. 75 Megj: Csizmazia Balázs mûve, a Hálózati alkalmazások készítése felettébb ajánlott mindazoknak, akiket komolyan érdekel a témakör, valamint a közeli területek (RMI, CORBA, szervletek stb).
A socket-alapú kommunikáció széles körben használt kommunikációs forma két végpont között - a Java-ban is. Amikor a Java socket-eket használ kommunikációra, a már tárgyalt stream-eket használja a kommunikációra. Mind a kliens (a kapcsolatot igénylô), mind a szerver (a kapcsolatfelépítési igényt vevô és azt elfogadó) egy input és egy output streammel rendelkezik a másik végpont felé. Ez a két stream nagyon egyszerûen kinyerhetô egy már felépített socket-bôl. Természetesen ami az egyik oldalon input, a másik oldalon output; valamint, gondoskodnunk kell arról, hogy ezek a párok egymás komplementerei legyenek. Amennyiben pl egyszerû alaptípusokat akarunk átküldeni az internet fölött, akkor használjunk DataInputStream / DataOutputStreameket, amikor pedig soronkét akarunk szöveget átküldeni, akkor a DataInputStream / PrintStream párosítást (lásd: readLine() és println()) - ahogy azt majd a példa- applikációnkban használni is
fogjuk. Természetesen nem kötelezô kétirányú kommunikációt használnunk - ha csak az egyik irányba menô stream-et nyerjük ki a socket-bôl, akkor csak azirányba leszünk képesek üzenetet küldeni, és viszont. Amikor egy kliens/szerver módban mûködô applikáció tervezési fázisában vagyunk, el kell azt is döntenünk, hogy melyik port-on kommunikáljon a kliens a szerverrel (ennek elméleti hátterét lásd a fent említett, Csizmazia Balázs-féle mûben!). Dióhéjban annyi, hogy az 1024 feletti portokat használhatjuk, egészen fel 65535-ig. Azért persze ellenôrizzük az adott rendszeren, hogy a kiszemelt, nem szabványos portot nem használja-e már más szerver a gépünkön (bár futási idejû hibát kapunk, ha ez így van, így gondunk ebbôl nem lehet.) A hálózatkezelési modell a következôképpen néz ki: Szerver Kliens ServerSocket(port #) konnekcióra várás socketPéldány = ServerSocket.accept() socketPéldány = Socket(host, port)
visszatér, amikor a szerver bejelentkezik OutputStream = OutputStream = socketPéldány. getOutputStream(); socketPéldány. getOutputStream(); InputStream = socketPéldány. getInputStream (); InputStream = socketPéldány. getInputStream (); socketPéldány.close() socketPéldány.close() Mint látszik, nagyon egyszerû a kapcsolat modellje. Lássunk is arra egy egyszerû, de annál nagyszerûbb példát, hogy hogyan lehet nagyon frappánsan egy GUI-alapú, kétirányú konnekciót megvalósító, a szerveroldalon többszálú programot írni. Az, hogy a szerver egyszerre több klienst ki tud szolgálni, nagyon fontos egy modern szervernél kicsit furcsa volna az olyan szerver, amely egyszerre csak egy klienssel tud törôdni. Egy IRC-szerû (www.irchelporg) applikációnál pedig egyenesen kötelezô, hogy egyszerre legalább kettô felhasználó legyen bent egyidejûleg a rendszerben. A szerver kódja: 76 import java.io*; import java.net*; import java.util*; class
Server extends Thread{ PrintStream os; DataInputStream is; static Vector osStreams = new Vector(); public static void main (String[] s) throws Throwable { ServerSocket ssoc = new ServerSocket(8888); while (true) { Socket soc = ssoc.accept(); new Server(soc).start(); } // while } // main Server(Socket soc) throws Throwable { is = new DataInputStream(soc.getInputStream()); osStreams.add(new PrintStream(socgetOutputStream())); } public void run() { String s; String userName=""; try { userName = is.readLine(); } catch (Throwable e) {} while(true) { try { s = is.readLine(); for (int i=0; i< osStreams.size(); i++) ((PrintStream) osStreams.elementAt(i))println("<"+userName+">"+s); } catch (Throwable e) {} } // while } // run } // class A kliens kódja: import import import import java.awt*; java.awtevent*; java.io*; java.net*; class Client implements ActionListener { TextArea ta = new TextArea(); TextField tf = new TextField(); PrintStream os; //
println()-hoz; Client() throws Throwable { Frame f = new Frame(); f.add(ta,"Center"); f.add(tf,"South"); f.pack(); f.show(); tf.addActionListener(this); Socket soc = new Socket("127.001",8888); // az IP-t persze írjuk át! DataInputStream is = new DataInputStream(soc.getInputStream()); os = new PrintStream(soc.getOutputStream()); while(true) { ta.append(isreadLine() + " "); } // while } // constructor public void actionPerformed(ActionEvent e) { os.println(tfgetText()); tf.setText(""); } // actionPerformed public static void main (String[] s) throws Throwable { new Client(); 77 } // main } // class Hogy is mûködik ez a program? Kezdjük a szerverrel: A szerver A szerver jó példája annak, hogyan lehet megúszni a külsô osztályok használatát, fô (és mellesleg egyetlen) osztályunk ugyanis egyszerre felel az egyes kliensek kiszolgálásáért (thread-ként), ill. az új kliens-kérések fogadásáért (egy végtelen
ciklusban), majd az új kliens számára egy új, az azt kiszolgáló thread lérehozásáért, és annak az adott kliensre nézve egyedi Socket példány átadásáért. Ez utóbbit a main()-ben teszi, azaz magából az osztályból nincs szüksége példányra - ezért is tehettük meg azt, hogy ugyanabba az osztályban implementátuk a kliensek kérésének fogadását és kiszolgálását. A main() a következôket tartalmazza: ServerSocket ssoc = new ServerSocket(8888); while (true) { Socket soc = ssoc.accept(); new Server(soc).start(); } // while Azaz: példányosítja egyetlen egyszer a ServerSocket osztályt, a konstruktornak átadva a figyelni kívánt (tehát a szerver által lefoglalt) port számát (8888). A ServerSocket osztály accept() metódusa akkor tér vissza, amikor egy kliens valóban bekonnektál a szerverre - ez a metódus tehát blokkoló mûvelet (majd látjuk, mit is jelent a blokkolás). Az accept() egy Socket példányt ad vissza, amibôl már - az osztály
konstruktorában - ki tudjuk nyerni az input, ill. output stream-eket Ezt a Socket példányt passzoljuk át tehát osztályunk konstruktorának, amely a következôképp néz ki (vegyük észre, hogy miután a konstruktor lefutott, azaz new visszatér, a new által visszaadott Server objektumra meghívjuk a Thread start() metódusát - azaz elindíttatjuk a szintén ugyanebben a Thread-leszármazottban definiált run()-t: new Server(soc).start()): Server(Socket soc) throws Throwable { is = new DataInputStream(soc.getInputStream()); osStreams.add(new PrintStream(socgetOutputStream())); } Látható, hogy a konstruktorban egyrészt az adott klienst kiszolgáló thread DataInputStream típusú is példányváltozójába betöltjük a getInputStream() által visszaadott, InputStream típusú fizikai folyamot a DataInputStream wrapper osztály egy példányába csomagolva. Hasonlóan járhattunk volna el, ha pl objektumokat akartunk volna TCP felett átküldeni (akkor természetesen az
ObjectInputStream wrapper osztályt kellett volna példányosítani). A getOutputStream()visszatérésébôl egy PrintStream példányt gyártunk, a célból, hogy annak println() metódusa segítségével teljes szövegsorokat átküldhessünk a hálózaton. Ennek a referenciáját viszont nem egy, a többi példány (azaz kliens) által nem látható példányváltozóba tesszük, hanem egy statikus, azaz osztályszintû változóba. Ennek oka az, hogy egy irc-típusú chat-applikációban, amikor csak broadcast-ot engedünk meg, akkor minden egyes kliens-kiszolgáló thread-nek tudnia kell az összes többi kliens thread OutputStream (valójában: PrintStream) referenciáit, hogy azokra kiírhassa a saját kliensétôl kapott sort. Ezért választottuk osStreams tárolási osztályának statikus tárolási osztályt. Ez ráadásul egy vektor, hogy (elvileg) tetszôleges számú klienst ki tudjon a rendszer szolgálni (a felsô határ mai átlaggépeknél és JVMeknél kb. 2000-3000
- megint csak a JavaWorld idevágó tesztjeit tudom ajánlani; érdemes megjegyezni, hogy a Sun Hotspot-ja ezt a határt lényegesen felemelte): static Vector osStreams = new Vector(); Még egy fontos megjegyzés: a Java 2 a Vector osztályt a generikus Collections osztályhierarchia részévé tette, ezért is van már benne add() - a Java 2 elôtti idôkben csak addElement() volt, erre vigyázzunk, ha a programot régebbi JDK-k alatt is futtatni szeretnénk! 78 A run() metódus a következôképpen néz ki: public void run() { String s; String userName=""; try { userName = is.readLine(); } catch (Throwable e) {} while(true) { try { s = is.readLine(); for (int i=0; i< osStreams.size(); i++) ((PrintStream) osStreams.elementAt(i))println("<"+userName+">"+s); } catch (Throwable e) {} } // while } // run Ez nem csinál mást, mint létrehoz egy lokális, s nevû stringet, amelybe mindig a thread-hez rendelt kliens felôl érkezô, és a többi
kliensnek elbroadcast-olandó stringet tárolja, ill. a for ciklusban olvassa A run(), mivel élete során csak egyszer (az ütemezô által, a mi explicit start() hívásunk után) hívódik meg, tartalmaz egy végtelen while ciklust, hogy az adott thread átal kiszolgált klienstôl akár vételen számú üzenetet broadcastolhasson az összes online usernek. A ciklus elôtt még bekérjük a kliens által használni kívánt nicket (feltesszük, a felhasználó által beírt elsô string ez lesz), és azt - természetesen - egy lokális változóba írjuk. A vételen ciklus a következô utasítást tartalmazza: for (int i=0; i< osStreams.size(); i++) ((PrintStream) osStreams.elementAt(i))println("<"+userName+">"+s); Azaz lépdeljünk végig a kliensek output streamjeinek referenciáit tartalmazó vektoron (a Vector osztály size() példánymetódusával kérhetjük le egy vektorpéldány aktuális, pillanatnyi elemszámát), és írjuk ki mindegyik
streamre a saját kliensünk által beírt sort, elé a felhasználó által választott becenevet fûzve, IRC-módi <>-k között. Természetesen, mivel a Vector osztály elementAt(i) metódusának visszatérési értéke Object, azt a valódi típusra le kell castolnunk. Ezt a valódi típust tudjuk, hiszen saját kezûleg töltöttük fel osStreams vektorunkat PrintStream típusú referenciákkal. Mindenképp érdemes a zárójelezést is megfigyelni: mivel a típuskonverzió prioritása kisebb, mint a metódushívásé, ezért kellett a elementAt() visszatérésének castolását zárójelezni. Ha nem zárójeleztünk volna, akkor a rendszer elôször megpróbálta volna az elementAt() által visszaadott Object példánynak meghívni a metódusát - természetesen ilyene nincs (még egyszer: ez fordítási idejû ellenôrzés!), ezért kaptunk volna egy fordítási hibaüzenetet (pontosabban szólva, println()-nak a visszatérési értékére panaszkodott volna, hogy azt nem
tudja lecast-olni). Viszont egy PrintStream típusú objektumnak természetesen már van println() metódusa. Ennyi a szerver - mint látjuk, pofonegyszerû. Mivel a kliens felôl jövô input folyamra való várakozás blokkol, és semmi mást nem kell közben csinálni (pl. nem a mi thread-ünk felel a többi kliens átal küldött üzenetek továbbításáért), egy klienshez elég egyetlen thread-et használnunk. A kliens A kliens grafikus felülettel rendelkezik. Ennek nem csak a kényelmi funkciói vannak, hanem egy óriási elônye is: mivel a grafikus felület (melynek TextField-je input mezôként szolgál) aszinkron mûködésû, a felhasználó által beírtak validálására nem kell külön thread-ben várnunk, ugyanis nem stream-elt a GUI input komponensek elérése, hanem eseményvezérelt. Ez teszi azt lehetôvé, hogy miközben a kliens egy végtelen ciklusban a szervertôl érkezô, más kliensek átal küldött szövegre várnak (és, ha az megérkezik, azt
append()-elje a TextArea-ba), addig a felhasználó is bármikor küldhet üzenetet beszélgetôpartnereinek, ugyanis egy TextField-ben ENTER-t nyomva az ActionListener által deklarált actionPerformed() is meghívódik - teljesen aszinkron módon, a fôprogrambeli stream-blokkolódás tényétôl függetlenül. 79 A main()-ben, mivel AWT eseménykezelés csak objektumok szintjén mûködik, és a saját osztályunk implementálja az ActionListener interfészt is, példányosítjuk önmagunkat: public static void main (String[] s) throws Throwable { new Client(); } // main Az osztály konstruktorában van a szerverre váró végtelen ciklus (természetesen a kapcsolat felvétele és a stream-ek kinyerése után): while(true) { ta.append(isreadLine() + " "); } // while Az osztály, mivel önmaga is ActionListener típusú, implementálja az actionPerformed() metódust is: public void actionPerformed(ActionEvent e) { os.println(tfgetText()); tf.setText(""); }
// actionPerformed Ez a metódus tehát akkor hívódik meg (aszinkron módon), amikor a felhasználó az inputját fogadó TextField-ben ENTER-t nyom. Ekkor a szöveget egyszerûen elküldi a szerver felé (a PrintStream típusú objektum println() metódusával), illetve az input mezô tartalmát kitörli a TextField átal a TextComponentbôl örökölt setText() metódus segítségével. További lépések és javítási lehetôségek 1. kezeljük dinamikusan a be- és kilépô felhasználókat, és beceneveiket egy javaawtList listában mIRC-módi a frame jobboldali részében tartsuk. Tervezzünk egy olyan, lehetôleg minél kisebb sávszélességigényû protokollt, amely ezt updateli (segítség: használjuk vagy általunk kijelölt vezérlôkaraktereket, vagy implementájuk az RFC 1459-es IRC protokollt és használjuk az általa használt protokollelemeket -pl. msg, publikus, broadcastolandó üzenet stb) 2. implementáljuk a privát üzenet lehetôségét Amikor az egyik
felhasználó egy másik kliens nevére kattint ebben a jobb oldali nick-listában, nyissunk neki egy üzenetablakot. A szerveroldalon figyeljünk a hatékonyság kérdésére - döntsük el, mi a jobb: minden kliensnek elbroadcast-olni ezt az üzenetet, hogy majd azok válasszanak, kiírják-e publikusan vagy sem, vagy a szerveren némi túlmunkával tároljuk el a résztvevôk beceneveinek listáját, szorosan hozzárendelve az output stream-juk referenciájához (bizonyítsuk, hogy az összerendelés valóságos, azaz egy adott output referencia mellé oszthatatlansági problémák miatt nem kerülhet-e egy másik felhasználó neve), a szerver pedig keresse ki a címzett nickjét, és küldje el neki egy privát flaggel az üzenetet (hogy az ne a publikus ablakban, hanem a megnyitott privát párbeszédablakban jelenjen meg). Amennyiben egy kliens vesz egy privát üzenet flaggel ellátott üzenetet, ellenôrizze, létezik-e már a felhasználó felé nyitott privát
párbeszédablak. Amennyiben nem, nyisson egyet, és fejlécébe írja ki: "Message: <usernév>". 3. azzal a kérdéssel is foglalkozzunk, hogy hogyan érdemes a kliensoldali privát üzenet-ablakokat implementálnunk - megtehetjük-e ezt pótlólagos szálak igénybevétele nélkül is. Egy helyes döntéssel és jó OO tervezéssel futási idôben rengeteg erôforrást kímélhetünk meg (gondoljuk el, micsoda terhelést jelent msg ablakonként egy szál elindítása)! 4. (nagyon fejlett funckió, csak profiknak!) Implementáljunk kliens- és szerveroldali csatornastruktúrát! Ehhez már mindenképp érdemes a RFC 1459-es ajánlást tanulmányozni, hogy késôbb, ha esetleg klienseinkbôl, urambocsá szerverünkbôl széles körben terjesztett mIRC-killert akarunk csinálni, ne legyen sok gondunk a program protokolljának szabványosításával! 80 Biztonsági kérdések A legtöbb könyv – így az ELTE-mû is – kihangsúlyozza, mit is jelent az, hogy az
appletek restriktáltak, azaz nem végezhetnek bármiféle mûveleteket. Mivel ennek a témának a – nagyon absztrakt! – irodalma az www.SecuringJavacom-on sajnos meglehetôsen középszerû (Wiley) mûben megtalálható, most – a rövidség kedvéért – csak az appletek gyakorlati szignálásával, aláírásával foglalkozom. Ezen felül mindazon témákat is tárgyalom, amelyek a témakör megértéséhez szükségesek, viszont az említett mûbôl hiányoznak. A Securing Java gyakorlatilag semmi konkrét példát nem tartalmaz arra, hogy a Java-t hogy használhatjuk arra, hogy akár a privát és a publikus kulcsunkat legeneráljuk vele; egyetlen igazán jó fejezete (Appendix C) is az Interneten is megtalálható Java Security FAQ egy az egybeni átvétele. A Java – különösen appletkénti felhasználását tekintve – egyik nagy elônye, hogy a felhasználók biztosak lehetnek abban, hogy a távoli szerverrôl letöltött applet semmi galibát nem csinál – nem
éri el a lokális fájlrendszert, nem konnektál Socket-en át más site-ra, mint ahonnan jött (ki kell hangsúlyozni, hogy ehhez a showDocument()-nek semmi köze, az annak passzolt URL bármi lehet!) stb A Java, amely (az erôs típusellenôrzés, a referenciákon végzett aritmetikai mûveletek hiánya, a tömbindex futásidejû, állandó ellenôrzése, a véletlenül a stack-en allokált lokális objektumok hiánya stb miatt) eleve kizárja a C-ben, C++-ben oly gyakori hibák felléptét, elhárítja a távolról letöltött, esetleg rosszándékú appletek rombolását vagy csak hibás mûködését. Ez hogy is állhatna elô? Egy lefordított, bájtkódú Java programot bármikor módosíthatunk hexa- vagy diskeditorral, amennyiben ismerjük az adott, a JDK javap programjával lekérhetô utasítások bájtkódját. Ezek – jópár kivétellel - az ELTE könyvének F appendix-ében megtalálhatóak. A táblázat hiányos, és a Sun könyve, a “The JavaTM Virtual Machine
Specification” (elsô és) második kiadása ingyen letölthetô a Sun site-járól (http://java.suncom/docs/books/vmspec/), így nem igazán érdemes az ELTE táblázatával bajlódnunk Fontos megjegyzés, hogy sok (fôleg 1.1-es) Java-könyv azt mondja, egy módosított class fájlt a java.exe (lokálisan, normál alkalmazáskét futtatva) minden további nélkül lefuttat Ez ma már nem igaz, a Java 2-ben a java.exe-nek default kapcsolója a -verify; igaz, ezt kikapcsolhatjuk a -noverify kapcsolóval Lássunk egy nagyon durva példát: egy int-ként deklarált lokális változót (amely négy bájtot foglal le a stack-en) double-ként (ami 8 byte) írunk felül, és megnézzük, a stack-en ez valóban korrupciót okozott-e. Kimondottan olyan példát választottam, amely futtatásának nem lehetnek tragikus következményei, azaz a stack pointer-t nem léptetem egy meghívott metódusban félre, hogy aztán ne talájuk meg a stack-en a visszatérési címet. class Korrupcio {
public static void main(String str[]) { int a; int b = 10; a = b; } } Ha a javac.exe által generált bytekódot (Korrupcioclass) a javap segédprogrammal disassembláljuk (javap -c Korrupcio), a következôt kapjuk (ne felejtsük el a -c kapcsolót, hogy ne csak a metódusdeklarációkat lássuk!): Compiled from Korrupcio.java class Korrupcio extends java.langObject { Korrupcio(); public static void main(java.langString[]); } Method Korrupcio() 81 0 aload 0 1 invokespecial #3 <Method java.langObject()> 4 return Method void main(java.langString[]) 0 bipush 10 2 istore 2 3 iload 2 4 istore 1 5 return A main() metódus diszasszemblált utasításainak bájtkódjai a táblázatból kikeresve: bipush 10 - 10 istore 2 – 3D iload 2 – 1C istore 1 – 3C return – B1 A 3C-t (istore 1) cseréljük ki 48-ra (dstore 1), azaz 4 byte tárolása helyett 8 byte-ot tároljunk. A hex editorban (használjuk pl. az UltraEdit-et, a screenshot is azzal készült) írjuk át az adott
byte-ot, mentsünk, és futtassuk az így ‘meghackelt’ bytekódot. A hibaüzenet, amit kapunk: C:java>c:jdk1.21injava Korrupcio Exception in thread "main" java.langVerifyError: (class: Korrupcio, method: mai n signature: ([Ljava/lang/String;)V) Expecting to find double on stack Azaz a stack-en a rendszer egy double-t várt, viszont mi egy int-et adtunk át neki. Eljátszadozhatunk még ezzel, pl. nem inicializált lokális (stack) változókat szerepeltetünk jobbértékként stb Az egész lényege azonban az, hogy megértsük, mi is az a class loader, az osztálybetöltô. Ez végzi el a betöltött osztályok ellenôrzését, hogy a benne szereplô operációk valóban megfelelnek-e a Java szigorú típuskompatibilitási stb szabályainak. 82