IV. NetREXX osztályok használata

Bevezetés

Az elôzô leckével ellentétben ebben a leckében szinte kizárólag a NetREXX objektum-orientált tulajdonságaival fogunk foglalkozni. Megtárgyaljuk mindazon ismereteket, amelyek a NetREXX, illetve Java osztályok használatához szükségesek. Aki ismer más objektum-orientált nyelveket (pl. Java, vagy C++), annak valószínûleg nem sok újat fog tartalmazni ez az anyag, így ôk gyorsabban haladhatnak majd. Mi mindenesetre abból indulunk ki, hogy az olvasó nem rendelkezik ilyen ismeretekkel, ezért a lehetô legalaposabban fogjuk körbejárni a témát.

Az osztály fogalma

Korábban már pedzegettük, hogy az osztályok tulajdonképpen változók (tulajdonságok, angolul properties) és metódusok (függvények, eljárások) gyûjteménye. A metódusok segítségével végzünk mûveleteket a változókon. Az osztályt tulajdonképpen egy tervrajznak is tekinthetjük. Olyan tervrajznak, amely alapján objektumokat készítünk. Az objektum az osztály konkrét megjelenési formája.

Sok emberben vetôdik fel a következô kérdés. Miért van szükségünk az osztályokra? Egy példa kapcsán világítjuk meg az osztályok hasznosságát. Tegyük fel, hogy három ember alapít egy bankot. A bank egy szoba, amelyben minden embernek külön betétkönyve van, a pénzt pedig egy kupában tartják. Ha valamelyik ember pénzt akar betenni, vagy éppen kivenni, akkor beírja a tranzakciót a könyvébe és beteszi vagy kiveszi a kupából a megfelelô pénzösszeget. Ez a rendszer egészen addig jól mûködhet, amíg nincs sok ember a bankban és a résztvevôk pontosan betartják a játékszabályokat. A hagyományos programozás is ilyen, hiszen ott is mindenkinek teljes hozzáférése van az adatokhoz és mindenkinek be kell tartania a hozzáférés szabályait. Amikor a bank ügyfeleinek száma nôni kezd, akkor elôbb utóbb hibák fognak becsúszni, s a régi rendszer akár csôdbe is viheti a bankot.

Kiutat jelenthet a problémából, ha a bank alkalmaz egy pénztárost, akinek kizárólagos hozzáférése lesz a könyveléshez és a pénzhez. A kliensek csak a pénztáros szolgáltatásait igénybevéve folytathatnak tranzakciókat. Ha objektum-orientált nyelvre akarjuk lefordítani ezt a példát, akkor azt mondhatjuk, hogy a takarékkönyvek (könyvelés) és a kupa (pénz) reprezentálják a tulajdonságokat. A kupa a bank osztály tulajdonsága (class variable), mivel az összes objektum (az ügyfelek) közösen használják azt. A takarékkönyvek egyediek (hiszen egy normális bankban minden ügyfélnek külön számlája van), ezért ezeket az objektumok tulajdonságainak (instance variable) nevezzük. A kezelô egy metódusnak felel meg, hiszen ô az egyetlen, aki változtathatja a könyveléseket és kezelheti a pénzt. Ha valamilyen hiba történik, akkor az csak a kezelô hibájából történhet. A régi rendszerben a hiba okozója elvileg bármelyik ügyfél lehetett volna, így a hiba okának felderítése nagyszámú ügyfél esetén gyakorlatilag kilátástalan küzdelem lenne.

A példa kapcsán láttuk, hogy kétfajta tulajdonság létezik. Az osztályok tulajdonságai közösek, az objektumok tulajdonságai pedig egyediek, s az objektum készítésekor jönnek létre. Ez a kettôsség megvan a metódusok terén is. Az osztályok metódusai (class methods) az osztályokhoz tartoznak, így csak egy van belôlük, s ezt az objektumok közösen használják. Az objektum metódusai (instance methods) az adott objektum létrehozásakor keletkeznek, s ha az objektum megszûnik, akkor a metódusait sem lehet már használni.

A tulajdonságok tulajdonságai

A tulajdonságokat viselkedés és hozzáférhetôség alapján is lehet csoportosítani. A viselkedés alapján beszélhetünk objektumhoz tartozó (instance), osztályhoz tartozó (class), konstans (constant) és asszinkron (volatile instance) objektumváltozóról. Az objektum és osztályváltozókról már beszéltünk. A konstansok tulajdonképpen osztályváltozók, amelyek értéke állandó. Az asszinkron változó pedig egy olyan speciális objektumváltozó, amely értéke a jelen folyamat környezetén kívül is változtatható. A hozzáférhetôség alapján beszélhetünk publikus (public), örökölhetô (inheritable) és privát (private) változókról. A publikus változók bármelyik osztály számára elérhetôek. Az örökölhetô változók csak az azonos csomagban elhelyezkedô, vagy az adott osztályból képzett alosztályok (leszármaztatott osztályok, angolul subclass) számára elérhetôek. A privát változók képezik a másik végletet, mivel ezek csak a saját osztály metódusai számára láthatóak. A tulajdonságok definiálásakor adhatjuk meg, hogy milyen tulajdonságokkal akarjuk ôket felruházni:

properties 	[public | private | inheritable]
	 	[constant | static | volatile]

Ha nem adunk meg semmit a definiáláskor, akkor a hozzáférés az alapértelmezés szerinti örökölhetô lesz. A végletek (privát és publikus változók) használata általában nem is ajánlott, csak akkor változtassunk az alapértelmezés szerinti hozzáférhetôségen, ha erre alapos okunk van. A jó áttekinthetôség kedvéért az alábbi táblázatban összefoglaltuk a változók definiálásakor használható kulcsszavak hatását a viselkedésre és a hozzáférhetôségre:

Hozzáférhetôség:
Viselkedés:PublikusPrivátÖrökölhetô
Objektumváltozópublicprivateinheritable
Osztályváltozópublic staticprivate staticstatic
Konstanspublic constantprivate constantconstant
Asszinkronpublic volatileprivate volatilevolatile

Metódusok

Mint már említettük, a metódusok az osztályokon belül definiált függvényeknek vagy eljárásoknak felelnek meg. A NetREXX-ben háromfajta metódust különböztetünk meg. Az objektummetódusok (instance methods) az objektumokhoz tartoznak, így csak akkor tudjuk ôket használni, ha olyan referenciával rendelkezünk, amely alapján az objektumuk azonosítható. Az objektummetódusok korlátlanul férhetnek hozzá az objektum és az osztály változóihoz. Az osztálymetódusok (class methods) az osztályokhoz tartoznak. Használatukhoz ezért nincs is szükség objektum-referenciára. Az osztálymetódusok korlátlanul érhetik el az osztályváltozókat, ám nem férhetnek közvetlenül hozzá az osztály alapján képzett objektumok változóihoz. A harmadik típusú metódus, a konstruáló metódus, amely tulajdonképpen egy speciális osztálymetódus. Elôzô leckéink során már láttuk, hogy a konstruáló metódus az objektum készítését végzi. Éppen ezért ezek a metódusok (annak ellenére, hogy tk. osztálymetódusok) mégis hozzáférnek a készítendô objektum változóihoz. Természetesen az osztályváltozók is elérhetôek (az egyébként az osztály nevével megegyezô nevû) konstruáló metódusok számára.

A tulajdonságok/változók csoportosításához használt viselkedési és hozzáférhetôségi kritériumok alapján a metódusok is csoportosíthatóak. A viselkedés alapján beszélhetünk módosítható (overridable), nem módosítható (final), elvont (abstract) és környezeti (native) metódusokról. A módosítható metódus azt jelenti, hogy a metódus törzse újra definiálható alosztály képzése esetén. Ezzel ellentétesen viselkedik a nem módosítható metódus, mivel ez nem definiálható újra az alosztályokban. Az elvont metódusok olyan metódusok, amelyeknek csak a paraméterlistáját definiálták. A metódus törzsét az adott osztályból képzett alosztályban kell definiálni. Az elvont metódusokat tartalmazó osztályokból objektumokat sem lehet készíteni, hiszen a metódusok egy részének mûködése nincs definiálva. A környezeti metódusokat a NetREXX (Java) környezet bocsátja rendelkezésre, így ezeket módosítani sem lehet. Mivel az osztályok újrafelhasználhatóságát rontja a nem módosítható metódusok alkalmazása, ezért ezeket inkább kerüljük programjainkban.

A hozzáférhetôség alapján beszélhetünk publikus (public), örökölhetô (inheritable) és privát (private) metódusokról. A publikus metódusokat bármelyik másik osztály meghívhatja. Ez az alapértelmezés szerinti beállítás. Az örökölhetô metódusokhoz az alosztályok, illetve az azonos csomagban található osztályok férhetnek hozzá. A privát metódusokat csak a saját osztály használhatja.

A metódusok definiálása az alábbi sablon szerint történhet:

method név[( [ paraméter1[, paraméter2]...] ) ]
                 [public | private | inheritable]
                 [abstract | static | constant | final | native]
                 [protect]
                 [returns classname]
                 [signals exceptionclass[, exceptionclass]...]

Az alábbi táblázatban összefoglaltuk a metódusok definiálásakor használható kulcsszavak, a viselkedés és a hozzáférhetôség kapcsolatát:

Hozzáférhetôség:
Viselkedés:PublikusPrivátÖrökölhetô
Módosítható objektummetódus- (alapértelmezett érték)privateinheritable
Nem módosítható objektummetódusfinalfinal privatefinal inheritable
Konstruáló metódus- (alapértelmezett érték)private-
Módosítható osztálymetódusstaticstatic privatestatic inheritable
Elvont objektummetódusabstractabstract privateabstract inheritable
Környezeti objektummetódusnativenative privatenative inheritable

Láttuk, hogy a metódusok definiálásakor általában egy paraméterlistát is megadunk. Ez természetesen nem kötelezô, hiszen lehetnek olyan metódusok is, amelyek paraméterlistája üres. Egyes paraméterek opcionálisak is lehetnek és kezdôértékkel is felruházhatjuk ôket. Ezek a paraméterek mindig a paraméterlista végén szerepelnek. A hagyományos programozástól eltérôen, az objektum-orientált nyelvekben a függvények és eljárások megkülönböztetéséhez nemcsak a nevüket, hanem a paraméterlistájukat is felhasználjuk. Helyesebben szólva a paraméterlistában szereplô adatok típusát, amelyek összességét aláírásnak (signature) is nevezzük. Amennyiben egy metódusnak opcionális paraméterei is vannak, akkor a metódusnak többféle aláírása is lehet, hiszen ezek az opcionális paraméterek tetszés szerinti kiosztásban el is hagyhatóak. Például a method a(b=int, c=int, d='IGEN', e=int 2) metódusnak a következô aláírásai lehetnek:

a(int, int, REXX, int)
a(int, int, REXX)
a(int, int)

Figyeljük meg, hogy a metódus visszatérési értékének típusa nem tartozik bele az aláírásba! Programírás közben vigyázzunk arra is, hogy egy adott osztályon belül nem legyen két olyan metódus, amelyek aláírása megegyezik. Az azonos nevû, ám különbözô aláírású metódusok módosítják (overload) egymást. A NetREXX környezet a program futtatása során elôször meghatározza a meghívott metódus aláírását, majd az azonos nevû metódusok közül az egyezô aláírással rendelkezôt választja ki és hajtja végre.

A konstruáló metódusok elsô utasítása mindig egy új objektumot hoz létre. Ez az utasítás lehet egy hivatkozás a szülôosztály (super class) konstruáló metódusára (super()), vagy pedig a NetREXX által definiált, alapértelmezett konstruáló metódusra (this()). Amennyiben ez programunkban nem így lenne, akkor a NetREXX automatikusan kiegészíti azt a szülôosztály konstruáló metódusának behelyettesítésével. Az objektum létrejötte után a this (ez) kulcsszóval lehet az objektumra hivatkozni, s ez a metódus szabadon használhatja az objektum egyéb változóit is. A konstruáló metódusnak nem kell definiálnia a visszatérési érték típusát. Amennyiben mégis a return utasítással zárjuk le ezt a metódust, akkor azt csak a this paraméterrel, vagy paraméter nélkül tehetjük meg.

Metódusok meghívására többféle mód van. Objektummetódusra hivatkozhatunk pl. az objektum nevének és a metódus nevének ponttal történô összekapcsolásával:

c = 'abcd'		-- REXX típusú karakterlánc-objektum
a = c.right(6, "*")	-- a értéke '**abcd' lesz

Amennyiben a metódus nem igényel paramétereket, akkor a zárójelek el is maradhatnak. Az objektum neve és a pont is elmaradhat, ha az aktuális objektummal dolgozunk. Amennyiben módosítunk egy örökölt metódust, akkor az eredeti metódusra hivatkozhatunk a super objektumnévvel. Osztálymetódusok meghívásakor az objektum neve helyett az osztály nevét kell elôtagként megadni. Amennyiben az osztály nem része az aktuális csomagnak (és nem is importáltuk a megfelelô csomagot az import utasítással), akkor a teljes elérési út megadásával lehet csak az osztályra hivatkozni:

MásikCsomag.Segédprogramok.Service.Test()

Osztályok konstruáló metódusát az osztály nevének megadásával lehet meghívni. A név után minden esetben zárójeleket kell használni, ez ugyanis kötelezô a konstruáló metódusok esetében.

Öröklôdés

Gondolatban térjünk vissza elôzô bankpéldánkhoz! Tegyük fel, hogy a már az új rendszer alapján mûködô (objektum orientált :-) bank nyitni akar egy új fiókot. Az új fiók mûködési rendszerét tekintve meg fog egyezni a régiével, az egyetlen különbség csak az lesz, hogy kupa helyett páncélszekrényben fogják tartani a pénzt. A bank számára az a legkézenfekvôbb, ha az új fiók átveszi a régi mûködési szabályzatát, és definiálnak két új eljárást a páncélszekrény nyitása és zárása céljából. Objektum orientált nyelvre fordítva a régi fiókot tekinthetjük a szülôosztálynak, amelybôl leszármaztatjuk az új fiók osztályát. Az új osztály örökli a régi minden kellékét (eszközök, eljárások), ám kiegészül egy új változóval (páncélszekrény), amely tartalmának megváltoztatására átírjuk (override) a régi osztályból örökölt pénz be- illetve kivételi metódusokat (új eljárások).

Az öröklôdés során tehát hasznosítjuk a korábban egyszer már megírt kódot, s ahol szükséges, ott átalakíthatjuk. Ezzel a módszerrel sokkal gyorsabban lehet fejleszteni, mint a hagyományos módon, ugyanakkor hiba esetén viszonylag könnyû meghatározni, hogy a program melyik részével van probléma. Mivel egy jól megírt objektum-orientált programban nem fordulnak elô megismételt kódrészletek, ezért a hibát csak egy helyen kell kijavítani. Ha másolnánk a kódot, nem pedig örökítenénk, akkor a hibát minden másolatban korrigálni kellene!

Mint már említettük, a leszármaztatott osztály (alosztály) metódusa automatikusan módosítja a szülôosztály metódusát, amennyiben az aláírásuk megegyezik. A leszármaztatott osztály használói számára a szülôosztály metódusa nem is lesz elérhetô. Magából a leszármaztatott osztályból még hivatkozhatunk rá a super névvel. Amikor egy objektummetódust hívunk meg, akkor az egyezô aláírású metódus keresése mindig az objektum osztályánál kezdôdik. Ha abban nem szerepel ilyen metódus, akkor a keresés tovább folytatódik a szülôosztályokban.

A leszármaztatott osztályok módosíthatják a szülôkben definiált változókat is. Ha egy változó ugyanazzal a névvel szerepel egy leszármaztatott osztályban mint a szülôosztályban, akkor módosításról beszélünk. Az ôsváltozóra a super elôtaggal hivatkozhatunk. A metódusok keresési mechanizmusával ellentétben a NetREXX a változót elôször az éppen futó metódus osztályában keresi. Amennyiben egy változót nem módosítottunk, akkor az arra való hivatkozáskor kötelezô használni a super elôtagot, különben a NetREXX azt feltételezi, hogy lokális változóról van szó!

Bizonyára sokakban vetôdik fel a kérdés, hogy mikor érdemes alkalmazni az öröklôdést, és mikor kell teljesen új osztályt készítenünk. A kérdést csak a konkrét feladat ismeretében lehet helyesen megválaszolni. A jó programozó mindig a valóságot próbálja meg modellezni, s mielôtt döntene ebben a kérdésben, a következôt kell megválaszolnia: rendelkezik-e a leszármaztatni kívánt osztály olyan tulajdonságokkal, amelyek alapján jogosan tételezhetô fel az öröklôdés. Ha igen, akkor érdemes az új osztályt leszármaztatni, ellenkezô esetben viszont bölcsebb az újrakezdés mellett dönteni.

Absztrakt osztályok

Az elvont (absztrakt) osztályok olyan speciális osztályok, amelyekbôl nem képezhetôk közvetlenül objektumok. Az elvont osztályokat mindig valamilyen közös viselkedés definiálására használják, mivel az abból leszármaztatott osztályok öröklik a szülôk tulajdonságait. A leszármaztatott osztályokból már természetesen képezhetôk objektumok. Az absztrakt osztályok tartalmazhatnak absztrakt metódusokat is, amelyeket a leszármaztatott osztályokban kell kiegészíteni azokkal az információkkal, amelyek az objektumképzést lehetôvé teszik. Az absztrakt osztályokat széles körben használják a Java fejlesztôkörnyezet grafikus elemeinek leírására. Az egyik osztály például általánosságban definiálja a téglalapot (méretek, koordináta rendszer, alapmûveletek), azonban nem tartalmaz arra vonatkozó információt, hogy milyen típusú vonallal kell a téglalapot megrajzolni. Ezt az információt a leszármaztatott osztályban kell megadni.

Senki sem akadályoz meg bennünket abban, hogy egy absztrakt osztályból több osztályt is leszármaztassunk és azok mindegyikében (egymástól eltérô módon) kidolgozzuk az absztrakt metódusokat. Ebben az esetben tehát több, ugyanolyan névvel, ám eltérô funkcióval rendelkezô metódust kapunk. Ezt a tulajdonságot nevezzük polimorfizmusnak. Egy ilyen metódus meghívásakor a Java környezetnek kell eldöntenie, hogy melyik metódust hajtsa végre.

Interfészek

Az interfészek olyan speciális osztályok, amelyek kizárólag elvont metódusokat és konstansokat tartalmaznak. Egy lényeges különbség az interfészek és az absztrakt osztályok között, hogy az interfész csak elvont metódusokat tartalmazhat, míg egy absztrakt osztályban lehetnek közönséges metódusok is. Az interfészeket alapján képzett osztályok egynél több interfészt is felhasználhatnak, megörökölve azok tulajdonságait (multiple inheritance). A teljesség kedvéért megemlítjük azt is, hogy a interfészek készítéséhez felhasználhatunk más interfészeket is.

Osztálykönyvtárak

Az osztálykönyvtár (class library) nem más, mint osztályok gyûjteménye, amelyet azzal a céllal hoztak létre, hogy a programozók abból építkezhessenek. Az osztálykönyvtárat más szóval csomagnak (package) is nevezik. Mint azt már az elôzôekben is megállapítottuk, a NetREXX és a többi objektum-orientált nyelv egyik nagy elônye, hogy az egyszer már jól megírt kód nagyon könnyen újra hasznosítható. Mielôtt belevágnánk egy új osztály megírásába, érdemes például az interneten körülnézni, hogy valaki nem írta-e már meg az adott osztályt. Nagyon sok esetben találni fogunk használható kódot. Ha mégis arra kerülne a sor, hogy az osztályt teljesen magunknak kell megírni, akkor érdemes arra ügyelni, hogy osztályunkat mások is fel tudják majd használni.

Arra is láttunk már példát, hogy az osztály írása során a package [csomagnév] utasítással adhatjuk meg, hogy az osztály melyik csomagba tartozik. A csomagnév paraméter értéke attól függ, hogy melyik alkönyvtárban tároljuk az adott osztályokat. A csomagokat olyan alkönyvtárban kell elhelyezni, amelyek fôkönyvtára benne van a CLASSPATH környezeti változó által definiált könyvtárak csoportjában. A csomag neve ennek az alkönyvtárnak a nevével egyezik meg. Ha például saját gyártmányú osztályainkat a C:\SAJATCLASS könyvtárban tároljuk, s ebben a könyvtárban a UTIL\MATH alkönyvtárban helyezzük el a csomagot alkotó osztályfájlokat, akkor a csomag neve UTIL.MATH lesz. Az osztályok teljes neve pedig C:\SAJATCLASS\UTIL\MATH\Valami.class lesz. Az osztályokra hivatkozhatunk a rövidített nevük (vagyis a forrást tartalmazó fájl neve) alapján is. Ezt tesszük többnyire a class utasítás használatakor. Az osztályok teljes neve alatt a csomag és a rövid név összekapcsolásával keletkezett (példánkban tehát UTIL.MATH.Valami) nevet is szokták érteni. Amikor egy másik alkönyvtárban elhelyezkedô csomag egyik osztályára akarunk hivatkozni, akkor mindig a teljes nevet kell megadni. Mivel ez nem túl kényelmes, ezért bevezették az import parancsot, amellyel egy lépésben lehet importálni egy komplett csomagot. Ezután a csomagban helyet foglaló osztályokra már a rövid névvel lehet hivatkozni:

import UTIL.MATH
...
class Proba implements Valami

További könnyebbséget jelent, hogy a NetREXX környezet automatikusan importálja a következô NetREXX és JDK csomagokat: netrexx.lang, java.lang, java.io, java.util, java.net, java.awt, java.applet.

A csomagokat zip fájlokba is lehet csomagolni. Ez jól jöhet olyan fájlrendszereken, amelyek nem támogatják a hosszú fájlneveket, vagy pedig korlátozzák az egy könyvtárban elhelyezhetô fájlok számát. OS/2 és Windows alatt az InfoZip tömörítô-programot ajánlott használni a zippeléshez, ám a fájlokat nem szabad tömöríteni (lásd a -0 opciót)! A tömörített zip-állományokat ugyanis a Java (és a NetREXX) nem támogatja. A kész zip-fájlokat aztán a teljes elérési úttal kell megadni a CLASSPATH környezeti változó által definiált keresési útvonalban, hogy a rendszer meg is találja az ily módon becsomagolt osztályokat. Ha belenézünk a Java könyvtár LIB alkönyvtárába, akkor találni fogunk néhány zip-fájlt is, mivel a Java osztályok és a NetREXX környezet is ily módon kerülnek kiszerelésre.

A Java nyelv tervezôi bevezettek egy csomagelnevezési-stratégiát, hogy elkerüljék az azonos csomagnevekbôl eredô problémákat. A stratégia szerint minden csomag nevének három fô részbôl kell állnia. Az elsô rész a programozó hovatartozását (például a vállalat neve, internet domain neve, stb.) mutatja. A második rész magát a programozót azonosítja (például User ID), a harmadik pedig a tetszôlegesen választott csomagazonosító. A com.sun.jndi.rmi.registry csomagról például el tudjuk mondani, hogy a sun.com tartományban készítette a jndi ID-val rendelkezô programozó, s a csomag tulajdonképpeni azonosítója az rmi.registry.

A NetREXX által automatikusan importált, legfontosabb JDK csomagokat és azok funkcióját összefoglaltuk az alábbi táblázatban:

Java csomag:Funkció:
java.langA Java nyelv alapeszközeit (primitív adattípusok, sztring-osztályok, környezetfüggô osztályok, kivételek lekezelése) tartalmazó csomag.
java.utilA Java nyelv segédosztályait (dinamikus tömbök, szótárak, sztring kezelés) tartalmazó csomag.
java.ioA fájlok és streamek (adatfolyamok) használatát elôsegítô csomag.
java.netA TCP/IP protokoll használatát lehetôvé tevô csomag.
java.awtA grafikus felületek készítését lehetôvé tevô osztályok csomagja.
java.awt.imageA képek használatát lehetôvé tevô csomag.
java.appletAz appletek (Java programocskák) készítését elôsegítô csomag.


REXX GYÍK:

K1. Használhatom-e Java programokban a NetREXX osztályokat?
V1. Természetesen a Java programok is használhatják a NetREXX osztályokat. Amennyiben a Java programokban a rövid osztályneveket is használni akarjuk, akkor importálni kell a netrexx.lang csomagot.

K2. Nem értek egy kukkot sem ebbôl a leckébôl. Mit tegyek?
V2. Semmi gond, haladjon tovább a többi leckével. Amikor olyan részhez ér, amelyben olyan fogalom fordul elô, amelyet itt vezettünk be, akkor térjen vissza ehhez a leckéhez és ismét olvassa el a megfelelô részt!


Gyakorlatok:

1. Tanulmányozza a Java környezet dokumentációját, és gyôzôdjön meg arról, hogy milyen osztályok állnak a programozók rendelkezésére!

Kádár Zsolt
2000. 01. 27.
[ Elôzô lecke | Következô lecke | Tartalom ]