7. fejezet

Fiókos szekrények garmadája, mindegyik hozzá való mamával. A Jáva alapépítõelemei, az objektumok. Objektumok deklarálása változókkal és függvényekkel, amelyeket ezek után metódusoknak fogunk hívni. Objektumok létrehozása és halála, életciklus a Jávában. A szemétgyûjtõ.

Emlékszel a mamára és a fiókos szekrényére a második fejezetben? Eddig határozottan egy készletnyi változóban és függvényben gondolkoztunk. Ez meg is felel a számítógépprogramozás hagyományos modelljének, ahol van egy darab fiókos szekrényünk, azaz adattárolónk és egy rakat függvényünk, ami ezen az egy állapottárolón manipulál. Ez nagyon kellemes is mindaddig, míg a programod kellõen nagyra nem nõ, utána azonban ekkor a program különbözõ funkciói és azoknak különbözõ változói igencsak összekavarják a dolgokat. Képzelj el most egy olyan modellt, ahol a program különbözõ funkcióit a hozzájuk tartozó változókkal egy egységbe fogjuk. Ez még nem elég: ilyen egységeket szabadon hozhatunk létre, egyfajtából akár többet is, mindegyiket saját változókészlettel, hívhatjuk függvényeiket, amelyek a saját változókészleten manipulálnak és aztán dönthetünk az egység elpusztításáról is, ha nincsen már rá szükségünk. Olyan ez, mintha a programunkat úgy építhetnénk, mint valami gépet; elõször megtervezzük az alkatrészeit, majd legyártjuk belõlük a megfelelõ mennyiséget - csavarból százával, fõdarabból csak egyet-kettõt. Ezek után megmondjuk, az alkatrészek hogyan legyenek összekötve, majd beindítjuk a gépet, mire az mûködni kezd. A fent leírt folyamat vészesen hasonlít az objektumorientált programok készítésére, mint amilyeneket Jávában is írunk.

Az objektumorientált programozásban az alkatrészeket objektumnak hívjuk. Objektumokat tervrajz alapján készítünk, pont annyit, amennyi kell. A tervrajzot, amibõl egy bizonyos fajta objektumokat legyártunk, az adott objektumhoz tartozó osztálynak hívjuk. Az objektumok összeköttetéseit az objektum függvényeinek, objektumorientált terminológiával metódusainak hívásával valósítjuk meg. Valahányszor létrehozunk egy objektumot, lefoglalódik hozzá az összes, az osztállyal deklarált változó (tehát nem azok, amelyeket a metódusokban deklaráltunk), ezeket a változókat egyedváltozónak hívjuk, merthogy minden objektumegyednek van belõlük egy teljes készlet. Minden objektum tehát az egyedváltozóin keresztül önálló egyéniség lehet.

Hagyjuk most a hosszas fejtegetést és nézzünk egy példát! Ismerõs?

public class Elso {
  public void prog( String args[] ) {
    System.out.println( "Hello, ez az elso Java programunk!" );
  }

  public static void main( String args[] ) {
    Elso e = new Elso();
    e.prog( args );
  }
}

Van itt egy rettenetes titok, ami az egész tananyag eleje óta lappang itt a kék ködök mögött: igazából mindig is objektumorientált programokat írtunk, mert Jávában máshogy nem lehet. Minden programunk egy osztálydefiníció volt és mindegyikben saját magunk is létrehoztunk legalább egy objektumot. Nézzük most át ezt a programot ebbõl a szempontból!

Mindenekelõtt definiáltuk az Elso nevû osztályt. Ezt a következõképpen tettük:

class Elso { ... }

ahol a kapcsos zárójelek között az osztályhoz tartozó cuccok vannak, ebben az esetben két metódus. Egészen pontosan nem ezt tettük, hanem még elé írtuk azt, hogy public. Egyelõre errõl elég annyit tudnunk, hogy a programunk futtathatósága miatt szükséges, hogy a program fõosztálya (amit a java parancs után írunk) public legyen. Ha egy forrásfájlban van egy public osztály, annak neve meg kell egyezzen a forrásfájl nevével. Ezen felül a fájlban tetszõleges számú egyéb class lehet public nélkül. Egyelõre jegyezzük meg azt, hogy a nem public osztályt csak az õt tartalmazó forrásfájlból lehet elérni. Ez nem teljesen pontos így, de egyelõre elég nekünk. Úgyse írunk még olyan programot, aminek forrása több fájlban van.

Nézzük meg most ezt a sort:

Elso e = new Elso();

Ez egy értékadás, de furcsa fajtájú, nem? Mindenekelõtt mi az Elso típus, mi csak int-et, double-t és boolean-t ismerünk. Az Elso egy új típus, egy objektumreferencia. A referencia típusú változó is csak egy fiók, de ebben egy adott osztályú objektumra hivatkozó referenciát, mutatót tárolunk. Egy referenciát úgy lehet elképzelni, mint egy elérési instrukciót: vedd balról harmadik bögrét a második polcon. Maga a bögre felel meg az objektumnak. Ha most kicseréljük a fiókban levõ cetlit egy másikra, ami a harmadik polcon a legszélsõt jelöli meg, akkor az, aki kiveszi a cetlit a fiókból, ezt a bögrét fogja megtalálni és nem az elõzõt. Tányérra mutató cetlit nem lehet a fiókba elhelyezni, ez a fiók a bögrereferenciáké.

Az e változó tehát egy Elso objektumra mutató referenciát tárol, de hogy jutunk ilyenhez? A new operátor használható arra, hogy egy már definiált osztályból egyedeket gyártson. Most egy Elso osztályú objektumból hoztunk létre egy új egyedet, erre az egyedre mutató referenciát szereztünk a new-tól és ezt rögtön hozzá is rendeltük az e változóhoz. Az e-n keresztül tehát most ezt a példányt tudjuk elérni.

Az objektumreferencia típusnak egyetlen definiált konstans értéke van, egy speciális referencia, ami nem mutat sehova. Ennek a konstansnak a neve null. Ha egy objektumreferenciának null értéket adunk, ezzel azt jeleztük, hogy a referencia "nem használható", mert nem áll mögötte objektum. Példa:

e = null;

A null egy különleges érték mert nincs típusa, bármilyen típusú objektumreferenciának értékül adható.

Mit tudunk tenni egy referenciaváltozóval, milyen mûveletek állnak rendelkezésre ilyen típusú értékekkel? Mindenekelõtt más megegyezõ típusú változónak lehet értékül adni. Ha azt mondanánk

Elso m;
m = e;

akkor az m és az e ugyanarra az objektumra hivatkozik, mindkettõn keresztül ugyanazt az objektumpéldányt érhetjük el. Ha ezek után tovább variálunk:

e = new Elso();

akkor létrehoztunk egy új egyedet az Elso-bõl és most az e erre mutat. A két változón keresztül most két különbözõ objektumpéldányt érhetünk el. De mit jelent az, hogy elérünk egy objektumot egy referencián keresztül?

e.prog( args );

A referenciát most felhasználtuk arra, hogy meghívjuk az általa hivatkozott objektum egy metódusát. A függvényhívás pont úgy néz ki, mint egy eddigi mezei függvényhívás, de most a referencia megadásával jeleztük, hogy nem a saját objektumunknak, hanem egy másiknak, a referencia által mutatottnak a metódusát akarjuk.

Egyelõre nem mondom el, miért van szükség arra, hogy programunk saját osztályát példányosítsuk, az majd egy kicsit késõbb jön. A static a kulcszó.

Minden objektumnak van egy speciális metódusa, amit konstruktornak hivunk. A konstruktor arról ismerkszik meg, hogy nincsen visszatérési értéke (void sem!) és a metódusneve megegyezik az osztály nevével. Paraméterei akárhányan lehetnek és persze egy objektumnak lehet több konstruktora is különbözõ fajtájú és típusú paraméterekkel. Nézzünk egy példát! Ha az Elso objektumnak lenne konstruktora, azt így kellene leírni.

Elso( int i ) {
  System.out.println( "Konstruktor lefutott, parameter: "+i );
}

Ez speciel egy olyan konstruktor, ami egy darab egészet vár paraméterként és akkor hívódik meg, ha pont egy egészt adtunk a meghívásakor. De hogyan és mikor hívódik meg a konstruktor? A konstruktort az egyed létrehozásakor hívja meg a rendszer (tehát nem mi) és a paramétereit a new mögött álló osztálynév mögött levõ gömbölyû zárójelek közül szerzi. Ha azt akarjuk, hogy ez a konstruktor hívódjon meg, az Elso egyedet így kell létrehozni:

Elso e = new Elso( 2 );

Mint mondtam, minden objektumnak van konstruktora. De akkor hol az Elso konstruktora, nincs olyasmi a kódban, hogy

Elso() { ... }

pedig mi eddig vígan hivatkoztunk rá, amikor azt mondtuk

Elso e = new Elso();

A megoldás az, hogy a rendszer van olyan kedves és egy konstruktort, a paraméter nélkülit automatikusan létrehozza nekünk, ha mi nem deklaráljuk. Így aztán még ha nem is foglalkozunk olyan földi hívságokkal, mint a konstruktor, objektumunkat biztosan létrehozhatjuk a paraméter nélküli konstruktoron keresztül. A paraméter nélküli konstruktort alapértelmezett konstruktornak (angolul default constructor) hívjuk.

Feladat

Módosítsd népszerû Elso programunkat úgy, hogy legyen az Elso osztálynak egy paraméterrel rendelkezõ konstruktora is továbbá az is hívódjon meg! Ha valamiért nem megy, itt a megoldás , de a lecke szövege tartalmazza az összes szükséges programdarabot, úgyhogy ezt meg kell tudni oldanod! Futtasd le a megoldást és értelmezd az eredményt! Ezek után deklarálj egy paraméterek nélküli konstruktort is és alakítsd át a programot úgy, hogy az hívódjon meg! A paraméter nélküli konstruktorba is tegyél egy kiírást, hogy biztosak legyünk, tényleg az hívódott-e meg. Futtasd le, értelmezd az eredményt majd ellenõrizd a megoldást!

A konstruktorokat persze elsõsorban nem arra használják, hogy buta kiírásokat eszközöljenek velük, hanem a születõ objektumpéldány belsõ állapotának beállítására, vagyis fõképp arra, hogy az objektum egyedváltozói (emlékszel, ezek azok a változók, amelyeket nem a metódusok törzsében, hanem az osztály törzsében, a metódusokkal egy szinten deklarálunk) megfelelõ kezdõértéket kapjanak. Így aztán egy osztály törzse így nézhet ki:

class ValamiOsztaly {
  int parameter;

  ValamiOsztaly( int parameter_init ) {
    parameter = parameter_init;
  }

  void irdkiaParametert() {
    System.out.println( "Parameter: "+parameter );
  }
}

Aztán valahol egy felhasználó kódrészletben mondhatunk valami ilyesmit:

ValamiOsztaly v = new ValamiOsztaly( 12 );
...
v.irdkiaParametert();

és ez persze 12-t írna ki. Az objektum létrehozasakor a konstruktor meghívodik a new utan levõ értékkel. Ezt a konstruktor berakja az objektumegyed parameter nevû változójába. Amikor késõbb meghívjuk ugyanazon objektumegyed egy metódusát, ami a parameter változót felhasználja és mellékesen ki is írja. Egy kicsit olyan, mint az itt-a-piros-hol-a-piros, te tudtad követni?

Ha már az egyedváltozóknál tartunk, az objektumreferenciát felhasználhatjuk arra, hogy egy egyedváltozót direktben elérjünk. Elõzõ példánkban például azt is mondhatjuk:

int k = v.parameter;

ahol is egy frissen létrehozott k nevû változóba pakoltuk a v referenciával hivatkozott objektum parameter nevû változójának értékét.

Megjegyzendõ, hogy az objektum egyedváltozóinak direkt elérése nem javasolt módszer, az obejktum belsõ állapotát a metódusain keresztül illik módosítani és lekérdezni. Ez azonban nem kötelezõ, csak afféle ajánlott programtervezési módszertan.

Feladat

Ragadd meg nagy sikerû Elso programunkat és alakítsd át Masodik-ká! Az új változatban próbáld ki a következõ kunsztokat:

Mindenképpen próbáld a programot megírni magad és ha készen van, nézd meg a megoldást!

Most már tudunk objektumokat létrehozni, de hogyan tûnnek ezek el? Minden objektumhoz tartozik memóriaterület, amelyet a rendszer lefoglal számára, amikor az objektum létrejön. Ha egyre csak foglalgatunk, felesszük elõbb-utóbb a gép memóriáját és a programunkra szomorú vég vár.

A Jáva sajátos módon oldja meg a már nem használt objektumok felszabadítását. A háttérben egy szemétgyûjtõ programot futtat, ami megtalálja a már nem használt objektumpéldányokat és felszabadítja õket. Mi alapján dönti el, ki a szükségtelen és ki nem? Minden objektum szükségtelen, amire már nincsen referencia. Például kiváló Masodik programunkban ha azt írnánk:

m1 = new Masodik( 3 );
m1 = new Masodik( 4 );

akkor a 3-as paraméterrel létrehozott Masodik egyedre a második értékadás után már nem mutatna referencia. Ugyanez a trükk akkor is igaz, ha a referenciaváltozó automatikusan takarítódik el, például egy metódus belsõ változójának megszûnése miatt.

void objektumLetrehozo() {
  Masodik m1 = new Masodik();
}

Ebben az esetben az m1 változó megszûnik, amikor a metódus visszatér a hívóhoz és minthogy a referenciát nem mentettük el egy biztos helyre (pl. objektum egyedváltozóba vagy a metódus visszatérési értékébe, ahol a hívó esetleg felhasználhatja), a létrehozott Masodik egyed szemétté válik.

A szemétgyûjtõ nem azonnal takarítja el a feleslegessé vált objektumokat, hiszen akkor programunkat alaposan lelassítaná állandó sepregetésével. Hasonlatosan egy rutinos házmesterhez, elõbb megvárja, hogy elég sok szemét legyen és akkor lendül akcióba. Mindez megmagyarázza, miért olyan hihetetlenül magas a Jáva programok memóriaigénye. Egy jól irányzott Jáva programsorral akár 3-4 szemétobjektumot is tudunk csinálni és egy komoly Jáva program terhelés alatt akár 2-3KBájt szemetet is létrehoz másodpercenként. Ezért cserébe viszont nem kell törõdni az egyik legundokabb hibával, a memóriafolyással, amikor a programozó elfelejti felszabadítani a lefoglalt és már nem használt memóriaterületet és a gép szabad memóriája lassan elfogy.

A szemétgyûjtõ mûködésének demonstrálására nem tudtam látványos és egyszerû példát kitalálni, így sajnos el kell hinned, hogy ez így van. Lássuk inkább a lecke fináléját, íme egy nagyszerû

Feladat

Írjál egy programot, ami egy játékost és egy bankot modellez, akik fej vagy írást játszanak forintos alapon. A játékos fogad fejre vagy írásra, a bank feldobja a pénzt és ha a játékos eltalálja, kap egy forintot a banktól, ha nem, õ fizet egyet. A játékos fogadjon véletlenszerûen, játsszanak ilyen mérkõzésbõl 10-et majd a végén írd ki, kinek mennyi pénze maradt. A feladat megoldásához mindenekelõtt meg kell tervezned a programban használt objektumokat. Minden szereplõt azonosítson egy objektum! Ezek szerint biztosan van Játékos és Bank objektumunk. Írd fel a két objektum tulajdonságait a következõ formában:

xxx objektumnak van
- ... (az objektum által tárolt adatok listája)
xxx objektum képes
- ... (az objektumon végrehajtható mûveletek listája)
xxx objektum kommunikál
- ... milyen objektumokkal kommunikál az objektum (csak ha mi vagyunk a kezdeményezõ)

Alkalmazd a fenti módszert a feladatunkra! A megfejtéshez görgess le néhány sort, de ennek mennie kell.

A Játékosnak van
- pénze
A Játékos képes
- fogadásokat tenni
- kiírni, mennyi pénze van
A Játékos kommunikál
- A Bankkal, amikor fogadásokat tesz

A Banknak van
- pénze
A Bank képes
- Fogadásokat fogadni, dobni, eldönteni, hogy a játékos vesztett vagy nyert
- kiírni, mennyi pénze van
A Bank kommunikál
- Senkivel, a Játékos kezdeményezi a fogadásokat

Ezen felül van még egy szereplõ, a Fogadó, ami a szálakat mozgatja. Õ veszi rá a Játékost, hogy 10-szer fogadjon a Banknál.
A Fogadónak van
- Játékos objektuma
- Bank objektuma
A Fogadó képes
- lejátszatni a mérkõzést
A Fogadó kommunikál
- A Játékossal, hogy fogadjon

A "van" szekcióból derülnek ki az objektum egyedváltozói. A példában a Játékosnak és a Banknak van pénz változója, a Fogadó meg referenciát tárol a Játékos és Bank objektumokra. A "képes" szekcióból derül ki, milyen metódusai lesznek. A Játékosnak lesz egy metódusa, amivel fogad és egy másik, ami kiírja a Játékos objektumban tárolt pénzt. A Banknak lesz egy metódusa, aminél fogadásokat lehet tenni és egy másik, ami kiírja a bank pénzét. A Fogadónak egy metódusa lesz, ami a játszmát lejátsza. A "kommunikál" szekcióból derül ki, melyik objektum melyik másik metódusait hívja meg, kire tárol referenciákat. A Játékos referenciát tárol a Bankra és meghívja annak fogadáskiértékelõ metódusát, a Fogadó pedig a Játékosra tárol referenciát. A továbbiak kedvéért a Bank objektumot is a Fogadó hozza létre és passzolja a Játékosnak a refernciáját a Játékos konstruktorán keresztül. Az alábbi ábrán ezeket a relációkat ábrázoltam.

kommunikáció az objektumok között

A fentiekben leírt egyszerû módszert objektumelemzésnek fogom hívni ezután. A módszer valóban egy profi programozók által is használt programtervezési módszertan lényegesen leegyszerûsített változata, amely most oktatásra teljesen megfelel.

A sok duma után írd meg a programot! Nézd meg az ábra függõségi listáját és ebbõl kiderül, célszerû a Bankkal kezdeni, mert az nem függ semmitõl, utána jöhet a Játékos, majd a Fogadó. Ha készen van, csinálj egy olyan változatot, ahol három játékos játszik ugyanannál a banknál. Ha nem untad meg, csinálj olyat is, ahol három játékos játszik három különbözõ banknál! Pusztán csak ellenõrzésképpen itt a megoldás.

Feladat

Írjál egy olyan programot, ami három játékost modellez, akik a kõ-papír-ollót játszanak körmérkõzéses alapon. Ha emlékszel, a kõ-papír-olló az a klasszikus játék, ahol a két játékos egyszerre választ a három tárgy közül és egy körkörös szabályrendszer alapján döntik el, ki gyõzött: a kõ gyõz az olló ellen, de veszít a papír ellen, a papír pedig veszít az olló ellen. Futtasd le a körmérkõzést 20-szor és irasd ki, melyik játékos hányszor nyert. Minden játékosnak legyen egy száma, ami azonosítja.

Elõször végezd el az objektumelemzést, ezt itt ellenõrizheted. Ezután készítsd el a programot, ha készen vagy, ellenõrizd a megoldást! Figyeld meg, hogy a megoldásunkban most nem tároltuk el a játékostárs refernciáját a saját objektumunkba (pedig megtehettük volna, hiszen a párok változatlanok), hanem a rugalmasabb szerkezet érdekében paraméterként adjuk át a fogadó metódusnak. Ha netán eltároltad volna apartner referenciáját, az is helyes.

Ellenõrzõ kérdések

  1. Mi az objektum, mi az osztály és mi a kapcsolatuk?
  2. Mi az összefüggés a függvények és metódusok között?
  3. Mi az egyedváltozó?
  4. Mi az objektumreferencia
  5. Mit jelent a null?
  6. Hogyan hívhatjuk meg egy objektum metódusait az objektumreferencián keresztül?
  7. Mi a konstruktor?
  8. Hogyan hívjuk meg a konstruktort?
  9. Mi az alapértelmezett konstruktor?
  10. Mikor generál alapértelmezett konstruktort a fordító maga?
  11. Hogyan hivatkozhatunk egy objektum egyedváltozóira az objektumreferencián keresztül?
  12. Mit jelent az, hogy a Jáva rendszerben egy szemétgyûjtõ mûködik?

Kovetkezo lecke Tartalomjegyzek