Befejezzük a kavarást

2018.04.10. · tudomány

Nem tudom, befejezzem-e a programozásról szóló sorozatot, mert úgy érzem, a leglényegesebbeket elmondtam, de elég pozitívak voltak a reakciók, talán kevésbé lényegesekről is lehetne még szólni. Viszont az előző részben megígértem, hogy befejezzük a kártyákat megkeverő és kiosztó Java-programot, ezt most megteszem. (Egy 52 lapos franciakártya-paklit osztunk négy egyenlő részre.) Hogy ez ne csak favágás legyen (mert a programozás, lássuk be, sokszor az), egy-két új fogalmat is bevezetek azért.

Kezdjük az érdektelenebb részével. Mivel a Javában az objektum-orientáltság kötelező, magának a fő eljárásnak, amelyik magát a program futását írja le, szintén egy osztályhoz kell tartoznia. (Ezt az eljárást, mint sok más nyelvben is, main-nek kell nevezni.) A fő programfájlunkban tehát ezt az osztályt kell leírnunk, és erről fogjuk elnevezni a fájlt, legyen mondjuk Oszt a neve. Az Oszt osztályt modulnak tekintjük, nem akarunk Oszt típusú objektumokat, példányokat. Ezért a main eljárásunk „statikus” lesz.

photo_camera Grafika: Tóth Róbert Jónás

A main eljárás nem függvény, nem ad vissza értéket (ezt a Java nyelvben is úgy jelöljük, hogy az érték típusának a helyére a void szót írjuk). Viszont van egy argumentuma, méghozzá egy stringekből álló tömb: ezek a stringek azok, amiket a parancssorba írhatunk a program neve után. Az Oszt.java fájl tartalma tehát valahogy így fog kinézni:

class Oszt

{

public static void main( String[] argumentumok )

{

// ...

}

}

A Java programokat úgy kell lefuttatni, hogy előtte a rendszerrel (egy külön erre szolgáló programmal) le kell fordíttatnunk mindazokat a fájlokat, amik a programunkhoz tartoznak. Ezt az előfeldolgozást, fordítást szakszóval kompilálásnak (compilation) nevezik. A Java előfeldolgozó programját (legalábbis az én rendszeremben) úgy hívják, hogy javac. Tehát majd azt kell először a parancssorba írnom, hogy „javac Oszt.java” (és utána persze le kell nyomnom az billentyűt), és ha ez sikerül, akkor majd úgy kell futtatnom a programot, hogy ezt írom be: „java Oszt” (és ha van értelme, akkor még utána azokat a stringeket, amiket szeretnék az argumentumok nevű tömb elemeiként feldolgozni).

Az unalmas részén túl vagyunk, tulajdonképpen bármit beírhatunk a main törzsébe (vagyis a definíciójába, a „{” és „}” jelek közé), aminek valami értelme van Javául, és akkor a fenti parancssorokkal (fordítás, aztán futtatás) el tudjuk indítani a programot, és ennek következtében az oda beírtakat hajtja végre. Például írjuk bele azt, hogy „System.out.println( "Kukucs!" );” – vajon mit fog akkor a programunk csinálni? :-)

Mint mindenhol, itt is maximális egyszerűségre törekszünk: a main nevű eljárást néhány jól olvasható sorban írjuk le, hogy világos legyen, mit csinál a programunk, a részleteket pedig máshol fejtjük ki. Ráadásul maximális modularitásra is törekszünk. Vagyis magában a programban, a main eljárásban semmi sem utalhat arra, hogy például hogyan ábrázoljuk a kártyalapokat, a paklikat stb. A legjobb, ha valami jól látható helyen, például a file legelején van valami, amiben mondjuk egyetlen szót kell csak megváltoztatni ahhoz, hogy az egyik implementációt egy másikra cseréljük ki.

Ha lehet, én mindig olyan programozási nyelvet használok, amiben erre mód van. A Java nyelvben például többféleképpen is megtehetjük. Egyszerű esetekben például használhatunk ún. csomagokat (package) – ezeket más nyelvekben, pl. a Pythonban moduloknak nevezik, és a korábbi Python-példában ilyen volt a francia.py modul. A Java rendszerben a következőképpen kell a csomagokat használni. Minden csomag egy-egy foldernek (mappának, directorynak, ahogy tetszik) felel meg, a folder neve egyben az illető csomag neve, a benne levő fájlok az illető csomaghoz tartozó osztályokat tartalmazzák. Ezeket az osztályokat használhatjuk a programunkban, csak meg kell jelölni, hogy melyik csomagban kell őket keresni. Például ezt írhatjuk az Oszt.java fájlunk elejére:

import francia.Kartya;

Ez azt jelenti, hogy a Kartya nevű osztályt, amit használni fogunk, a francia nevű csomagban kell keresni. Így, ha többféleképpen implementáljuk ezt a Kartya nevű osztályt, és ezeket külön-külön csomagokba tesszük ugyanolyan neveken, akkor csak a francia szót kell megváltoztatni, ha másik implementációt akarunk használni.

Magában a Kartya.java file-ban is (most vegyük azt, amelyik a francia nevű folderben van) meg kell jelölnünk, hogy melyik csomaghoz tartozik. Ezért így fog kinézni a tartalma:

package francia;

public class Kartya

{

// ...

}

Most akkor még egy újdonságot mesélek el. A Java nyelvben (és sok más objektum-orientált nyelvben) az osztályok tagjai között osztályok is lehetnek! A main eljárásunkban elég a Kartya osztályra hivatkoznunk, és az tud magával hozni olyan alosztályokat, mint például a Pakli (kártyacsomagok ábrázolása) vagy a Lap (kártyalapok ábrázolása). Így az, hogy mit akarunk csinálni, ilyen egyszerűen leírható:

import francia.Kartya;


class Oszt

{

public static void main( String[] argumentumok )

{

Kartya.Pakli pakli = new Kartya.Pakli();

pakli.kever(); // megkeverjük

pakli.oszt(); // kiosztjuk

}

}

Tehát a Pakli nevű osztály a Kartya nevű osztály egyik tagja (statikus, hiszen magához az osztályhoz tartozik, nem pedig egy Kartya típusú objektumhoz, mert olyan nem is lesz), a kever és az oszt nevű eljárások pedig az egyes Pakli-instanciák tagjai („módszerei”) lesznek:

package francia;


public class Kartya

{

public static class Pakli

{

// ...

public Pakli()

{

// ...

}

public void kever()

{

// ...

}

public void oszt()

{

// ...

}

}

}

Most már csak azt kell leírnunk, hogy hogyan kell ábrázolni és előállítani egy paklit, meg megkeverni és kiosztani. Ezeket többé-kevésbé már láttuk, szóban vagy konkrétan, „lekódolva” is, ezért különösebb magyarázat nélkül leírom, hogy például hogyan nézhetnek ezek ki. A következők tehát a Kartya osztályon belül lesznek:

public static class Lap

{

private int szin;

private int ertek;

public Lap( int sz, int e )

{

szin = sz;

ertek = e;

}

}

public static class Pakli

{

private Lap[] lapok = new Lap[52];

public Pakli()

{

int i = 0, szin, ertek;

for ( szin = 0; szin < 4; ++szin )

{

for ( ertek = 0; ertek < 13; ++ertek )

{

lapok[i++] = new Lap( szin, ertek );

}

}

}

}

Ebben csak két apró újdonság van. Az egyik, hogy a Javában a tömbök deklarálása, ha rögtön helyet is csinálunk nekik, ugyanúgy a new szó segítségével történik, mint bármilyen új objektum létrehozása. A másik, hogy az egész számok eggyel való növelésének egyszerű módja a „++” jel használata. Ha a szimbólum után írjuk, az azt jelenti, hogy azon a helyen, ahol szerepel, még a kisebb értéket használjuk, és csak utána növeljük eggyel az értékét.

A pakli megkeverése (a Pakli objektumokhoz tartozó egyik módszer) így nézhet ki:

public void kever()

{

int i;

// Ez szükséges a véletlen számok előállításához:

java.util.Random rand = new java.util.Random();

// Az utolsó lap kivételével mindegyik lapot

// kicseréljük egy véletlenszerűen választott

// utána következő lappal (ami véletlenül akár

// ő maga is lehet):

for ( i = 0; i < 51; ++i )

{

// a nextInt( n ) értéke véletlenszerű szám,

// legalább 0 és legfeljebb n - 1:

int masikLapSzam = i + rand.nextInt( 52 - i );

// megőrizzük a másik lapot, mert a helyére

// fogjuk rakni az i-ediket:

Lap masikLap = lapok[masikLapSzam];

// a másik lap helyére betesszük az i-ediket:

lapok[masikLapSzam] = lapok[i];

// és most az i-edik helyre a másik lapot:

lapok[i] = masikLap;

}

}

Végül a lapok kiosztásán csak annyit értünk, hogy a lapokat egyenként leírjuk (négy külön sorba, mindegyik sor 13 lapot tartalmaz). A System.out.print eljárás végzi a képernyőre nyomtatást.

public void oszt()

{

// i-vel számoljuk a sorokat,

// j-vel soronként a lapokat,

// k-val meg a pakli lapjait:

int i, j, k = 0; // egyszerre 3 számot deklarálunk!

for ( i = 0; i < 4; ++i )

{

for ( j = 0; j < 13; ++j )

{

// ha nem az első lap a sorban,

// írunk elé egy betűközt:

if ( j > 0 )

{

System.out.print( " " );

}

// a k-adik lap nyomtatásához használjuk

// a 'kep' nevű módszert:

System.out.print( lapok[k++].kep() );

}

// a sor végére egy 'sor vége' jelet írunk:

System.out.print( "\n" );

}

}

}

Elegánsabb lenne, ha minden sorban sorrendbe állítanánk a lapokat, és úgy nyomtatnánk ki, de ezt már az olvasóra bízom.

A szerző nyelvész, az MTA Nyelvtudományi Intézetének főmunkatársa.