A programozáshoz algoritmikus gondolkodás kell. Algoritmuson vagy inkább eljáráson olyan megengedett lépésekből álló módszert, utasítás(sorozato)t, részletes útmutatást, receptet értünk, amely valamely felmerült probléma megoldására alkalmas. Ha képesek vagyunk algoritmust létrehozni képesek leszünk programozni is.
A FUTTATÁSHOZ JAVA KÖRNYEZETE KELL !
A program írásához egy szövegszerkesztő akár egy jegyzettömb is elég.
A Java programnyelv tisztán objektumorientált, ami azt jelenti, hogy maga a program is egy osztály, amiből a Java virtuális gép, ami a tényleges futtatást végzi, egy objektumot hoz létre, ami változókkal és metódusokkal rendelkezik. Első Java programunk legyen mondjuk ez:
public class Alapok
{
public static void main( String[] args )
{
System.out.println("Hello Vilag");
}
}
Az első és nagyon fontos dolog, hogy a Java programozási nyelv különbséget tesz kis- és nagybetűk között. A “valami” és “Valami” szavak különbözőnek számítanak! Ez a későbbiekben számos hiba forrása lehet és lesz is! Nézzük meg, milyen részekből áll ez a program, amit egyfajta alapnak is tekinthetünk!
Rögtön az első sor magyarázatra szorul: public class Alapok
A public kulcsszó azt jelenti, hogy az adott osztály publikus, vagyis bárki láthatja, készíthet belőle példányt. A class kulcsszó azt jelenti, hogy ez a kód egy osztályt ír le, aminek a neve Alapok. Nagyon fontos, hogy ez nem egyenlő az alapok nevű osztállyal. Emlékszel? Kis és nagybetű. Az osztály neve meg kell, hogy egyezzen a fájlnévvel, ahova a forráskódot mentjük, jelen esetben a forráskódunk neve a következő legyen: Alapok.java
OSZTÁLY
Jegyezd meg: A Java nyelvben minden osztály nevét nagy kezdőbetűvel írjuk!
Máris sok mindent megtudtunk, amivel később rengeteg problémától kíméljük meg magunkat. Javaslom mindenkinek, hogy ezt az alap kódot a Java programozás elején sokszor gépelje be, hogy rögzüljön ez a szerkezet. Nézzük a következő sort, ami jelen esetben csak egy { jel. Ez egy blokk nyitását jelenti, egyfajta zárójelezés. A Java nyelvben a blokkokat (utasítások sorozatát) { } jelek közé tesszük. Mint a matematikában, úgy a programozásban is nagyon fontos, hogy a jelekhez, legyen az { [ vagy ( jel, mindig tartozzon nyitó és záró pár is. Ha ezek közül akár csak egy is hiányzik, akkor a programunk nem fordítható le, fordítási hibát eredményez.
A fordítási hiba egy programkód esetén azt jelenti, hogy nem felel meg az adott programnyelv “nyelvtani” szabályainak. Ezek jellemzően zárójel hibákat, pontosvessző hibákat jelentenek, melyeknek mindnek a helyén kell lennie, hogy nyelvtani szempontból hibátlan kódot kapjunk. Ez persze nem azt jelenti, hogy a programunk tökéletes, csak a fordító fogadja el. Az már csak megszokás kérdése, hogy ezeket a blokk jeleket ki hogyan használja. A következő sor a legbonyolultabb, de programozási szempontból ezt most elég, ha csak így megtanuljuk és alkalmazzuk. Később majd megértjük mit is jelent. Elég annyit tudni, hogy a main() metódus a programunk úgynevezett belépési pontja, vagyis itt kezdődik a programunk végrehajtása. Nem szabad viszont elsiklani egy nagyon fontos dolog felett ebben a sorban, amit nagyon komolyan kell vennie annak, aki programozni akar. Ezzel a fontos dologgal kezdődik az egész sor. A behúzásról van szó. Ez a sor beljebb kezdődik, mint az előző. Miért? Mert ez a sor egy blokkon belül található. Az előző sorban ugye nyitottunk egy blokkot, mert az osztály esetén kötelező blokkot használni. Az osztályban változókat és metódusokat (ami utasítások sorozata) fogunk használni. Egy blokkon a sorokat ajánlott beljebb kezdeni, hogy lássuk, ezek a sorok az adott blokkhoz tartoznak. Ennek nagyon fontos szerepe lesz a kód olvashatóságának szempontjából.
Nagyon fontos ezt már az elején megszokni, ha blokkot nyitunk, akkor a blokk sorai beljebb kell kezdeni. A blokk behúzását többféle módszerrel oldják meg. Van aki tabulátorral, van aki szóközökkel, van aki 2 szóközzel, van aki 4-gyel, 8-cal, stb. A lényeg a következetesség. Az első blokk egy szinttel kezdődjön beljebb, a következő, ami ezen a blokkon belül van egy újabb szinttel, stb. Erre a példakódban is látunk egy mintát, a System.out.println() sor már a main() metódus blokkjában van, ezért az nem 2 hanem 4 szóközzel kezdődik beljebb.
A következő sor már egy tényleges program utasítás. A System.out.println() egy alap metódus, amit a Java a konzolra történő kiíratásra használ. Itt arra kell odafigyelni, hogy a System szó nagybetűvel kezdődik. Mi kezdődött még nagybetűvel? A program osztálya, az Alapok. A System is egy osztály, ezért írjuk nagybetűvel, csakúgy, mint minden más osztályt. A beépítetteket másképp el sem fogadja a Java, egyszerűen annyit mond, ismeretlen szimbólum. (System és nem nem system) Ezért fontos az, hogy a saját osztályaink nevét is nagybetűvel kezdjük megszokásból, mert ez az íratlan szabály az osztályok használatára. Külön felhívnám a figyelmet a System.out.println() metódusra. Ez egy több részből álló metódus hívás. A System osztályon belüli out nevű objektum println metódusát hívja meg.
Az osztály van a legmagasabb szinten, annak van egy objektuma, az objektumnak pedig egy metódusa. De egy konkrét metódusra hivatkozáskor azt meg kell címezni. Melyik osztály melyik objektumában található. Egyszerűen egymás mellé kell őket írni megfelelő sorrendben . jelekkel (operátorral) elválasztva. Ügyeljünk rá, hogy a System egy osztály, tehát nagybetűs. Ami szöveget pedig odaadtunk a println() metódusnak, azt kiírja a konzolra (parancssorba).
Az utolsó nagyon fontos dolog, a kiíratást végző utasítás végén lévő pontosvessző! Java-ban minden egyes utasítást pontosvesszővel zárunk le. Egy utasítás, egy pontosvessző. Ha egymás után utasítások sorozatát írjuk (például egy blokkban), akkor az egyes utasításokat pontosvesszőkkel választjuk el egymástól. Ezeket az elválasztott utasításokat írhatnánk akár egy sorba is, de az olvashatóság azt kívánja, hogy egy utasítás egy sor. Így olvashatóbb és tagoltabb a kód. Nem a fordítónak szükséges ez, hanem az embernek, aki a kódot nézi. A pontosvessző viszont igencsak megkeserítheti az életünket, ha nem jó helyen használjuk. Ha olyan helyre rakjuk, ahol semmi keresnivalója, akkor a fordító jelezni fogja, hogy gond van. Persze akkor sem azt mondja, hogy rossz helyen van a pontosvessző, hanem maga a rossz helyen lévő pontosvessző okoz máshol gondot, de ha párszor belefutunk ebbe a hibába, utána rendszerint már hamar megtaláljuk. A gond az, hogy olyan helyre is tehetjük, amiért még a fordító se szól, hogy a kódunk nyelvtanilag helytelen, viszont a program nem egészen azt csinálja, amit meg elvárnánk tőle. Ez egy újfajta probléma. A már emlegetett nyelvtani hibát szintaktikai hibának hívjuk, ez az utóbb említett, amikor a programunk lefordul, de nem az elvárásnak megfelelően működik, szemantikai hibáról beszélünk. Annyit jegyezzünk meg most, hogy a pontosvesszőre nagyon ügyeljünk és csak utasítások végére tegyük ki, mert ott a helyük.
Vége is van a programunknak. Illetve mégsem. Mi maradt ki? A két blokkot lezáró } jel. Az első (belső) blokk a main() metódus a benne lévő kiíratással, a második (külső) blokk pedig maga az Alapok osztály, a tényleges programunk. Ha megnézzük a blokkokat nyitó és záró jelek függőlegesen egy oszlopban vannak, így könnyebb a párjukat megtalálni, az azon belül lévő részek pedig minden új megnyitott blokkban például 2 szóközzel beljebb kerültek.
Változók, avagy dobozok minden esetre
Ismerkedjünk meg a változókkal. A változóknak alapvetően két típusát különböztetjük meg: primitív és referencia. A referencia típusú változók nem egy egyszerű értére mutatnak, hanem egy összetettebb adatszerkezetre, maga a változó csak egy hivatkozás az adatszerkezet memóriabeli címére. Most a primitív változókat fogjuk tárgyalni.
A programunkban szinte mindig előfordul az, hogy valamilyen adatot tárolni szeretnénk. Az elméleti bevezetés cikkben foglalkoztunk azzal, hogy a Java egy objektum orientált programozási nyelv. Az objektumok pedig rájuk jellemző tulajdonságokból és az ezeket kezelő (beállító, lekérdező, módosító) metódusokból állnak. Ezeket a tulajdonságokat mi rendeljük hozzá az objektumokhoz, hiszen mi döntjük el, miket kívánunk használni. Minden tulajdonságot alapvetően három fontos elem határoz meg. A tulajdonság típusa, neve és értéke. A változó a háttérben valójában egy memóriaterület, ami valamilyen értéket tárol. Felsorolásképp nézzünk meg pár fontos megállapítást a változókkal kapcsolatban:
Változók
A változók neve egyedi kell hogy legyen (adott blokkon belül), csak betűvel kezdődhet, de utána bármennyi és bármilyen karakter állhat.
A változók értéke alapvetően nem fix, ezt módosíthatjuk, erre utal a változó név. (léteznek fix változók is, melyek értékét csak egyszer adhatjuk meg.)
Minden változó egy időpontban egyetlen értéket tárolhat. Ha új értéket adunk neki, az előző érték törlődik.
A változó neve a programunkon belül minden esetben a benne tárolt értéket jelenti. Ahol a változó nevét a programba illesztjük, ott az abban tárolt értéket fogja felhasználni. Említettem, hogy a változó típussal, névvel és értékkel rendelkezik. Ebből a típusról nem volt még szó. A típus azt határozza meg, hogy milyen jellegű értéket tárolhatunk az adott változóban. A primitív változók alapvetően négyféle típusúak lehetnek, egy-egy konkrét példával:
egész szám: 32
valós szám: 1.125
karakter: c
logikai érték: true
Ezek az alaptípusok, de az egész és valós számok váltózói a bennük tárolt szám nagyságától függően még tovább bonthatók. Az egész számok egész értékeket tárolnak tizedesjegyek nélkül. A valós számok tizedesjegyekkel rendelkező számokat jelentenek. A karakter típusú változó valamilyen billentyűzeten lévő begépelhető karaktert tárolhat (betűk, számok, írásjelek, speciális karakterek, szóköz, stb), valamint speciális, önmagában be nem gépelhető vezérlő karaktereket tartalmazhat. A logikai érték pedig egy kétállású kapcsoló, mely igaz vagy hamis értékeket tárolhat, pontosabban ezek angol nyelvű megfelelőjét (true, false).
A változó tehát egy típussal, névvel és a típus által meghatározott értékkel rendelkező adatelem. Nézzünk a változók használatára egy példát. Vegyünk egy autó objektumot és pár ahhoz tartozó tulajdonságot. Legyen az autónak sebességfokozata, pillanatnyi sebessége, színkódja, és egy jelző, ami a színkódot bővíti ki, hogy metálfényű-e vagy sem. Ebben a példakódban mind a négy alaptípus megtalálható.
public class Auto
{
public static void main( String[] args )
{
int fokozat;
double sebesseg;
char szinkod;
boolean metal;
System.out.println("Ez egy virtualis auto.");
fokozat = 4;
sebesseg = 48.6;
szinkod = 'R';
metal = true;
System.out.println( "Az auto sebessegfokozata: " + fokozat );
System.out.println( "Az auto pillanatnyi sebessege: " + sebesseg );
System.out.println( "Az auto szinkodja: " + szinkod );
System.out.println( "Az auto metalszinu: " + metal );
}
}
A változók használatával kapcsolatban két nagyon fontos fogalmat kell tisztázni:
deklarálás
inicializálás
A deklarálás a változó típusának és nevének megadását jelenti. Ennek általános formája:
típus változónév;
Az inicializálás a változónak történő kezdőérték adás. Általános formája:
változónév = kezdőérték;
Ez a két lépés akár össze is vonható, ekkor a következőt írjuk:
típus változónév = kezdőérték;
A változók tekintetében fontos ügyelni arra, hogy addig ne használjuk – nem is nagyon tudjuk – a változót, amíg nem rendelkezik kezdőértékkel, ami akár nulla vagy típustól függően speciális null (üres) érték is lehet. Használatnak minősül az is, ha a változó értékét ki szeretnénk íratni a képernyőre. A változó értékét természetesen többször is meg lehet változtatni, ilyenkor az előző érték, mint már említettem, törlődik. Ez az értékadás formailag ugyanolyan, mint az inicializálás. A különbség a kettő között csak annyi, hogy az inicializálás a legelső értékadás.
Ahogy már említettem, a változó nevét használva a benne tárolt értéket kaphatjuk meg. Amikor például ki szeretnénk írni, hogy milyen értéket tárol, akkor a következőt tesszük:
"Az auto sebessegfokozata: " + fokozat
Az idézőjelek közötti szövegrészt String-nek nevezzük. Ez egy karakterekből álló karakterlánc (szövegnek is nevezik, bár nem csak betűket tartalmazhat). A String egy referencia változótípus, később fogjuk tárgyalni, most elég annyit tudni róla, hogy amit idézőjelek közé teszünk, az String típusú lesz. Azért fontos ez, mert jellemzően szövegeket írunk ki a képernyőre. Látjuk azt azonban, hogy a szöveghez “hozzáadjuk” a változót. A Java nyelvben a műveletek többségét (csakúgy, mint matematikában) balról jobbra értékeljük ki. Ekkor az történik, hogy a megadott szöveghez hozzáfűzi a változó értékét. De nem csak ennyi történik, hanem egy nagyon fontos dolog is!
Az összeadás, mint művelet itt összefűzést jelent, vagyis a változó tartalmát odailleszti a szöveg végére. De a szöveg és a példában említett fokozat nevű változó nem egyforma típusú. Az összefűzés során a számérték szöveg típusúvá alakul egy automatikus konverziót (típusváltást) követően. Ezért lehet így kiíratni a változók értékét.
Még a primitív változóknál is léteznek adott típuson belül különféle “méretű” tárolók. Attól függetlenül, hogy egy változó mondjuk egész számokat tartalmazhat, meg lehet adni a méretét is. A mérete alatt azt értjük, hogy a számítógép hány bájton tárolja a szám értékét, ezáltal meghatározza azt az intervallumot, amekkora értéket a változó felvehet. Egész típuson belül a következő állnak rendelkezésünkre:
Típus Leírás Tárolás Intervallum
byte bájt méretű egész 8 bit [-128;127]
short rövid egész 16 bit [-32768;32767]
int egész 32 bit [-2147483648;2147483647]
long hosszú egész 64 bit [-9223372036854775808;9223372036854775807]
Az egész típusokhoz hasonlóan lebegőpontos számokat tartalmazó változóból is többféle, szám szerint kettő van.
Típus Leírás Tárolás
float egyszeres pontosságú lebegőpontos 32 bit
double kétszeres pontosságú lebegőpontos 64 bit
Mivel azonos típusból többféle méret létezik, már a kezdőérték megadásakor problémák lehetnek. Vegyük ezt a példát:
int szam = 10;
Ezzel semmi gond nincs, ugyanis az int típusba ez a méretű szám elfér. A byte és short típusoknál is hasonlóan lehet megadni kezdőértéket, arra kell csak ügyelni, hogy megfelelő méretű számot tároljunk csak benne. Az utolsóval már gond lenne, mert a tárolni kívánt érték már nincs benne a változónak megfelelő intervallumban.
byte b = 10;
byte b = -40;
byte b = 120;
byte b = 130;
A short típussal is hasonló a helyzet, ott például már nem lehetne egy 35000 értéket tárolni, mert nem fér el ebben a típusú változóban.
Mi a helyzet a long típussal? Egy érdekes hibába futhatunk bele egy ilyen sorral
long szam = 3000000000;
Tegyük fel, egy 3 milliárdos kezdőértéket akarunk adni. Fura, hiszen a változóban sokkal nagyobb méretű szám is beleférne. Ez a 3 milliárd melyik változónak az intervallumából log ki? Az int típuséból. Arról van szó, hogy ha csak egy számot leírunk mindenféle sallang nélkül, azt a Java int típusú értékként kezeli. A kisebb számoknál akkor hogyan oldja meg, hogy egy byte változóba belerakhatja a nagyobb méretű int típusú értéket? Konverzióval, vagyis a byte változóban már az átalakított, megfelelő méretű értéket helyezi el. Igen ám, de maradva az előző hibánál itt a gond az, hogy ekkora méretű int szám nem is létezik, ezért el sem fogadja így leírva. Ekkor külön jeleznünk kell a számérték végén a szám típusát a következőképp:
long szam = 3000000000L;
A leírt fix értékeket (nem csak ezeket, általában a leírt fix értékeket) literálnak nevezzük. Ez a long típus literálja. Középiskolában ekkora számokkal nem dolgozunk, de jó ha tudsz erről. Érdekes módon a Java a 10L helyett a 10l literált is elfogadja, holott alaphelyzetben a kis és nagybetűk között különbséget tesz.
A változók, mint adott méretű tárolók természetesen csak a nekik megfelelő méretű számokat képesek tárolni. A nagyobb méretű tárolókba elhelyezhetjük egy kisebb méretű változó értékét, de fordítva ez nem fordulhat elő, mert már fordítási hibával figyelmeztet a rendszer:
int i = 10;
short s = i;
Összefoglalva tehát az egész számokat:
négyféle méretű típusa létezik
nagyobb méretű típusba be lehet tenni a kisebbet, fordítva nem
a számokkal megadott literál int típusú, ha long típust szeretnél akkor 10L
A lebegőpontos számok esetén is hasonló a helyzet. Itt a jelzés nélküli literál alaphelyzetben double típust jelent: 10.0 Ha azonban mindenképpen float típust szeretnénk, akkor a 10F vagy 10f literál használatos. Itt is fordítási hibát ad, ha a nagyobb méretet a kisebb méretűben szeretnénk elhelyezni:
double d = 10.0;
float f = d;
Tizedesvessző helyett tizedes pontot használunk, valamit nem kötelező ezt sem megadni ebben az esetben:
double d = 10D;
float f = 10F;
Itt látszólag egész számot adunk kezdőértéknek, de a 10D literál miatt ez a 10.0 lesz valójában, vagy float típusnál hasonlóan csak kisebb méretben. Float típus értékmegadásánál viszont a literálnál kötelező ezt a formát használni, mivel az F elhagyásakor a számot alapból double méretűként kezelné a rendszer, ami viszont a float-ba nem fér bele. Ez a példa ezt a hibás használatot mutatja meg a jó megoldással együtt:
float f = 10.3; // hibás!
float f = 10.3F; // helyes
KIIRATÁS MÓDJA
A programozás során sokszor az a feladat, hogy valamilyen eredményt, vagy épp változók tartalmát írjuk ki a képernyőre. A kiíratás egyszerű kommunikáció a program és a felhasználó között. Ennek használata egyszerű, de vannak fontos szabályok, melyeket be kell tartani.
System.out.println() és System.out.print()
Kezdetben ezt a két metódust fogjuk kiíratásra használni, az alapvető igényeinket teljesen ki fogják szolgálni. A Java nyelvben a szövegeket idézőjelek “” közé tesszük. Nem macskaköröm, ahogy többször is hallottam. Na még egyszer: idézőjel.
Amit ilyen jelek közé írunk, azt a rendszer szövegnek tekinti. A kiíratás során a két metódusnak ilyen szövegeket szoktunk megadni, de ezekhez sokszor hozzá is fűzünk valamit. Lássunk akkor erre példákat:
1
2
3
4
5
6
int szam1 = 10;
int szam2 = 20;
int osszeg;
osszeg = szam1 + szam2;
System.out.println( "A szamok osszege: " );
System.out.println( osszeg );
A két kiemelt sorban láthatod azt, hogy a println() metódusnak odaadhatsz egy szöveget is, valamint egy változót is. A változót a kiíratás során átalakítja szöveggé, így a megjelenítés nem lesz gond. A két sort azonban össze is vonhatod:
System.out.println( "A szamok osszege: " + osszeg );
Ebben az esetben az történik, hogy az összeg változó tartalmát, ami egy egész szám, hozzáfűzi a szöveghez úgy, hogy közben át is alakítja azt is szöveg típusúvá.
Abban az esetben, amikor a + jel valamelyik oldalán szöveg található, akkor a + jel nem az összeadás, hanem az összefűzés műveletét jelenti!
Fontos, hogy az átalakítás csak a kiíratásra korlátozódik, az összeg változó továbbra is azt az egész számot tartalmazza, amivel továbbra is végezhetsz számításokat.
Az összefűzés tekintetében teljesen mindegy, hogy mi az összefűzés sorrendje, maximum a kiíratásnak nem lesz értelme:
System.out.println( osszeg + " a szamok osszege." );
Egy fontos problémára felhívnám a figyelmet, ami sokszor gondot jelent. Tömörítsük még a programunkat, ne számítsuk ki külön változóba az összeget, hanem magába a kiíratásba tegyük bele:
1
2
3
int szam1 = 10;
int szam2 = 20;
System.out.println( "A szamok osszege: " + szam1+szam2 );
A kiemelt sorban van egy nagyon fontos probléma, de egy picit félreteszem, és azonnal visszatérünk.
Nézzük meg a következő programot: adott egy egész szám, írjuk ki a kétszeresét.
int szam = 7;
System.out.println( "A szam ketszerese: " + szam * 2 ); // 14
Ez a megoldás teljesen helyes, és semmi gond nincs vele. Most írjuk ki a számot úgy, hogy hozzáadunk kettőt:
1
2
int szam = 7;
System.out.println( "A szam kettovel megnovelve: " + szam + 2 ); // 72???
Mi a gond? Azonos rangú műveletek esetén mi a műveleti sorrend? Balról jobbra haladunk. Vagyis:
kiírjuk a szöveget: “A szam kettovel megnovelve: “
hozzáfűzzük ehhez a szam-ot. “A szam kettovel megnovelve: 7”
hozzáfűzzük ehhez a 2-őt: “A szam kettovel megnovelve: 72”
Vagyis mivel a műveletek egyenrangúak, balról-jobbra haladva hatja végre. A szöveghez hozzáfűzi a a változót, majd az egészhez a 2-őt. Ezek alapján már értheted az átugrott feladatnál is mi a gond.
Mit tehetünk? Bíráljuk felül a műveleti sorrendet egy egyszerű zárójelezéssel.
1
2
int szam = 7;
System.out.println( "A szam kettovel megnovelve: " + (szam + 2) ); // 9
A szorzás esetén miért nem volt gond? Azért, mert először a szorzást végezte el, majd annak az eredményét fűzte hozzá a szöveghez.
A kiíratás során a szövegben használhatunk olyan speciális vezérlő karaktereket, melyek valamilyen plusz funkciót adnak hozzá a kiíratáshoz. Ezekből keveset használunk ténylegesen, de azért felsorolom azokat is, amelyek használhatók, csak gyakorlati haszna már nincs. Fontos, hogy ezeket a vezérlő karaktereket minden esetben \ jellel (backslash) vezetjük be, ami a mögötte elhelyezkedő karakternek más jelentést ad.
\n – sordobás (új sort kezd ennél a pontnál
\t – tabulátor (alapérték által meghatározott mezőnyit ugrik)
\b – backspace (balra egy karakter visszatörlés)
\r – visszaáll a kurzor a sor elejére, bármit írunk ezután, a sorban lévő szöveget törli
\a – néhány terminálon esetleg megszólaltatja a gép speaker-jét
\\ – maga a \ karakter
\” – idézőjel
Lássunk erre ömlesztett példákat, minden különösebb magyarázat nélkül:
System.out.println( "foo\bbar" );
System.out.println( "foo\rbar" );
System.out.println( "foo\nbar" );
System.out.println( "Gyakran hasznalt vezerlo karakterek:\n\\n \\\\ \\\"" );
System.out.println("Elso szam:\t" + 10);
System.out.println("Masodik szam:\t" + 20);
System.out.println("Harmadik szam:\t" + 30);
A println() és print() metódusok között annyi a különbség, hogy a println() úgy írja ki az adott szöveget, hogy utána új sort kezd. A print() pedig a kurzort a sor végén hagyja, vagyis a kiírt szövegek egymás mellé kerülnek. A példában azonban ha ismét kiírunk valamit, akkor az közvetlenül a Zsolt után folytatódik.
System.out.print("Kiss ");
System.out.print("Zsolt");
A kiíratásba akár komplexebb dolgok is belekerülhetnek, erre most csak egy példát írnék, a későbbi tananyagokban úgyis lesz több példa is ezekre, melyeket ez alapján meg fogsz érteni. Adott két szám, írd ki a kisebb szám kétszeresét:
int a = 15;
int b = 20;
System.out.println("A kisebb kétszerese: " + Math.min(a,b) * 2);
METÓDUSOK
A Java készítői rengeteg dolgot megírtak helyettünk, amelyeket kezdő programozóként sokszor úgy használunk, hogy nem is tudatosul. Ezek végzik a munka nagyját, ők azok, akik a változókat kezelik, és a programunk különböző részei között a kapcsolatot tartják. Mielőtt azonban megismernénk őket, említés szintjén tisztázzunk pár rövid mondatban két fogalmat.
Objektumok: Az objektum az adott feladat szempontjából fontos, a valódi világ valamilyen elemének a rá jellemző tulajdonságai és viselkedései által modellezett eleme. Ilyen lehet egy modellezett kutya, a saját tulajdonságaival (név, fajta, szín, magasság, stb), valamit a rá jellemző viselkedések (evés, ivás, ugatás)
Osztály: Megírt kódok, amelyek az objektumok tervrajzai, benne a tulajdonságokkal és a viselkedésekkel. Az objektumokat ezekből hozzuk létre.
Metódusok
Anélkül, hogy a metódusok készítéséről beszélnék, nézzük meg, mik ezek.
A metódusokat tekinthetjük egyfajta üzeneteknek, melyek valamilyen feladat megoldását kérik valakitől. Az üzenet 3 részből állhat.
Címzett objektum vagy osztály
A címzett metódusának neve
Esetleges paraméterek
Az üzenet címzettjét abban az esetben kötelező megadni, ha a címzett nem önmaga. A programozásunk elején, amíg nem készítünk saját objektumokat, javarészt így fogjuk használni a metódusokat, hogy megadjuk a címzettet.
Minden esetben kötelező megadni, hogy melyik metódust szeretnénk használni. A metódusok neve adott osztályon belül rendszerint egyedi. Valójában a metódus neve, a bemenő adatok típusai és száma együttesen kell hogy egyedi legyen. Az ezzel kapcsolatos úgynevezett túlterhelésről később szót ejtek. Különböző kódokban lehetnek azonos nevű metódusok, ilyenkor a metódus meghívását ki kell egészítenünk az objektum vagy osztály nevével, hogy melyikét szeretnénk használni.
Mivel az üzenetek (metódusok) valamilyen feladat végrehajtását kérik, ezért a feladattal kapcsolatos adatokat is oda lehet adni annak a metódusnak, amelyik azt elvégzi. Természetesen lehet olyan metódus is, ami nem használ bemenő adatokat.
Lássunk pár példát olyan metódusokra, melyeknél a címzettet is meg kell adni. Ezeknél a példáknál a címzett minden esetben egy osztály:
1
2
3
4
Math.min(23,b);
Math.sqrt(18);
Integer.parseInt("123");
System.out.println("Ez egy rovid mondat.");
két bemenő adat, egy szám és egy számot tartalmazó változó közül a kisebb értéket adja eredményül
megadja 18 négyzetgyökét
egész számmá alakítja a “123” szöveget
kiír egy szöveget a képernyőre
A Java programnyelv metódusait alapvetően két csoportba sorolhatjuk:
metódusok, melyek valamilyen értéket állítanak elő
metódusok, melyek valamilyen tevékenységet hajtanak végre
Az első csoport nagyjából olyan, mint egy táblázatkezelő program függvényei. Ezek a metódusok valamilyen bemenő adatok segítségével számítást végeznek, és adott típusú (szám, szöveg, karakter, stb) eredményt állítanak elő. Ez az eredmény a metódus visszatérési értéke. A bemenő adatok, melyeket innentől nevezzünk paramétereknek, valamilyen változók vagy literálok (a típusnak megfelelő formában megadott értékek), de az sem törvényszerű, hogy legyen bemeneti érték. Vannak metódusok, amelyek bemeneti egy előre meghatározott keretek közötti értéket adnak eredményül. A bemenő adatok száma is sokfajta lehet, de ebből egyelőre legyen elég ennyi.
A második csoportba tartozó metódusok valamilyen tevékenységet hajtanak végre, ide tartozik például a képernyőre való kiíratás.
Ez a tananyag elsődlegesen a gyárilag előre megírt metódusokról szól, a saját magad által megírt metódusok egy másik lecke része lesz. A gyári metódusokat tehát formailag a következőképp lehet használni:
Osztalynev.metodus();
Osztalynev.metodus( parameter );
Osztalynev.metodus( parameter1, parameter2, … );
Ahogy láthatod először hivatkozni kell arra az osztályra, amelyiknek a metódusát használni szeretnéd, aztán a speciális . operátor használatával hivatkozok az osztály adott nevű metódusára. Ez az a két dolog, ami minden esetben kötelező. A harmadik dolog, hogy odaadd a metódusnak azokat az adatokat melyeket neki használnia kell. Vagy azért, hogy egy eredményt állítson elő (1. típus), vagy hogy annak segítségével hajtsa végre a kért tevékenységet.
Minek kell az osztálynév, ha a metódusoknak neve van? Azért, mert létezhet több azonos nevű metódus, (sőt sokszor ez a helyzet), ezért meg kell mondani, hogy ez a metódus melyik osztályba tartozik.
A tanulás elején a Math osztályt szoktuk kiemelni egyszerűbb programok írásakor. Központi metódus természetesen sokkal több van, de a tanulmányaink elején ezekre jó eséllyel szükség lesz:
Math osztály
A Math osztály matematikai témakörrel kapcsolatos metódusokat tartalmaz. A metódusok többsége sokféle számot is elfogad, beleértve egész és valós értékeket is, azok összes altípusával együtt. Sőt, több ezeket keverve is működik. A teljesség igénye nélkül álljanak akkor itt a legfontosabbak:
Math.min(a,b); // a két változó közül a kisebb értéket adja eredményül
Math.random(); // kisorsol egy lebegőpontos számot a [0;1[ intervallumból
Math.round(a); // matematikai szabály szerint kerekíti a változó értékét
Math.abs(a); // az adott változó értékének abszolút értékét adja vissza
Math.sqrt(a); // az adott változó négyzetgyökét adja vissza
Math.pow(a,b); // az a számot a b-edik hatványra emeli
Math.PI; // Pi értékét adja vissza
Math.E; // E értékét adja vissza
OPERÁTOROK
A programozási nyelvek fontos részét képezik az operátorok. Nevezzük őket műveleti jeleknek, habár nem a szó matematikai értelmében. Programozás során sokszor úgynevezett kifejezésekkel dolgozunk, amelyek valamilyen értékek és közöttük értelmezett műveletek. Megnövelünk egy számot, összeadunk két változót, hogy egy harmadiknak megkapjuk az értékét, összehasonlítunk egy változót egy számmal, hogy egyenlőek-e, stb. Operátornak magát a műveletet nevezzük, operandusnak pedig a kifejezés azon részét, amit változtatni akarunk vagy amit felhasználunk a számításhoz. Az operandus lehet egy megadott literál (egy direkt érték), változó vagy kifejezés.
a = b;
b == 2
c = a + b;
i++;
a += b * 2;
c = a == 3 ? 0 : a;
Az előző példákban pirossal emeltem ki az operátorokat, a maradékok pedig az operandusok. Igyekeztem minél több példát felsorolni, de operátor ennél sokkal több van és ezeket többféle elv mentén csoportosíthatjuk.
Csoportosíthatjuk az operátorokat annak megfelelően, hogy hány operandust kötnek össze. Ennek megfelelően megkülönböztetünk:
egyoperandusú,
kétoperandusú,
többoperandusú operátort.
Logikusabb azonban aszerint csoportosítani őket, hogy milyen jellegű műveletet hajtanak végre, bár vannak olyanok, amelyek nem sorolhatók be egyértelműen művelet alapján egy csoportba sem.
Aritmetikai operátorok
Relációs operátorok
Értékadó operátorok
Logikai operátorok
Inkrementáló (növelő) operátorok
Feltételes operátor
Bitléptető és bitenkénti operátorok (esetleg később kifejtem)
Aritmetikai operátorok
Ezek jellemzően valamilyen matematikai műveletet hajtanak végre két számértéken. Ezek az operátorok a következők:
+
–
*
/
%
Az első hármat nem kell nagyon megmagyarázni, ezek matematikai alapműveletek, de az utolsó kettő már érdekesebb. Változókból, mint az 5. leckében már olvashattad léteznek egész és lebegőpontos típusok. A / jel az osztás jele, azonban ez kétféleképpen működik.
Egész osztás
Amennyiben a műveletet két egész szám között hajtjuk végre, akkor egész osztásról beszélünk. Ez azt jelenti, hogy hányszor van meg az egyik szám a másikban és a maradékkal nem foglalkozunk.
int a = 10;
int b = 3;
System.out.println(a / b);
a = 12;
b = 5;
System.out.println(a / b);
Az első kiíratás 3-at, a második 2-őt fog kiírni, és nem érdekel minket a maradék.
Valós osztás
Az / operátor használatakor másfajta eredményt kapunk akkor, ha a két szám közül legalább az egyik nem egész:
int a = 10;
double d = 3.0;
System.out.println(a / d);
Az eredmény 3.3333333…
Na de mi van akkor, ha két egész számunk van, de a teljes valós eredmény érdekel minket? Az nem elég, hogy 10/3, mert az operátor csak a két szám típusa alapján tudja eldönteni, hogy egész vagy valós osztást szeretnénk. Ehhez egy kis trükköt kell alkalmazni:
System.out.println(10 / 3.0);
// vagy
System.out.println(10.0 / 3);
Lényegtelen melyiket bővítem ki lebegőpontos számmá, a lényeg, hogy legalább az egyik az legyen. De változók használata esetén ezt nem tehetem meg, mert annak semmi értelme, hogy a.0/b. Mit tehetünk ilyenkor?
int a = 10;
int b = 3;
System.out.println(a / (b + 0.0));
Egyszerűen az egyik változó értékéhez hozzáadunk 0.0-t. Ettől az értéke nem változik meg, csak a típusa, tehát valós osztás lesz belőle. És minek a zárójel? Próbáld ki nélküle:
int a = 10;
int b = 3;
System.out.println(a / b + 0.0);
Mi is matematikában a műveletek sorrendje? Először elvégzi az osztást, ami egész osztás lesz, annak eredménye 3. Majd ehhez hozzáad 0.0-t, ami miatt 3.0 lesz és nem 3.33333. Nagyon sokszor előfordul ez a hiba, amikor az a feladat, hogy átlagot kell számolni. Figyeljünk oda, hogy egész osztásra vagy valós osztásra van szükségünk, és ha valós osztásra van szükségünk egész számok között, akkor ne maradjon le a +0.0
Maradékos osztás
Amikor két szám osztásakor nem a hányados, hanem a maradék érdekel minket, akkor van szükségünk a % operátorra. Használni is egyszerű:
int a = 10;
int b = 3;
System.out.println(a % b);
a = 12;
b = 5;
System.out.println(a % b);
Az első kiíratás 1, a második 2 lesz. Az osztás elvégzése után ennyi a maradék. A maradékos osztást számok osztóinak keresésekor szoktuk használni. Ha egy szám például 5-tel osztva nulla maradékot ad, akkor mit tudtunk meg a számról? Hogy osztható 5-tel. Ha a szám % 3 nullával egyenlő? Akkor 3-mal osztható. Ez később még többször előfordul, emlékezzünk rá. A maradékos osztás egyébként nem csak egész számok között működik, akkor is helyes eredményt ad, ha nem egész mindkét szám. A lényeg, hogy ha az egyik szám nem egész, akkor az eredmény is a nem egész típusnak megfelelő lesz. A következő példákat érdemes kipróbálni:
10 % 3
10.0 % 3
10 % 3.0
10.0 % 3.0
10.5 % 3
10 % 3.5
10.0f % 3.0
Relációs operátorok
Az operátorok következő csoportja a relációs operátorok. Ezek a matematikában is ismeretes relációk, melyek a következők:
Reláció Jele
kisebb <
nagyobb >
kisebb vagy egyenlő <=
nagyobb vagy egyenlő >=
egyenlő ==
nem egyenlő !=
A relációk első 4 fajtáját nem nagyon kell kifejteni, ellenben az == már magyarázatra szorul. Ez semmiképpen nem keverendő a = jellel. Nagyon sokszor keverik a kezdő Java programozók ezt a két operátort. Az == a két oldalon szereplő literál, változó vagy kifejezés egyenlőségét vizsgálja. Az = pedig az értékadást jelenti, amit lejjebb ismertetek. A relációk, így az egyenlőségvizsgálat is, egy logikai értéket adnak eredményül, ami igaz, vagy hamis lehet. Megjegyzésbe odaírtam a kiíratások mellé az eredményt is.
System.out.println( 5 >= 6 ); // false
System.out.println( 4 == 4 ); // true
System.out.println( 4 == 6 ); // false
System.out.println( 4 != 3 ); // true
System.out.println( 4 <= 5 ); // true
System.out.println( 6 < 3 ); // false
Értékadó operátorok
Ezek az operátorok valamilyen változónak adnak értéket. Az értékadás alapformája:
változó = kifejezés;
Az értékadás bal oldalán mindenképpen egy változónak kell szerepelnie, jobb oldalon pedig egy literál, változó, vagy olyan kifejezés (operátorok és operandusok összessége), amely egy értéket határoz meg, amit eltárolunk az értékadás bal oldalán lévő változóban. Az értékadó kifejezésben maga a bal oldali változó is szerepelhet. A kiemelt sorban egy olyan értékadás látható, ahol a kifejezésben is megtalálható a bal oldali változó, ebben a lépésben valójában a változót 1-gyel megnöveljük. Fontos, hogy a kezdőérték megadásakor (inicializáció) ilyen nem lehetséges, mert addig nem használható fel egy változó egy kifejezésben, amíg nincs kezdőértéke! Itt a kiemelt sor előtt a kezdőérték megadása megtörtént, tehát utána már növelhetem ilyen értékadással.
int a = 0;
a = a + 1;
Értékadó operátorból azonban több is van. Ezek többsége valamilyen matematikai művelettel van összekapcsolva:
+=
-=
*=
/=
%=
Ezek a típusok a már tanult aritmetikai operátorokkal kapcsolja össze a műveletet. Ilyen operátor használatakor kiértékelésre kerül a jobb oldal, és a bal oldali változó értékét az értékadáshoz kapcsolt műveletnek megfelelően végzi el. Megnöveli a változó értékét a jobb oldallal, csökkenti a változó értékét a jobb oldallal, szorozza a változó értékét a jobb oldallal, osztja a változó értékét a jobb oldallal, stb. Itt is igaz az, hogy ha a két oldalon egész értékek szerepelnek akkor a /= egész osztást jelent, ha legalább az egyik lebegőpontos érték, akkor valós osztás. Az utolsó típus a bal oldali változó eredeti értékét osztja a jobb oldallal és a maradékot tárolja el a bal oldali változó új értékének. Az utolsó esetben nem azonos típusok esetén tizedesjegy csonkolások is előfordulhatnak.
Logikai operátorok
A logikai operátorok feltételeket kapcsolnak össze. Ezek a feltételek rendszerint a már előzőleg ismertetett relációkhoz kapcsolódnak. Programozásban középiskolai szinten jellemzően 3 logikai műveletet használunk:
negálás (tagadás)
logikai és
logikai vagy
Ezek közül az első különbözik a másik kettőtől, mert ő nem feltételeket kapcsol össze, csak egy logikai kifejezés eredményét változtatja meg az ellenkezőjére.
Negálás:
A negálás (tagadás) egy logikai kifejezés értékét az ellenkezőjére változtatja. Ami igaz volt, az hamis lesz, ami hamis volt, az igaz lesz. Vagy egy eldöntendő kérdést fordíthatunk meg vele. A szám NEM páros? (tehát páratlan, mivel más lehetőség nincs)
!(szam % 2 == 0) // nem páros
!(szam > 5) // nem nagyobb, mint 5
!true // nem igaz, tehát hamis
Programozás során azonban a feltételek sokszor nem önmagukban állnak, hanem többet össze kell kapcsolni. Ezeket összetett feltételeknek nevezzük. Ha több feltételünk van, de azok együtt értendők, akkor azokat össze kell kapcsolni valamilyen logikai művelettel, erre szolgál a logikai és, valamint a logikai vagy művelet.
a szám páros és pozitív?
a szám nagyobb, mint 10 és páratlan?
a szám nagyobb, mint 10 és kisebb, mint 30?
a szám osztható 3-mal vagy 7-tel?
a szám nem negatív vagy páros?
Itt láthatóan összetett feltétellel dolgozunk, de nem mindegy, hogy azokat mi kapcsolja össze. Ráadásul a feltételek száma nem csak 2 lehet, bármennyi feltételt összekapcsolhatunk.
Logikai és:
A logikai és két vagy több feltételt kapcsol össze egyetlen összetett logikai kifejezéssé. Ha azt mondom, hogy a piros és gyors autókat szeretem, akkor szóba sem jöhetnek a kékek, zöldek, lassúak, stb, de egy tűzpiros Jaguar igen. A két feltételnek egyszerre kell teljesülnie. Definíció szerint ez a következőt jelenti: A logikai és művelettel összekötött részfeltételek akkor adnak együtt igaz értéket, ha a kifejezés minden részfeltétele igaz. Ebből következik, hogy ha egy részfeltétel hamis, akkor hamis az egész kifejezés. Természetesen több feltételt is megadhatok. Piros, gyors, Ferrari. Ettől kezdve az előző tűzpiros Jaguar is kiesett a kosárból, míg az előző két részfeltételes esetben még megfelelt volna. Minél több feltételt kötök össze, annál kevésbé kapok végeredményként igaz értéket. A logikai és művelet jele: &&
szam > 5 && szam % 2 == 0 // a szám 5-nél nagyobb ÉS páros
szám < 0 && szam % 2 != 0 // a szám negatív ÉS páratlan
szam > 10 && szam < 20 // a szám 10 ÉS 20 között van
Logikai vagy:
A logikai vagy szintén két vagy több feltételt kapcsol össze egyetlen összetett logikai kifejezéssé. Ha azt mondom, hogy a piros vagy gyors autókat szeretem, akkor ez jóval megengedőbb, mint az előző példa. Szóba jöhet a fekete Ferrari és a tűzpiros Trabant is, de természetesen a tűzpiros Ferrari is. A két feltételnek nem kell egyszerre teljesülnie ahhoz, hogy az összetett feltétel igaz legyen. A logikai vagy művelettel összekötött részfeltételek akkor adnak együtt igaz értéket, ha a kifejezés legalább egy részfeltétele igaz. Vagyis ha bármi igaz benne, akkor igaz az egész együtt is. Ha minden hamis, csak akkor hamis az egész kifejezés. A logikai vagy művelet jele: ||
szam > 0 || szam < 0 // a szám 0-nál nagyobb, VAGY 0-nál kisebb
szam > 10 || szam < 0 // a szám 10-nél nagyobb vagy negatív
Kizáró vagy:
Nem említettem meg egy logikai műveletet, ami még előfordulhat a programozási feladatokban, igaz ritkán. Ez a kizáró vagy. Nem győzöm eleget hangsúlyozni:
logikai vagy != kizáró vagy
Az igazi probléma a magyar nyelvvel van. Szeretem, használom, imádom, de a programozásban használatos gondolkodásmóddal sokszor szöges ellentétben áll:
Moziba menjek vagy tanuljak?
Négyes vagy ötös lesz a dolgozatom?
Fej vagy írás?
Fej vagy gyomor?
A magyar nyelvben nagyon sokszor a kizáró vagy műveletet használjuk. Vagy moziba megyek, vagy tanulok, a kettő együtt nem igazán működik. A dolgozatom vagy négyes vagy ötös lesz, de csak az egyik (jobb esetben). A kizáró vagy akkor igaz, ha pontosan egy részfeltétele igaz. Vagyis a két vagy több feltételből nem teljesülhet több egyszerre. De hogy a magyar nyelvben melyik vagy műveletet kell érteni a vagy szócskán ezt mindig a szövegkörnyezet és a feladat típusa határozza meg. Ha a két dolog egyszerre nem fordulhat elő, akkor csak a kizáró vagy jöhet szóba. De ha a barna hajú vagy szemüveges nők tetszenek, akkor a barna hajú és szemüveges is valószínűleg megfelel. A kizáró vagy művelet jele: ^
Relációk problémája
Térjünk vissza kicsit a relációkra. A tagadás, mint már említettem, megfordít valamit. Egy logikai értéket az ellenkezőjére változtat, de a relációkra is hatással van. Ez nem igazán a logikai kifejezésekhez kapcsolódó témakör, inkább logikai-szövegértési feladat, amivel kapcsolatban az a tapasztalat, hogy nagy problémák vannak ezzel a területtel.
Vegyük például a következőt: Mit jelent az, hogy nem nagyobb? A tipikus válasz: kisebb. NEM! A nem nagyobb azt jelenti, hogy kisebb vagy egyenlő. Hiszen, ha valami nem nagyobb, attól még vele egyenlő is lehet! Hasonlóan a nem kisebb jelentése: nagyobb vagy egyenlő. Lássuk akkor, hogy melyik relációnak melyik a tagadása:
Reláció Tagadása
kisebb nagyobb vagy egyenlő
nagyobb kisebb vagy egyenlő
kisebb vagy egyenlő nagyobb
nagyobb vagy egyenlő kisebb
egyenlő nem egyenlő
nem egyenlő egyenlő
Inkrementáló operátorok
Létezik két speciális operátor, mely egy változó értékének 1-gyel való növelésére és csökkentésére szolgál:
változó++;
változó--;
Úgy tűnik, hogy maga a növelés vagy csökkentés a következő utasítást helyettesíti teljes egészében:
változó = változó + 1;
változó = változó - 1;
Ez a két eset valójában 4 esetet jelent, a példákból egyértelmű lesz, mire gondolok. Ezek az operátorok rendkívül sokszor fordulnak elő, és a kód átláthatóságát sem rontja akkora mértékben, hogy ez gondot jelentene. Ez a 4 eset a következőképp néz ki változó növelés/csökkentés esetén:
változó++;
++változó;
változó--;
--változó;
Az alaphelyzet tehát az, hogy a ++ operátor megnöveli eggyel a változó értékét, míg a — csökkenti azt. Ezek a példák önálló utasításként működnek, ezért zártam le ezeket ; jellel.
Látható azonban, hogy mindkét operátor szerepelhet a változó előtt és után. Amikor az operátor a változó mögött szerepel, azt postfix alaknak nevezzük, ha előtte, akkor prefix alakról beszélünk. Nyilván nem csak esztétikai jelentősége van, lássuk a gyakorlati hasznát. Az első két példában mivel ebben a sorban csak annyi szerepel, hogy a változó értékét növeljük meg, ezért nincs a két megoldás között különbség. Azonban amikor a növelés vagy csökkentés egy kiíratás vagy összetettebb kifejezés része, akkor már fontos különbség adódik:
int a = 10;
System.out.println(a++); // 10
System.out.println(a); // 11
int a = 10;
System.out.println(++a); // 11
System.out.println(a); // 11
Az első példában a növelés, mint művelet, a változó után található. Ez a gyakorlatban azt jelenti, hogy a kiíratás először felhasználja a változó eredeti értékét (10), majd ha minden művelet lezajlott ebben a sorban, utána megnöveli a változó értékét (11), vagyis a következő sorban, ahol már művelet nélkül írjuk ki, már a megnövelt értékét láthatjuk.
A második példában a növelés, mint művelet, a változó előtt található. Ez azt eredményezi, hogy ebben a sorban először megnöveli a változó értékét, majd a változó már megnövelt értékét írja ki. A következő sorban is ugyanazt az értéket írja ki, mivel itt szintén az előzőleg megnövelt értéket használhatjuk. Ugyanez igaz a csökkentésre is.
Ezek összetettebb kifejezésben is így működnek. Amennyiben a változó előtt szerepelnek az inkrementáló operátorok, akkor ezeket hajtja végre, majd a megnövelt értékekkel dolgozik tovább. Ezek a műveletek keverhetők az aritmetikai operátorokkal is, tehát ennek is van értelme: a++ + ++b
A következő példákat tessék tesztelni, ezeken keresztül világos lesz ezen operátorok működése. Számold ki a tesztelés előtt, hogy melyik sorban mit kellene látnod eredményül. Ha elsőre nem sikerült, gyakorolj még egy kicsit.
int i = 4;
int j = 3;
System.out.println( i + ++j );
System.out.println( i++ + j++ );
System.out.println( ++i + j++ );
System.out.println( i + j );
Az ilyen operátorok gyakorlati felhasználása azonban jellemzően mégis inkább a ciklusokhoz kapcsolódik. Fontos azonban megjegyeznem, hogy használatuk sok esetben inkább önálló utasításként érdemes, mert a kód átláthatóságát nagyban rontják, ami programozáskor az egyik legfontosabb szabály!
Feltételes operátor
Ezt az operátort egyelőre nem fejteném ki részletesen. Abból a szempontból egyedi, hogy egyedül ő kapcsol össze 3 operandust. Alakja:
feltétel ? ha_igaz : ha_hamis;
Ez a forma így önmagában nem is fordulhat elő, ez valamilyen kifejezés része, legyen az egy értékadás, vagy egy szöveg összefűzés. Ez annyit jelent, hogy egy általunk megadott feltétel ha teljesül, akkor a ? utáni ha_igaz eredményt kapja a kifejezés, ellenkező esetben a ha_hamis helyre írtat. A feltételvizsgálatkor majd látni fogod, hogy ez voltaképp egy egyszerű if-else szerkezet jóval tömörebb formája is lehet. Nem használható minden if-else szerkezet kiváltására, és nem használjuk gyakran. Általában egysoros utasítások rövidítésénél fordul elő, de bonyolultabb kifejezésekben szinte mindig érdemes kikerülni. Vannak olyan programfejlesztéssel foglalkozó csapatok, ahol kifejezetten tiltják a használatát átláthatósági problémák miatt.
Operátorprecedencia – végrehajtási sorrend
Operátorból ettől azért sokkal több van, de ezeket a legfontosabb megemlíteni ahhoz, hogy értsük a működésüket, és a programozást elkezdhessük. Fontos azonban megemlíteni azt, hogy az operátorok között is létezik egyfajta erősorrend, csakúgy, mint a matematikai műveletek között. Ezt hívjuk az operátorok precedenciájának, más néven kiértékelési sorrendjének. A lista tetején a végrehajtási sorban előbb végrehajtandó műveletek vannak. Az összes értékadó operátorok a lista legalján van, ahol a “leggyengébb” műveletek helyezkednek el. A lista nem teljes, ettől több operátor is létezik, de alapnak ezek teljesen elegendők.
Operátor leírás Operátorok
postfix operátorok változó++ változó- –
prefix operátorok, előjel
operátorok, negálás ++változó – -változó +változó -változó !
Aritmetikai operátorok
(multiplikatív) * / %
Aritmetikai operátorok (additív) + –
Relációs operátorok < > <= >=
Relációs operátorok
(egyenlőségvizsgálat) == !=
Kizáró vagy ^
Logikai és &&
Logikai vagy ||
Feltételes operátor ? :
Értékadó operátorok = += -= *= /= %=
Ha jobban megnézed, akkor a precedencia nagyrészt megegyezik a matematikai műveletek sorrendjével, valamint itt is igaz az, hogy zárójelezéssel felül lehet, és sokszor felül is kell bírálni azt. Egyetlen fontos szabályt hagytunk csak ki. Mi van akkor, ha azonos szinten lévő operátorok szerepelnek a kifejezésben? Akkor mi a sorrend? Ebben az esetben mindig a balról-jobbra kiértékelési sorrend az érvényes.
Itt is van azonban egy kakukktojás, igaz, ez inkább elméleti dolog. Emlékszel, hogy az értékadó operátoroknál mi a kiértékelési sorrend? Hiszen az is két operandust köt össze. A bal oldalon egy változó, a jobb oldalon pedig literál, változó vagy kifejezés állhat. Na de a két operandus közül melyiket kell először használni? A jobb oldalit, mert annak az eredménye kerül a bal oldali változóba. Akkor jöjjön az elméleti példa:
int a, b;
a = b = 2;
Mi lesz ennek az eredménye? Minden változó 2 lesz. De az előbb említettem, hogy azonos precedencia szintű operátorok esetén a végrehajtási sorrend balról jobbra halad. Igen, kivéve az értékadásnál. Itt mindig a jobb oldal kerül először kiértékelésre. Vagyis:
az a változó értéket kap: a = b = 2;
a jobb oldalon megint egy értékadás szerepel: b = 2;
a b változó jobb oldalán lévő érték bekerül a b változóba (tehát a b = 2 eredménye 2 lesz)
a b = 2 kifejezés eredménye (2) bekerül az a változóba
Vagyis: jobbról balra haladva értékelte ki a többszörös értékadást. Ritka, de előfordulhat és működik. És természetesen, mivel az átláthatóságot rontja, kerülendő
LOGIKAI MŰVELETEK
Logikai kifejezésnek nevezzük azt, amelynek az eredménye igaz vagy hamis (true – false) lehet. Ezek valójában eldöntendő kérdések:
a szám páros?
a szám osztható 3-mal?
a szám kisebb, mint 100?
A logikai kifejezések előfutáraként az operátorokat bemutató leckében szót ejtettem a különféle logikai operátorokről:
Negálás: Egy logikai értéket az ellenkezőjére állít (ami true volt, false lesz, és fordítva)
Logikai és: Akkor igaz az összetett kifejezés, ha minden részfeltétele igaz (ha bármelyik hamis, hamis az egész)
Logikai vagy: Akkor hamis az összetett kifejezés, ha minden részfeltétele hamis (ha bármelyik igaz, igaz az egész)
Kizáró vagy: Akkor igaz összetett kifejezés, ha a részfeltételek közül csak egy igaz (ha több részfeltétel igaz, vagy mind hamis, akkor hamis az egész)
Jöjjön akkor pár kapcsolódó példa:
Adj meg olyan logikai kifejezést, mely igaz értéket ad pozitív páros számok esetén:
szám % 2 == 0 && szám > 0
Adj meg olyan logikai kifejezést, mely igaz értéket ad, ha a szám nagyobb, mint 10 és páratlan:
szám > 10 && szám % 2 != 0
Adj meg olyan logikai kifejezést, mely igaz értéket ad, ha a szám a 10 és 30 között van:
szám > 10 && szám < 30
Adj meg olyan logikai kifejezést, mely igaz értéket ad, ha a szám osztható 3-mal vagy 7-tel:
szám % 3 == 0 || szám % 7 == 0
Adj meg olyan logikai kifejezést, mely igaz értéket ad, ha a szám nem negatív vagy páros:
szám >= 0 || szám % 2 == 0
Adj meg olyan logikai kifejezést, mely a négyes vagy ötös dolgozatjegyre ad igaz értéket:
jegy == 4 || jegy == 5
Rövidzár kiértékelés
A logikai kifejezésekkel kapcsolatban fontos megemlíteni az úgynevezett rövidzár kiértékelést. Ez a szabály a logikai és, valamint a logikai vagy esetén érvényes. A rövidzár kiértékelés picit másképp működik a két esetben, de teljesen logikus lesz, ha megérted.
Logikai és műveletnél emlékszel arra, hogy csak akkor igaz az összetett kifejezés, ha minden részfeltétele igaz. Ez azt jelenti, hogy ha akár csak egyetlen hamisat találunk, akkor a többit felesleges is megvizsgálni. Nézzünk rá egy példát. Ha egy olyan feltételt szeretnénk megadni, amely olyan számokat fogad el, melyek 3-mal és 4-gyel is oszthatók, akkor a következőt tesszük:
szám % 3 == 0 && szám % 4 == 0
Mit is csinál a Java pontosan? A logikai és két oldalát a balról jobbra elv alapján vizsgálja meg. Ha a szám osztható 3-mal, akkor meg kell nézni a jobb oldali feltételt is, mert csak akkor igaz az egész, ha minden része igaz. És ha a bal oldal hamis? Akkor már nem is lehet soha igaz, és – ez a legfontosabb! – a jobb oldali feltételt már meg sem vizsgálja! Nagyon fontos ezzel tisztában lenni, mert sokszor használatos.
Ugyanez az elv létezik a logikai vagy esetén is, csak pont fordítva. Egy olyan feltételt szeretnénk megadni, amely olyan számokat fogad el, melyek 3-mal vagy 4-gyel is oszthatók (esetleg mindkettővel), akkor a következőt tesszük:
szám % 3 == 0 || szám % 4 == 0
Akkor egy kis deja vu. Mit is csinál a Java pontosan? A logikai vagy két oldalát a balról jobbra elv alapján vizsgálja meg. Ha a szám nem osztható 3-mal, akkor meg kell nézni a jobb oldali feltételt is, mert csak akkor igaz az ha van benne legalább egy igaz. És ha a bal oldal igaz? Akkor már igaz az egész kifejezés, és a jobb oldali feltételt már meg sem vizsgálja! Olyan ez, mint amikor amikor a kitűnő vagy bukott diákokat vizsgáljuk. Akkor kitűnő, ha minden jegye 5-ös, és akkor bukott, ha van 1-es érdemjegye. Feltételekkel ez hogy nézne ki? Egy későbbi példa kedvéért legyen csak két tantárgya:
jegy1 == 5 && jegy2 == 5
Ha már az első jegye nem 5-ös, akkor a többit meg se nézi a program, hiszen felesleges. Hasonlóan a bukott diák:
jegy1 == 1 || jegy2 == 1
Ha már az első jegye 1-es, akkor a többit meg se nézi a program, mert már igaz az összetett feltétel, ha van 1-es jegye, akkor megbukott.
Negálás
A negálás olyan terület, ahol könnyen hibázhat az ember. Ugyanazt a vizsgálatot két oldalról is meg lehet közelíteni, és mindkettő helyes. Vegyük például a már emlegetett kitűnő tanulónkat. Azt, hogy valaki kitűnő úgy definiáljuk, hogy minden jegye 5-ös. Igen ám, de azt is mondhatom, hogy nincs olyan jegye, ami nem 5-ös. Elsőre meredek lehet, a dupla tagadás amúgy kedvenc a magyar nyelvben. Lássuk akkor példával:
jegy1 == 5 && jegy2 == 5
Ez már ismerős volt, ő a kitűnő. Akkor nézzük meg így:
!(jegy1 != 5 || jegy2 != 5)
Mit is írtam itt pontosan? A vagy miatt, ha legalább az egyik jegye nem 5-ös, akkor igaz a zárójeles kifejezés – ami azt jelenti, hogy nem kítűnő – majd ezt az egészet negálva hamisat kapok, mégiscsak kitűnő. Nincs olyan jegye, ami nem 5-ös. A vaggyal összekötött részfeltételek együtt csak akkor hamisak, ha mindegyik hamis, vagyis minden jegye NEM 5-ös. Ha minden jegye NEM 5-ös és ezt tagadom, az pedig azt jelenti, hogy minden jegye 5-ös, vagyis kitűnő. Nem egyszerű példa, ez az egész a matematikai logikában és halmazelméletben ismert De Morgan azonosságokra vezethető vissza. Ami a lényeg az egészből: ugyanarra kétféle megoldás is létezik, melyek teljes mértékben megegyeznek, neked csak az a feladatod, hogy a számodra egyszerűbbet megtaláld. Hasonlóan immár magyarázat nélkül megmutatom két példával a bukott diák esetét is:
jegy1 == 1 || jegy2 == 1
vagy
!(jegy1 != 1 && jegy2 != 1 )
Na jó, egy kis magyarázat a második esethez. Ha egyik jegye sem 1-es, és ezt tagadom, az mit jelent? Nem azt, hogy minden jegye 1-es! Azt jelenti, hogy van legalább egyetlen olyan, ami 1-es!
Ha a kifejezésben csak ÉS vagy csak VAGY logikai kapcsolatot használsz, de egyszerre a kettőt nem, akkor általános formában ez az átalakítás a következőképp néz ki:
a logikai kapcsolatot változtasd át a másikra (és-t vagy-ra meg vagy-ot és-re)
a használt relációkat változtasd az ellenkezőjére (vigyázz, emlékezz a relációknál tanultakra!)
negáld az egész kifejezést
Na, még egy pár példa erre az átalakításra:
Írj kifejezést, ami a 3-mal és 5-tel nem osztható számokra ad igaz értéket:
szám % 3 != 0 && szám % 5 != 0
vagy
!(szám % 3 == 0 || szám % 5 == 0)
Írj kifejezést, ami csak a [10;30] intervallumba NEM tartozó számokat fogadja el:
szám < 10 || szám > 30
vagy
!(szám >= 10 && szám <= 30)
Írj kifejezést, ami csak a negatív páratlan számokat fogadja el:
szám < 0 && szám % 2 != 0
vagy
!(szám >= 0 || szám % 2 == 0)
Oké, mondhatnád, hogy itt bonyolítjuk a dolgot, hiszen a negálást, mint műveletet beletesszük egy kifejezésbe, ami egyénként nincs benne. És ha fordítva van?
!(szám % 5 != 0 || szám < 0) // ööö, ez mit csinál?
Egyszerűsítsük!
szám % 5 == 0 && szám >= 0 // 5-tel osztható nem negatív szám
BLOKKOK VAGY UTASÍTÁSOK
Blokknak nevezzük azokat a programrészeket, melyeket egy speciális határolóval összekapcsolunk. A blokk jele a { és } jel. Ezek a jelek mindig párban állnak, jelzik a blokk elejét és végét. Blokkot nyitni a programban szinte bárhol lehet, ahol utasításokat kell csoportba foglalnunk. De blokkok tartoznak kötelező jelleggel az osztályokhoz és metódusokhoz is. Az osztály és metódus blokkokra most külön nem térnék ki, ezeket csak használjuk a mintakódok alapján. Példa a blokkokra:
{
utasítás1;
utasítás2;
...
...
utasításN;
}
A fenti példában a kiemelt blokk határolók közé foglalt utasításokat a program mindig együtt fogja végrehajtani. Amint a program ehhez a ponthoz ér a program futása közben, elkezdi soronként feldolgozni őket. Ilyen blokkot, ami semmihez nem kapcsolódik, a programokban ritkán használunk, ezek legtöbbször valamilyen feltételvizsgálathoz vagy ciklusokhoz kapcsolódnak.
A blokknak nem csak az a szerepe, hogy összefogja az utasításokat, hanem úgynevezett hatókört is meghatároz. Ha egy blokkon belül deklarálunk egy változót, akkor az a változó csak és kizárólag azon a blokkon belül létezik. Értelemszerűen a blokk előtt nem létezhet, hiszen ott még a deklaráció sem történt meg, de a blokk után sem értelmezhető, hiszen a változó a csak blokkon belül használható. Álljon itt egy elméleti példa erre az esetre:
{
int i = 10;
i = i + 5;
}
i = i + 5;
A kiemelt sorra a fordító hibát fog jelezni, mert a változót a blokkon belül deklaráltuk, a blokk után nem létezik. Ehhez hozzátartozik az, hogy a blokk után újra deklarálhatok egy ugyanilyen változót, a kettő nem fog ütközni, hiszen az előző élettartama a blokk után véget ér, és a változó nevét újra felhasználhatom.
A blokkok nem csak több utasítást foghatnak össze, akár egyetlen utasítást is blokkba lehet foglalni, de olyan is előfordulhat, hogy a blokk üres.
Kötelező blokkok
Attól függetlenül, hogy egy utasítást nem kötelező blokkba tenni, én mindenkinek azt ajánlom, hogy egyetlen utasítást is tegyenek { } jelekkel határolt blokkba. Ez azért fontos, mert a későbbiekben lehet, hogy az utasítás mellé szükség lesz egy másikra is, amit ha csak az előző utasítás után írunk az már nem lesz megfelelő. Abban az esetben engedett csak meg a rövidítés, amikor egyértelmű, hogy a későbbiekben biztosan nem lesz bővítés.
Példák
Végezetül minden különösebb nélkül álljon itt pár példa feltételvizsgálathoz és ciklushoz kapcsolt blokkokra, kiemelve a blokkokat. Az utolsó példa blokkja üres lesz, ezt a kódrészt nem magyaráznám el különösebben, a fájlkezelésnél majd találkozunk vele.
int szam = 25;
if( szam % 2 == 0 )
{
System.out.println("A szam paros.");
}
—–
int j = 0;
for( int i = 0; i < 20; i++ )
{
j = i * 2;
System.out.println(j);
}
—–
int i = 10; while( i > 0 )
{
i--;
System.out.println(i + 1);
}
—–
try
{
...
...
}
catch( IOException e ) { }
FELTÉTELEK
A programozási nyelvek egyik alapja az elágazás, más néven szelekció. Programjainkban szinte minden esetben előfordul az, hogy valamilyen pontnál dönteni kell, hogy most erre vagy arra induljunk, ami valamilyen feltételtől függ. Például:
Ha a szám kisebb, mint 0, akkor a szám negatív.
Ha a szám kettővel osztva 0 maradékot ad, akkor a szám páros.
Ha a szám a [100;999] intervallumban van, akkor a szám 3 számjegyű.
Ha a testtömegindexem 25 vagy attól nagyobb, akkor túlsúlyos vagyok.
Ezek a példák olyan értelemben egyformák, hogy bizonyos feltétel teljesülése esetén a program elvégezhet olyan utasítást, amit a feltétel nem teljesülése esetén kihagy.
A feltételvizsgálatokat több típusra bonthatjuk, attól függően, hogy hány elágazásra bomlik és hogyan kezeljük az úgynevezett “egyéb” ágat. Nézzük az első, legegyszerűbb típust:
if( feltétel ) utasítás;
vagy blokk használata esetén
if( feltetel )
{
utasitas1;
utasitas2;
..
utasításN;
}
Ezek tehát azok az alkalmazási formák, melyeknél a feltétel teljesülése esetén a program végrehajtja a feltételhez kötött utasítást vagy utasításblokkot, egyébként nem csinál semmit sem, csak átugorja őket. Személy szerint én jobb szeretem az egy utasításos feltételvizsgálatnál is kitenni a blokk határolókat, hogy ha később bővíteni kell, nehogy kimaradjon. Igaz, ezt inkább a diákok miatt teszem így, mert náluk ez valóban gondot szokott okozni, hogy elfelejtik. A feltételvizsgálatok alapja a logikai kifejezés, melyet előzőleg már ismertettem.
Nézzünk pár példát. A kiemelt sorokat külön megmagyarázom, ezeket érdemes megjegyezni azért, mert ezek olyan alapvizsgálatok, melyekkel sokszor fogsz a programozás során találkozni.
Adott egy szám. Vizsgáld meg, hogy negatív-e a szám és írd ki, ha igen.
int szam = 25;
if( szam < 0 )
{
System.out.println("A szam negativ.");
}
Adott egy szám. Vizsgáld meg, hogy páros-e a szám és írd ki, ha igen.
int szam = 25;
if( szam % 2 == 0 )
{
System.out.println("A szam paros.");
}
A kiemelt sort érdemes megjegyezni, mert ez olyan alapfeltétel, amelyikkel sokszor találkozol majd a programozásban: Ez a párosságot vizsgálja. Ha egy számot kettővel osztunk és a maradék 0, az azt jelenti, hogy a szám páros. Felhívnám a figyelmet itt egy tipikus hibára, amellyel nagyon sokszor találkozom. Sokan így vizsgálják a páratlanságot:
if( szam % 2 == 1 ) ...
Mi ezzel a gond? Negatív számok esetén ha kettővel osztom a számot és az páratlan volt, akkor a maradék nem 1 lesz, hanem -1, tehát ez a feltétel a -3 számot párosnak adná meg, ami nyilvánvalóan helytelen. Viszont a maradék így lehet 1 vagy -1 is, ami mindjárt két vizsgálatot jelent:
int szam = 25;
if( szam % 2 == 1 || szam % 2 == -1 )
{
System.out.println("A szam páratlan.");
}
A kiemelt sorban a két részfeltételt (1 vagy -1 a maradék) egy LOGIKAI VAGY kapcsolattal köti össze, így a program teljesen hibátlan lesz. De vegyük észre azt, hogy egy szám csak páros vagy páratlan lehet, más eset nincs. A két eset egymás ellentettje. Másképp megfogalmazva: ha nem páros, akkor páratlan. Így picit egyszerűbb ellenőrizni, hiszen a párosságot egy feltétellel vizsgálhatom. Lássuk, mire gondolok:
int szam = 25;
if( szam % 2 != 0 )
{
System.out.println("A szam páratlan.");
}
A kiemelt sorban ha a maradék nem nulla (ami ugye 1 vagy -1 is lehet), akkor a szám biztosan páratlan. Ezt sem árt megjegyezni a későbbiekre való tekintettel. Adott egy szám. Vizsgáld meg, benne van-e ebben az intervallumban: [10;40] és írd ki, ha igen.
int szam = 25;
if( szam >= 10 && szam <= 40 )
{
System.out.println("A szam benne van az intervallumban.");
}
Az intervallum vizsgálatnál láthatod, hogy akkor van benne egy adott szám egy intervallumban, ha annak alsó határánál nagyobb vagy egyenlő ÉS a felső határánál kisebb vagy egyenlő. Ezt a két részfeltételt a LOGIKAI ÉS művelettel kell összekötni.
A következő típusú feltételvizsgálat az, amikor megjelenik az “egyéb” ág. Formailag ez a következőképp néz ki:
if( feltétel ) utasítás1;
else utasítás2;
vagy
if( feltetel )
{
utasitas1;
utasitas2;
..
utasításN;
}
else utasításX;
Ebben az esetben szintén adott egy feltétel, melyhez kapcsolódik valamilyen utasítás vagy blokk, de a feltétel nem teljesülése esetén is van teendő, ilyenkor az else ág utasításai futnak le (végrehajtódnak). Ennél a típusnál a blokkok használata miatt 4 különböző forma különíthető el, de nem fogom mindet felsorolni példával, az utolsó példa esetén láthatjuk, hogy akár keverhetjük az egy utasításos formát az egyik ág esetén blokkhasználattal a másik ágnál. Fontos az, hogy itt a két ágból az egyik utasításai minden esetben lefutnak. Vagy a feltételhez kötöttek, vagy minden más esetben. Ha egy szám nem páros, akkor páratlan. Ha a pénzfeldobás eredménye nem fej, akkor írás. Ezt a szerkezetet tehát olyan esetekben lehet használni, amikor az elágazás két oldala egymás ellentettje. viszont ha egy autó nem benzinüzemű, az nem jelenti azt, hogy csak dízel lehet.
A következő esetben már két ágnál is több létezik. Formailag ez így néz ki:
if( feltétel1 ) utasítás1;
else if( feltétel2 ) utasítás2;
else utasítás3;
vagy blokkok használata esetén
if( feltétel1 )
{
utasítás1;
utasítás2;
...
utasításN;
}
else if( feltétel2 ) utasításX;
else
{
utasításY;
utasításZ;
}
Az utolsó blokkos példa esetén láthatjuk azt, hogy a blokkok használata mindig a példától függ. Ahol több utasítás kapcsolódik egy ághoz, oda kötelező blokkot tenni, az egy utasítású ágakhoz nem kötelező (de lehetne).
Ilyen 3 ágú elágazásra jó példa a pozitív-negatív vizsgálat.
int szam = (int)(Math.random() * 21) - 10;
if( szam < 0 )
{
System.out.println("A szam negativ.");
}
else if( szam > 0 )
{
System.out.println("A szam pozitiv.");
}
else
{
System.out.println("A szam nulla.");
}
Egymást kiegészítő ágak vannak, vagyis egy szám a három eset valamelyikének megfelelő lehet. Van else ág, mert mindenképp valamilyen kategóriába be kell sorolni a számot, kihagyni nem lehet. Viszont a feltételeket másképp is megadhatom, más sorrendben vizsgálom, hogy milyen a szám. Ez az else ágra is hatással van, hiszen akkor ott mást jelent majd az “egyéb”. Lássuk akkor az előző példa megoldását másképp:
int szam = (int)(Math.random() * 21) - 10;
if( szam = 0 )
{
System.out.println("A szam nulla.");
}
else if( szam < 0 )
{
System.out.println("A szam negativ.");
}
else
{
System.out.println("A szam pozitiv.");
}
A kétféle feltételvizsgálat a működése során ugyanazokra a számokra ugyanolyan eredményeket ad. Egyszerűen csak más sorrendben adtam meg a feltételeket. Az else if ágak száma nem kötött, bármennyit lehet használni belőle. Nem ritka a 6-7 ágú feltételvizsgálat sem.
int honap = (int)(Math.random() * 12) + 1;
if( honap == 3 || honap == 4 || honap == 5 )
{
System.out.println("Tavasz");
}
else if( honap == 6 || honap == 7 || honap == 8 )
{
System.out.println("Nyar");
}
else if( honap == 9 || honap == 10 || honap == 11 )
{
System.out.println("Osz");
}
else
{
System.out.println("Tel");
}
Az ilyen típusú többágú feltételvizsgálatok, ahol egy változó különféle értékeit kell vizsgálni, könnyebben és hatékonyabban megoldható egy speciális többágú feltételvizsgálatnak, melyet switch-nek nevezünk. Erről majd később bővebben írok.
Végezetül jöjjön az a típus, amikor csak az if és else if ágak szerepelnek, de else ág nincs. Adott egy változó, mely egy számot, egy adott ember testmagasságát tárolja. Meg kell vizsgálni, hogy az illető a testmagassága alapján túl magas vagy túl alacsony-e. Ha a testmagassága normális, akkor ne írjunk ki semmit sem:
// a testmagassag a [140;200] intervallumban legyen
int magassag = (int)(Math.random() * 61) + 140;
if( magassag < 160 )
{
System.out.println("Tul alacsony");
}
else if( magassag > 190 )
{
System.out.println("Tul magas");
}
Ilyenkor két egymással összefüggő feltételt vizsgálok meg, de nincs else ág, vagyis lehetséges, hogy egyik feltétel sem teljesül és a program kihagyja ezeket az utasításokat. A programrész 160 alatti magasság esetén kiírja a Tul alacsony, 190 felett pedig a Tul magas szöveget. Átlagosnak vélt magasság esetén semmit nem ír ki.
A többágú feltételvizsgálatok esetén minden esetben csak egyetlen ág utasításai kerülnek végrehajtásra. Sőt, nem és értékeli ki az összes feltételt a program, az első teljesült feltétel esetén végrehajtja az ahhoz kapcsolt utasításokat és a többit automatikusan átugorja, meg sem vizsgálja. Maradva a hónapos példánál. Ha a hónap száma mondjuk 5, akkor az első feltétel lesz igaz, kiírja a program, hogy Tavasz és rögtön az else ág végére ugrik, a többi esetet nem vizsgálja. Nyilván ezt akkor kell így megoldani, ha a feltételek egymást kizáró vizsgálatokat tartalmaznak. Egy hónap csak egy évszakhoz tartozik, és ha megvan a megfelelő évszak, a többi vizsgálat felesleges. Nézzük ugyanezt a hónapvizsgálatot másképp:
int honap = (int)(Math.random() * 12) + 1;
if( honap == 3 || honap == 4 || honap == 5 )
{
System.out.println("Tavasz");
}
if( honap == 6 || honap == 7 || honap == 8 )
{
System.out.println("Nyar");
}
if( honap == 9 || honap == 10 || honap == 11 )
{
System.out.println("Osz");
}
if( honap == 12 || honap == 1 || honap == 2 )
{
System.out.println("Tel");
}
Ez is ugyanazt eredményezi, csak itt egyrészt nincs else ág, tehát a Tel évszakot is külön meg kell vizsgálni, másrészt itt hiába teljesül 4. hónap miatt az első feltétel, mivel a többi feltétel nem az első elágazása (nem else if ágak), ezért ilyenkor minden feltételt a többitől függetlenül megvizsgál teljesen feleslegesen. Ez ugyanis nem egy többágú feltételvizsgálat, hanem 4 egymástól független eset, melyeknek semmi köze egymáshoz. Az összefüggő többágú feltételvizsgálat esetén sem mindig mindegy, hogy milyen sorrendben adom meg a vizsgált feltételeket. Erre jó példa a szökőév vizsgálat, amit majd külön feladatként ismertetek. Álljon itt akkor egy példa független és egy többágú feltételvizsgálatra:
// 1. példa
int szam = (int)(Math.random() * 50) + 1;
if( szam % 3 == 0 )
{
System.out.println("A szam oszthato harommal.");
}
if( szam % 5 == 0 )
{
System.out.println("A szam oszthato ottel.");
}
// 2. példa
int szam = (int)(Math.random() * 50) + 1;
if( szam % 3 == 0 )
{
System.out.println("A szam oszthato harommal.");
}
else if( szam % 5 == 0 )
{
System.out.println("A szam oszthato ottel.");
}
Nézzük meg mit ír ki az 1. és a 2. példa a következő számokra:
Szam 1. példa
független 2. példa
többágú
4 — —
10 A szam oszthato ottel. A szam oszthato ottel.
9 A szam oszthato harommal. A szam oszthato harommal.
15 A szam oszthato harommal.
A szam oszthato ottel. A szam oszthato harommal.
A probléma a 15-ös számnál bukik ki, amikor a szám mindkét feltételnek megfelel. Az 1. példa megvizsgálja a 3-mal oszthatóságot és az 5-tel oszthatóságot is minden esetben. A többágú ellenőrzés esetén a 15-re teljesül a 3-mal oszthatóság – amit a program ki is ír – de az 5-tel oszthatóságot már meg sem vizsgálja, hiszen többágú vizsgálat esetén csak akkor vizsgálja a további feltételeket a program, ha az előzőek közül egyik sem teljesült. A vizsgálat első igaz feltételnél meg fog állni! Ilyenkor a független ellenőrzés a jó megoldás, hiszen nem egymást kizáró feltételekről van szó, hiszen a 15 mindkét számmal osztható.
SWICHEK
Vannak olyan esetek, amikor a sok if-else if-else ág helyett egyszerűbb lenne egy olyan vizsgálat, ahol egy változó értékeit közvetlenül lehetne vizsgálni, és azokhoz más-más utasításokat rendelhetnénk hozzá. Erre szolgál a switch.
Switch szerkezete
switch( valtozo )
{
// case után a konkrét előfordulás
case 1_tipus :
// az előforduláshoz kapcsolt egyetlen utasítás
elso_tipus_utasitasa;
// lezárjuk a kapcsolt utasítást
break;
// több előforduláshoz ugyanaz az utasítás is tartozhat
case 2_tipus : case 3_tipus :
masodik_vagy_harmadik_tipusok_utasitasa;
break;
// sokszor a rovid egy utasitasos agakat egy sorba irjuk
// a tomorebb forma miatt, a break mehet a vegere
case 5_tipus : otodik_tipus_utasitasa; break;
// irhattam volna a 2 case agat azonos sorba is, mint a 2-3-nal
case 4_tipus :
case 6_tipus :
// több előforduláshoz több utasítás is tartozhat
negyedik_vagy_hatodik_tipusok_elso_utasitasa;
negyedik_vagy_hatodik_tipusok_masodik_utasitasa;
....
break;
// a 7-es agnal nem veletlenul hianyzik a break!!
case 7_tipus : hetedik_tipus_utasitasa;
case 8_tipus : nyolcadik_tipus_utasitasa; break;
// egy előforduláshoz több utasítás is tartozhat
case 9 :
kilencedik_tipus_elso_utasitasa;
kilencedik_tipus_masodik_utasitasa;
break;
...
...
// a default ag nem kotelezo! egyfajta else agkent ertelmezheto
default :
minden_mas_eset_utasitasai;
....
break;
}
Akkor pár magyarázat ezzel kapcsolatban:
A switch után zárójelben meg kell adni azt a típust, amelynek a különféle értékeihez kapcsolódó ágakat a switch-en belül kezelni szeretnénk. Ezek a típusok jórészt primitív típusok lehetnek, valamint pár speciális típus. A lista nem teljes, de a lényeges elemek benne vannak:
byte
short
int
char
String(!) (java 7 óta)
enum
Az egyes ágakat case kulcsszóval vezetjük be, ami után odaírjuk a konkrét előfordulást, amihez a kettőspont utáni utasításokat kapcsolni szeretnénk. Ha egyetlen rövid utasítást használunk csak, akkor írhatjuk a példa ötödik előfordulásának megfelelő szerkezetben egy sorba.
Több különböző előforduláshoz tartozhatnak közös utasítás vagy utasítások, lásd 2-3 és 4-6 esetek. Az előfordulásoknak nem kötelező egymás utániaknak lenni, pl sorszámok esetén. Több előfordulás közös vizsgálatakor a case elofordulas : utasításokat egy vagy több sorba is írhatjuk (pláne ha sok van), de minden case-t kettőspont választ el az utána következő case-től vagy az utasításoktól!
Az egyes ágak utasításait break-kel zárjuk le a következő előfordulás vizsgálata előtt. Nagyon speciális esetekben a break elhagyható. A példában szereplő 7-es előfordulás esetén végrehajtja a hetedik_tipus_utasitasa-t, de mivel itt hiányzik a break, a 7-eshez végrehajtja a 8-as utasításait is, függetlenül attól, hogy a két előfordulás különbözik egymástól. Ez az utasításszerkezet nagyon elnézhető, éppen ezért csak nagyon speciális esetben szokás használni! Több mint 15 éves programozói munkám során emlékeim szerint egyszer használtam, ott is teleraktam kommentekkel még a környékét is, nehogy valaki jóhiszeműen kirakja az általam ott kihagyott break-et! Attól eléggé fejre állt volna a program 🙂
A default ágat egyfajta else-ként értelmezhetjük, sok esetben arra használjuk, hogy az egyes előfordulások vizsgálata figyelmeztessünk az esetleges hibás értékekre. Nem kötelező, de ha használjuk, mindenképp a switch szerkezet végén kell lennie.
Amilyen előfordulások nem szerepelnek a vizsgálatok között, azokat a switch figyelmen kívül hagyja.
Látjuk azt, hogy nagyon rugalmasan variálható szerkezetről van szó, amivel változók tartalmának direkt vizsgálatakor az esetleges sok esetet átlátható módon kezelhetjük. A helyzet az, hogy rugalmassága ellenére is viszonylag ritkán használom. Egyrészt amiatt, mert sok esetben valóban elég egy továbbmásolt if-else if-else alapú feltételvizsgálat, másrészt néhány esetben kifejezetten bonyolítja a switch a helyzetet. Kicsit olyan ez, mint amikor megtanulja az ember a while, do-while és for ciklusokat. Bármelyikkel kiváltható bármelyik, a helyzet azonban az, hogy mindegyiket akkor használjuk, amikor az a feladat azzal a szerkezettel oldható meg egyszerűbben vagy átláthatóbban. A switch esetében a rengeteg fajta testreszabási lehetőséggel együtt is azt javaslom, hogy akkor használjuk, amikor egyértelmű megoldásokat kaphatunk vele.
Switch példák
Hónapok
Tegyük fel, hogy a hónapok sorszámait szeretnénk átalakítani szöveges formává. Az if-else if-else szerkezetekkel ez a következőképpen nézne ki:
int honap = (int)(Math.random() * 12) + 1;
string honev = "";
if( honap == 1 )
{
honev = "Januar";
}
else if( honap == 2 )
{
honev = "Februar";
}
else if( .... )
...
...
else
{
honev = "December";
}
Az általam szeretett ctrl-c és ctrl-v billentyűkombinációval ez viszonylag hamar összerakható, de valójában a legtöbb gépelést a hónapok nevei okozzák. A helyzet az, hogy a switch utasítás sem kímél meg a hónapok gépelésétől, de magát az elágazási szerkezetet leegyszerűsíti. Lássuk hogyan:
int honap = (int)(Math.random() * 12) + 1;
string honev = "";
switch( honap )
{
case 1 : honev = "Januar"; break;
case 2 : honev = "Februar"; break;
case 3 : honev = "Marcius"; break;
....
....
// mivel a veletlen szam sorsolas csak 1-12-ot sorsolhat
// a default-ot nem hibakezelesre hasznalom, ez a december
default : honev = "December"; break;
}
Láthatod, hogy a switch a sok case meg break miatt ez sem kevesebb gépelés, mint egy if-else if-else szerkezet, de természetesen megoldható ezzel is. Rád bízom melyiket tekinted egyszerűbbnek.
Napok
Mi van akkor, ha a hét napjának a sorszámából szeretnénk megkapni, hogy az hétköznap vagy hétvége:
int nap = (int)(Math.random() * 7) + 1;
switch( nap )
{
case 1 : case 2 : case 3 : case 4 : case 5 :
System.out.println("Hetkoznap");
break;
default :
System.out.println("Hetvege");
break;
}
És if-else if-else-szel hogy néz ki ugyanez?
int nap = (int)(Math.random() * 7) + 1;
if( nap <= 5 )
{
System.out.println("Hetkoznap");
}
else
{
System.out.println("Hetvege");
}
Itt a switch szerintem feleslegesen bonyolította is a megoldást.
Hiányzó break
Lássunk akkor példát arra, hogy mikor lehet elhagyni a break-et. Sorsoljuk ki egy nap sorszámát, írjuk ki a hét hátralévő napjainak nevét:
int nap = (int)(Math.random() * 7) + 1;
switch( nap)
{
case 1 : System.out.println("Kedd");
case 2 : System.out.println("Szerda");
case 3 : System.out.println("Csutortok");
case 4 : System.out.println("Pentek");
case 5 : System.out.println("Szombat");
case 6 : System.out.println("Vasarnap");
}
Itt sehol nincs break. Az előfordulások vizsgálatának sorrendje viszont megadja azt, hogy ha mondjuk 3-as napot sorsoltunk, akkor onnan kezdődően a hiányzó break-ek miatt az utasítások mindegyike végrehajtódik. Amelyik előfordulás vizsgálatnál nincs break, az addig végrehajtja az összes alatta lévő utasítást – az ottani case értékektől függetlenül -, ameddig bele nem szalad egy break utasításba, vagy el nem éri a switch végét. És mi a helyzet a 7-es nappal? Nincs sehol és nincs default ág. Semmi. Amire nincs vizsgálat, azt figyelmen kívül hagyja.
Kerekítés
Adott a magyar készpénzes fizetés kerekítési problémája. Az 1-2-re végződő összegeket lefelé az alatta lévő tízesre kerekítjük, a 3-4-et a felette lévő 5-ösre, stb. Erre nagyon sokféle elegáns, kevésbé elegáns megoldást lehetne adni. Switch szerkezettel is megoldható sokféleképp. Mutatok két megoldást, 1-1 leheletnyi különbséggel.
Első verzió:
int penz = 0;
for (int i = 0; i < 20; i++)
{
penz = (int) (Math.random() * 991) + 10;
System.out.print(penz + " -> ");
switch (penz % 10)
{
case 2: case 7: penz -= 2; break;
case 1: case 6: penz--; break;
case 3: case 8: penz += 2; break;
case 4: case 9: penz++; break;
}
System.out.println(penz);
}
Második verzió:
int penz = 0;
for (int i = 0; i < 20; i++)
{
penz = (int) (Math.random() * 991) + 10;
System.out.print(penz + " -> ");
switch (penz % 10)
{
case 2: case 7: penz--;
case 1: case 6: penz--; break;
case 3: case 8: penz++;
case 4: case 9: penz++; break;
}
System.out.println(penz);
}
A második esettel trükkös megoldás, ahol kihasználom azt, hogy a hiányzó break miatt a következő utasításokat is végrehajtja a break nélküli előfordulásokhoz egészen addig, ameddig egy break-be bele nem szalad. A helyzet azonban az, hogy mivel a programozók legtöbbször csapatban dolgoznak, a legkevésbé szeretik mások trükkös megoldásait bogarászni. A lényeg az, hogy a megoldás átlátható és világos legyen, hiszen akkor annak működését, esetleges hibáit is jóval egyszerűbb felismerni. Természetesen vannak ettől egyszerűbb, switch mentes megoldások is, ezeket most csak a szerkezet bemutatása miatt írtam meg.
Switch használata String-gel
Az alábbi feladatban a hónapokat nevük alapján szeretnénk visszaalakítani számokká. Természetesen ennek is van egyszerűbb módja, de a példa kedvéért álljon itt switch szerkezettel:
String[] honapok = { "januar","marcius","december","november",
"januar","julius","majus","majus","szeptember",
"oktober","aprilis","majus","februar" };
int ho = 0;
for( int i = 0; i < honapok.length; i++ )
{
switch( honapok[i] )
{
case "januar" : ho = 1; break;
case "februar" : ho = 2; break;
case "marcius" : ho = 3; break;
case "aprilis" : ho = 4; break;
case "majus" : ho = 5; break;
case "junius" : ho = 6; break;
case "julius" : ho = 7; break;
case "augusztus" : ho = 8; break;
case "szeptember" : ho = 9; break;
case "oktober" : ho = 10; break;
case "november" : ho = 11; break;
case "december" : ho = 12; break;
default : ho = -1; break;
}
System.out.print(ho + " " );
}
A java 7-es verziójától kezdődően String is használható a switch bemeneteként. Ilyenkor nem szükséges az egyes előfordulásokat az equals metódussal ellenőrizni, csak adjuk oda a switch-nek a String változót, az egyes előfordulásoknál pedig a vizsgálandó értékeket, a többit a switch megoldja.
Összefoglalásként csak ismételni tudom magam: Nagyon elegáns, és rugalmas eszköz a switch, de itt is ügyelni kell arra, hogy lehetőleg akkor használjuk, amikor ez a legjobb megoldás, akár a megoldás egyszerűsége, akár az átláthatósága a cél. Ne nézzünk mindent szegnek, ha kalapács van a kezünkben
VÉLETLEN
Programozás során sokszor előfordul, hogy valamilyen értéket véletlenszerűen kell megadni, vagy fel kell tölteni egy tömböt véletlen számokkal. A véletlen szám generálásának módszere a következő:
Először meg kell határozni annak az intervallumnak a határait, amelyből az adott számot sorsolni kell:
[0;20]
[10;30]
[-10;10]
[-20;0]
[-40;-20]
Ha egész számokat akarunk sorsolni, akkor ezek a típusok jöhetnek szóba. A jó az egészben az, hogy bármilyen egészeket tartalmazó intervallumra egy általános “képlettel” meg lehet adni a sorsolandó számot. Először meg kell határozni az intervallum alsó és felső határát. Ha ezeket tudjuk, akkor jöhet a sorsolást végző programkód. Ennek általános formája a következő:
(int)( Math.random()*(felső-alsó+1) )+alsó;
Bontsuk akkor részekre ezt a kódot. Kezdjük belülről kifelé haladva:
A Math.random() függvény egy véletlen számokat generáló függvény, mely egy lebegőpontos számot (nem egész) sorsol ki a [0;1[ intervallumból. Nem rontottam el az intervallum zárójelét, ez ugye azt jelenti, hogy a kisorsolt érték legkisebb értéke nulla, a legnagyobb viszont mindenképpen 1-nél kisebb lesz. Így is írhattam volna, hogy a Math.random() függvény ilyen értékeket sorsol: 0 <= szám < 1
Ezt a számot meg kell szorozni az intervallum méretével, amit minden esetben úgy kapunk, hogy a felső határból kivonjuk az alsót és 1-et hozzáadunk. Az egyik példánál maradva a [0;10] intervallum mérete 11, hiszen 10-0+1 = 11. Miért adunk hozzá egyet? Mert ha csak a két szám különbségét vennénk, akkor az intervallumba a felső határ nem tartozna bele. Miért? Ha emlékszel, a Math.random() 1-et sosem sorsol, ezért az egyik alul részletezett lépés miatt a felső határ kimaradna. Ha ezt az intervallum méretet behelyettesítjük a megfelelő helyre egyszerűsödik a képlet:
(int)(Math.random() * intervallum_mérete) + alsó;
Ha most nézzük a belső részt, akkor alakul a dolog. Nézzük újra a példákat immár behelyettesítve az eddig tanultakat:
[0;20] (int)(Math.random() * 21) + alsó;
[10;30] (int)(Math.random() * 21) + alsó;
[-10;10] (int)(Math.random() * 21) + alsó;
[-20;0] (int)(Math.random() * 21) + alsó;
[-40;-20] (int)(Math.random() * 21) + alsó;
Érdekes módon habár 5 különféle típust adtam meg intervallumra, az intervallumok mérete mégis egyforma. Nincs ezzel semmi gond, mert a véletlen szám sorsolás első lépése a megfelelő méretű sorsolási intervallum meghatározása, ami ismétlésképp: felső – alsó + 1
Ha ez megvan, akkor már csak ezt a megfelelő méretű intervallumot kell eltolni a számegyenesen a megfelelő irányba úgy, hogy az intervallum alsó határa a megfelelő kezdőpontban legyen. Ez pusztán csak annyit jelent, hogy a zárójelben lévő részhez a zárójelen kívül – hozzáadom az intervallum alsó határát (ami negatív érték esetén természetesen kivonást jelent). Lássuk így a megoldásokat:
[0;20] (int)(Math.random() * 21); // a nullát nem adom hozzá
[10;30] (int)(Math.random() * 21) + 10;
[-10;10] (int)(Math.random() * 21) - 10;
[-20;0] (int)(Math.random() * 21) - 20;
[-40;-20] (int)(Math.random() * 21) - 40;
Ezzel már majdnem készen is vagyunk, de még ott van egy fura rész a kód elején: (int) Ez az úgynevezett típuskényszerítés, angolul typecast. Ez azt jelenti, hogy a közvetlenül utána szereplő értéknek közvetlenül megmondja, hogy milyen típusa legyen. A Math.random(), ha emlékszel, egy lebegőpontos (valós) számot sorsolt, ami nem egész, de az intervallum egész számokat kell, hogy tartalmazzon. Ezzel megadtuk, hogy az (int) után lévő szám legyen egész, ami valójában azt jelenti, hogy levágjuk a tizedesjegyeket.
(int)(Math.random() * 21) - 20;
Így lesz az intervallum mérete egész szám, hiszen eredetileg a [0;21[ intervallumból bármilyen valós szám lehetett volna.
Fontos még megjegyeznem, hogy az intervallum eltolásakor az alsó határ hozzáadását mindenképp úgy adjuk meg, ahogy a példában látjuk. Több helyen ilyen megoldással találkoztam, amik rosszak:
(int)(Math.random() * 21 - 40);
Ez a megoldás azért rossz, mert a csonkolás és az intervallum negatív tartományba tolása miatt az alsó határ (a példában a -20) sosem szerepelhet a sorsolásban. Ennek a valódi oka egyébként az, hogy a Math.random() 1-et sosem sorsolhat, így a csonkolás miatt a negatív számoknál gond lesz a legnagyobb (valójában legkisebb) értékkel.
CIKLUSOK FAJTÁI
Programozás esetén nagyon sok esetben előfordul az, hogy valamilyen tevékenységet (utasításokat) többször meg kell ismételni. Ilyenek a való életben is sokszor előfordulnak.
Szúrj be 5 üres sort a táblázatba.
Készíts 3 szendvicset
Dobj 3 kockával (egyszer dobunk, de 3 számot sorsolunk)
Írd le 100x, hogy nem felejtem el a házi feladatomat
Ezek a többször ismételt tevékenységek megegyeznek abban, hogy előre tudjuk, hányszor kell elvégezni őket. Persze olyan is előfordul, hogy addig kell végezni valamit, amíg lehet.
Hámozz meg 2 kg almát
Mosogass el
Készíts annyi szendvicset, amíg el nem fogy a felvágott
Ezeket a tevékenységeket is többször kell ismételni, de nem tudom hányszor. Lehetnek kisebb almák, abból többet kell hámozni, de ha nagyobbak, kevesebb darabból is kijön a 2 kg. Addig mosogatok, amíg van edény. Addig készítem a szendvicseket, amíg van mit rátenni.
A programozásban is ezek az elvek érvényesülnek. Ezek szerint 3 különféle ciklus típust különíthetünk el:
Növekményes ciklus
Elöl tesztelő ciklus
Hátul tesztelő ciklus
A működés alapelve szerint az első különbözik a többitől. Ebben az esetben előre tudjuk, hogy hányszor akarjuk ismételni a teendőinket, míg az utóbbi két esetben az ismétlés darabszáma feltételhez kötött. Ettől a kép valójában kicsit árnyaltabb, mert a Java nagyon rugalmas a ciklusaival, de az alapelvek ezek.
Növekményes ciklus – for
Kezdjük az elsővel, a növekményes ciklussal. Az ilyen típusú ciklus a következőképp néz ki formailag:
for( ciklusváltozó beállítása; futási feltétel; ciklusváltozó növelése )
{
utasítás1;
utasítás2;
...
utasításN;
}
Konkrét példával:
for( int i = 0; i < 20; i++ )
{
System.out.println(i);
}
A for kulcsszó vezeti be a ciklust. Ezután jön a ciklus feje, ahol 3 dolog állítható be:
a használandó ciklusváltozó kezdőértéke
a futási feltétel, vagyis mikor kezdődjön újabb “kör”
a ciklusváltozó növelése
A szép (vagy épp csúnya, de ez nézőpont kérdése) a dologban az, hogy ebből a 3 dologból semmi nem kötelező. Bármelyik, vagy akár mind elhagyható, az egyetlen megkötés, hogy a 3 részt elválasztó ; jelek mindegyike megmaradjon:
int i = 0;
for( ; ; )
{
if( i == 20 ) break;
System.out.println(i);
i++;
}
Igaz, ez kicsit kifordítja a feladatot és eddig ismeretlen utasítást is tartalmaz, de látjuk azt, hogy lehetséges.
Nos, a for ciklus tehát alapesetben arra szolgál, hogy egyesével növelve egy változót valamilyen tevékenységet addig hajtsunk végre, ameddig azt az általunk megadott feltétel engedi. Példákon keresztül ez érthetőbb lesz:
Számoljunk el 1-től 50-ig és írjuk ki a képernyőre a számokat:
for( int i = 1; i <= 50; i++ )
{
System.out.println(i);
}
Ha megnézzük a ciklusfejet, a következőket láthatjuk:
a ciklusváltozót 1-től indítjuk
addig megyünk, amíg el nem érjük az 50-et
a ciklusváltozót egyesével növeljük
Persze ezt így is írhattam volna:
for( int i = 0; i < 50; i++ )
{
System.out.println(i + 1);
}
0-tól megyek 49-ig, de mindig eggyel nagyobb számot írok ki.
Sorsoljunk ki 10 véletlen számot az [1;50] intervallumból és írjuk ki őket:
for( int i = 0; i < 10; i++ )
{
System.out.println( (int)(Math.random() * 50) + 1 );
}
A fenti ciklusfej nagyon erős típusfeladat és egy alapszabályt láthatsz benne: Az i-t indítsd 0-ról és addig menj, amíg kisebb, mint az a szám, amennyiszer a ciklust futtatni akarod:
for( int i = 0; i < 10; i++ ) // 10-szer
for( int i = 0; i < 20; i++ ) // 20-szor
for( int i = 0; i < 100; i++ ) // 100-szor
Írjuk ki 2-től indulva 20 páros számot:
for( int i = 1; i <= 20; i++ )
{
System.out.println(i * 2);
}
Itt annyi trükköt alkalmazok, hogy 1-től számolok el 20-ig (ennyi szám kell), de ezeket 2-vel szorozva mindig páros számot kapok. Persze ha az alapszabályt tekintem, amikor 0-tól indítom a ciklust, és a határ-1-nél állok meg, akkor így is írhatnám ugyanezt:
for( int i = 0; i < 20; i++ )
{
System.out.println((i + 1) * 2);
}
És ha az 1-től indulva kell 20 páratlan?
for( int i = 1; i <= 20; i++ )
{
System.out.println(i * 2 - 1);
}
A párosokból 1-et kivonva páratlanokat kapunk. Vagy 0-tól indulva:
for( int i = 0; i < 20; i++ )
{
System.out.println(i * 2 + 1);
}
De csak hogy lássuk milyen rugalmas is a for ciklus, lássunk a párosokra egy másik megoldást:
for( int i = 2; i <= 40; i += 2 )
{
System.out.println(i);
}
Na jó, kicsit csaltam. Tudom, hogy a 40 lesz az utolsó, viszont nem szorozgatok, hanem a ciklusváltozót most nem 1-gyel, hanem 2-vel növelgetem. Itt egy jó példa a += operátorra.
Egy szó, mint száz, a for ciklus egy rugalmas és hatékony eszköz akkor, ha előre tudom, hogy hányszor akarok valamit végrehajtani. De az már egyértelmű, miért olyan szerteágazó az egész, mert ugyanarra a problémára rengeteg fajta megoldást adhatok, és mindegyik tökéletesen megoldja a feladatot. Annyira árnyalatnyi különbségek vannak közöttük, hogy ezzel középiskolai szinten egyáltalán nem kell foglalkozni, a lényeg: helyes megoldást adjon.
Elöl tesztelő ciklus – while
Az elöl tesztelő ciklust jellemzően akkor használjuk, ha nem tudjuk előre, hogy hányszor kell az ismétlődő tevékenységet végrehajtani. Azzal nincs gond, ha ki kell sorsolni 10 számot egy intervallumból. Ez egy for ciklusnak megfelelő típusfeladat. De ha az a feladat, hogy egy adott intervallumból 10 darab páratlan számot kell sorsolni? Akkor ha véletlenül páros számot kaptál azt figyelmen kívül kell hagyni. Lássuk először a while ciklus általános alakját:
while( feltétel )
{
utasítás1;
utasítás2;
...
utasításN;
}
Mint láthatod itt is van egy ciklusfej, ami a futási feltételt tartalmazza. Ez működési szempontból azt jelenti, hogy a ciklus akkor fut le (hajtja végre a ciklusmagot), ameddig a feltétel igaz. Természetesen arra figyelni kell, hogy a feltétel egyszer teljesülhessen, vagy a ciklusmagban szakítsuk meg a futást, nehogy végtelen ciklusba fussunk. Ez azt jelenti, hogy soha nem állhat le, mert vagy nem állítjuk meg, vagy a futási feltétel soha nem lehet hamis. A legegyszerűbb végtelen ciklus:
while( true )
{
System.out.println("fut");
}
Lássuk akkor az előző példát, sorsoljunk ki 10 páratlan számot egy adott intervallumból [1;100]
int db = 0;
int szam;
while( db != 10 )
{
szam = (int)(Math.random() * 100) + 1;
if( szam % 2 != 0 )
{
System.out.println(szam);
db++;
}
}
Nézzük akkor sorban, mit is csinál ez a program:
kell egy változó, ami azt számolja majd, hogy hány páratlan számot sorsoltunk, mert a párosokkal nem foglalkozunk
deklaráltam egy szam nevű változót, ahol az aktuálisan kisorsolt számot tároljuk
a ciklust futási feltétele az, hogy amíg nincs 10 páratlan szám, addig sorsolgasson
a ciklusban sorsolok egy számot, és eltárolom
miután kisorsoltam, megvizsgálom, hogy páratlan-e
ha páratlan, akkor kiírom a sorsolt számot, és növelem eggyel a számlálót
ha nem páratlan, akkor a ciklusmagból semmi nem hajtódik végre, mert nem megfelelő a szám és ismét próbálkozik egy sorsolással
Jöjjön egy másik jó példa az elöl tesztelő ciklusra. Számítsuk ki két egész szám osztóját. Nem, nem a prímtényezős alakra bontással oldjuk meg, hanem egy jól programozható megoldást adunk, mely a következő – egyébként már meglévő – algoritmust takarja:
Addig kell kivonni a két szám közül a nagyobból a kisebbet, amíg a két szám egyenlő nem lesz. Ha a két szám egyenlő, az az eredeti számok legnagyobb közös osztója. Az könnyen belátható, hogy megjósolhatatlan, hányszor kell az említett kivonást elvégezni, tehát for ciklust nem igazán alkalmazhatunk. (lehetne, de elég kitekert megoldás lenne)
int szam1 = 660;
int szam2 = 366;
while( szam1 != szam2 )
{
if( szam1 > szam2 )
{
szam1 = szam1 - szam2 ;
}
else
{
szam2 = szam2 - szam1 ;
}
}
System.out.println("A ket szam legnagyobb kozos osztoja: " + szam1);
Na, nézzük, mit is csinál ez a program:
deklarálunk 2 változót a vizsgált számoknak
kell egy ciklus azzal a futási feltétellel, hogy addig kell a kivonásokat ismételni, amíg a két szám nem egyenlő
ha a szam1 a nagyobb, akkor abból vonjuk ki a szam2-őt
fordított esetben a szam2-ből vonjuk ki a szam1-et
amikor a ciklus befejeződik, akkor a két szám bármelyike (mivel egyenlőek) a legnagyobb közös osztót jelenti, amit ki is íratunk
Ha észrevetted, az is előfordulhat, hogy a ciklus egyszer sem fut le. Mi van akkor, ha a két szám alapból egyenlő? Akkor is kiírathatom bármelyiket, vagyis a ciklus utáni sorra lép a program és kiírja az egyiket. Az elöl tesztelő ciklusnál lehetséges, hogy a ciklus egyszer sem fut le.
Hátul tesztelő ciklus – do-while
A do-while ciklus az előzőleg ismertetett while-ra hasonlít abban a tekintetben, hogy ezt a fajta ciklust is akkor használjuk, amikor nem tudjuk előre, hogy hányszor kell egy utasítás sorozatot végrehajtani. Azonban mégis van egy fontos különbség a kettő között. Lássuk az általános alakot, akkor egyértelmű lesz:
do
{
utasítás1;
utasítás2;
...
utasításN;
}
while( feltétel );
Mint a neve is mutatja, itt a ciklus feje hátul van (a feltétellel együtt) és a ciklusmagba tartozó utasítások elöl. Ez azt jelenti, hogy a ciklusmag 1-szer mindenképpen lefut, mert ez a ciklus először végrehajt, utána vizsgálja meg, hogy szükséges-e többször! Jellemzően olyan feladatoknál használjuk, amikor egyszer mindenképp végre kell hajtani valamit, de utána ellenőrizni kell, hogy amit kaptunk megfelelő-e, mert ha nem, csináljuk meg újra. Ilyen például az a számsorsolás, ami valamilyen feltételhez kötött:
Sorsolj ki egy páros számot a [10;50] intervallumból. Azt még egyszerűen megoldjuk, hogy az adott intervallumból sorsoljunk, de azzal a plusz feltétellel már nem tudunk mit kezdeni, hogy ez páros is legyen. Ezért addig sorsolunk, hogy a feltételnek megfelelő számot kapjunk:
int szam;
do
{
szam = (int)(Math.random() * 41) + 10;
}
while( szam % 2 != 0 );
Nézzük akkor a programot részenként:
sorsolunk egy számot
ha a szám 2-vel osztva nem 0 maradékot ad (páratlan), akkor a ciklus újraindul, vagyis megint sorsol egyet
a ciklus akkor áll meg, ha a feltétel hamis lesz (páros)
Azért jó itt a do-while ciklus, mert mindenképpen sorsolnom kell egy számot ahhoz, hogy megvizsgálhassam, meg kell-e ismételni a sorsolást. Természetesen összetett feltételt is megadhatok. Mondjuk olyan számot sorsoljunk az adott intervallumból, ami 2-vel és 5-tel is osztható:
int szam;
do
{
szam = (int)(Math.random() * 41) + 10;
}
while( !(szam % 2 == 0 && szam % 5 == 0) );
Itt a ciklus futási feltételeként a kiemelt sorban egy összetett feltételt láthatsz, ami azért nem biztos, hogy annyira egyértelmű, mint amilyennek elsőre tűnik. A ciklus ugye akkor működik, ha a feltétel igaz. De itt eredetileg két részfeltételünk van, 2-vel és 5-tel osztható szám kell. Az már nem jó, ha esetleg egyik, vagy az sem, ha mindkét részfeltétel hamis. Igen ám, de a ciklus futási feltételeként nem azt kell megadni nekünk, amilyen számra nekünk szükségünk van, hanem pont az ellenkezőjét. Azt kell megadni, hogy milyen szám nem jó nekünk! Nézzük akkor lépésenként:
szerkesszük meg azt a feltételt, ami nekünk megfelelő (ami összetett feltétel is lehet)
negáljuk az egészet
Természetesen itt is igaz, hogy ha akarjuk, egyszerűsíthetjük az összetett feltételt a már tanult módon:
while( !(szam % 2 == 0 && szam % 5 == 0) ); // 1. verzió
helyett
while( szam % 2 != 0 || szam % 5 != 0 ); // 2. verzió
Akkor milyen ciklust válasszunk?
Azt, hogy milyen ciklust válasszunk egy feladat megoldásához hosszú távon inkább a tapasztalatunk mondja majd meg. A programozás sajátossága, mint előzőleg már említettem, hogy ugyanazt a feladatot nagyon sokféle módon lehet helyesen megoldani. Gyakorlatilag a ciklusoknál is minden feladat megoldható mindhárom ciklussal, néha valóban árnyalatnyi különbségek vannak közöttük. Mégis, az alap támpontot a következők adják:
Ha tudom hányszor fusson a ciklus, akkor for ciklus.
Ha nem tudom hányszor fusson a ciklus ÉS lehet, hogy egyszer sem kell, akkor while ciklus.
Ha nem tudom hányszor fusson a ciklus ÉS egyszer mindenképpen kell, akkor do-while ciklus.
Maradjunk az egyik előző példánál, ahol a legnagyobb közös osztót kerestük. Ha emlékszel, addig vonjuk ki a nagyobb számból a kisebbet, amíg a két szám egyenlő nem lesz. Melyik ciklussal célszerű megoldani?
Mivel nem tudjuk hány kivonás kell, ezért ne for ciklus legyen.
Lehet, hogy a két szám már az elején egyforma, akkor egyszer sem kell kivonni a nagyobból a kisebbet, ezért do-while se legyen (mert az egyszer feltétel nélkül lefut).
Vagyis maradt a while ciklus.
Ha mondjuk tudom, hogy 10x kell futni a ciklusnak, mert 10 számot kell sorsolni, akkor adja magát, hogy for ciklus legyen, mivel abban egyszerűbb egy számlálót kezelni.
Saját tapasztalat szerint azt mondom, hogy talán a do-while ciklus a legritkább, és a for ciklus a leggyakrabb a programozásban. A for ciklust is inkább a tömbök miatt használjuk gyakrabban.
OBJEKTUMOK OSZTÁLYOK
Objektum
A Java programozási nyelv alapvető eleme az objektum. Az ilyen nyelveket objektum orientált (OO) programozási nyelveknek nevezzük. Az objektum az adott feladat szempontjából fontos, a valódi világ valamilyen elemének a rá jellemző tulajdonságai és viselkedései által modellezett eleme. Az objektumokkal kapcsolatban valamilyen feladatokat szeretnénk megoldani. A nyelv tervezésekor fontos szempont volt az, hogy az objektumok többé-kevésbé állandóak, de a hozzájuk tartozó feladatok nem, ezért az objektumok kapnak nagyobb hangsúlyt. A mai programok nagyon sok, egymással kölcsönhatásban álló elemből állnak, így nem is igazán programokról, hanem programrendszerekről beszélhetünk.
Az objektumokkal kapcsolatban alapvetően négy fontos dologról beszélhetünk:
Adat és kód egysége
Zártság
Öröklődés
Polimorfizmus
Ezekből az utolsó kettőt nagyvonalúan későbbre teszem, ha folyamatosan haladsz a tanulással, akkor egyelőre inkább csak összezavarna.
Adat és kód egysége
Az elején már utaltam rá, hogy az objektum tulajdonságokból és viselkedésekből áll. Az előzőt nevezzük változóknak (adat), az utóbbit metódusoknak (kód). A kettő közötti kapcsolat az, hogy a változókat a metódusok kezelik, metódusok nélkül csak a bennük lévő egyetlen adat tárolására alkalmasak. Ezek szorosan együttműködve alkotják az objektum nagy részét. Az osztály ismertetésénél elmondom a többi alkotóelemet is.
Zártság
Az előzőekben tisztáztuk, hogy a változókat a metódusok kezelik. Ez alaphelyzetben nem így van, a változókhoz kívülről, direkt formában is hozzá lehet férni, ha nem vigyázunk. Vagyis metódusok nélkül is kezelhetőek. Sajnos. Ez viszont mindig roppant veszélyes. Egyrészt nem tudjuk biztosítani, hogy csak az kezelhesse, aki erre jogosult, másrészt lehet, hogy fogalmunk sincs az osztály felépítéséről, valamint hogy a változóit hogyan is kell kezelni.
Tegyük fel, írtál egy kutyát. Van egy éhséget, pontosabban annak mértékét tároló változója. Te úgy gondoltad, hogy ez a [0;10] intervallumból vehet majd fel értéket. A 0 jelenti azt, hogy a kutya nem éhes, tele van, a 10 pedig a majd éhen hal állapotot. És ha valaki ezt kívülről ellenőrzés nélkül -3-ra állítja? Fejre áll a programod. Azt, hogy egy változó milyen értékeket vehet fel, csak egyvalaki tudja jól, önmaga. (Meg remélhetőleg az, aki megírta) Ahhoz, hogy egy objektumot biztonsággal kezelhessünk, meg kell védenie a változóit, be kell zárni őket. Ha az objektum változóit priváttá tesszük, akkor csak és kizárólag ő férhet hozzájuk közvetlenül, mindenki más csak a metódusain keresztül ellenőrzött körülmények között használhatja. Ez szintaktikailag csak annyit jelent, hogy a változó deklarációját a típus megadása előtt a private kulcsszóval kell kiegészíteni:
private int ehseg;
Távirányítók
Az objektumok megszületésükkor az úgynevezett kupacra kerülnek. Mi az objektumhoz nem férhetünk hozzá közvetlenül, egyedül a címe az, amivel hivatkozhatunk rá. Egy változóban eltárolhatjuk, sőt el is kell tárolni az objektum címét. Így a változóval bármikor elérhetjük őt, beszélgethetünk vele, érdeklődhetünk a változói felől, stb. Erre egy példa:
Dog mydog;
mydog = new Dog("Buksi", "Tacsko", "Barna");
A mydog annak a változónak a neve, egyfajta “távirányító” lesz a létrehozott Dog objektumhoz. Ez is egy egyszerű változó deklaráció: típus változónév; Aztán teremtünk egy Kutyát! Előre megmondjuk, milyen tulajdonságai legyenek. Vedd észre, hogy az objektum címét tároló változó neve nem olyan típusú, amit eddig megszokhattunk. Azt is kiszúrhattad, hogy nagy betűvel írtam. Nem véletlenül. Ugye még mindig emlékszel, miket írunk nagy kezdőbetűvel?
Nagyon fontos dolog: Ne veszítsd el a távirányítódat, és ne is programozd át egy másik Dog-ra! Akkor az objektumodat is elvesztetted! A távirányítóval tudod kezelni a kutyádat. Megetetheted, megkérheted, hogy ugasson, stb.
A new egy speciális operátor, ami egy adott osztályból egy új példányt hoz létre. Ez a létrejött, betöltődött, beállított példány lesz a misztikus Objektum. A new utáni részt konstruktornak nevezzük, mindjárt előszedjük ezt is.
Szép dolog az objektum, szinte még el se kezdtem beszélni róla, de akkor miből is lesz? Osztályból.
Osztályok
Amikor egy Objektumot létrehozunk, azt valójában egy osztályból hozzuk létre. Az osztály egy megírt Java forráskód, aminek nagy vonalakban a következő részei vannak:
Változók
Metódusok
Konstrukciós műveletek
Az első kettőről már hallottál, az utolsó volt az, amit megígértem, hogy előszedjük. A konstrukciós műveletek olyan speciális részei az osztálynak, amelyben azt írjuk le, hogy az objektumot hogyan kell felépíteni, létrehozni a terv alapján.
Az osztály az objektumok terve!
public class Dog
{
// Változók
String nev;
String fajta;
String szin;
// Metódusok
public String getNev()
{
return nev;
}
public String getFajta()
{
return fajta;
}
public String getSzin()
{
return szin;
}
// Konstruktor
public Dog(String nev, String fajta, String szin)
{
this.nev = nev;
this.fajta = fajta;
this.szin = szin;
}
}
Amikor az osztályból egy új példányt szeretnénk létrehozni, a konstruktor az, amit megkérünk, hogy építse fel nekünk. Kell egy kutya! Lássuk akkor újra azt a pillanatot, amikor tényleg létrehozzuk egy kutyát. A terv már megvan, már csak le kell gyártani.
d = new Dog("Buksi", "Tacsko", "Barna");
Tehát a new utáni rész a konstruktor, ami a példában az osztály végén látható. Amikor egy kutyát létre akarunk hozni, meg kell mondanunk a nevét, fajtáját és színét. Pontosan ebben a sorrendben. Ha ezt nem tesszük meg, nem lesz kutyánk!
Miért kell megadni mindhármat? Nem lehet ezt megadni később? Lehetne, de ha megnézed, a Dog osztály konstruktora 3 paramétert vár, mert csak akkor tudja a kutyádat felépíteni, ha ezeket megkapja. Mindet. Pontosan ebben a sorrendben! Ha ezekből akár egy is hiányzik (vagy több van), akkor már a fordítótól hibaüzenetet fogsz kapni, mert ő nyilvántartja, hogy melyik tervnek hány és milyen típusú bemenő adat kell ahhoz, hogy objektumot hozhasson létre.
Egy osztálynak lehet több konstruktora is!
Ezt hívjuk a konstruktor túlterhelésének. Lehet olyan is, hogy a kutya összes adatát meg kell adni. Lehet olyan is, hogy csak a nevét. Lehet olyan is, hogy a nevét és a fajtáját. Ezzel kapcsolatban fontos tudnivaló:
Csak olyan konstruktorok lehetnek egy osztályban melyek a paraméterek számában és típusában is eltérnek!
Így hibás, mert a két konstruktor azonos típusú és számú paraméterrel rendelkezik:
public class Dog
{
String nev;
String fajta;
String szin;
// Metódusok... nem idézem újra, elképzeled
// Konstruktor 1
public Dog(String nev, String fajta)
{
this.nev = nev;
this.fajta = fajta;
}
// Konstruktor 2
public Dog(String nev, String szin)
{
this.nev = nev;
this.szin = szin;
}
}
Egy ilyen példa esetén a második konstruktorra hibát fog jelezni a fordító, mert az osztály már rendelkezik olyan konstruktorral, amely két Stringet vár paraméterként.
Nagyon fontos, hogy a létrehozott objektumnak azonnal legyen egy rá mutató változója is (távirányító), különben úgy kerül a kupacra, hogy senkihez nem tartozik, így hamarosan csúnya véget ér! Vagyis soha, de soha ne csinálj ilyet (ha szereted az állatokat).
Hibás:
new Dog("Bodri", "Kuvasz", "Feher");
Szegény Bodri…
Csakis úgy példányosítsd, hogy az objektumot egy osztállyal megegyező típusú változóban, tömbben, listában, stb tárolod el. Azért, hogy később használni tudd, és ne kóboroljon a kupacon, amíg nem jön a sintér.
Helyes:
Dog d = new Dog("Bodri", "Kuvasz", "Feher");
// vagy
Dog[] kutyak;
kutyak[0] = new Dog("Bodri", "Kuvasz", "Feher");
// a listáról később...
Természetesen több kutyát is létrehozhatunk ebből az osztályból, csak legyen elég helyünk tárolni őket. Az osztályok egyik fontos szerepe az újrahasznosíthatóság. Nem kell 3 külön kódot írni arra, hogy legyen 3 kutyád, elég ugyanabból az osztályból létrehozni őket, de a tulajdonságaik lehetőleg legyenek különbözőek. Látható az is, hogy ha tényleg ilyen egyformák a kutyák, akkor minek készítsek nekik külön tervet? Hiszen ugyanolyan tulajdonságaik vannak, pontosabban a tulajdonságok típusa egyezik meg. A konkrét értékeik mások. Ezt az azonos tulajdonságok és viselkedések szerinti összevonást nevezzük osztályozásnak. Akkor ezt ismételjük meg újra, mert ez egy fontos dolog:
Osztályozásnak nevezzük azt a folyamatot, amikor az általunk modellezni kívánt valódi világbeli objektumokat közös tulajdonságok és viselkedések alapján csoportokba soroljuk.
Azt is meg kell jegyeznem, hogy még ha két tulajdonságaikban teljesen ugyanolyan kutyát hozok létre, az objektumaik akkor sem lesznek ugyanazok:
d1 = new Dog("Buksi", "Tacsko", "Barna");
d2 = new Dog("Buksi", "Tacsko", "Barna");
System.out.println(d1 == d2); // false
A fenti példában a két kutya minden tulajdonsága megegyezik, de a két objektum mégsem egyforma. Két teljesen különálló valami van ott a kupacon, de attól még nincsenek összekötve. Amikor használod a new-t, akkor mindig egy új objektumot teremtesz. Akkor is, ha már van egy látszólag pont ugyanolyan tulajdonságokkal rendelkező. Azt, hogy két objektum mikor egyenlő, azt csakis Te határozhatod meg, a Java nem találhatja ki helyetted. Ha tehát háziállatokat szeretnénk modellezni, akkor a lábaik száma, viselkedésük, táplálékuk, stb szerint csoportokba lehet őket sorolni. Mondjuk ezt előttünk a biológusok megtették, hiszen a fajokba sorolás is osztályozás. De az azonos tulajdonságok és azonos viselkedések alapján elég egy tervet írni az egy osztályba tartozó állatoknak. Emlékszel, a tulajdonságokat változóknak, a viselkedéseket metódusoknak nevezzük. Ezek az osztályok tehát a megírt forráskódok, és ezeket használjuk akkor, amikor a programjainkat működtetjük.
Objektumból egyelőre példának legyen elég ez a kutya, később készítünk bonyolultabbakat is, amelyek már tényleg működnek is!
TÖMB FOGALMA
A tömb, mint adattípus az összetett adattípusok közé tartozik. A tömb valójában egy sorszámozott egyforma típusú elemeket tartalmazó halmaz. Halmaz alatt csak annyit értek, hogy több elemet tartalmaz. A tömbök mérete (hogy hány elemet tartalmaz) középiskolai szinten lényegtelen, akkora tömbökkel nem dolgozunk, ami a túl nagy méret miatt problémát okozna. Ami viszont fontos: a tömb mérete csak egyszer adható meg, amikor deklaráljuk a tömböt. Vagyis ha megadtam, hogy ez egy 10 elemet tartalmazó tömb, akkor ezen később nem változtathatok. Különösen figyelni kell erre akkor, amikor nem tudod, hogy hány elemet szeretnél tárolni, akkor kénytelen vagy az elméleti maximális méretet beállítani, amit a feladat ad meg. A tömböt logikailag ugyanúgy kell deklarálni, mint egy egyszerű változót. Megadjuk a típusát és nevét.
int[] tomb;
A tömb deklarálás formailag ettől el is térhet, a következő alakok is használhatóak:
int []tomb;
vagy
int tomb[];
Én az első deklaráció típust használom, számomra így logikus. Ha felolvasnám az általam használt alakot, akkor így hangzana: ez egy egészeket tartalmazó tömb, melynek neve: tomb. Félkövérrel kiemeltem a fontos részeket, amelyek pont ilyen sorrendben szerepelnek a deklarációban.
Észrevehetted, hogy itt csak a tömb típusát és nevét adtam meg, de a méretét nem. Pedig azt írtam, hogy a tömbnek mérete is van, ami a megadása után nem változhat. A tömb méretének megadása megtörténhet közvetlenül a deklarációkor. Ezt akkor célszerű így használni, ha már ekkor tudod, hogy hány elemet szeretnél tárolni benne. Adott a következő feladat: Sorsolj ki 10 egész számot és tárold el őket. Ilyenkor azonnal megadható a tömb mérete is:
int[] tomb = new int[10];
Az is előfordulhat, hogy szükségem lesz egy egészeket tartalmazó tömbre, de még nem ismert számomra, hány darab elemet szeretnék tárolni. Jellemzően ez fájlbeolvasáskor fordul elő, esetleg kiválogatás, szétválogatás témakörben, melyeket majd később ismertetek. Ilyenkor a tömb deklarálása után tetszőleges programkódok lehetnek, melyek nem használják még a tömböt, nem is használhatják, hiszen nincs mérete. Maga a tömb már név szerint létezik, de még nem foglalt le neki a rendszer memóriát az elemek tárolásához. Ellenben a méret megadása előtt megszámolhatom, hogy majd mekkora tömbre lesz szükségem, és akkor állítom be a méretet, ha már biztosan tudom mekkorára van szükségem.
int[] tomb;
// ...
// ...
tomb = new int[10]
A tömbméretet mindenképpen azelőtt kell megadni, mielőtt használni szeretnénk. Fontos azt is tudni, hogy habár a tömb méretének megadásakor a tömbbe mi még nem helyeztünk el elemeket, a tömb nem üres. A méret megadásának kulcsa a new operátor. Ez létrehozza magát a tömb objektumot, ami a méretnek megfelelő darabszámú elemet képes tárolni, és aminek a rendszer azonnal lefoglalja a tároláshoz szükséges memóriaterületet. Ennek során a tömbben lévő minden elem kap egy kezdőértéket, amely a tömb típusától függ. A kezdőérték minden elemnél számok esetén 0, logikai típus esetén pedig false lesz. Később, ha mi magunk helyezünk el bárhová a tömbben egy értéket , csak az adott helyen lévő elem kezdő értékét írjuk át. A tömb new operátorral történő létrehozását – ezáltal kezdőértékekkel való feltöltését – inicializálásnak nevezzük.
Lássuk hogyan lehet egy tömböt használni.
Egy tömb, mint már említettem egy sorszámozott egyforma típusú elemeket tartalmazó halmaz. A sorszámozásnak fontos szerepe van, mert az elemek sorrendje – amíg meg nem változtatjuk – kötött. Mindenkinek megvan a saját azonosítója, amit nevezzünk indexnek. A sorszám annyiban nem a legpontosabb elnevezés, hogy itt a sorszámozás – amit innentől nevezzünk indexelésnek – 0-val kezdődik. Ebből következik, hogy a legutolsó elem indexe mindig a tömbméret-1. Az indexek mindig pozitív egész számok!
Mivel minden tömbelem helye fix, és a helyét az elem indexe adja meg, ezért lehet egy tetszőleges elemre hivatkozni a következő módon:
tomb[index]
Ez az adott indexű helyen tárolt elem konkrét értékét adja vissza, és amíg direkt nem cserélgetjük össze az elemeket, vagy nem változtatjuk meg az értéküket, mindig ugyanazt az értéket adja. Így lehet például a tömb feltöltése közben az adott helyen lévő “tárolóban” értéket elhelyezni. Értelemszerűen az index legkisebb értéke 0 lehet (ez az első elem), a legnagyobb pedig tömbméret-1 (ez az utolsó). De hogyan tudjuk a tömbméretet megkapni? Az egy dolog, hogy mi adtuk meg a programban valahol, de már nem emlékszünk rá, vagy nem akarjuk mindenhova azt a konkrét számot beírni, mert lehet, hogy később átírjuk a tömb méretét. A tomb.length mindig megadja egy tetszőleges tömb méretét.
tomb[0] // mindig ez az első elem
tomb[tomb.length - 1] // mindig ez az utolsó elem
Fontos, hogy nem a tömbben általunk eltárolt elemek számát adja meg, mert az lehet kevesebb is, mint a tömb mérete. Például tudom, hogy legfeljebb 20 értéket akarok tárolni, akkor egy 20 elemű tömbre van szükségem. És ha csak 15-öt tároltam el? Akkor az utolsó 5 üres lesz. Van ilyen. A tomb.length tehát azt a darabszámot adja meg, amennyi elem maximálisan elfér a tömbben és nem a már eltárolt elemek számát. Ha nem használtuk ki a tömb teljes méretét, akkor nekünk kell külön nyilvántartani, hogy melyik az utolsó elem, amit mi helyeztünk el a tömbben. Hiszen utána is vannak elemek a tömbben, a kezdőértékek. Az is lehet, hogy az általunk elhelyezett elem értéke 0, ami különbözik az utána következő 0 értéktől, ami már a kezdőérték miatt annyi! A lényeg: saját változóban tárold azt az indexet, ami annak az utolsó elemnek a helye a tömbben, amit te helyeztél el benne. Legyen ez a változó mondjuk db nevű.
tomb[db] // ez az utolsó általam elhelyezett elem
tomb[tomb.length - 1] // ez pedig a tömb utolsó eleme, ami kezdőérték
Egyik kedvenc hibám, amit a diákoknál látok: tomb.lenght Ezt a hibát percekig lehet keresni. Hányszor futottam már bele ilyenbe. Tessék ügyelni a helyesírásra! Még ha nem is tanulsz angolt – ami az informatikában elég nagy hátrány – tanuld meg helyesen leírni az angol szavakat!
A feladat a következő: Töltsünk fel egy 10 elemű egészeket tartalmazó tömböt az [1;100] intervallumból és tároljuk el a kisorsolt értékeket. Amikor egy tömbbel dolgozunk, szinte mindig ciklusra van szükség. Hogy a ciklus milyen típusú (elöl tesztelő, hátul tesztelő vagy növekményes, esetleg speciális foreach), azt mindig az adott feladattípus dönti el. Amikor például egy tömböt feltöltünk értékekkel vagy ki akarjuk íratni a tartalmát, akkor úgyis végig kell nézni az egészet.
Mivel a ciklusokat már feltételezem, hogy ismeri az olvasó, így lássuk a megoldásokat alap feladatokra, egyelőre nem teljes programban. A lenti kódban a már ismertetett módon használom a számsorsolást:
1
2
3
4
for( int i = 0; i < tomb.length; i++ )
{
tomb[i] = (int)(Math.random() * 100) + 1;
}
Ez a programrész egy tetszőleges méretű tömböt feltölt egy adott méretű intervallumból. A két kiemelt sor tartalmazza a lényege (a többi csak formaság). Az 1. sorban indítunk egy növekményes ciklust, ami egy ciklusváltozót (i) elindít 0-tól és addig megy amíg kisebb, mint a tomb nevű tömb mérete. Vagyis mi lesz az i utolsó értéke? Tömbméret-1. Ismerős? Ez az utolsó elem indexe. Vagyis ez a ciklus végiglépteti az i változót a tömb összes lehetséges indexén. Akármekkora is a tömb. Ezért kérdeztük meg a méretét tőle, mert lényegtelen, hogy a program elején mekkora tömbméretet adtunk meg. A 3. sorban pedig a léptetett indexeket felhasználva a tömb minden elemének egy véletlenszerű értéket adunk a megadott intervallumból. Apropó, ez melyik intervallum? … … … Számold ki!
Mi van akkor, ha ellenőrizni akarjuk, hogy a tömböt tényleg megfelelően töltöttük-e fel? Hátha elszúrtuk az intervallumot.
1
2
3
4
for( int i = 0; i < tomb.length; i++ )
{
System.out.print(tomb[i] + " ");
}
Ezt a programrészt már nem is kell nagyon magyarázni. Az 1. sor ciklusa segítségével végigmegyünk a tömb összes indexén. A 3. sorban pedig mindig kiíratjuk a tömb aktuális indexű (vagyis mindegyik) elemét úgy, hogy egy szóközt is hagyunk utána, hogy az elemek elkülönüljenek egymástól.
Hogy néz ki ez az egész egyben?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Tombfeltoltes
{
public static void main( String[] args )
{
// tömb deklarálása és méretének megadása
int[] tomb = new int[10];
// tömb feltöltése
for( int i = 0; i < tomb.length; i++ )
{
tomb[i] = (int)(Math.random() * 100) + 1;
}
// tömb elemeinek kiíratása
for( int i = 0; i < tomb.length; i++ )
{
System.out.print(tomb[i] + " ");
}
// extra sordobás
System.out.println();
}
}
Talán a 20. sor nem világos mit keres ott. Mivel a tömb elemeit egymás mellé írjuk le, ezért az utolsó elem után – vagyis a ciklus befejeztével – illik egy új sort kezdeni.
STRING
A Java programozási nyelvben, mint megtudhattad, többféle változótípus létezik. Ezek egy része egyszerű (primitív) típus, de vannak összetett, komplexebb típusok, ezek minden esetben objektumok. Ezek egyike a karakterlánc, más nevén String.
Mint a neve is mutatja, ez egy osztály. Ez honnan látszik? Nagy kezdőbetűvel írtam a nevét. Nem csak én, már az alap program main programrészében is láthattad számtalanszor:
public static void main( String[] args )
A Stringek karakterekből állnak, melyek egymás után meghatározott sorrendben karakterláncot alkotnak. A Stringek nem csak kiíratható karaktereket tartalmazhatnak, olyanok is lehetnek benne, amelyek nem látszanak, ezekről majd később szót ejtek.
String deklaráció és értékadás
A String deklarációja a következőképpen néz ki:
String s;
A Stringnek meglehetősen sokféle módon adható kezdőérték, ebből lássunk egyet, igaz, ez a forma nem túl gyakori, létrehozok egy s nevű Stringet:
String s = new String();
Létrehoztam egy új, üres Stringet. Ezt mondjuk gyakran nem így használjuk. A new kulcsszóval egy új objektumot tudok létrehozni, ami az adott osztályból egy új példány. Az előző, tömbökről szóló anyag után ez már nagy meglepetést talán nem jelent, hiszen ott is ilyesmit írtunk, de a new-ról és szerepéről az Osztályok és objektumok témakörben olvashattál részletesebben. A String mérete is állandó, csakúgy, mint a tömböké.
A String a Java nyelvben megváltozhatatlan!
Ha egyszer beállítottad az értékét, akkor az attól kezdve mindig ugyanaz marad. Nézzünk akkor pár másik értékadási lehetőséget:
String s1 = "abcd";
char data[] = {'a', 'b', 'c'};
String str = new String(data);
String str2 = new String("xyz"); // ezt azért kerüljük..
String s2 = s1 + "efgh";
Az első esetben a String formai megjelenésének megfelelően adok meg egy értéket. A második esetben létrehozok egy karakterekből álló tömböt, amit odaadok a new-val létrehozandó String objektumnak, hogy abból állítsa elő magát. A harmadik esetben szinten new-val hozok létre egy új String objektumot, és odaadom neki egy literálként azt a karakterláncot, ami a tartalma legyen. Az utolsó példában egy Stringet egy másik bővítésével hozok létre, itt a + operátor mint az összefűzés műveleti jele szerepel.
A következő példa kicsit fura lehet:
String s = "abcd";
System.out.println(s); // abcd
s += "efgh";
System.out.println(s); // abcdefgh
Azt mondtam, hogy a String megváltoztathatatlan. Itt mégis hozzáfűztem valamit és az s-nek látszólag tényleg megváltozik a tartalma. És még működik is! Igen ám, de a háttérben nem ez történik:
Létrehozunk egy String objektumot, beállítjuk a kezdőértékét, és a címét hozzárendeljük az s változóhoz.
Amikor az s-hez hozzáfűzünk valamit, létrejön egy új String objektum a háttérben.
Ennek értéke az eredeti String objektum tartalma és az “efgh” literál összefűzéséből kialakított karakterlánc lesz.
Az s változóhoz az új objektum címe lesz hozzárendelve.
Az eredeti s-hez rendelt objektum meg változó nélkül marad, hiszen a változóhoz már egy új címet rendeltünk.
A változóhoz nem rendelt objektumot emiatt hamarosan kitakarítják a kupacból (de addig is foglalja a memóriát!)
Speciális String literálok
Amikor egy Stringnek értéket szeretnénk adni, léteznek bizonyos speciális karakterek. Ezek megadásához szükségünk lehet a backslash karakterre, ami a \ (visszaper jel). Ezen speciális karakterekre példa:
\n (sordobás)
\t (tabulátor)
\” (idézőjel karakter)
\\ (maga a backslash karakter)
Mi van akkor, ha a Stringben szeretnék idézőjeleket megjeleníteni?
Michael "Air" Jordan
Ebben az esetben a következő dolgot tehetjük:
String s = "Michael \"Air\" Jordan\n";
Itt a normál módon megadott idézőjelek a literál elejét és végét jelentik. Viszont a Stringben szeretnénk idézőjelet szerepeltetni, ezért a backslash (kivétel) karaktert tesszük elé, így tudni fogja a Java, hogy az Air szó előtti és utáni idézőjelek nem a String határait jelzik, hanem a String részei! A végén a \n pedig egy sordobást tesz a név végére.
És mi van akkor, ha magát a \ karaktert szerepeltetnénk a Stringben? Önmaga hogy lesz kivétel? Ezt szeretnénk megjeleníteni:
Gondolkodom, tehat vagyok. \Descartes\
Itt a String része lenne az a karakter, amivel a kivételeket szoktuk jelezni, de itt önmagát akarjuk megjeleníteni. Akkor hogy oldjuk meg, hogy a \ jelet a String részeként kezelje? Ekkor a következő a teendő:
String s = "Gondolkodom, tehat vagyok. \\Descartes\\";
Egyszerűen a \ jel elé kell egy másik, ami ezáltal önmagát teszi kivétellé. És mi van akkor, ha ez a String végén szerepel: \\” ? Ez sem gond, mert ezt nem egy kivételes idézőjelnek \” fogja tekinteni, hanem az első \ jel teszi kivétellé a második \ jelet, így az idézőjel normál módon a String literál végét jelenti.
String metódusok
Lássuk akkor, hogy mit kezdhetünk a Stringekkel. Amikor egy String típusú változót használunk, valójában egy objektummal dolgozunk. A változó csak az objektumra mutat. Az objektumoknak, mint a már említett Osztályok és objektumok témakörből megtudhattad változói és metódusai vannak. A Stringek változóiról nem kell tudnod, de a metódusairól, mellyel magát a Stringet kezelheted, annál többet! Lássuk akkor ezeket példákon keresztül, hogy mire is használhatók. A felsorolás nem lesz teljes, de a legfontosabbakat úgy gondolom, hogy tartalmazza.
Stringek egyenlőségének vizsgálata – equals()
A Stringek objektumok, és két objektum csak akkor egyenlő, ha az valójában ugyanaz az objektum.
Objektumok között az == operátorral végzett egyenlőség vizsgálat nem használható!
Itt egy példa, ami ennek látszólag ellentmond. Az s1 és s2 Stringek ugyanazt az értéket kapják meg kezdőértékként, és az == működik. Itt a háttérben a Java fordítóprogram csal egy kicsit, látva, hogy a két literál ugyanaz, amivel a String-et létrehozod, ezért ugyanazt az objektumot rendeli hozzá mindkettőhöz:
String s1 = "abcd";
String s2 = "abcd";
System.out.println(s1 == s2); // true (???)
Rögtön kibukik azonban a következő példánál az, hogy miért is emeltem ki azt, hogy == operátorral nem hasonlítunk össze objektumokat. Nézzük a következő példát:
String s1 = "abcd";
String s2 = new String("abcd");
System.out.println(s1 == s2); // false (!!)
Itt már valóban különbözik egymástól a két String. De akkor hogyan nézhetjük meg, hogy a két String egyenlő-e, ha az objektumok nem azok? Két Stringet akkor tekintünk egyenlőnek, ha ugyanaz a tartalmuk, vagyis ugyanazt a karakterláncot tartalmazzák. Ezt az összehasonlítást egy metódussal oldották meg.
String s1 = "abcd";
String s2 = new String("abcd");
System.out.println( s1.equals(s2) ); // true
Az equals() metódust egy String objektumban (formailag az objektum címét tároló változóban) kell meghívni, és oda kell adni neki azt a másik Stringet, aminek a tartalmát szeretnénk a sajátjával összehasonlítani. A feladat szempontjából teljesen mindegy, hogy melyiket hasonlítjuk melyikhez, az s1.equals(s2) helyett az s2.equals(s1) is használható.
Stringek összefűzése – concat()
Előfordulhat, hogy egy Stringet bővítenünk kell. Hozzá akarunk fűzni valamit. Emlékszel, azt mondtam, hogy a String megváltoztathatatlan. Amikor bővítjük, akkor egy újat hozunk létre, melynek a hivatkozását átállítjuk az eredeti változóban.
Mégis rendelkezésre áll egy metódus, concat() néven, mellyel Stringeket lehet összefűzni. Még egyszer hangsúlyozom, csak Stringeket.
String s = "Indul";
s = s.concat(" a");
s = s.concat(" gorog");
s = s.concat(" aludni");
System.out.println(s);
Amennyiben nem csak Stringeket szeretnénk összefűzni, minden további nélkül használhatjuk a + operátort. Ez a nem String változókat String változóra konvertálva végzi el az összefűzést. Ez az operátor, azonban lassabb, mint a concat() metódus, ezért ha több összefűzésről van szó és csak String típusúakat szeretnél összefűzni, akkor érdemesebb a concat()-ot használni.
A sebesség mérések alapján egyébként a concat() metódus úgynevezett StringBuilder-t használ a háttérben, vagyis a fordító trükköket vet be a sebesség és memóriakímélés érdekében, és ezeket a felhasználó tudta nélkül oldja meg.
Stringek hossza – length()
Bármely String méretét (hosszát) megkaphatjuk, ha meghívjuk a length() metódusát:
s.length()
String adott karaktere – charAt()
Egy adott String bármelyik karakterét megkaphatjuk a charAt(i) metódussal, ahova az i helyére írjuk be, hogy hányadik karaktert szeretnénk megkapni. A karakterek indexelése a tömbökhöz hasonlóan 0-val kezdődik. Fontos, hogy ez egy karakter típust ad vissza! Bármely String első karaktere az s.charAt(0), az utolsó pedig az s.charAt( s.length()-1 )
s.charAt(3) // a 4. karakter (3-as index!)
s.charAt(0) // első karakter (üres Stringre indexelési hiba!)
s.charAt(s.length() - 1) // utolsó karakter
Stringek összehasonlítása rendezés miatt – compareTo()
A Stringek összehasonlításán már túl vagyunk, de van egy másik típus is, amely fontos, ez pedig a betűrend.
Két String összehasonlítása rendezési szempontból a compareTo() metódussal történik. Ezt hasonlóan az equals() metódushoz mindkét Stringre meg lehet hívni a másikat odaadva paraméterként, de itt már nem mindegy a sorrend! A compareTo() egy számot ad vissza eredményül. Ha a szám pozitív, akkor az a String amelyikre meghívtuk a metódust a paraméterben megadott String mögött található az abc rendnek megfelelően. Ha a szám negatív, akkor előtte. 0 esetén a két String tartalma egyforma. Ezt a metódust használhatjuk akkor, ha az a feladat, hogy Stringeket rendezzünk sorba.
Az összehasonlítás megkülönbözteti a kis és nagybetűket!
String s1 = "Geza";
String s2 = "Bela";
System.out.println(s1.compareTo(s2)); // 5
/* Az eredmény pozitív, ez azt jelenti, hogy az s1 String (amire a
* metódust meghívtuk) a paraméterben szereplő s2 Stringhez képest
* hátrébb található rendezési szempontból. Maga az 5-ös érték azt
* jelenti, hogy annál a pontnál, ahol a két String különbözik
* a két összehasonlított karakter távolsága 5 (B-G)
*/
String s3 = "Geza";
String s4 = "bela";
System.out.println(s3.compareTo(s4)); // -27
/* Az eredmény negatív, ez azt jelenti, hogy az s3 Stringhez képest
* az s4 String hátrébb(!) található. Ez azért van, mert a kódtáblában
* a nagybetűk megelőzik a kisbetűket, és a compareTo() figyelembe
* veszi ezt. Ez kiküszöbölhető a következő metódussal:
*/
System.out.println(s3.compareToIgnoreCase(s4)); // 5
/* a compareToIgnoreCase() metódus úgy hasonlítja össze a Stringeket,
* hogy figyelmen kívül hagyja a kis és nagybetűk közötti különbségeket.
*/
Stringek kis-nagybetűs átalakítása – toLowerCase() és toUpperCase()
A Stringeket egyszerűen átalakíthatunk csupa nagybetűssé, vagy kisbetűssé. Erre szolgálnak az s.toUpperCase() és s.toLowerCase() metódusok.
String nev = "Miko Csaba";
System.out.println(nev.toUpperCase()); // "MIKO CSABA"
System.out.println(nev.toLowerCase()); // "miko csaba"
Keresés Stringben – indexOf(), lastIndexOf()
Egyszerűen kereshetünk a Stringekben. Kíváncsiak vagyunk, hogy egy karakter vagy szövegrészlet megtalálható-e benne, sőt arra is, hogy hol található. Erre szolgál az s.indexOf() metódus.
String s = "abrakadabra";
System.out.println(s.indexOf("rak")); // 2
// A 2. indexű (3. karakternél) található a rak szócska.
System.out.println(s.indexOf("br")); // 1
/* Az 1. indexű (2. karakternél) található a br rész
* Fontos, hogy az indexOf() mindig az első találat helyét adja meg!
*/
System.out.println(s.indexOf("Br")); // -1
/* Egy nem létező indexet adott eredményül, vagyis a keresett
* részlet nem található meg a Stringben.
*/
System.out.println(s.lastIndexOf("br")); // 8
/* A 8. indexű (9. karakternél) található a br rész, de most a
* keresést hátulról kezdte, és onnan adja vissza az első találatot!
*/
Az indexOf() és lastIndexOf() metódusok alaphelyzetben mindig a String elejéről/végéről kezdik a keresést, de meg lehet adni nekik, hogy adott karaktertől kezdjék: indexOf(mit, honnan) Ehhez kapcsolódó feladat lehet, hogy adjuk meg, hol található a második ‘r’ betű a szóban:
String s= "abrakadabra";
int elso = s.indexOf("r");
System.out.println(s.indexOf("r", elso + 1));
/* Először megkeressük az első 'r' betűt, majd amikor a másodikat
* akarjuk megkeresni, akkor megadjuk, hogy az első utáni pozíciótól
* induljunk. Ezt a két lépést akár össze is vonhatjuk:
*/
System.out.println(s.indexOf("r", s.indexOf("r") + 1));
System.out.println(s.lastIndexOf("br", s.lastIndexOf("br") - 1));
/* Ha ugyanezt hátulról végezzük, akkor figyelni kell arra, hogy
* az első találat előtt kell folytatni, vagyis itt -1
* kell az első találat helyéhez képest, mivel visszafelé keresünk
*/
String kezdete és vége – startsWith(), endsWith()
Egyszerűen megvizsgálhatjuk, hogy a String egy adott karaktersorozattal kezdődik vagy végződik. Erre szolgálnak a startsWith() és endsWith() metódusok. Ezek is kis-nagybetű érzékenyek, vagyis megkülönböztetik őket.
String s = "abrakadabra";
System.out.println(s.startsWith("ab")); // true
System.out.println(s.endsWith("ab")); // false
System.out.println(s.startsWith("Ab")); // false(!)
Hogy vehetem figyelmen kívül a kis-nagybetű különbséget? Nincs startsWithIgnoreCase() metódus. A trükk annyi, hogy a String kisbetűs verzióját kell összehasonlítani a keresett kezdőrésszel.
String s = "Abrakadabra";
System.out.println(s.startsWith("ab")); // false, nem meglepő
System.out.println(s.toLowerCase().startsWith("ab")); // true!
String karaktereinek cseréje – replace(), replaceFirst()
Egy Stringben kicserélhetünk karaktereket. Erre szolgál a replace() metódus. Ezzel egy tetszőleges karakter minden előfordulását kicseréljük egy másikra. Az is előfordulhat, hogy csak az elsőt kell kicserélni, erre szolgál a replaceFirst().
String s = "abrakadabra";
System.out.println(s.replace("a","A")); // AbrAkAdAbrA
System.out.println(s.replace("z","A")); // abrakadabra
// Nem volt mit cserélni, maradt az eredeti.
System.out.println(s.replaceFirst("a","A")); // Abrakadabra
// Kicserélte az elsőt, ahogy vártuk.
s = "Abrakadabra";
System.out.println(s.replaceFirst("a","A")); // AbrAkadabra(??)
/* Láthatod, hogy az eredeti szó már nagybetűvel kezdődött. Ekkor az
* első betű, amit cserélni tudott nyilván a második 'a' betű lesz,
* de itt sem felejtetted el: kis-nagybetű érzékeny metódus!
*/
String részének kinyerése – substring()
Előfordulhat, hogy egy Stringből ki kell szednünk egy kisebb részletet. Erre szolgál a substring() metódus.
Amikor egy részt akarunk kinyerni egy Stringből, akkor meg kell mondanunk, hogy milyen karakter határokhoz (indexek) viszonyítva akarom ezt megkapni. Melyiktől kezdjük, és melyik előtt fejezzük be. Ha csak a kezdő pozíciót adjuk meg, akkor onnantól a String végéig az egészet megkapjuk. A substring() mindig String típusú eredményt ad vissza.
String s = "abrakadabra";
System.out.println(s.substring(0,5)); // abrak
System.out.println(s.substring(2,5)); // rak
System.out.println(s.substring(5,8)); // ada
System.out.println(s.substring(6)); // dabra
System.out.println(s.substring(s.length())); // mindig üres String
A String tartalmazza-e? – contains()
Megtudhatjuk, hogy a String tartalmaz-e egy keresett részt a contains() metódus segítségével. Ez minden esetben logikai eredményt ad. True ha benne van, false ha nincs. Az utolsó trükk meg már ismerős.
String s = "Abrakadabra";
System.out.println(s.contains("rak")); // true
System.out.println(s.contains("Rak")); // false
System.out.println(s.contains("abra")); // true (a vegen van!)
System.out.println(s.contains("abrak")); // false
System.out.println(s.toLowerCase().contains("abrak")); // true(!)
Egyébként a contains() kiváltható akár egy indexOf() metódussal is, annyi a különbség, hogy az önmagában nem logikai eredményt ad:
String s = "Abrakadabra";
System.out.println(s.indexOf("rak") > -1); // true
System.out.println(s.indexOf("Rak") > -1); // false
String szétdarabolása – split()
Több feladat esetén előfordulhat, hogy egy Stringet azért kell darabokra szedni, mert valamilyen elválasztó karakterekkel határolva több adatot tartalmaznak. Erre a darabolásra szolgál a split() metódus. A split() minden esetben egy String tömböt ad eredményül, melynek elemei a megadott karakternél széttört String darabjai lesznek. Láthatod majd a példákból, hogy csak meg kell adni a split() metódusnak, milyen karakter mentén törje szét a Stringet. Az eredmény azonnal eltárolható egy String tömbben.
Az utolsó példa kicsit furcsa. Ne lepjen meg, hogy van benne egy üres String. Mivel a String elején volt egy töréspont, ezért a bevezető ‘a’ betűnél is eltöri a Stringet, és az előtte lévő semmit is eltárolja egy üres String darabként. Ha a töréspont a String végén található, akkor azt nem veszi figyelembe, és nincs nyoma az eredménytömbben sem. Alaphelyzetben a String végén elhelyezkedő töréspontokat a split() figyelmen kívül hagyja. Legalábbis ez a verziója.
String nevsor = "Geza Eva Pal";
String[] nevek = nevsor.split(" "); // { "Geza", "Eva", "Pal" }
String nevsor2 = "Geza,Eva,Pal";
String[] nevek2 = nevsor2.split(","); // { "Geza", "Eva", "Pal" }
String s = "abrakadabra";
String[] tomb = s.split("a"); // { "", "br", "k", "d", "br" }
Nem csak egy karakter adható meg töréspontként, akár karaktersorozatot is használhatsz. Itt is igaz az, hogy elől lévő töréspont miatt üres Stringgel kezdődik az eredménytömb, a végén lévővel itt sem foglalkozna, ha lenne 🙂
String s = "abrakadabra";
String[] tomb = s.split("ab"); // { "", "rakad", "ra" }
A split() metódus másik formája két paramétert vár. Itt egy limitet lehet megadni, hogy hány elemű eredménytömböt kapjak. A metódus a következő:
s.split(töréspont,n);
Itt többféle eredmény is lehet. Az n-nel jelölt szám értéke többféle lehet, ennek megfelelő a végeredmény is.
Ha n > 0,akkor n valójában azt jelenti, hogy hány darabra törje a Stringet. (vagyis n-1 esetben töri). Ha a kívánt darabok száma több, mint amennyi lehetséges, akkor a lehetséges maximumot kapjuk. Az “abrakadabra” szót nem törhetem az ‘a’ betűknél 100 részre. Mivel összesen 5 ‘a’ betű van benne, de ebből egy a végén, így maximum 6 darabra törhető. És ha az előző szónál 3 a limit? Akkor kétszer töri el a szót az elejétől kezdve, és 3 darabunk lesz. Az utolsó darabban viszont benne marad az összes olyan töréspont, ameddig a limit miatt nem jutott el.
Ha n == 0, az gyakorlatilag az alap split() metódus eredményét hozza. Vagyis, a String elején lévő töréspontokat figyeli, a végén lévőket nem, és annyi darabra töri, amennyire ennek megfelelően lehetséges.
Ha n < 0, akkor annyi darabra töri, amennyire csak tudja n értékétől függetlenül. És itt figyelembe veszi a String végén lévő töréspontokat is! Ilyenkor a darabok és a töréspontok ismeretében bármikor helyreállítható az eredeti String!
String s = "abrakadabra";
String[] tomb;
// n > 0
tomb = s.split("a", 1); // { "abrakadabra" } 1 darab
tomb = s.split("a", 3); // { "", "br", "kadabra" } 3 darab
tomb = s.split("a", 5); // { "", "br", "k", "d", "bra" } 5 darab
tomb = s.split("a", 8); // { "", "br", "k", "d", "br", "" } 6 darab,
// de nem 8, mert nincs annyi töréspont!
// n == 0
tomb = s.split("a", 0); // { "", "br", "k", "d", "br" } mint split("a")
// n < 0
tomb = s.split("a", -1); // { "", "br", "k", "d", "br", "" } hátsók is!
Speciális határolók
Vannak olyan speciális karakterek, melyeket nem lehet csak úgy odaadni a split-nek. Nem tudom, hogy a lista teljes-e, de ha valamelyik határoló esetén a split nem jó eredményt ad, érdemes majd az alább ismertetett módon megpróbálni.
String s = "abra.kad.abra";
String[] tomb;
tomb = s.split("."); // hibás!
tomb = s.split("\\."); // { "abra", "kad", "abra" }
s = "abra|kad|abra";
tomb = s.split("|"); // hibás!
tomb = s.split("\\|"); // { "abra", "kad", "abra" }
s = "abra\\kad\\abra"; // már a megadáskor ügyelni kell a \ jelre!
tomb = s.split("\"); // hibás!
tomb = s.split("\\"); // hibás!!!
tomb = s.split("\\\\"); // { "abra", "kad", "abra" }
Az utolsó példa esetleg kis magyarázatot igényel. Itt határoló karakternek szeretnénk megadni a \ jelet. A splitnek ha \\ módon adjuk meg a határolókat, azt sem dolgozza fel, mert ő ezt egy \ jelnek veszi. Itt a helyes megoldás a \\\\, amiből literálként \\ marad, és ő ezt dolgozza fel \ jelként. Ha egy fájlból beolvasott Stringben vannak ilyen jelek, akkor nem kell kivételként megadni, tehát a fájlban elég, ha így szerepel:
abra\kad\abra
Literálként viszont a \ jel önmagában nem adható meg, így Stringként megadva ebből ez lesz:
s = "abra\\kad\\abra";
Csak zárójelben jegyzem meg, hogy a split()-nek megadandó töréspont nem csak String lehet, hanem egy úgynevezett reguláris kifejezés is. Ez egy nagyon jól paraméterezhető illesztési minta, amivel teljesen átláthatóvá tudunk tenni összetett mintákat is.
Több határoló együttes használata
Előfordulhat olyan feladat, amelynél egy adott Stringet úgy kell több darabra törni, hogy nem csak egyféle határolót használunk. Nyilván meg lehetne oldani az eddig leírtak alapján is, de az meglehetősen körülményes lenne. Tegyük fel adott egy String, ami így néz ki:
String s = "123a4a56b78b9a0";
Ebben a sorban olyan Stringet látunk, ahol az egyes darabokat (a leendő sázmokat) betűk választják el egymástól. A helyzet azonban az, hogy nem csak egyfajta betű jelenik meg határolóként. Split-nél eddig azt tanultuk, hogy meg kell adni azt a határolót, aminél szét akarjuk törni a Stringet. A határoló állhat több karakterből is, de ez akkor is csak egyetlen darab lesz. Lássuk hogy lehet megoldani azt, hogy a fenti String-et a betűknél tördelve megkapjuk a benne lévő számokat:
String s = "123a4a56b78b9a0";
String[] darabok = s.split("a|b|c");
for( int i = 0; i < darabok.length; i++ )
{
System.out.println(darabok[i]);
}
Ha lefuttatod a fenti kódot, akkor láthatod, hogy valóban az összes számot megkaptuk, és egyetlen betűt sem találunk a darabokban. Gyakorlatilag annyi a teendőnk, hogy egy | jellel elválasztottuk egymástól a határolókat egy felsorolásban. Ez a | jel valójában egy vagy műveletnek, de ez nem a logikai vagy, ne keverjük vele, csak egyetlen jelből áll. Ha tehát több határolónál kell egy Stringet darabolnunk, akkor használjuk bátran. Egy jó példa erre az emelt informatika érettéségi feladatok fehérje nevű feladata (2006 május).
Stringet karakterekre bontása – toCharArray()
Előfordulhat, hogy egy Stringet szét kell bontani karaktereire.
String s = "hokusz";
char[] tomb = s.toCharArray(); // { 'h', 'o', 'k', 'u', 's', 'z' }
String feladatok
Akkor kombináljuk az eddig tanultakat, nézzük meg a metódusok használatát komplexebb feladatok esetén.
Írjuk ki a nevünket vízszintesen kicsit széthúzva (tegyünk a nevünk betűi közé plusz szóközöket):
String nev = "Miko Csaba";
for (int i = 0; i < nev.length(); i++)
{
System.out.print(nev.charAt(i) + " ");
}
Adjuk meg a nevünket, írjuk ki egy oszlopba:
String nev = "Miko Csaba";
for( int i = 0; i < nev.length(); i++ )
{
System.out.println( nev.charAt(i) );
}
Számoljuk meg, hány a betű található a nevünkben:
String nev = "Miko Csaba";
int adb = 0;
for( int i = 0; i < nev.length(); i++ )
{
if( nev.charAt(i) == 'a' )
{
adb++;
}
}
System.out.println( "A nevben " + adb + " darab 'a' betu talalhato." );
Írjuk ki, hányszor szerepel a mondatban ‘a’ névelő. Kis és nagybetűs változat is számít!
String nev = "A Java tanulasa nem egyszeru feladat, "
+ "de a szorgalom meghozza gyumolcset.";
String[] tomb = nev.toLowerCase().split(" ");
int adb = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i].equals("a") )
{
adb++;
}
}
System.out.println( "A nevben " + adb + " darab 'a' nevelo talalhato." );
ALAPALGORITMUSOK
Bármilyen bonyolult programot veszünk szemügyre és bontunk részekre, a végén ugyanaz a 4 építőelem marad:
szekvencia (utasítások egymás utáni sorozatának végrehajtása)
változóhasználat
elágazások
ciklusok
A sorrend nem véletlen, ebben a sorrendben kell ezeket megtanulni használni, mert ezek egymásra épülő darabok a programozásnak nevezett kirakó játékban. Ha nem az építőelemeit nézzük a programoknak, akkor is találhatunk olyan sablonokat, olyan már tanult megoldásokat, amelyek újra és újra előfordulnak a programjainkban. Ezeket a sablonokat, kész megoldásokat nevezzük programozási tételeknek.
Ezek valójában betanulható kész algoritmusok, melyek egy adott problémára kész megoldást adnak. Nem mindig fordulnak elő tiszta formában, vagyis néha apró változtatásokra szükség van, hogy ezeket az algoritmusokat egy adott feladathoz igazítsuk, de ha ezeket ismerjük és biztosan használjuk, akkor sokféle programozási feladatot meg tudunk oldani.
Ezek az alap algoritmusok tömbökhöz kapcsolódnak, vagyis sok egyforma adattal végeznek valamit. Megkeresik egy tömbből a legnagyobb értéket, sorba rendezik a számokat, eldöntik, hogy benne van-e egy adott érték a tömbben, megadják két halmaz metszetét, stb. Lássunk akkor néhány alap algoritmust:
Megszámlálás
Összegzés
Eldöntés
Kiválasztás
Keresés
Minimum/maximum keresés
Rendezés
Kiválogatás
Szétválogatás
Metszet
Unió (hiányzik)
Ezen algoritmusok mindegyikére igaz, hogy ciklusokhoz kapcsolódnak, hiszen ha tömbökkel dolgozunk, akkor mindenképpen ciklusra van szükség, hogy az elemeket egyenként megvizsgálhassuk, összehasonlíthassuk, stb. Ezek az algoritmusok kicsit leegyszerűsítik a programozást, hiszen ezekkel a megtanulható kész receptekkel sokféle feladatot megoldhatunk. A probléma az, hogy a feladatban fel kell ismerni, hogy valójában mit is akarunk eredményként megkapni, és az melyik algoritmusnak felel meg. Ha ez megvan, onnantól szinte csak gépelési feladattá sikerült egyszerűsíteni a programozási feladatot.
Az alap algoritmusok valamennyi fajtájához létezik pszeudokód, olyan általános leírás, amely programozási nyelvtől független. Ráadásul, mivel 3 fajta ciklus létezik, ezért alapból szerteágazó megoldásokat adhatunk ugyanarra a feladatra. Az alap algoritmusokat nagyon sok helyen ugyanazzal a megoldási formával adják meg, és biztos vagyok benne, hogy több tanár csak így fogadja el megoldásként. Én azt vallom, hogy bármilyen jó megoldás elfogadható, a lényeg, hogy a diák alkalmazni tudja azt, amit tanult. Léteznek lecsupaszított, hatékony és egyszerű megoldások, de sokszor én sem azt alkalmazom, mert nem írunk olyan szintű programokat, hogy ennyire optimalizált és gyors algoritmusra lenne szükség. Aki esetleg az alap algoritmusaimban hibát talál, mondván, hogy ő ezt nem így tanulta, az nem feltétlenül hiba, egyszerűen más a megoldás. Az példáknál sok helyen kész ténynek veszem azt, hogy rendelkezésre áll az a tömb a megfelelő adatokkal, amelyekkel dolgozni kell. Ezeknek a tömböknek a feltöltésével, ellenőrzésével nem foglalkozok. Vegyük akkor sorra ezeket az algoritmusokat:
Megszámlálás
Kezdjük valami egyszerűvel. Az alapfeladat az, hogy számoljuk meg, hogy egy adott tömbben hány darab adott tulajdonságú elem van. Ez jelentheti azt is, hogy nincs ilyen tulajdonságú elem a tömbben, akkor a darabszám nyilván 0. Ennél a feladatnál minden esetben végig kell menni a tömbön, hiszen minden elemről meg kell állapítanom, hogy rendelkezik-e a tulajdonsággal, vagy sem. Mivel megszámolunk, ezért valahol tárolnom kell, hogy éppen hol járok a számolásban, hány olyat találtam, ami megfelelt a feltételemnek. Ehhez szükség van egy úgynevezett gyűjtőváltozóra. Az adott algoritmus egy darabszámot ad eredményül minden esetben, ami a [0;méret] intervallumban lesz, vagyis lehet, hogy egy elem sem felel meg a feltételnek, de az is előfordulhat, hogy mindegyik. Nézzünk pár példát, hogy mikor alkalmazható ez az algoritmus:
Hány 180 cm-nél magasabb diák jár az osztályba?
Hány napon esett az eső tavaly?
Hány férfi tanár tanít az iskolában?
Láthatjuk, hogy minden esetben egy darabszámra kíváncsi minden kérdés. Lássuk akkor azt az algoritmust, ami ezekre a kérdésekre választ ad. A példában az első kérdésre keressük a választ.
int szamlalo = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] > 180 )
{
szamlalo = szamlalo + 1;
}
}
System.out.println("Az osztalyba "+szamlalo+" db 180 cm-nel "
"magasabb diak jar.");
Nézzük akkor részletesebben, mi történik.
Deklarálunk egy gyűjtőváltozót, ahol a feltételnek megfelelő elemek darabszámát tároljuk.
A gyűjtőváltozót 0 kezdőértékre állítjuk be. Ez egyébként általános szabály, hogy minden gyűjtőváltozót legkésőbb a használata előtt (a ciklus előtt) nullázni kell!
Indítunk egy ciklust, ami a tömb összes elemén végigmegy.
Megvizsgáljuk, hogy az adott elem megfelel-e a feltételnek
Ha megfelel, a számlálót eggyel megnöveljük.
A ciklus után kiírjuk az eredményt.
A kiemelt sorban a változó növelését kicserélhetjük a már tanult inkrementáló operátorra. Azért, mert lusták vagyunk, és nem akarunk sokat gépelni 🙂
szamlalo = szamlalo + 1;
helyett
szamlalo++;
A többi feladatnál gyakorlatilag ugyanezt kell begépelni, igazából az egyetlen dolog ami változik az maga a feltétel, ami alapján megszámolunk.
Összegzés
Az összegzés tétele kísértetiesen hasonlít a megszámlálásra. Egyetlen különbség van csak, a gyűjtőváltozó növelése. A feladatok is hasonlóak, de az összegzés csak számszerű adatokra vonatkozik. Néhány példa ilyen kérdésekre:
Mennyi a tömbben található páros számok összege?
Mennyi a negatív számok összege?
Mennyi a páratlan számok átlaga?
Lássuk akkor mondjuk az első megoldását:
int osszeg = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 == 0 )
{
osszeg = osszeg + tomb[i];
}
}
System.out.println("A tombben levo paros szamok osszege: "+osszeg);
Láthatjuk, hogy az összegzés algoritmusa szinte ugyanaz, mint a megszámlálásé.
Deklarálunk egy gyűjtőváltozót, ahol a feltételnek megfelelő elemek összegét tároljuk.
A gyűjtőváltozót 0 kezdőértékre állítjuk be.
Indítunk egy ciklust, ami a tömb összes elemén végigmegy.
Megvizsgáljuk, hogy az adott elem megfelel-e a feltételnek
Ha megfelel, az összeghez hozzáadjuk az aktuális elemet.
A ciklus után kiírjuk az eredményt.
A lényegi különbséget kiemeltem. Látható, hogy szinte ugyanaz. Ettől függetlenül ne keverjük a két algoritmust, mert teljesen más a feladatuk!
A kiemelt sorban a változó növelését kicserélhetjük már tanult összeadással kombinált értékadó operátorra. Ismét csak azért, mert lusták vagyunk, és nem akarunk sokat gépelni.
osszeg = osszeg + tomb[i];
helyett
osszeg += tomb[i];
A harmadik feladat kilóg a többi közül, ez nem csak tiszta összegzés. Itt egyszerre kell az előzőleg ismertetett megszámlálást és összegzést elvégezni. Szükségünk van a páratlan számok összegére, valamint a darabszámára is az átlagoláshoz:
int osszeg = 0;
int db = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 != 0 )
{
osszeg = osszeg + tomb[i];
db++;
}
}
double atlag = (double)osszeg/db;
System.out.println("A tomb paratlan szamainak atlaga: "+atlag );
Láthatjuk, hogy a ciklussal ugyanúgy végigmegyünk az egész tömbön. Ha találunk egy megfelelő számot, akkor hozzáadjuk az összeghez és növeljük a darabszámot is. Az átlag már csak egy osztás. De nem egyszerű osztás. Az osszeg és db változók egész típusok. Mi lenne az eredménye annak, ha mondjuk az összeg 11 a darabszám pedig 5? 11/5 = ?
Ne felejtsd el! Ha két egész számot osztunk egymással, az egész osztás! Az eredmény nem a sokszor várt 2,2 lenne, hanem 2,0. Az osztás akkor nem egész osztás, ha legalább az egyik műveletben részt vevő szám nem egész. Előzőleg már mutattam egy trükköt, írhatnánk így is:
osszeg/(db+0.0)
Helyette azonban legyünk elegánsabbak, használjunk típuskényszerítést, amit a véletlen számok témakörben már bemutattam:
(double)osszeg/db
A típuskényszerítés során az összeg változóból kiolvasott értéket lebegőpontos számmá alakítjuk, majd osztjuk egy egésszel. Ennek eredménye már megfelelő: 2,2.
Fontos, hogy ez a típuskényszerítés nem az eredeti összegváltozóban tárolt értéket változtatja meg, nincs értékadás! Nem is változtathatja meg az összegváltozó tartalmát, mivel annak típusa egész. Csak a változóból felhasznált értéket alakítja át a megadott típusra.
Eldöntés
Ennél a feladattípusnál azt vizsgáljuk, hogy egy tömbben található-e egy bizonyos tulajdonságú elem. Nem érdekel, hogy hány ilyen elem van, csak az a fontos, hogy van-e benne ilyen. Itt logikai eredményt kapunk, vagyis a válasz igaz vagy hamis lehet. Lássunk pár példát olyan kérdésekre, amelyekre ezzel az algoritmussal kaphatunk választ:
Van-e az osztályban lány?
Van-e az osztályban 190 cm-nél magasabb diák?
Volt-e melegebb 38 foknál tavaly nyáron?
Van-e 30 évnél fiatalabb tanár az iskolában?
Ezekhez a feladatokhoz természetesen szükség van tömbökre, melyek azokat az adatokat tárolják, amelyek között keressük azt a bizonyos tulajdonságú elemet, azok közül is a legelsőt. Emlékezz, nem érdekel hány ilyen elem van, csak az számít, hogy van-e ilyen, és ez nyilván a legelső megtalált elem lesz. Az első példához szükség van egy tömbre, amely a tanulók nemét tárolja, akár logikai típusként (lány – true, fiú – false). A második esetben kell egy tömb, ami az osztályba járó diákok magasságait tartalmazza, a harmadikban egy tömb, ami a nyári napok maximum hőmérsékletét tartalmazza, a negyediknél egy tömb, amiben benne van az iskolában tanító tanárok életkora. Maradjunk a második példánál, és vegyük úgy, hogy rendelkezésre áll egy „tomb” nevű tömb, ami az osztályba járó diákok magasságait rögzíti.
int i = 0;
while( i < tomb.length && tomb[i] <= 190 )
{
i++;
}
if( i < tomb.length )
{
System.out.println("Van az osztalyban 190 cm-nel magasabb diak.");
}
Na de mit is csinál ez pontosan?
Először deklarálunk egy ciklusváltozót, amit arra fogunk használni, hogy indexelhessük (hivatkozhassunk) az egyes tömbelemekre, jelen esetben a diákok magassági adataira. Ez a sorszám természetesen 0-tól indul, mert a Java nyelvben a tömbök indexei 0 számmal kezdődnek.
Aztán indítunk egy ciklust, melynek az a feladata, hogy végigmehessünk egyenként a tömb elemein. A ciklus feje viszont egy összetett feltételt tartalmaz. Ennek első fele azt vizsgálja, hogy végigértünk-e már a tömbön – vagyis, hogy az index kisebb-e, mint a tömb mérete. Ha az i egyenlő lenne a tömbmérettel, az már azt jelentené, hogy túljutottunk az utolsó elemen, tehát a ciklus megáll. Mivel a tömbök indexe 0-val kezdődik, ebből következik, hogy az utolsó elem indexe tömbméret-1. A feltétel másik része a tulajdonság vizsgálat, amelyre csak akkor kerül sor, ha még nem értünk a tömb végére. Itt azt vizsgáljuk, hogy a tomb[i] – vagyis az aktuális diák magassága – NEM RENDELKEZIK a keresett tulajdonsággal. Ez fura, mert nem pont ennek a fordítottját keressük? De igen, ez az algoritmus lényege. Addig kell keresni, amíg NEM találtuk meg, amit kerestünk, hiszen ha megvan, akkor megállhatunk. Ha ez a két feltétel egyszerre igaz (nem értünk még a tömb végére ÉS nem rendelkezik az aktuális diák a keresett tulajdonsággal), akkor lépünk be a ciklusba, ami semmi mást nem csinál, csak lépteti a számlálót, vagyis jöhet a következő vizsgálandó diák. Az algoritmus nagyon fontos része az, hogy a ciklus két feltételének sorrendje kötött! Először kell azt vizsgálni, hogy nem értünk-e a végére, és ha nem, csak akkor szabad megvizsgálni, hogy az aktuális elem nem rendelkezik-e a keresett tulajdonsággal. Hiszen ha már végignéztük a tömböt, akkor már nincs mit vizsgálni.
A ciklus befejeződése után már csak értelmeznünk kell a kapott eredményt. Az i változó értéke az, ami a megoldást tartalmazza. Ha találtunk olyan diákot, aki rendelkezett a keresett tulajdonsággal, akkor a ciklus idő előtt megállt, vagyis az i értéke kisebb, mint a tömb mérete. Ha egyetlen diák sem volt 190 cm-nél magasabb, akkor a ciklus azért állt meg, mert az i változó már nem kisebb a tömb méreténél (vagyis egyenlő), tehát nem találtunk olyat, aki a feltételnek megfelelt volna
Természetesen a többi feladatra is hasonló a megoldás, lássuk mondjuk a negyedik feladatot:
int i = 0;
while( i < tomb.length && tomb[i] >= 30 )
{
i++;
}
if( i < tomb.length )
{
System.out.println("Van az iskolaban 30 evnel fiatalabb tanar.");
}
Nagyon fontos eleme tehát az eldöntésnek, hogy második részfeltételnek azt adjuk meg, hogy az aktuális elem a keresett tulajdonsággal nem rendelkezik! Mivel a feltételek többsége relációt tartalmaz, itt a relációk ellentettjét kell használni!
// 30 évnél fiatalabbat keresünk
while( ... && tomb[i] < 30 )
helyett
// 30 évnél nem fiatalabb kell a feltételbe
while( ... && tomb[i] >= 30 )
Írhatnám úgy is, hogy valóban tagadom az eredeti állítást:
// 30 évnél nem fiatalabb
while( ... && !(tomb[i] < 30)
Az eredeti állítás valódi tagadását tomb[i] < 30 -> !( tomb[i] < 30) én inkább a reláció ellentettjével helyettesíteném, mivel ott egy reláció marad csak, így csak egy műveletet kell végrehajtani. Valódi tagadás esetén ott marad az eredeti reláció, majd a reláció logikai értékének tagadása is kell, ami két művelet, de ezekből az egyik megspórolható. Itt visszautalnék a relációs operátoroknál arra a táblázatra, ahol ismertettem a relációk tagadásait. Ne feledd: a kisebb tagadása nem a nagyobb!
Mi az, amit még észrevehettél ebben az algoritmusban? Ezzel vissza is kanyarodtunk egy nagyon fontos részhez, a logikai kifejezéseknél. Láthatod, hogy a while ciklus fejében egy összetett kifejezés szerepel. Ennek az első fele az, hogy elértünk-e már a tömb végéig, a második pedig az, hogy az aktuális elem nem rendelkezik a keresett tulajdonsággal. Látható, hogy a második feltétel kész tényként veszi azt, hogy ott csak valódi elemet vizsgálhatok. Egy tömbnek, ha emlékszel, fix tulajdonsága a mérete. Vagyis csak akkora indexet lehet használni, amilyen elem még szerepel a tömbben. Egy 10 elemű tömbben nem lehet pl a tomb[12] elemre hivatkozni, mert ilyen elem nem létezik.
Ez futási hibát eredményez. Na de itt hol ellenőrzöm azt, hogy nehogy túl nagy indexet használjak? Az első feltételben. Ez egy logikai rövidzár. Ha az első részfeltétel, hogy az i számláló kisebb, mint a tömbméret (vagyis az i lehet a tömb egy indexe) teljesül, csak akkor vizsgálja meg a második részfeltételt, az adott elem keresett tulajdonságának hiányát. Ha az i elérte a tömbméretet (vagyis nem kisebb), akkor a logikai rövidzár miatt a második feltételt logikai és esetén már meg sem vizsgálja. Nem fog olyan elemet vizsgálni, ami nem létezik! Nagyon fontos eleme az algoritmusnak az, hogy a két részfeltételnek pontosan ilyen sorrendben kell szerepelnie, mert így oldja meg a rövidzár azt az ellenőrzést is, hogy csak valódi emelet vizsgáljunk meg, és ne kapjunk futási hibát.
Kiválasztás
Ezzel az algoritmussal azt adhatjuk meg, hogy egy adott elem pontosan hol szerepel a tömbben. Ez természetesen az adott elem indexét jelenti, amellyel a tömbben hivatkozunk rá. Ez az algoritmus feltételezi azt, hogy az elem tényleg benne van a tömbben, ez ugyanis nem keverendő össze a keresés algoritmusával, amit következőként fogok ismertetni. Lássunk erre egy pár kérdést.
Válasszuk ki a tömbből az 50-es számot (nem index, hanem érték!).
Hányadik a sorban az a diák, akinek a magassága 190 cm-nél nagyobb.
Lássuk az első példa megoldását:
int i = 0;
while( tomb[i] != 50 )
{
i++;
}
System.out.println("Az 50-es szám indexe: "+i);
Ha megnézzük, ez egy lecsupaszított eldöntés algoritmusnak tűnik, amikor ciklusban működési feltételként furcsa módon azt adjuk meg, hogy a ciklus addig menjen, amíg az aktuális elem NEM rendelkezik a tulajdonsággal. Vagyis addig megyünk, amíg meg nem találjuk. Hiányzik viszont a eldöntéses algoritmus összetett feltételének első része, ami azt vizsgálja, hogy túlszaladtunk-e a tömb végén. Itt erre nincs is szükség, mivel abból indultunk ki, hogy a kiválasztandó elem biztosan benne van a tömbben.
Kiválasztásnál lehetséges, hogy több elem is megfelel a feltételnek, ez az algoritmus a legelső olyan elemet választja ki, akire a feltételünk igaz lesz. Viszonylag könnyen megoldható az is, hogy a legutolsó olyat válasszuk ki, ez csak a ciklus haladási irányától és az i kezdőértékétől függ.
Keresés
A keresés algoritmusa gyakorlatilag szinte ugyanaz, mint az eldöntés algoritmusa, mindössze az i változó ciklus utáni értelmezésénél van különbség. Azért szerepeljen itt újra az algoritmus egy konkrét példával. A feladatban azt keressük, hogy van-e 190 cm-nél magasabb diák és hogy ő hányadik a tömbben:
int i = 0;
while( i < tomb.length && tomb[i] <= 190 )
{
i++;
}
if( i < tomb.length )
{
System.out.println("A 190 cm-nél magasabb diák helye: "+i);
}
else
{
System.out.println("Nincs ilyen diák.");
}
Látható az, hogy ez biztonságosabb algoritmus az előzőnél. Ez akkor is használható, ha nem tudjuk, hogy egyáltalán létezik-e ilyen diák, ezért eggyel több a feltétel is, mert azt is figyelni kell, hogy a tömb végén ne szaladjunk túl. A ciklus után pedig az i értékéből határozhatjuk meg a keresett elem helyét, ha ugyanis az i kisebb a tömb méreténél (vagyis nem szaladtunk túl rajta, tehát benne van), akkor az i már a keresett elem helyét jelenti. Ha nem így van, akkor nincs benne. Itt is logikai rövidzárat használunk, tehát a két feltétel sorrendje nagyon fontos. Az első feltétel biztosítja azt, hogy a második nem lehet hibás. Keresési algoritmusból többféle létezik, ez csak a legegyszerűbb lineáris keresés algoritmusa.
Minimum/maximum keresés
Nagyon gyakori feladat az, amikor egy tömbből meg kell határozni a legkisebb/legnagyobb elemet. Ez nem csak egyszerű típusoknál használható, akár objektumok (több tulajdonsággal rendelkező adattárolók) közül is kiválaszthatjuk a legkisebb/legnagyobb tulajdonságút. Technikailag az, hogy a minimum vagy maximum értéket keressük csak egy reláció megfordítását jelenti. Nézzük akkor hogy néz ki ez az algoritmus. Keressük meg a tomb nevű tömbben a legnagyobb értéket!
int max = 0;
for( int i = 1; i < tomb.length; i++ )
{
if( tomb[i] > tomb[max] ) max = i;
}
System.out.println("A tombben levo legnagyobb szam: "+tomb[max]);
Nézzük akkor részenként a programot. Először is deklarálunk egy max nevű változót, amelynek azonnal adunk is egy 0 kezdőértéket. Fontos, hogy ez nem egy változó nullázás, mint a megszámlálás vagy összegzés algoritmusánál tettük. Ennek a 0 értéknek jelentése van. Azt jelenti, hogy a legnagyobb elem a legelső, vagyis a 0 indexű! A max változóban tehát nem a legnagyobb elem értékét, hanem a helyét (indexét) tároljuk. Mindjárt világos lesz, miért. Azt mondjuk tehát, hogy a legnagyobb elem a 0. helyen van, vagyis ez az első elem. Ez teljesen egyértelmű, hiszen amíg meg nem vizsgálom a tömböt, az első elem tekinthető a legnagyobbnak, mivel a többit még nem ismerem. A ciklust, amivel végigmegyek az egész tömbön természetesen a 2. elemtől indul (indexe 1) és a tömbméret-1 indexű az utolsó, amit vizsgálnom kell. Ha az éppen vizsgált elem (tomb[i]) nagyobb, mint az eddigi legnagyobb tomb[max], akkor az új maximum helye megváltozik az aktuálisra -> max = i.
Fura lehet, hogy miért a legnagyobb elem helyét tároljuk és nem az értékét. Mi van akkor, ha ez a kérdés: Hányadik elem a legnagyobb a tömbben? Ha a maximumban a legnagyobb elem értékét tárolnánk, azzal a helyét nem tudjuk megmondani, csak az értékét. A helyéből viszont meghatározhatjuk mindkettőt.
Ha a legkisebb elemet keressük, akkor a kiemelt sorban fordul meg a relációs jel, és máris a legkisebb elemet kapjuk meg a végén. Természetesen minimum keresésnél célszerű a max változó nevét min-re változtatni, hogy utaljon arra, mit is keresek.
Ne felejtsd el tehát, minimum és maximum keresésnél a helyet tároló változó kezdőértéke 0, mivel az első elem lesz először a legkisebb vagy legnagyobb, ha elkezdem a keresést.
Más oka is van annak, hogy a helyet és nem az értéket tároljuk. Tételezzük fel, hogy csak negatív számokat tartalmaz a tömbünk és a legnagyobbat keressük közülük. Létrehozunk egy max változót, azt nullázzuk, de ez most a legnagyobb elem értékét jelentené. Találhatunk negatív számok között olyat, ami nagyobb, mint 0? Könnyen belátható, hogy csak pozitív számokat tartalmaz a tömb és a legkisebbet keressük, akkor sem állja meg a helyét a nullázás. A nulla nem pozitív, tehát nem találsz ettől kisebb pozitív számot, vagyis a tömb egyik eleme sem kerülhet a helyére.
Rendezés
Nagyon gyakori a programjainkban az a típusfeladat, hogy sorba kell rendezni egy tömb elemeit. A Java nyelvben az egyszerű típusokra, és a Stringekre is létezik beépített rendezés, mégis ritkán használjuk őket, mert javarészt objektumokkal fogunk dolgozni, azokra pedig ezek nem működnek. Rendezési algoritmusból nagyon sokféle létezik, vannak egyszerűbb, de lassabb típusok, és vannak nagyon hatékonyak. A valódi helyzet az, hogy a rendezendő adatoktól mennyiségétől is függ az, hogy melyik rendezési algoritmus a hatékony, de középiskolai szinten mindegy hogyan rendezünk, csak oldjuk meg a feladatot. Két rendezési algoritmust fogok megmutatni, amelyeket használni/tanítani szoktam, ha ezeket tudod, akkor bármilyen típusú tömböt rendezni tudsz.
A rendezések legtöbbje összehasonlításokon és cseréken alapul. Összehasonlítunk két elemet, és ha azok sorrendje nem megfelelő, akkor megcseréljük őket. Az algoritmusok sokszor abban különböznek, hogy melyik kettőt hasonlítjuk össze és utána melyik kettőt, stb. Létezik olyan speciális rendezés is, amelyik nem használ összehasonlításokat és cseréket, de ezek csak bizonyos esetekben használhatóak, akkor viszont hihetetlen gyorsak.
A rendezés esetén már összetettebb módon kell bejárni a tömböt, amelynek elemeit rendezni szeretnénk. Itt is igaz az, hogy nem csak egyszerű típusú értékeket tartalmazó tömböket lehet rendezni, az elemek lehetnek összetett objektumok is, melyek többféle típusú értéket tartalmazhatnak.
A tömbök kezelésekor, és az alap algoritmusok használatakor minden esetben ciklusokat használunk arra, hogy bejárjuk az adott tömböt, és annak értékeihez egymás után hozzáférjünk. Abban vannak csak különbségek, hogy ténylegesen bejárjuk-e az egészet, vagy sem, esetleg a bejárás iránya változik. Itt azonban másról lesz szó. Itt találkozunk először az egymásba ágyazott ciklusokkal.
A rendezések, melyeket jellemzően használunk minden esetben azt az elvet követik, hogy a tömb bizonyos elemeit hasonlítják össze, hogy azok egymáshoz képest a kívánt sorrendben helyezkednek-e el. Ha ez nem így van, akkor ezt a két elemet meg kell cserélni. Itt azonban nem csak az egymás melletti szomszédokat vizsgáljuk,
Egyszerű cserés rendezés
Ezt a rendezést több néven is megtalálhatjuk az alap algoritmusok között, én ezt a nevet használom. Az elv, ami alapján dolgozik az az, hogy minden elemet összehasonlít az összes mögötte lévővel, és ha azok sorrendje nem megfelelő, akkor megcseréli őket. Két egymásba ágyazott ciklust igényel, ezeket tradicionálisan i és j ciklusváltozókkal használjuk. Lássuk akkor magát az algoritmust, ahol feltételezzük, hogy van egy tomb nevű tömbünk, amely véletlen számokkal van feltöltve és a meret nevű változóban a tömb méretét találjuk meg:
1
2
3
4
5
6
7
8
9
10
11
12
13
int csere;
for( int i = 0; i < tomb.length-1; i++ )
{
for( int j = i+1; j < tomb.length; j++ )
{
if( tomb[i] > tomb[j] )
{
csere = tomb[i];
tomb[i] = tomb[j];
tomb[j] = csere;
}
}
}
A ciklus úgy dolgozik, hogy a j változó mindig az i utáni helyet jelöl, mivel a j kezdőértéke minden esetben i+1-ről indul. Éppen ezért az i soha nem mehet el a tömb végéig, mert akkor az utolsó elem utáni összehasonlítást is elvégezne, ami mindenképp hibás.
Tehát még egyszer a lényeg: az i van elöl, a j van hátul!
Lássuk a kiemelt részek magyarázatát:
1 – Kell egy csere változó az esetleges cserékhez segédváltozónak. A változó típusának meg kell egyezni a tömb elemeinek típusával, hiszen azon közül fogjuk az egyiket eltárolni benne.
2 – Az i változó soha nem mehet el a tömb végéig, vagyis i < tomb.length-1;
4 – A j mindig az i után áll, ezért int j = i+1;
6 – Mindig összehasonlítjuk az elöl és hátul lévő elemeket, és ha ezek sorrendje nem megfelelő…
8-10 – Akkor jön az elemek cseréje.
A rendezés iránya csak és kizárólag a 6. sorban megadott relációs jeltől függ. Ha az elöl lévő nagyobb és akkor cserélünk, akkor a nagyok kerülnek hátra, vagyis növekvő rendezést alkalmazunk. Ha az elől lévő kisebb és akkor cserélünk, akkor a kicsik kerülnek hátra, és csökkenő rendezést írunk. A fenti példa tehát növekvő rendezést valósít meg, mivel az első esetnek megfelelő a relációs jel.
A csökkenő rendezés ehhez képest tehát minimális változtatással jár:
1
2
3
4
5
6
7
8
9
10
11
12
13
int csere;
for( int i = 0; i < tomb.length-1; i++ )
{
for( int j = i+1; j < tomb.length; j++ )
{
if( tomb[i] < tomb[j] )
{
csere = tomb[i];
tomb[i] = tomb[j];
tomb[j] = csere;
}
}
}
Minimum/maximum kiválasztásos rendezés
Ez a rendezés az előző továbbfejlesztett változata. Az előző algoritmus úgy dolgozik, hogy minden esetben megcseréli a két elemet, ha az aktuális két elem helyzete nem megfelelő. Ez azt eredményezi, hogy több csere is lesz, mire a legkisebb a tömb elejére kerül növekvő rendezés esetén. De ha már egyszer növekvő rendezést akarunk megvalósítani, akkor nem lenne jobb, hogy ha először megkeresnénk a legkisebb elemet, majd azt helyeznénk a lista elejére, majd utána megkeresnék a második legkisebbet, azt beraknánk az első után, és így tovább? Jóval kevesebb cserével járna, mint az előző. Természetesen megoldható, az előző rendezési algoritmusa tökéletesen kombinálható a már tanult minimum/maximumkeresési algoritmusokkal. Lássuk akkor hogyan:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int csere;
int min;
for( int i = 0; i < tomb.length-1; i++ )
{
min = i;
for( int j = i+1; j < tomb.length; j++ )
{
if( tomb[j] < tomb[min] )
{
min = j;
}
}
if( min != i )
{
csere = tomb[i];
tomb[i] = tomb[min];
tomb[min] = csere;
}
}
Lássuk akkor a magyarázatot:
1 – Kell egy csere változó az esetleges cserékhez segédváltozónak. A változó típusának meg kell egyezni a tömb elemeinek típusával, hiszen azon közül fogjuk az egyiket eltárolni benne.
2 – Kell egy változó, ahol a legkisebb elem helyét tároljuk (mint a minimumkiválasztásnál), de ennek itt még nem adunk kezdőértéket.
5 – Mielőtt elkezdjük a belső ciklust, ami az elöl lévő elem mögöttiek indexén megy végig, az elöl lévő elemet feltételezzük a legkisebbnek. Itt a belső ciklus futását gyakorlatilag egy minimum kiválasztásnak írtuk meg. A tomb[i] az első elem, ezért ennek a helyét feltételezzük a legkisebb elem helyének,
8 – majd, ha az eddigi minimumtól valamelyik mögötte lévő (tomb[j]) tőle kisebb,
10 – akkor a hátul lévő elem helyét (j) jegyezzük meg, mint aktuális legkisebbet.
13 – Ha a belső ciklussal végeztünk, akkor a min változóban benne van a hátul lévő elemek közül a legkisebbnek a helye. Ha ez a hely nem egyenlő az elöl lévővel (vagyis nem önmaga a legkisebb), akkor találtunk az elöl lévő (i) elem mögött tőle kisebbet, melynek helyet a min változóban tároljuk.
15-17 – Ebben az esetben a két elemet (i és min helyen lévőket) megcseréljük.
Ezt az algoritmust inkább csak érdekességképp mutattam meg, érettségire tökéletesen elég, ha az egyszerű cserés rendezést megtanulod, mert csak az a követelmény, hogy rendezni tudj, teljesen mindegy, melyik algoritmussal. Nyilván a legegyszerűbbet célszerű megtanulni, a hatékonyság nem követelmény.
Kiválogatás
Szintén az alap algoritmusok közé tartozik az a feladattípus, amikor bizonyos tulajdonságnak, vagy tulajdonságoknak megfelelő elemeket kell egy tömbből egy másik tömbbe kiválogatni. Tegyük fel, van egy egészeket tartalmazó tömbünk, melyet a [-9;9] intervallumból töltöttünk fel. Hogyan oldhatjuk meg, hogy ebből a tömbből egy másik tömbbe kigyűjtjük a negatív számokat? Minden esetben létre kell hozni egy másik tömböt, amibe a megfelelő elemeket másoljuk. De mekkora legyen ez a tömb? Ez az ami alapvetően meghatározza, hogy milyen megoldási módot alkalmazunk. Kétféle esetet különböztetünk meg:
Létrehozunk egy eredeti tömbnek megfelelő méretű tömböt, azt feltételezve, hogy akár az összes elem lehet negatív, így mindet át kell másolni. Ha azonban nem minden elem negatív, akkor az új tömbben maradnak üres helyek, ahova nem rakunk át semmit. Így nyilván kell tartanunk, hogy hány elemet másoltunk át az új tömbbe, és mennyi maradt “üresen” a végén.
Megoldhatjuk úgy is, hogy először megszámoljuk, hogy hány elem felel meg a feltételnek, ami alapján a kiválogatást el akarjuk végezni, és az új tömböt pontosan akkorának állítjuk be. Így a megoldás végén az új tömbben csak azok az elemek lesznek benne, amelyeket mi helyeztünk el benne.
A két megoldásból a második nyilván picivel több munkával jár, mert kapcsolódik hozzá egy megszámlálás is, viszont utána már nem kell attól tartanunk, hogy az eredmény tömbben olyan elem is előfordul, ami nem felel meg a kiválogatás feltételének.
Lássuk akkor a két különböző megoldást. Mindkét esetben feltételezzük, hogy van egy tomb nevű tömbünk, amely véletlen számokkal van feltöltve. Az új tömbbe a páratlan számokat szeretnénk kiválogatni:
1
2
3
4
5
6
7
8
9
10
11
int[] paratlan = new int[tomb.length];
int db = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 != 0 )
{
paratlan[db] = tomb[i];
db++;
}
}
Lássuk akkor a kiemelt sorok magyarázatát:
1 – Létrehozok egy ugyanakkora tömböt, mint az eredeti, lehetőséget adva arra, hogy akár minden elemet kiválogathassak.
3 – Létrehozok egy db nevű változót, ami jelen esetben az első üres helyet fogja tárolni, ahova a következő kiválogatott elemet elhelyezhetem, a kiválogatás végeztével pedig tárolni fogja, hogy az új tömbbe hány elem került bele.
4 – Végigmegyek az eredeti tömbön,
6 – és ha az eredeti tömbben páratlan számot találunk,
8 – akkor az új tömbben a első üres helyre (db) elhelyezem az elemet,
9 – majd megnövelem a db-ot, hogy az esetleges következő átmásolt elem ne írja felül az előzőt.
Látható, hogy nem olyan bonyolult algoritmusról van szó, a kulcs az, hogy mindig tárolom egy változóban, hogy hol van az új tömbben az első üres hely, mert csak oda rakhatok bele a kiválogatás során elemeket. A gond csak annyi, hogy ha van egy 1000 méretű tömböm, amibe 3 elemet kellene csak kiválogatni, akkor is a memóriában foglalja az 1000 elemnyi helyet a 3 kedvéért.
Lássuk akkor a másik megoldást:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int db = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 != 0 )
{
db++;
}
}
int[] paratlan = new int[db];
db = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 != 0 )
{
paratlan[db] = tomb[i];
db++;
}
}
Ha jól megnézed nem sokkal bonyolultabb, picit többet kell gépelni, és ki kellett egészíteni a megszámlálás alap algoritmusával:
1-8 – Megszámoljuk, hány elemet kell majd kiválogatni az új tömbbe.
10 – Létrehozunk egy ugyanakkora tömböt.
12-20 – Ez pedig pontosan az első megoldás.
Az egész algoritmus kulcs momentuma a db változó használata! Ennek több szerepe is van. Először a kiválogatandó elemek darabszámát gyűjtjük bele, utána a következő üres helyet jelöli az új tömbben, végül a kiválogatás végeztével az új tömb méretét jelenti, bár ezt a tömbből úgyis ki lehet nyerni a .length változóból.
Szétválogatás
A szétválogatás algoritmusa a kiválogatás kibővítése. Az alapfeladat az, hogy az eredeti tömb minden elemét két külön tömbbe kell elhelyezni. Feltételezzük, hogy minden elem bekerül valamelyik új tömbbe, vagyis nem hagyunk ki semmit sem. A kiválogatásnál ennek a feladatnak a felét gyakorlatilag megoldottuk. Amit egy kiválogatásnál kiválogatunk, az itt az egyik tömb elemeinek felelne meg. Az összes többi elemet a másik tömbbe pakoljuk. Így már nem is tűnik olyan nehéznek, igaz?
A szétválogatás feltétele minden esetben gyakorlatilag egyetlen feltétel.
Válogassuk szét a tömb elemeit 5-től nagyobb és nem nagyobb elemekre. (emlékezz a relációs jelekre!)
Válogassuk szét a tömb elemeit 5-tel osztható és nem osztható elemekre.
Válogassuk szét az elemeket egyjegyű és nem egyjegyű számokra
Válogassuk szét a tömb elemeit páros és páratlan elemekre.
Ha megfigyelted, a feladatok jó része úgy fogalmazza meg a feltételt, hogy szétválogatjuk valamilyen és NEM valamilyen elemekre. Egy feltétel és annak az ellentettje minden elemet le kell hogy fedjen. Ezért szétválogatás, nem maradhat ki egyetlen elem sem. És az utolsó esetben? Amelyik szám nem páros, az páratlan, tehát ez is lefed minden számot.
A szétválogatásnál is ugyanaz a dilemma lesz először, mint amit a kiválogatásnál írtam:
Nem foglalkozok az új tömbök méreteivel, a legrosszabb esetből indulok ki, hogy minden elemet be kell tennem az egyik tömbbe, a másik pedig üres marad. Ebben az esetben mindkét új tömb méretének az eredeti tömb méretét állítom be, így a két új tömb kétszer annyi helyet foglal majd, amennyire valóban szükség lenne.
Előre megszámolom, hány elem felel meg a szétválogatás feltételének, ezután beállítom a kívánt tömbméreteket, majd utána válogatom szét az elemeket. Ez a legtakarékosabb megoldás, mert mindkét tömb mérete pontosan akkora lesz, amekkorára szükségem lesz. Ha emlékszel, a kiválogatásnál itt egy megszámlálással bővítettem ki az alap megoldást.
Lássuk akkor az első esetet. Tételezzük fel, hogy van egy adott méretű tömböm, feltöltve elemekkel. Válogassuk szét az elemeket páros és páratlan elemeket tartalmazó tömbökbe. Vedd észre, hogy ez valójában egyetlen feltétel. Ami nem páros, az páratlan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int[] paros = new int[tomb.length];
int[] paratlan = new int[tomb.length];
int dbparos = 0;
int dbparatlan = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 == 0 )
{
paros[dbparos] = tomb[i];
dbparos++;
}
else
{
paratlan[dbparatlan] = tomb[i];
dbparatlan++;
}
}
Ha a kiválogatás algoritmusát megértetted, akkor ezzel se lesz probléma. Nem véletlenül csak két részt emeltem ki. A kiválogatásnál kellett egy számláló, amelyben nyilvántartottam, hogy hány elemet válogattam ki, ami egyúttal azt is jelezte, hogy hol van az új tömbben a következő üres hely. Itt is hasonló a helyzet, csak itt két tömb esetén két külön változóban kell tárolni, hogy hány elem van az egyikben-másikban, és ezáltal melyik tömbben hol van a következő üres hely, ahova az elemeket pakolhatom. A belső feltétel sem sokat változott, ha a feltételnek megfelel az elem, akkor berakom az egyik tömbbe, ha nem, akkor mindenképpen (else) a másik tömbbe kell tennem. Emlékszel: minden elem bekerül valamelyik tömbbe, ha nem az elsőbe, akkor a másodikba, nem hagyhatok ki semmit sem.
Ne felejtsd el, a két új tömb mérete nagyobb, mint amennyi tényleges elemet tartalmaznak. Az algoritmus után a két darabszámot tároló változó az, amiből megtudhatod, hogy mekkora valójában a tömb, amit kezelned kell. Nem a paros.length lesz az a határ, ameddig be kell járnod egy ciklussal, hanem a dbparos változó.
Lássuk akkor a második megoldást. Emlékeztetőül: megszámolom hány elemet kell majd beraknom az egyik tömbbe, akkor meglesznek a megfelelő tömbméretek.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int parosdb = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 == 0 )
{
parosdb++;
}
}
int[] paros = new int[parosdb];
int[] paratlan = new int[tomb.length-parosdb];
parosdb = 0;
paratlandb = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] % 2 == 0 )
{
paros[parosdb] = tomb[i];
parosdb++;
}
else
{
paratlan[paratlandb] = tomb[i];
paratlandb++;
}
}
Lássuk akkor a kiemelt részeket:
1-9 – Megszámolom, hány elem felel meg a szétválogatás feltételének.
11-12 – Létrehozom a két megfelelő méretű tömböt. A páratlan tömb méretét úgy kapom meg, hogy a tömb elemeinek darabszámából kivonom a párosok darabszámát, így megvan a páratlanok száma.
14-31 – Lenullázom a két számlálót, és elvégzem a szétválogatást az első megoldásnak megfelelően, csak itt már biztos lehetek benne, hogy mindkét új tömböt teljesen feltöltöm.
Az előzőhöz képest ez nyilván bonyolultabb megoldás. Cserébe takarékosabb, másrészt nem kell külön tárolni, hogy a tömbök valójában meddig vannak feltöltve, mivel a méretük pontosan megfelel a szétválogatott elemek darabszámának.
És ha nem mindent válogatok szét?
Ez az algoritmus csak abban az esetben használható, ha minden elemet szét kell válogatni. Ez mondjuk a szétválogatás elvéből is következik, mivel nem hagyhatunk ki elemeket, különben nem szétválogatásnak neveznénk. Mégis a példa kedvéért tételezzük fel, hogy egy tömbből szeretnénk a pozitív és negatív számokat két másik tömbbe átpakolni. Ebben az esetben már figyelnünk kell arra, hogy mi a helyzet a nullákkal. Természetesen ezt is meg kell oldani, csak itt az elemek megszámolásánál figyelembe kell venni, hogy kihagyunk elemeket, valamint a tényleges válogatásnál is ügyelni kell rájuk. Szándékosan kerültem a szétválogatás szót, mert ez valójában a kihagyott elemek miatt nem az lesz. Lássunk akkor erre egy példát.
Válogassuk ki egy tömb elemei közül a pozitív és negatív számokat. (Észrevetted? Kiválogatás)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int pozitivdb = 0;
int negativdb = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i] > 0 )
{
pozitivdb++;
}
else if( tomb[i] < 0 )
{
negativdb++;
}
}
int[] pozitiv = new int[pozitivdb];
int[] negativ = new int[negativdb];
pozitivdb = 0;
negativdb = 0;
for( int i = 0; i < tomb.length; i++ ) { if( tomb[i] > 0 )
{
pozitiv[pozitivdb] = tomb[i];
pozitivdb++;
}
else if( tomb[i] < 0 )
{
negativ[negativdb] = tomb[i];
negativdb++;
}
}
1-13 – Egy ciklusban megszámolom a pozitív és negatív számokat.
15-16 – Létrehozom nekik a megfelelő méretű tömböket.
És kiválogatom őket egyetlen ciklusban.
Ez gyakorlatilag két kiválogatás egy ciklusba pakolva, a két feltételnek (pozitív vagy negatív) lényegében semmi köze egymáshoz, a számlálóik is teljesen függetlenek, mert nem tudom, hogy a két feltétel lefedi-e az összes eredeti elemet vagy sem. Ha a két feltétel minden elemet besorol valahova, akkor szétválogatás, egyébként két egymástól független kiválogatásról beszélünk.
Metszet
A metszet algoritmus egy kis magyarázatot igényel. Az alap algoritmusok metszetképzése nem egyezik meg a halmazelméletben tanult metszettel. A halmazt elemek sokaságának tekintjük, ahol az elemeknek nincs sorrendje, és minden elem csak egyszer szerepelhet a halmazban. Ez a tömböknél nyilvánvalóan nem áll fenn. A halmazoknál metszetként azon elemek halmazát vesszük, amelyek mindkét halmazban megtalálhatóak.
Tömbök esetén ez azt jelenti, hogy az egyik tömbből vesszük azokat az elemeket, amelyek benne vannak a másikban. Ezzel az algoritmussal csak az a bajom, hogy nem mindegy, hogy melyik tömb oldaláról kezdjük ez a dolgot. Lássuk a következő példát, hogy miről is van szó.
{2,2,3,4}
{3,5,2,6,6}
Ha az első tömb elemeiből hagyjuk meg azokat, amelyek benne vannak a másodikban, akkor ezt az eredményt kapjuk:
{2,2,3}
Ha a második tömb elemeiből hagyjuk meg azokat, amelyek benne vannak az elsőben, akkor ezt az eredményt kapjuk:
{3,2}
Nyilván látszik mi a gond. Ez pedig abból fakad, hogy egy elem többször is lehet egy tömb eleme. A sorszámozás miatt ezek egyértelműen megkülönböztethetőek. A halmazban viszont az elemek nem sorszámozottak, ezért két azonos értékű elemet nem különböztethetnénk meg. Az algoritmus nem foglalkozik ezzel a problémával, és nekünk sem kell. Más kérdés, hogy meg tudnánk oldani azt is, hogy minden elem egyszer szerepeljen csak a metszetben, később ezt is megmutatom.
Mint már fent említettem, az algoritmus annyiból áll, hogy az vesszük az egyik tömb elemei közül azokat, amelyek benne vannak a másikban. Nézzük meg jobban, mi is ez? Kiválogatjuk az egyik tömb elemei közül azokat, amelyek megfelelnek annak a feltételnek, hogy benne vannak a másik tömbben. Kiválogatás, amiben van egy eldöntés. A metszetképzés tehát két tanult algoritmus kombinációja.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int[] t1 = new int[] {2,2,3,4};
int[] t2 = new int[] {3,5,2,6,6};
int[] metszet = new int[t1.length];
int j;
int db = 0;
for( int i = 0; i < t1.length; i++ )
{
j = 0;
while( j < t2.length && t2[j] != t1[i] )
{
j++;
}
if( j < t2.length )
{
metszet[db] = t2[j];
db++;
}
}
Lássuk a kiemelt részek magyarázatát:
3 – A metszet tömb mérete akkora, mint az első tömb mérete, hiszen lehet, hogy annak minden eleme megtalálható a másikban. Természetesen a kiválogatáshoz hasonlóan itt is ügyelni kell arra, hogy nem feltétlen kerül minden elem a metszet tömbbe, ezért majd a db változó fogja tárolni a metszet tömb valódi elemeinek számát.
7 – Ebben a sorban elkezdünk egy kiválogatást, vagyis elindulunk az első tömbön azt keresve, hogy ezek közül melyiket kell majd átrakni a metszetbe.
9-14 – Ez gyakorlatilag az eldöntés algoritmusa, addig haladunk a második tömb elemein, és addig megyünk, amíg nem találunk egyezést a második tömb eleme és az első tömb éppen aktuális eleme között. Ezt az algoritmust most nem magyaráznám el újra, de ami a lényeg: ha az elemet megtaláltuk, akkor visszatérünk a kiválogatáshoz
16-17 – Ha találtunk olyan első tömbbeli elemet, ami megfelelt a feltételünknek (benne van a másodikban is), akkor berakjuk a metszet tömbbe, és növeljük a számlálóját. Ez a kiválogatás algoritmus vége.
Metszet egyedi elemekkel
Mi van akkor, ha valóban csak annyit szeretnénk megtudni, hogy mik azok a számok, melyek mindkét tömbben megtalálhatóak? Ha valami többször szerepel a tömbben, attól mint szám csak egyszer szerepel. Ez nem alap algoritmus, hanem az eddig tanultakat kell alkalmazni. Akár teljesen eltérő megoldásokat is adhatunk:
A két tömb közül az elsőből létrehozok egy olyan tömböt, ami az eredetiben szerepelő számokat csak egyszer tartalmazza. Majd ha erről az oldalról metszetet képzek, akkor a metszetben is minden elem csak egyszer fog szerepelni.
Az első tömbből csak akkor teszek be egy számot a metszetbe, ha benne van a másodikban, és még nincs benne a metszetben. Vagyis a kiválogatáson belül két eldöntésre van szükségem, melyeknek egyszerre kell teljesülnie. Azzal még finomíthatom, hogy ha a második tömbben nincs benne, akkor felesleges a metszetben ellenőrizni, mert akkor oda semmiképpen nem kerülhetett be.
Lássuk az első megoldást:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int[] t1 = new int[] { 2, 2, 3, 4 };
int[] t2 = new int[] { 3, 5, 2, 6, 6 };
int[] metszet = new int[t1.length];
int[] egyedi = new int[t1.length];
int dbe = 0;
int j;
for( int i = 0; i < t1.length; i++ )
{
j = 0;
while( j < dbe && t1[i] != egyedi[j] )
{
j++;
}
if( j == dbe )
{
egyedi[dbe] = t1[i];
dbe++;
}
}
int db = 0;
for( int i = 0; i < dbe; i++ )
{
j = 0;
while(j < t2.length && t2[j] != egyedi[i])
{
j++;
}
if( j < t2.length )
{
metszet[db] = t2[j];
db++;
}
}
Mit is csinálunk pontosan?
5 – Létrehozom azt a tömböt, ahova kiválogatom az első tömb számait. Ennek mérete az eredetivel megegyező, mert lehet, hogy egyik szám sem szerepel többször, akkor mindet át kell pakolni.
6 – Létrehozok egy számlálót, hogy nyilvántartsam, valójában hány elem lesz az egyedi tömbben.
8-21 – Kiválogatom az egyedi számokat. (kiválogatásban egy eldöntés) Fontos, hogy akkor rakom bele az egyedi tömbbe a számot, ha az eldöntés hamis eredményt ad, vagyis nincs benne: if( j == dbe )
23-36 – Ez pedig a metszetképzés algoritmusa, de az egyedi tömb és a második között. A 24-es sorban fontos a feltétel, hogy az egyedi tömbnek nem az összes elemét kell vizsgálni, hanem csak addig, ameddig valóban vannak benne elemek. Ezt a saját dbe számlálója tárolja.
A két részfeladat (egyedi tömb előállítása, majd metszetképzés) ugyanarról a tőről fakad, hiszen mindkét esetben egy elemről akarom eldönteni, hogy benne van-e egy tömbben. A különbség csak az, hogy egyedi elemek válogatásakor akkor rakom bele, ha nincs még benne, metszetképzésnél pedig akkor rakom bele, ha benne van.
Nézzük a másik megoldást, amikor a két eldöntést teszek a kiválogatásba:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int[] t1 = new int[] { 2, 2, 3, 4 };
int[] t2 = new int[] { 3, 5, 2, 6, 6 };
int[] metszet = new int[t1.length];
int j;
int jm;
int db = 0;
for( int i = 0; i < t1.length; i++ )
{
j = 0;
while( j < t2.length && t2[j] != t1[i] )
{
j++;
}
if( j < t2.length )
{
jm = 0;
while( jm < db && metszet[jm] != t1[i] )
{
jm++;
}
if( jm == db )
{
metszet[db] = t1[i];
db++;
}
}
}
Lássuk a lényegi részeket:
10-14 – Eldöntjük, hogy az első tömb eleme benne van-e a másodikban.
15 – Ha igen, akkor
15-27 – Eldöntjük, hogy benne van-e a metszetben.
22 – Csak akkor tesszük be a metszetbe, ha még nincs benne. Ha már egyszer betettünk ilyen számot, akkor nem tesszük bele még egyszer.
Ez a megoldás talán rövidebb és egyszerűbb is, mint a másik, és minden esetben egyedi elemeket tartalmazó metszet tömböt kapunk.
Természetesen ez a metszetképzés algoritmus több hasonló feladatnál is használható, hiszen ha metszetet tudunk képezni, akkor olyan kérdésekre is választ kaphatunk ennek segítségével, hogy van-e két tömbnek azonos eleme, hány közös eleme van két tömbnek, stb.
Komplex feladat
Lássunk egy komplexebb feladatot. Adott egy 10 elemű tömb melyet véletlen számokkal töltöttünk fel a [-9;9] intervallumból. Írjuk ki növekvő sorrendben a tömbben szereplő páros számokat.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
*
* @author http://webotlet.hu
*/
package webotlet_alapalg_komplex;
public class Webotlet_alapalg_komplex
{
public static void main(String[] args)
{
int[] tomb = new int[10];
for (int i = 0; i < tomb.length; i++)
{
tomb[i] = (int) (Math.random() * 19) - 9;
}
for (int i = 0; i < tomb.length; i++)
{
System.out.print(tomb[i] + " ");
}
System.out.println();
int db = 0;
for (int i = 0; i < tomb.length; i++)
{
if (tomb[i] % 2 == 0)
{
db++;
}
}
int[] paros = new int[db];
db = 0;
for (int i = 0; i < tomb.length; i++)
{
if (tomb[i] % 2 == 0)
{
paros[db] = tomb[i];
db++;
}
}
int csere;
for (int i = 0; i < db - 1; i++)
{
for (int j = i + 1; j < db; j++)
{
if (paros[i] > paros[j])
{
csere = paros[i];
paros[i] = paros[j];
paros[j] = csere;
}
}
}
for (int i = 0; i < db; i++)
{
System.out.print(paros[i] + " ");
}
System.out.println();
}
}
Ez egy tökéletes feladat arra, hogy az eddig tanultakat összefoglalja. Sok ismerős részletet láthatunk benne, de lássuk akkor részenként:
12-17 – Adott méretű tömb létrehozása, majd feltöltése véletlen számokkal.
19-22 – A kisorsolt tömb kiíratása.
24 – Sordobás a sorsolt tömb kiíratása után, hogy ne folyjon egybe majd a rendezett tömb kiíratásával.
26-33 – A kiválogatáshoz megszámoljuk, hány elemet kell majd átrakni az új tömbbe.
35 – Létrehozzuk az új tömböt.
37-45 – Kiválogatjuki (átmásoljuk) a páros számokat az új tömbbe.
47-59 – Rendezzük az új tömböt.
61-64 – Kiírjuk a kiválogatott és rendezett új tömböt.
66 – Egy bónusz sordobás a végére, hogy ha bővíteném a programot, akkor az új kiíratás új sorban kezdődjön.
Adott tehát egy elsőre bonyolultnak tűnő feladat, amit szétbontottuk olyan részekre, melyeket már külön-külön meg tudunk oldani. Ezeket a kész megoldásokat (tömb feltöltés, kiíratás, megszámlálás, kiválogatás, rendezés, stb) megfelelő sorrendben hibátlanul összerakjuk, és kész a feladat teljes megoldása. Ugye így jobban belegondolva nem is olyan nehéz? Feltéve hogy az eddigi tananyagokat már készségszinten alkalmazni tudod. Sokszor az a legnehezebb feladat, hogy felismerjük azt, hogy az aktuális feladat milyen kisebb alkotóelemekre bontható, melyekre már kész megoldásaink vannak. Ha ez a részekre bontás megy, akkor gyakorlatilag sokszor gépelési feladattá tudjuk egyszerűsíteni a feladatok nagy részének megoldását.
MÁTRIX
A tömb, mint összetett adattípus az előző anyagokból már ismerős lehet. Míg a tömbök egy adatsort tartalmaznak, a többdimenziós tömbök pedig többet. A többdimenziós tömbök valójában tömbök tömbjei. A dimenziók száma elméletileg nincs korlátozva, gyakorlatilag 3 dimenziónál többel dolgozni nem feltétlenül praktikus.
Egy általános tömb deklarációja a következőképp néz ki:
// deklarálás és inicializálás, ami csak 0 értékekkel tölti fel a tömböt
int[] tomb = new int[10];
// deklarálás és azonnali kezdőérték adás
int[] tomb = {1,2,3,4,5,6,7,8,9,10};
// adott indexű elem kiválasztása
tomb[5]
Ez a többdimenziós tömbök esetén is hasonló, de mivel ezek tömbök tömbjei, ezért ezt formailag is jelezni kell.
Kétdimenziós tömbök
// kétdimenziós tömb deklarálása és inicializálása
int[][] tomb = new int[2][3];
// kétdimenziós tömb adott elemének kiválasztása
tomb[1][2]
Az előző deklarálás azt jelenti, hogy létrehozunk egy 2 sorból és 3 oszlopból álló kétdimenziós tömböt. A sorok és oszlopok sorszámozása (indexelése) itt is 0-val indul, mint általában a tömbök esetén. Mint már említettem, a többdimenziós tömb valójában tömbök tömbje, de formailag ez hogy néz ki? Nézzük meg egy konkrét példán keresztül:
int[][] tomb = { { 2,4,6 }, { 3,7,8 } };
Mit is jelent ez? Adott két sor (a két kicsi tömb darabszáma, ami a számokat tartalmazza) és adott 3 oszlop (ami a kis tömbökben lévő számok darabszámát jelenti). Láthatod, hogy a kis tömbökben lévő számok darabszáma megegyezik, ez nem véletlen. Valójában ez a szám az oszlopok száma. Hogy jobban látható legyen, átrendezem az előző példában szereplő tömb szerkezetét:
int[][] tomb = {
{ 2,4,6 },
{ 3,7,8 }
};
Így már egyértelműbb, hogy mit jelent a sorok és oszlopok száma. A két kis belső tömb jelenti a sorokat, egymás alá írva őket, valóban sorokat alkotnak. A bennük lévő elemek száma pedig kötött, mert ez jelenti az oszlopok számát. A két sort összefogó külső tömb határolóit direkt külön sorba írtam, hogy az ne zavarjon, de az is a struktúra része. Amikor hivatkozunk egy elemre (tomb[1][2]), akkor azt mondjuk meg, hogy az 1-es indexű kis tömbnek (a másodiknak) a 2-es oszlopában (a harmadikban) lévő 8-as elemre gondolunk. Ne feledd, a sor és oszlop indexelése is 0-val kezdődik.
A kétdimenziós tömbök kezeléséhez szinte minden esetben két egymásba ágyazott ciklusra van szükség, olyanokra, mint amilyeneket a rendezéseknél is láthattál. A külső ciklus a sorszámot, a belső az oszlopszámot lépteti. Nézzük meg, hogy néz ez ki:
1
2
3
4
5
6
7
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
tomb[i][j] = (int)(Math.random()*10);
}
}
Ez a példa végigmegy a tömb összes elemén, és mindegyiket egy [0;9] intervallumból sorsolt számmal tölt fel. Láthatod, hogy van egy tömbelem kiválasztás (tomb[i][j]), ami az előzőleg ismertetett módon [sor][oszlop] választja ki az adott elemet. Mivel a későbbiekben nagy valószínűséggel mindig ugyanolyan nevű változókat használsz, ezért jó ha megjegyzed, hogy az i változóval jelölöd a sorokat, és j-vel az oszlopokat. Ez a későbbiekben fontos lesz, hogy tudd, melyik melyik.
A kiemelt sorban van igazából az érdekesség, ami elsőre furcsa lehet. A tömb i indexű elemének tömbmérete? Kétdimenziós tömbben a tömb deklarálása után az inicializáláskor meg kell határozni a tömb méretét. Így van ez az alap tömbök esetén is, és így van ez itt is. A különbség az, hogy itt külön kell beállítani a sorok és oszlopok számát. Először a sorok, utána az oszlopok számát. De akkor a tomb.length melyiket adja meg a kettő közül, és hogy kapjuk meg a másikat? Tisztázzunk akkor pár sarokpontot az ilyen tömbök kezelésével kapcsolatban
tomb.length; // sorok száma (a kis tömbök darabszáma)
tomb[1]; // az 1-es indexű sor elemei (2. sor tömbje)
tomb[i].length; // oszlopok száma (az i indexű tömbben lévő elemek száma)
tomb[3][2]; // tömbben tárolt elem, ami a 4. sor 3. oszlopában van
Az oszlopok számát miért egy i indexű sor méretéből kapjuk meg, miért nem fixen a 0 indexű sor méretéből? Azért, mert létezik egy speciális többdimenziós tömbtípus, melyet nagyon ritkán használunk, és ott eltérhet az egyes sorok (kis tömbök) mérete, így mindig az aktuális sor méretével dolgozzunk.
Kétdimenziós tömbök bejárása
az összes elem bejárása:
Ebben az esetben két ciklusra van szükség, amire már láttál példát a tömb feltöltésénél.
egy sor bejárása:
Ekkor elég csak egy konkrét soron végigmenni egyetlen ciklussal. Ebben a példában a 3. sor összes elemét írjuk ki egymás mellé. Ez a következőképp néz ki:
for( int j = 0; j < tomb[2].length; j++ )
{
System.out.print(tomb[2][j]+" ");
}
Ebben az esetben láthatjuk, hogy a tomb[2]-re hivatkozok fixen, több helyen is. Először a ciklus fejében, ahol a 2-es indexű (3.) sor elemeit akarom kiírni. Valamint a konkrét elem kiválasztásánál is látszik, hogy csak a 2-es indexű sor szerepel, de azon belül a j-vel végiglépkedek a sor összes elemén (oszlopán). Technikailag a j helyett itt i is lehetne ciklusváltozó, a program akkor is tökéletesen működne. Logikailag azért szoktam javasolni, hogy j legyen, mert akkor jobban rögzül, hogy a j az oszlopokat jelenti, és most csak az oszlop változik, a sor kötött.
egy oszlop bejárása:
Az előzőhöz hasonlóan itt is elég egyetlen ciklus, hiszen egyetlen oszlopon kell csak végigmenni. Ekkor az oszlop száma kötött és csak a sorszám változik. Ez így néz ki:
for( int i = 0; i < tomb.length; i++ )
{
System.out.println(tomb[i][4]);
}
Láthatod, hogy a ciklus fejében máshogy szerepel a futási feltétel, csak tomb.length szerepel, ami a sorok számát jelenti. A ciklusmagban pedig az adott elem kiválasztásakor a oszlopszám fix (jelen esetben a 4-es indexű 5. sor) és az sorszám az, ami változik, ezért használtam i ciklusváltozót.
Most már tetszőleges kétdimenziós tömböt be tudunk járni, jöhetnek az ezzel kapcsolatos feladatok. Az első feladat a tömb feltöltése, a többi feladatban pedig ezzel a tömbbel dolgoznánk.
Gyakorló feladatok
Tölts fel egy 3×5-ös kétdimenziós tömböt a [-10;30] intervallumból:
int[][] tomb = new int[3][5];
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
tomb[i][j] = (int)(Math.random()*41)-10;
}
}
Írd ki a tömböt sorokba és oszlopokba rendezve:
int[][] tomb = new int[3][5];
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
// egymás mellé írom ki egy sor elemeit
System.out.print(tomb[i][j]+" ");
}
// ha végeztem egy sor kiírásával, akkor új sort kezdek
System.out.println();
}
Írd ki a tömbben szereplő számok összegét:
int osszeg = 0;
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
osszeg = osszeg + tomb[i][j];
// vagy osszeg += tomb[i][j];
}
}
System.out.println("A tomb elemeinek osszege"+osszeg);
Írd ki a 2. sor összegét:
int osszeg = 0;
for( int j = 0; j < tomb[1].length; j++ )
{
osszeg = osszeg + tomb[1][j];
// vagy osszeg += tomb[1][j];
}
System.out.println("A 2. sor osszege"+osszeg);
Számold meg, hány negatív szám szerepel a tömbben:
int db = 0;
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
if( tomb[i][j] < 0 )
{
db++;
}
}
}
System.out.println("A tombben "+db+" negativ szam van.");
Számold meg, hány páros szám található a 3. oszlopban:
int db = 0;
for( int i = 0; i < tomb.length; i++ )
{
if( tomb[i][2] % 2 == 0 )
{
db++;
}
}
System.out.println("A tomb 3. oszlopaban "+db+" paros szam van.");
Írd ki, melyik a legkisebb elem a tömbben:Ez a feladat nem teljesen ugyanaz, mint amit a minimumkeresésnél láthattál. Arra remélem emlékszel, hogy a minimumnak a helyét, és nem az értékét tároljuk, mert a helyéből két dologra is válaszolhatunk, ezt most nem írnám le újra. De itt a hely nem egy index, hanem kettő: [oszlop][sor] Két dolgot tehetsz. Vagy két változót használsz a hely tárolására (egyet a sornak, egyet az oszlopnak), vagy egy két elemű tömbben tárolod, valahogy így:
int[] min = new int[2];
min[0] = sor;
min[1] = oszlop;
Vagy tárolhatod két változóban is:
int minI = sor;
int minJ = oszlop;
Rád bízom melyiket használod, a lényeg, hogy helyesen tedd. Lássunk akkor példát a bonyolultabbra:
int[] min = new int[2];
// ebben a két sorban állítom be, hogy az első elem az első minimum
min[0] = 0;
min[1] = 0;
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
// ha a tömb aktuális eleme kisebb, mint az eddigi minimum
// ahol a minimum elem sora min[0], oszlopa min[1]
if( tomb[i][j] < tomb[ min[0] ][ min[1] ] )
{
min[0] = i;
min[1] = j;
}
}
}
// na itt ne keverd össze a [ ] jeleket...
System.out.println("A tomb legkisebb eleme: "+tomb[min[0]][min[1]]);
Azért hasonlítsuk ezt össze azzal, ha két külön változóban tárolod a minimum elem sorát és oszlopát:
// ebben a két sorban állítom be, hogy az első elem az első minimum
int minI = 0;
int minJ = 0;
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
// ha a tömb aktuális eleme kisebb, mint az eddigi minimum
// ahol a minimum elem sora min[0], oszlopa min[1]
if( tomb[i][j] < tomb[minI][minJ] )
{
minI = i;
minJ = j;
}
}
}
// na itt ne keverd össze a [ ] jeleket...
System.out.println("A tomb legkisebb eleme: "+tomb[minI][minJ]);
Talán a két külön változó kicsit barátságosabb.
Ha igazán figyeltél az eddigiekben, kiszúrhattad, hogy más különbség is van a minimumkereséshez képest, azon kívül, hogy itt a minimum helyét értelemszerűen két számként tároljuk. Figyeld meg, honnan indulnak itt a ciklusok. Nem 1-től! A minimumkeresésnél emlékezhetsz, hogy az első (0 indexű) elem a legkisebb, ezért a ciklus 1-es indextől kezdődik, hogy önmagával már ne hasonlítsuk össze. Itt ezt nem tehetjük meg. Miért?
Ha a külső ciklusban az i változó 1-től indulna, akkor az első (0 indexű) sor teljesen kimaradna a vizsgálatból. Ha a belső ciklusban a j változó indulna 1-től, akkor pedig minden sor első eleme, vagyis a teljes első (0. indexű) oszlop maradna ki. Itt kénytelenek vagyunk az első minimumot önmagával is összehasonlítani, ami azért valljuk be, nem túl nagy veszteség. De ha nem így oldod meg, akkor súlyos hiba.
Háromdimenziós tömbök
Többdimenziós tömböket 3 dimenzió felett nem igazán használunk. A 3. dimenzióval még van értelme dolgozni, mondjuk térbeli koordináták, vagy képfeldolgozás esetén mondjuk egy RGB kód tárolása esetén. Ebben az esetben formailag így néz ki a tömbünk:
// háromdimenziós tömb deklarálása és inicializálása
int[][][] tomb = new int[4][5][2];
// háromdimenziós tömb adott elemének kiválasztása
tomb[1][2][1]
Itt a 3. dimenzió mondjuk mint egyfajta magasság értelmezhető térbeli pontok tárolása esetén. Színkódoknál pedig a 3 színkomponens értékét tárolhatjuk a tömbben. Ezekben az esetekben a tömb teljes bejárása értelemszerűen 3 ciklust jelent, de csak az első sorbeli magasságadatok bejárása is két ciklust igényel. Akkor van szükség egy ciklusra, ha a 3 dimenzióból 2 rögzített. Például az első sor második eleméhez tartozó pontok (a tomb[0][1] magasságoszlopa) bejárása esetén.
Fűrészfogas tömbök
Láthattad, hogy a kétdimenziós tömbök esetén az oszlopok száma minden esetben megegyezik. Ez azonban nem mindig van így. Megadható az is, hogy az egyes sorok változó (de megadásuk után fix) hosszúak legyenek. Ezt a szerkezetet fűrészfogas tömbnek is szokás nevezni. Ilyen szerkezetet nagyon speciális esetekben használunk, de a kezelése a fentiek alapján meglehetősen egyszerű. Lássuk hogyan deklaráljuk ezt:
// először csak a sorok számát adjuk meg
int[][] tomb = new int[3][];
// ezután használat előtt egyenként adjuk meg a sorok méreteit
tomb[0] = new int[5];
tomb[1] = new int[7];
tomb[2] = new int[3];
// töltsük fel a tömböt a [0;9] intervallumból
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
tomb[i][j] = (int)(Math.random()*10);
}
}
// írjuk ki a tömböt
for( int i = 0; i < tomb.length; i++ )
{
for( int j = 0; j < tomb[i].length; j++ )
{
System.out.print(tomb[i][j]+" ");
}
System.out.println();
}
Láthatod, hogy a tömb sorai nem egyforma hosszúak. Itt csak arra kell vigyázni, hogy direkt hozzáféréssel soha ne hivatkozz olyan indexű elemre, ami nem létezik. Nem értelmezhető például a 4. oszlop, mivel a 3. sorban csak 3 oszlop található. De a tömb bejárása, minimum/maximum keresés, sorösszeg, tömb összeg, megszámlálások, gond nélkül kivitelezhetők a fent kidolgozott példák alapján, de csak akkor, ha ezek nem egy oszlopra korlátozódnak. Csak akkor kell nagyon figyelni, ha csak oszlopban akarunk mozogni, mert tisztázni kell előre, létezik-e teljesen az adott oszlop, vagy valamelyik sorban lyukas. Ez a fűrészfogas szerkezet 3 és több dimenzióra is létrehozható, de azzal már szinte csak elméleti síkon kell számolni.
ADAT BEKÉRÉSE
Ahogy előre haladunk a programozás tanulásában egyre inkább felmerül az az igény, hogy ne csak előre megadott adatokkal dolgozzon a program, hanem menet közben mi is adhassunk neki munkát. Vagy egyszerűen csak befolyásoljuk a program működését. Az is szempont lehet, hogy adatbekéréssel tesztelésre szolgáló bemeneti értékekkel kideríthessük az esetleges szemantikai hibákat.
Az adatbekérést többféle módon is megoldhatjuk, én maradok az egyik egyszerű, direkt erre a feladattípusra készített osztály, a Scanner használatával. Látni fogjuk, hogy ez rugalmasan használható többféle típusú adat bevitelére is, és a használata meglehetősen egyszerű.
Mivel ez a Scanner egy előre megírt osztály, a program elkészítésének első lépése importálni azt a készítendő kódunkba, még a program osztályainak megadása előtt: import java.util.Scanner;
Ha ez megvan, ettől a ponttól kezdve deklarálhatunk Scanner típusú változót, és létrehozhatunk belőle egy Scanner objektumot, ami az adatbekérésben segítségünkre lesz. Nézzünk akkor egy példakódot, amiben lépésenként elmagyarázom, hogy melyik kiemelt sornak mi a szerepe.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.Scanner;
public class Adatbekeres
{
public static void main( String[] args )
{
Scanner sc;
sc = new Scanner(System.in);
int szam;
szam = sc.nextInt();
sc.close();
System.out.println("A bekert szam ketszerese: "+szam*2);
}
}
A lényegi részeket kiemelve nézzük meg a program részeit:
A kód elején a Scanner osztály importálásával kezdünk, mert enélkül nem tudjuk használni.
Ha már használhatjuk, akkor létre kell hoznunk egy Scanner objektumot, ami majd az adatbekérést fogja végrehajtani. Ez történik meg a 7. sorban.
A létrehozott Scanner objektumunkat egy sc nevű változóban fogjuk tárolni, így bármikor egyszerűen elérhetjük. Természetesen más nevet is megadhatunk neki, de lustaságból én nem adok meg hosszabb nevet, minek annyit gépelni 🙂
A Scanner osztálynak meghívjuk a konstruktorát, és odaadjuk neki a System.in bemenetet, ami alapértelmezetten a billentyűzet. Ettől kezdve az objektumunk a billentyűzetről fogja beolvasni az általunk megadott adatokat.
A Scanner objektumnak meghívjuk a nextInt() metódusát, amely a begépelt és Enter billentyűvel lezárt adatbevitel esetén a begépelt számot azonnal eltárolja egy int típusú változóba.
Lezárjuk a Scanner-t, miután már nincs rá szükségünk.
Scanner-rel többféle típusú adatot is bekérhetünk. Ez csak a használt metódustól függ. Példaként álljon itt néhány úgy, hogy előtte szerepeljen a változó is, amiben eltároljuk a beírt adatot:
String s = sc.nextLine();
float f = sc.nextFloat();
double d = sc.nextDouble();
byte b = sc.nextByte();
long l = sc.nextLong();
A Scanner használható arra is, hogy egy adatbekérésnél egy adott határoló mintát megadva darabolva kérjünk be valamit, mondjuk neveket szóközzel elválasztva. Erre azonban majd a Stringek split() metódusát fogjuk használni, így erre külön most nem térnék ki.
STRING BUILDER OSZTÁLY
String megváltoztathatatlan. Ez tény. Ha mégis módosítjuk, akkor egy új Stringet hozunk létre, az előzőt meg magára hagyjuk. Ez 1-2 lépés esetén nem túl nagy gond, de amikor egy Stringet sokszor kell megváltoztatni, vagy sok kis darabból kell összerakni, akkor ez a módszer rendkívül lassú, pazarló, és erősen kerülendő. Ha sokszor változtatni akarjuk, akkor másra van szükségünk.
StringBuilder
A StringBuilder osztályt kifejezetten azért írták, hogy segítségével a Stringek módosíthatóak legyenek. Ez egy módosítható karakterlánc. Rendkívül hatékonyan tudjuk bővíteni, módosítani a benne lévő tartalmat, amelyből bármikor újra statikus Stringet készíthetünk.
A StringBuilder osztály használatához semmilyen speciális csomagot, osztályt nem kell importálni. Nézzük meg akkor pár példán keresztül, mi mindenre használhatjuk.
StringBuilder deklaráció és értékadás
StringBuilder deklarációja formailag így néz ki:
StringBuilder sb;
A StringBuilder-nek a Stringhez hasonlóan sokféleképp adható érték:
// literálként adjuk meg a tartalmát
StringBuilder sb = new StringBuilder("abrakadabra");
// egy Stringet kap paraméterként
String s = "abrakadabra";
StringBuilder sb = new StringBuilder(s);
StringBuilder sb = new StringBuilder(); // üres is lehet
StringBuilder metódusok
Mint már említettem, a StringBuilder arra szolgál, hogy karakterláncokat kezeljünk, megváltoztassunk. A StringBuilder valamennyire hasonlóságot mutat a Stringekkel, de ez csak pár metódusban nyilvánul meg. Ezek olyan metódusok, melyek nem módosítják a StringBuilder-t.
StringBuilder hossza – length()
Bármely StringBuilder méretét (hosszát) megkaphatjuk, ha meghívjuk a length() metódusát:
StringBuilder sb = new StringBuilder("abrakadabra");
System.out.println( sb.length() );
StringBuilder adott karaktere – charAt()
Egy adott StringBuilder bármelyik karakterét megkaphatjuk a charAt(i) metódussal, ahova az i helyére írjuk be, hogy hányadik karaktert szeretnénk megkapni. A karakterek indexelése a tömbökhöz hasonlóan 0-val kezdődik. Fontos, hogy ez egy karakter típust ad vissza! Bármely StringBuilder első karaktere az sb.charAt(0), az utolsó pedig az sb.charAt( sb.length()-1 )
StringBuilder sb = new StringBuilder("abrakadabra");
sb.charAt(3); // a 4. karakter (3-as index!)
sb.charAt(0); // 1. (üres StringBuilder-nél indexelési hiba!)
sb.charAt( sb.length()-1 ); // utolsó karakter
Keresés StringBuilder-ben – indexOf(), lastIndexOf()
Egyszerűen kereshetünk a StringBuilder-ekben. Kíváncsiak vagyunk, hogy egy karakter vagy szövegrészlet megtalálható-e benne, sőt arra is, hogy hol található. Erre szolgál az s.indexOf() metódus.
StringBuilder sb = new StringBuilder("abrakadabra");
System.out.println( sb.indexOf("rak") ); // 2
// A 2. indexű (3. karakternél) található a rak szócska.
System.out.println( sb.indexOf("br") ); // 1
/* Az 1. indexű (2. karakternél) található a rak szócska
* Fontos, hogy az indexOf() mindig az első találat helyét adja meg!
*/
System.out.println( sb.IndexOf("br") > -1 ); // true
/* Itt nem a keresett szöveg helye érdekel, hanem az, hogy benne
* van-e. Ha a helye -1-től nagyobb, akkor benne van, de nem érdekel,
* hogy pontosan hol.
*/
System.out.println( sb.indexOf("Br") ); // -1
/* Egy nem létező indexet adott eredményül, vagyis a keresett
* részlet nem található meg a Stringben.
*/
System.out.println( sb.lastIndexOf("br") ); // 8
/* A 8. indexű (9. karakternél) található a br szócska, de most a
* keresést hátulról kezdte, és onnan adja vissza az első találatot!
*/
Az indexOf() és lastIndexOf() metódusok alaphelyzetben mindig a StringBuilder elejéről/végéről kezdik a keresést, de meg lehet adni nekik, hogy adott karaktertől kezdjék: indexOf(mit, honnan) Ehhez kapcsolódó feladat lehet, hogy adjuk meg, hol található a második ‘r’ betű a szóban:
StringBuilder sb = new StringBuilder("abrakadabra");
int elso = sb.indexOf("r");
System.out.println( sb.indexOf("r", elso+1 ) );
/* Először megkeressük az első 'r' betűt, majd amikor a másodikat
* akarjuk megkeresni, akkor megadjuk, hogy az első utáni pozíciótól
* induljunk. Ezt a két lépést akár össze is vonhatjuk:
*/
System.out.println( sb.indexOf("r", sb.indexOf("r")+1 ) );
System.out.println( sb.lastIndexOf("r", sb.lastIndexOf("r")-1 ) );
/* Ha ugyanezt hátulról végezzük, akkor figyelni kell arra, hogy
* az első találat előtt kell folytatni, vagyis itt -1
* kell az első találat helyéhez képest, mivel visszafelé keresünk
*/
StringBuilder részének kinyerése – substring()
Előfordulhat, hogy egy StringBuilder-ből ki kell szednünk egy kisebb részletet. Erre szolgál a substring() metódus. Amikor egy részt akarunk kinyerni egy StringBuilder-ből, akkor meg kell mondanunk, hogy milyen karakter határokhoz (indexek) viszonyítva akarom ezt megkapni. Melyiktől kezdjük, és melyik előtt fejezzük be. Ha csak a kezdő pozíciót adjuk meg, akkor onnantól a StringBuilder végéig az egészet megkapjuk. A substring() mindig String típusú eredményt ad vissza.
StringBuilder sb = new StringBuilder("abrakadabra");
System.out.println( sb.substring(0,5) ); // abrak
System.out.println( sb.substring(2,5) ); // rak
System.out.println( sb.substring(5,8) ); // ada
System.out.println( sb.substring(6) ); // dabra
System.out.println( sb.substring(sb.length()) ); // mindig üres
Ezek a metódusok nagyon ismerősek lehetnek, feltéve, ha olvastad a String témakört. Megmondom őszintén még a magyarázatokat is szinte egy az egyben a onnan vettem át, mert eddig a pontig a két osztály nagyon hasonló.
Jöjjenek akkor azok a metódusok, melyek a StringBuilder igazi erejét adják. Azok, melyek a StringBuilder tartalmát megváltoztatják. Nagyon fontos, hogy ezek valóban az eredeti tartalmat módosítják, onnantól, ami előzőleg volt benne, már nem kaphatjuk vissza.
Hozzáfűzés a StringBuilder végéhez – append()
StringBuilder sb = new StringBuilder(); // üres StringBuilder
sb.append(1);
sb.append(2.0);
sb.append(2.0f);
sb.append(1L);
sb.append(true);
sb.append('c');
sb.append("Bela");
sb.append(sb);
sb.append("abcd".toCharArray());
System.out.println(sb);
A StringBuilder-t az .append() metódussal lehet bővíteni, ezzel tudunk hozzáfűzni a végéhez bármit. A bármit szinte tényleg bármit, mert hozzáfűznivalójuk az összes primitív típust. Igen, boolean-t is, karaktert is! Ezen kívül Stringet, StringBuilder-t, karaktertömböt is hozzáfűzhetünk. Amikor egy Stringet menet közben kell felépíteni, akkor StringBuilder-t használunk, mert ez a legtakarékosabb, és leggyorsabb megoldás. Az egy dolog, hogy ennek a példának ebben a formában nem sok értelme van, pusztán azt akartam bemutatni, hogy tényleg minden hozzáfűzhető. A lényeg tehát:
A StringBuilder-t szinte bármivel bővítheted az .append() metódussal.
Láthattad, hogy az eredményt közvetlenül ki lehet íratni. De amikor az összefűzött eredményt Stringként szeretnéd tovább használni, akkor a már ismerős toString() metódusra van szükséged:
StringBuilder sb = new StringBuilder(); // üres StringBuilder
sb.append(1);
sb.append(true);
sb.append("Bela");
String s = sb.toString(); // "1trueBela"
Beszúrás StringBuilder-be – insert()
A StringBuilder bővítése nem csak annyit jelent, hogy hozzáfűzünk valamit a végéhez, hanem lehetőségünk van arra, hogy tetszőleges helyre illesszünk be dolgokat. A beszúrás természetesen azt jelenti, hogy ha valahova beszúrunk, akkor a beszúrás pontja utáni dolgok hátrébb tolódnak, de a legfontosabb az, hogy ezzel nem nekünk kell foglalkozni.
StringBuilder sb = new StringBuilder("abrakadabra");
System.out.println( sb );
sb.insert(0,"ABR"); // beszúrás az elejére
System.out.println( sb );
sb.insert(1,"B"); // beszúrás adott helyre
System.out.println( sb );
sb.insert( 5, "ZABRA");
System.out.println( sb );
sb.insert( sb.length(), "A"); // beszúrás a végére
System.out.println( sb );
Beszúrni egyébként az append() metódushoz hasonlóan szinte bármit lehet. Amiket az append()-del hozzáfűzhetünk a StringBuilder-hez, azt az insert()-tel be is szúrhatjuk bárhova. A különbség annyi, hogy az insert() esetén először a beszúrás helyét kell megadni. Az is nyilvánvaló, hogy az append() voltaképp az insert() egy speciális esete:
StringBuilder sb = new StringBuilder("abrakadabra");
sb.insert( sb.length(), "A"); // beszúrás a végére
// ugyanez
StringBuilder sb = new StringBuilder("abrakadabra");
sb.append("A");
StringBuilder egy részének törlése – delete()
Előfordulhat, hogy egy StringBuilder-ből valamilyen részt egyszerűen ki kell törölni.
StringBuilder sb = new StringBuilder("Kiss Bela Jozsef");
sb.delete( 5, 10 );
System.out.println( sb );
sb = new StringBuilder("Kiss Bela Jozsef");
sb.delete( 9, sb.length() );
System.out.println( sb );
A törléskor meg kell adni, hogy melyik karaktertől kezdődően törlünk, valamint meg kell adni, hogy melyik karakter előtt fejezzük be. Ha egy StringBuilder végéről akarunk törölni, akkor a második példa alapján oldhatjuk meg. Megadjuk, hogy honnan kezdjük, és megadjuk, hogy a StringBuilder hossza előtt (vagyis az utolsó karakterrel bezárólag) fejezzük be a törlést.
StringBuilder adott karakterének törlése – deleteCharAt()
Az előző metódushoz hasonlóan ez is töröl a StringBuilder tartalmából, de ez csak egy adott helyen lévő karaktert. Természetesen ez úgy töröl, hogy a mögötte lévő karakterek eggyel előrébb lépnek a sorban.
StringBuilder sb = new StringBuilder("abrakadabra");
sb.deleteCharAt( 0 ); // első karakter törlése
sb.deleteCharAt( 4 ); // 4-es indexű karakter törlése
sb.deleteCharAt( 4 ); // az új 4-es indexű karakter törlése
sb.deleteCharAt( sb.length()-1 ); // utolsó karakter törlése
Ha több egymás melletti karaktert szeretnénk törölni, akkor célszerűbb a delete() metódust használni, ahol megadhatjuk a törlendő karakterek intervallumát. De ha a két első karaktert szeretnénk törölni, akár a következő módszert is használhatjuk:
StringBuilder sb = new StringBuilder("abrakadabra");
sb.deleteCharAt( 0 ); // első karakter törlése
sb.deleteCharAt( 0 ); // az eredetileg második karakter törlése
// helyette ez is szerepelhet
sb.delete( 0, 2 ); // az első két karakter törlése
StringBuilder adott karakterének megváltoztatása – setCharAt()
Amikor egy StringBuilder-ben valamit meg akarunk változtatni, akkor ezt akár karakterenként is megtehetjük. Ez a metódus valahol a charAt() párja, de amíg a charAt() csak visszaadja a StringBuilder adott karakterét, addig a setCharAt() metódussal egy adott indexű karaktert tudunk megváltoztatni valami másra. Az indexnek nyilván valósnak kell lenni, és itt is használhatók azok a sablonok, amelyeket a charAt() esetén megismerhettél. A metódusnak meg kell adni a cserélni kívánt karakter indexét, és azt, hogy mire akarod azt kicserélni. Fontos, hogy csak karakter típusra cserélhetsz!
StringBuilder sb = new StringBuilder("abrakadabra");
sb.setCharAt( 0, 'A' ); // első karakter megváltoztatása
sb.setCharAt( 4, 'C' ); // 4-es indexű karakter megváltoztatása
sb.setCharAt( 4 'G' ); // az új 4-es indexű karakter megváltoztatása
sb.setCharAt( sb.length()-1, 'A' ); // utolsó karakter megváltoztatása
StringBuilder adott részének kicserélése – replace()
Mindenek előtt arra hívnám fel a figyelmet, hogy ez a metódus csak nevében hasonlít a String osztály replace() metódusára, teljesen máshogy működik! Itt egy karakter intervallumot kell megadni, ami azt jelenti, hogy mettől-meddig akarod a StringBuilder adott részét kicserélni valamilyen Stringre. Az intervallum megadása ugyanúgy történik, mint a substring() vagy delete() metódusoknál, vagyis megadott, hogy melyik karaktertől kezdődően és melyik karakter előttig tartson az a rész, amit kicserélsz a 3. paraméterként megadott Stringre. Lássunk akkor példákat:
StringBuilder sb = new StringBuilder("Kiss Janos Jozsef");
System.out.println( sb );
sb.replace(0, 4, "Nagy");
System.out.println( sb ); // Nagy Janos Jozsef
sb.replace(0, 4, "Kovacs");
System.out.println( sb ); // Kovacs Janos Jozsef
sb.replace(7, 12, "Pal"); // Kovacs Pal Jozsef
System.out.println( sb );
A példaprogramban odaírtam az eredményeket, de ettől függetlenül néhány dolgot kiemelnék:
Nem kell azzal foglalkoznod, hogy amit kicserélsz ugyanolyan hosszú legyen, mint amire kicseréled.
Ha egy rövidebb részt hosszabbra cserélsz (lásd 2. csere), akkor a hosszabb új rész odébb tolja az utána lévőket: Nagy -> Kovacs
Ha hosszabb részt cserélsz rövidebbre (lásd 3. csere), akkor a rövidebb új rész előrébb húzza a mögötte lévőket: Janos -> Pal
StringBuilder megfordítása – reverse()
Nem egy feladatban előfordul az, hogy egy String tartalmát meg kell fordítani.
Stringekkel ez a feladat a következőképpen néz ki, feltéve, hogy ismerjük a Stringeket:
String s1 = "abrakadabra";
String s2 = "";
for( int i = s1.length()-1; i > -1; i-- )
{
s2 = s2.concat(s1.charAt(i)+"");
}
System.out.println(s2);
Vagy esetleg így:
String s1 = "abrakadabra";
String s2 = "";
for( int i = 0; i< s1.length(); i++ )
{
s2 = s2.concat(s1.charAt(s1.length()-i-1)+"");
}
System.out.println(s2);
A lényeg az, hogy ez egy elég érdekes feladat. Persze a concat() nélkül is megcsinálhatod a += operátorral, de megbeszéltük, hogy akkor új String objektumok jönnek létre, melyeket utána magára fog hagyni a rendszer, és ha hosszú a String akkor még lassú is lesz:
De hogy néz ez ki StringBuilder-rel.
String s = "abrakadabra";
StringBuilder sb = new StringBuilder(s);
sb.reverse();
System.out.println( sb );
A .reverse() metódus bármilyen StringBuilder tartalmát megfordítja és utána azt úgy használjuk, ahogy akarjuk. Kicsit egyszerűbb, nem?
StringBuilder méretének beállítása – setLength()
A StringBuilder mérete akár közvetlenül is beállítható a setLength() metódussal. A metódus egyetlen paramétere egy nem negatív szám, mely azt jelenti, hogy mekkora méretűre szeretnénk beállítani a StringBuilder méretét.
Ha a mérete kisebb, mint a tartalom, ami jelenleg benne található, akkor a StringBuilder-ben lávő tartalom csonkolódik, vagyis a méreten felüli részek törlődnek.
Ha a megadott méret nagyobb, mint az eddigi, akkor az úgynevezett null karakterrel tölti ki a tartalommal nem rendelkező új részt a megadott méretig.
A méret beállítása nem jelenti azt, hogy a StringBuilder nem bővíthető, ezután is azt csinálunk vele, amit akarunk. Ha azonban a méret bővítés során null karakterek kerültek a végére, akkor ha mondjuk append()-del bővítjük, akkor minden a null karakterek után kerül a StringBuilder végére.
Egy szó, mint száz, láthatod, hogy a StringBuilder kifejezetten arra való, hogy a Stringeket manipuláljuk, megváltoztassuk, vagy akár csak több lépésben felépítsük. Bizonyos manipulációkat sokkal-sokkal hatékonyabban meg tudunk oldani vele, mint Stringekkel. Elég ha csak a beszúrásra, és törlésre gondolunk, melyeket Stringekkel megvalósítani nemcsak bonyolultabb, hanem jóval lassabb is.
Természetesen a StringBuilder-nél is azt az elvet vallom, mint amit a tömböket helyettesítő listákkal kapcsolatban szoktam mondani:
Csak akkor használd, ha már a Stringeket nagyon jól ismered, és használod, mert csak akkor fogod megérteni a StringBuilder igazi erejét, és akkor fogod tudni, mikor kell a Stringek helyett használni.
ArrayList
A tömbök korlátjai
Szerintem mindenki emlékszik arra a pillanatra, amikor megismerte a tömböket. Vagy szerelem volt első látásra, vagy ekkor esett először komolyan kétségbe. De ha túltette magát az első sokkon, akkor rájött, hogy nem is olyan bonyolultak. A tömböket nagyon szeretjük. Nagyon sok és sokfajta adatot képesek tárolni. Ezek lehetnek primitív, vagy referencia típusok is. Mi több, az elemeknek sorrendje van. A nagyon sok adatból bármikor kivehetünk egyet. Akár megvizsgálhatjuk az összeset, szigorúan sorban haladva. Akkor mi a gond vele?
A mérete
A bővítése
Keresés az elemei között
Sorolhatnám még, de bevezetésnek ennyi pont elég.
Az első problémával már biztosan találkoztál. A tömbnek elsőre jó méretet kell választani. Miért? Mert a mérete fix. Ez egy nagyon komoly döntés. Ez nem egy hajvágás. Az kinő újra. De egy tömb méretét megváltoztatni… Aztán rájössz, hogy nagyobb kell, akkor készíthetsz másikat, és abba átpakolhatod az eredeti értékeket, meg azokat, amik nem fértek el. És ha az is kicsi lesz? Vagy elsőre kiszámolhatod, hogy mekkorára van szükség, és létrehozod amekkora kell. És ha valami nem várt esemény miatt mégiscsak kicsi? Vagy épp túl nagy?
A második gond elsőre hasonlíthat az elsőhöz, valójában teljesen más. Itt bővítés alatt nem feltétlenül arra gondolok, hogy a tömb kicsi. Tételezzük fel, hogy van egy 100 elemű tömböd. Okosan ekkorát hoztál létre, mert tudtad, hogy ennél több elemet soha nem kell tárolnod. De csak 65-öt tettél bele. Akkor is felkészültél mindenre, mert az új elemeket bármikor odarakhatod a tömb végére. Az már csak apróság, hogy ha nincs tele a tömb, akkor a méretét megadó tömb.length értelmét vesztette. Neked kell külön nyilvántartanod és folyamatosan frissítened, hogy mennyi valódi elem van benne. Ráadásul olyan ügyes vagy, hogy így még ki is vehetsz elemet a tömb végéről (pontosítok, nullázod az ottani elemet), és ekkor csökkentheted a valódi elemszámot tároló változót. Profi. Hozzáadhatsz és el is vehetsz belőle. Az is apró szépséghiba, hogy a tömb végén a nem valódi elemek ugyanakkora memóriát foglalnak, mint az elején lévő valódiak. Képzeljük el, hogy gyerekek neveit tárolod annak megfelelően, hogy a tornasorban hol állnak. Érkezett egy új gyerek. Hova állítod? A sor végére? Elég ritka eset. De a tömbbe nem lehet csak úgy akárhova beszúrni egy elemet. A többit odébb kell pakolni. Neked. És ha távozik egy gyerek? Az sem feltétlenül a sor végéről fog eltűnni. És a többi üresen hagyja a helyét? Vagy pakoljunk mindenkit eggyel előrébb, aki utána állt?
A harmadik probléma akkor jött elő, amikor meg akartuk tudni, hogy egy tömbben benne van-e egy elem, akkor meg kellett keresni. Ha ügyesek voltunk, és lehetőségünk volt rá, akkor valamilyen rendezett tömbbel dolgoztunk. Abban lehet, hogy nem lineárisan, minden elemet megvizsgálva kell keresni. De milyen jó lenne, ha a kereséssel nem nekünk kell foglalkozni, hanem azonnal választ kaphatnánk arra, hogy benne van-e a keresett elem, vagy nem.
A tömbök buták. Szeretjük őket, de buták. A tanulmányaink elején muszáj megismernünk őket. Rajtuk keresztül tanulunk meg programozni. És minél jobban megtanulunk, annál jobban megismerjük a korlátait. Felismerjük azt, hogy amit mi eddig félistenként tiszteltünk, mert mindent meg tudtunk oldani vele (igaz, néha körülményesen), valójában inkább spanyolcsizma. Szűk, rugalmatlan, és ha sokat akarunk ugrálni, akkor nagyon szúr.
Ismerjük meg azt, ami minden gondunkat megoldja.
ArrayList
Ha nagyon sarkosan szeretnénk fogalmazni, mondhatnánk, hogy az ArrayList egy változtatható méretű tömb. Sőt, a méretével egyáltalán nem kell foglalkoznunk, ha nem akarunk. Megkérdezni azért szabad.
Az ArrayList valójában egy osztály, ami a motorháztető alatt szintén egy tömbbel dolgozik. De nem most mondtam, hogy a tömb mérete fix? És ha változtatni kell a tömb méretén? Akkor létrehoz egy újat és azzal dolgozik. Az ArrayList osztály tele van pakolva olyan hasznos metódusokkal, amelyek az összes előzőleg felsorolt problémát nemcsak hogy megoldják, hanem még többre is képesek. Oké, így picit becsapva érezheted magad, hiszen mégis csak tömböt használsz. Csak nem Te. És ez sok gondtól megkímél.
Lássuk akkor, hogyan használhatjuk az ArrayList-et, és mi mindenre jó. Tételezzük fel, szükségünk van egy olyan listára, mely egész számokat tárol.
Ahhoz, hogy létrehozhassunk egyet, importálni kell azt a kódot, ahol ő található, az ArrayList osztályt:
import java.util.ArrayList;
public class Lista
{
public static void main(String[] args )
{
ArrayList<Integer> szamok = new ArrayList<>();
}
}
Az első kiemelt sor mutatja az osztály importálását, ilyet már láthattál a Scanner esetén. A második egy változó deklarálás és egy példányosítás. Akkor most dekódoljuk, hogy mit is látunk:
Megadunk egy ArrayList osztályú változót.
Rögzítjük, hogy ebben Integer osztályú objektumokat szeretnénk tárolni.
A változó neve: szamok
Egy új listát hozunk létre, ahol a típust már nem kell újra megadni.
És nem adunk meg semmit sem a konstruktorának.
Miért nem <int> szerepel a típusmegadásnál, ahogy a tömböknél láttuk? Mi ez az Integer? Nagy betűvel kezdődik, akkor ez egy osztály?
Igen. Ez egy burkoló vagy csomagoló osztály. Arra való, hogy becsomagolja magába a primitív értéket, így olyan helyen is használhatjuk azokat, ahol csak Objektummal állnak szóba.
Azért van erre szükség, mert a lista csak és kizárólag referencia típusú adatokat képes tárolni, primitív típusokat nem rakhatunk bele. Ha mégis azokat szeretnénk tárolni, akkor a primitív típusok megfelelő csomagoló osztályát kell használnunk típusként:
int helyett Integer
double helyett Double
char helyett Character
boolean helyett Boolean
(valamint a többi egész és valós típus, azonos névvel)
Vegyünk egy tömb témakörrel kapcsolatos komplex feladatot, de most új barátunkat használjuk. Sorsoljunk ki 20 egész számot a [-10;40] intervallumból és tároljuk el őket. A kiemelt sorokat a példa után megmagyarázom, ezek tartalmazzák a gyakran használt ArrayList metódusokat és a lényegi részeket, melyek a lista általános használatához szükségesek.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import java.util.ArrayList;
public class Lista
{
public static void main(String[] args )
{
ArrayList<Integer> szamok = new ArrayList<>();
// töltsük fel a listát
for( int i = 0; i < 20; i++ )
{
szamok.add( (int)(Math.random() * 51) - 10 );
}
System.out.println("A lista mérete: " + szamok.size());
// írjuk ki az elemeit
for( int i = 0; i < szamok.size(); i++ )
{
System.out.print( szamok.get(i)+" " );
}
System.out.println();
// töröljünk ki a lista legkisebb elemét
// ha több legkisebb van, akkor az elsőt
int min = 0;
for( int i = 0; i < szamok.size(); i++ )
{
if( szamok.get(i) < szamok.get(min) )
{
min = i;
}
}
System.out.println("A legkisebb eleme: " + szamok.get(min));
szamok.remove(min);
System.out.println("A lista merete: " + szamok.size());
// a lista elemei
for (Integer i : szamok)
{
System.out.print(i+" ");
}
System.out.println();
// szúrjunk be egy véletlen elemet a lista elejére
szamok.add( 0, (int)(Math.random() * 51) - 10 );
for (Integer i : szamok)
{
System.out.print(i + " ");
}
System.out.println();
// nézzük meg, benne van-e az intervallum legnagyobb
// eleme a listában, és ha igen, hol?
int hely = szamok.indexOf(40);
if( hely > -1 )
{
System.out.println("A 40-es elem helye: " + hely);
}
else
{
System.out.println("Nincs 40-es elem a listában.");
}
// vizsgáljuk meg, van-e 0 érték a listában
if( szamok.contains(0) )
{
System.out.println("A lista tartalmaz 0-at.");
}
else
{
System.out.println("A lista NEM tartalmaz 0-at.");
}
// rendezzük a listában szereplő számokat növekvő sorrendbe
int csere;
for (int i = 0; i < szamok.size() - 1; i++)
{
for (int j = i + 1; j < szamok.size(); j++)
{
if( szamok.get(i) > szamok.get(j) )
{
csere = szamok.get(i);
szamok.set(i, szamok.get(j));
szamok.set(j, csere);
}
}
}
// írjuk ki a rendezett számokat
System.out.println("Rendezett sorrend:");
for (Integer i : szamok)
{
System.out.print(i + " ");
}
System.out.println();
// töröljük ki a negatív elemeket a rendezett listából
for( int i = 0; i < szamok.size(); i++ )
{
if( szamok.get(i) > -1 )
{
szamok.removeAll(szamok.subList(0, i));
break;
}
}
// írjuk ki a listában maradt elemeket
for (Integer i : szamok)
{
System.out.print( i +" ");
}
}
}
Akkor lássuk a feladat megoldását részenként, melyen keresztül az ArrayList működését is megértjük. A felsorolás elején lévő számok a kiemelt sorokat jelentik.
8 – Figyeld meg, hogy nem hivatkozok a lista méretére a feltöltésekor, mivel a mérete alaphelyzetben 0. A for ciklusban a futási feltételben számként adom meg, hogy 20x fusson le a ciklus, vagyis 20 elemet fogok eltárolni a listában. Minden elem hozzáadás után a lista mérete eggyel nő.
10 – Itt láthatod, hogyan adunk hozzá egy elemet a listához, ami mindig a lista végére kerül.
12 – A lista méretét a .size() metódussal kaphatod meg.
15 – A .size() már szerepelt, de most már a lista bejárásához használom egy for ciklus futási feltételében.
17 – A lista bármelyik eleme indexelhető, hasonlóan a tömbökhöz, csak itt a hivatkozáshoz a .get(index) metódust használjuk, és nem a tömböknél tanult tomb[index] szerkezetet.
32 – Bármilyen elemet eltávolíthatok az indexe alapján a .remove(index) metódussal. Az utána elhelyezkedő elemek eggyel balra tolódnak és a lista mérete eggyel csökken.
36-39 – Foreach ciklus használható az elemek eléréséhez, például kiíratás esetén. Ha csak az elemek számítanak és az indexük nem, akkor a foreach ciklus mindig használható a for helyett.
43 – Az add(index, elem) metódussal a lista tetszőleges helyére beszúrhatunk egy elemet. Ha nem a lista végére szúrunk be elemet, akkor a beszúrás helyén lévő és a mögötte állók eggyel jobbra tolódnak, vagyis valódi beszúrásról beszélünk, nem cseréről!
53 – Az String kezelésből már ismert .indexOf(elem) metódussal megkaphatjuk egy adott elem helyét a listában. Ha az eredmény -1, akkor nincs a listában. Az indexOf() mindig a lista elejéről indítja a keresést, és több előfordulás esetén az első találat helyét adja meg. A lastIndexOf(), hasonlóan a String témakörben tanulthoz hátulról adja meg az első előfordulás helyét, és -1-et ha nincs találat.
64 – A .contains(elem) logikai választ (boolean) ad arra a kérdésre, hogy az adott elem benne van-e a listában.
82-83 – A set(index, elem) metódus az index helyen lévő elemet cseréli fel az általunk megadottra. Ilyenkor a mögötte álló elemek a helyükön maradnak, vagyis nem beszúrás történik. Jellemzően az elemek felcserélésekor használjuk, hiszen az elemek eltávolítása és hozzáadása nem így történik.
102 – Ez egy komplexebb példa. Egy listából ki lehet törölni egy másik lista elemeit. Jelen esetben a .subList(int start, int end) metódust használom. A for ciklusban megnézem, hogy a rendezett tömbben hol található az első nem negatív elem. Ennek a helye i lesz. A szamok.subList(0, i) azt jelenti, hogy a 0 indextől az i előtti indexig tartó elemeket kiemelem a listából, majd ezt a kapott listát odaadom a removeAll metódusnak, hogy ezeket törölje a szamok listából.
Ezek a példák lefedik az ArrayList témakör nagy részét. Persze vannak még finomságok benne, de úgy gondolom indulásnak ennyi pont elég. Egy fontos dolgot viszont megemlítenék:
Az ArrayList is túlindexelhető! Nem hivatkozhatsz olyan indexű elemre, ami nem létezik!
ArrayList, de nem minden áron
A helyzet az, hogy nem minden esetben éri meg az ArrayList-et használni. Tény, hogy rengeteg mindent tud, de a tömböket nem válthatja ki teljes mértékben. Tisztázzunk akkor pár irányelvet, melyet figyelembe kell venni, hogy ha választanod kell a tömb és az ArrayList között.
Tömb:
Ha a tanulmányaid elején jársz.
Ha előre tudod, hány elemet szeretnél tárolni, és nem akarod bővíteni a számukat.
Ha csak primitív értékeket tárolsz.
Ha az alap algoritmusokat még nem alkalmazod hibátlanul.
ArrayList:
Ha már az alap algoritmusokat tetszőleges feladatokban hibátlanul alkalmazni tudod.
Ha objektumokkal dolgozol.
Ha a tárolt elemeid száma változhat.
Ha a tömbök már inkább korlátoznak, mint segítenek.
Diamond operátor
A 7-es verziójú Java-tól kezdődően bevezették az úgynevezett diamond operátort. Ez valójában nem operátor, de hivatalos Java oldalon is így nevezik, valamint rengeteg hivatkozás is ilyen névvel illeti. Arról van szó, hogy a lista deklarálása után az inicializáláskor nem kötelező a típust megadni, a szerkezetből elhagyható. A 6. sorban lévő eredetileg ismertetett deklarálást és inicializálást rövidítheted a 7. sorban látható módon. A diamond talán a típuselhagyás után ottmaradó <> jelek alakjára utal. Azért mutattam meg ezt a dolgot, mert újabb kódokban már nem találkozhatsz ilyennel, de régiekben még a megjegyzésben szerepló forma is előfordulhat. Nem hiba, csak már felesleges ismét kiírni a típust.
1
2
3
4
5
6
7
8
9
import java.util.ArrayList;
public class Lista
{
public static void main(String[] args )
{
// ArrayList<Integer> szamok = new ArrayList<Integer>();
ArrayList<Integer> szamok = new ArrayList<>();
}
}
FÁJLKEZELÉS
A középiskolai programozás során szinte minden esetben parancssoros felületen keresztül dolgozunk, ott tekintjük meg a kimeneteket, és az esetleges interakciókat (gépeld be a neved, adj meg egy számot, stb) és azon keresztül bonyolítjuk.
Tanulmányaink során egy nagyobb ugrásnak tekinthető az, ha már külső adatokkal is tudunk dolgozni. Ettől kezdve nagyobb mennyiségű adatot kezelhetünk, rendszerezhetünk, összetettebb feladatokat oldhatunk meg. Maga a fájlkezelési része nem bonyolult, gyakorlatilag a fájlokat kezelő utasításokat kell csak megtanulni, komolyabban gondolkodni sem kell rajta.
Az összes ilyen folyamatot, ami a program és a külvilág közötti kommunikációért felelős I/O (input/output) műveleteknek nevezzük. Ezt a kommunikációt a Java adatfolyamokon, más néven Stream-eken keresztül valósítja meg. Az adatfolyamok nagy részét úgy kell elképzelni, mint egy csövet egy csappal, amelyet meg kell nyitni ahhoz, hogy áthaladhasson rajta az, amit szállít. Vannak azonban olyanok is, melyeket ettől azért egyszerűbb használni.
Konzol
Kezdőként kizárólag konzolon keresztül kommunikálunk a programunkkal. A konzol kezelésére a Java három olyan adatfolyamot biztosít, melyeket nem kell nyitni-zárni ahhoz, hogy kommunikálhassunk rajta keresztül, ezek a Standard Stream-ek. Ezekből a Java a következőket biztosítja számunkra:
Standard kimenet: System.out
Standard bemenet: System.in
Standard hiba: System.err
Az első onnan lehet ismerős, hogy szinte a kezdetektől ezt használtuk kiíratásra, vagyis már akkor is Stream-et használtunk. A második az Adatbekérés témakörből lehet ismerős. Amikor a felhasználóval adatokat szeretnénk begépeltetni, akkor a Scanner osztálynak ezt kellett odaadni, ez alapértelmezetten a billentyűzetet jelenti a konzolban. A harmadik szintén egy kimeneti csatorna, de annak egy speciális fajtája. Ez is gyakorlatilag egy olyan kiíratást végez el, mint a System.out, de ezt csak hibaüzenet kiíratásra szokás használni. A gyakorlati haszna talán annyi, hogy bizonyos fejlesztői környezetek (Eclipse), megkülönböztető vörös színnel emelik ki az ebbe írt üzeneteket, ezzel is nyomatékosítva, hogy ez egy hibaüzenet.
Fájlkezelés, mint kockázat
A Java nyelvben a fájlkezelés is Stream-eken keresztül valósul meg. Ezeket azonban csak akkor használhatjuk, ha a programunk elején importáljuk a java.io osztályt, mely ezeket a Stream-eket tartalmazza. A programunk elejét tehát kezdjük ezzel:
import java.io.*;
Ez nem csak a Stream-ek használatához szükséges kódokat tartalmazza, hanem a hibakezelés megfelelő osztályait is. A fájlkezelés mindig rizikós. Nincs meg a fájl. Vagy csak a helyét adtuk meg rosszul. Esetleg a nevét írtuk el. Pont megdöglött az adathordozó, ahol egyébként jó helyen és jó néven megtaláljuk. Attól függetlenül, hogy fájlokat olvasni vagy írni akarunk, mindenképpen egy kivételkezelő szerkezettel kell megoldani. Formailag ez a következőképp néz ki:
try
{
/* Itt megkísérlünk végrehajtani valami kockázatos dolgot,
* ami lehetséges, hogy nem működik, akár rajtunk kívülálló
* okok miatt.
*/
}
catch( IOException e )
{
/* Ha megtörtént a baj, akkor a végrehajtás a catch ágra ugrik,
* de a programunk nem áll le futási hibaüzenettel, hanem itt
* megadhatjuk, hogy hiba esetén mi történjen.
*/
}
finally
{
/* Végezetül akár sikeres volt a végrehajtás a try ágon, akár
* hibás a catch ágon, végül mindenképp ide jutunk. Ide
* helyezhetjük azokat az utasításokat, melyeket hibátlan és
* hibás futás esetén is szükséges végrehajtani. Például a
* fájlkezelés akár sikeres, akár sikertelen volt, a fájlt nem
* hagyhatjuk nyitva, itt lezárhatjuk. Maga a lezárás is
* egyébként kockázattal jár, vagyis ide is egy try-catch
* szerkezet kell, csak hogy ne legyen egyszerű.
* Ez az ág azonban nem kötelező!
*/
}
Nézzünk akkor példákat, milyen fájlkezelési feladatokkal kell megküzdenünk. A feladatok során szöveges állományokat fogunk kezelni: beolvassuk, módosítjuk azokat, hozzáfűzünk, beszúrunk sorokat. A fájlokat egyelőre a try ágban zárjuk majd le, a finally lehetőséget hanyagoljuk.
RandomAccessFile
A fájlkezeléshez sokféle előre megírt osztály nyújt segítséget, mi a RandomAccessFile osztályt fogjuk használni. Ez lehetőséget nyújt arra, hogy a fájlban tetszőleges helyre pozicionáljunk, de azért pár dologra majd figyelni kell.
Az osztály használata meglehetősen egyszerű. A try-catch szerkezetre mindenképp szükségünk van, erről az előző részben láthattál példát, de akkor most kicsit konkrétabban nézzük meg ezt. A finally ággal majd később foglalkozunk.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.*;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
try
{
raf = new RandomAccessFile("nevek.txt","r");
raf.close();
}
catch( IOException e )
{
System.err.println("HIBA");
}
}
}
Lássuk akkor a kiemelt sorokban a lényeget:
1 – Importáljuk a megfelelő osztályokat (ebben benne vannak a kivétel kezelő osztályok és a RandomAccessFile is).
7 – Létrehozunk egy változót raf néven a RandomAccessFile osztálynak, mert ettől kezdve ezzel hivatkozhatunk rá.
8 – A majdan beolvasásra kerülő sorokat valamilyen Stringben tárolni kell. Nem az összeset, mindig csak az altuálist.
12 – Itt hozunk létre egy új objektumot a RandomAccessFile osztályból, vagyis meghívjuk a konstruktorát. Két paramétert vár, az egyik a fájl elérési útvonala, a másik, hogy milyen módban (olvasás-írás) nyitjuk meg a fájlt. Ha a fájl a forráskódunk mellett van (NetBeans esetén pedig a projekt gyökérkönyvtárában), akkor elég csak a fájlnév. A kiterjesztés is kell! A megnyitási mód jelen esetben read “r”, mert módosítani nem akarjuk.
14 – Miután a fájllal megtettük, amit akartunk, le kell zárni.
18 – A System.err kimenettel az a gondom, hogy sokszor a kimenetben nem jó helyen jelenik meg az általa kiküldött kiíratás, így ez zavaró lehet. Ettől kezdve itt is a System.out-ot fogom használni a későbbi példákban.
Ez az alapja annak, hogy egy fájlt kezelni tudjunk. Ha módosítani szeretnénk a tartalmát, akkor “rw” módban kell megnyitni. A sorok beolvasásához mindenképp ciklusra van szükségünk, hiszen minden egyes sort azonos módon olvasunk be. A használt ciklus a 3 tanult fajtából bármelyik lehet. Ez a gyakorlatban a tapasztalatlan tanulóknál inkább gondot szokott okozni, de pár sablon megtanulásával bármilyen szöveges fájlt kezelni lehet, tehát tessék ezt is megtanulni!
Fájl beolvasása
A listát, mint szerkezetet nagyon jól lehet használni a fájlkezelés során, de most azért, hogy értsük, miért fognak lassan korlátozni a tömbök, velük kezdjük a feladatok megoldását. A fájl sorait minden esetben mint karakterláncokat olvassuk be. Ha ezek egyébként számokat tartalmaznak, azokat át kell majd alakítanunk. Ha csak a nyers beolvasott sorokat akarjuk tárolni, akkor ehhez egy String tömbre van szükségünk.
Az első gond tehát az szokott lenni a fájlkezelés során, hogy a beolvasott állományt valahol tárolni kell. Hogyan? Soronként? A sorokat még tovább bonthatjuk? Ne szaladjunk ennyire előre, kezdjük az elején.
Különböző szerkezetű források esetén beolvasás szempontjából az alábbi esetek lehetségesek:
Előre tudjuk, hány sorból áll a fájl
Nem tudjuk, hány sorból áll a fájl, de az első sorban megtaláljuk a sorok darabszámát
Nem tudjuk, hány sorból áll a fájl
Az első eset a legegyszerűbb, hiszen azonnal létrehozhatunk egy sorok számának megfelelő méretű tömböt, és a beolvasott sorokat eltároljuk.
A második eset csak annyival bonyolultabb, hogy az első sort külön kell beolvasnunk, majd az ott kapott értéknek megfelelően kell a tömb méretét beállítani.
A harmadik esetben nincs mese, számolnunk kell, hiszen a tömb méretét előre kell beállítanunk, de fogalmunk sincs, hány sorból áll a fájlunk. Ekkor kétszer olvassuk be a fájlt. Egyszer azért, hogy megszámoljuk, hány sorból áll. Ekkor a tömbméretet beállítva beolvassuk a fájlt újra, ekkor már azért, hogy eltároljuk a tartalmát.
Lássuk, hogy néz ki ez a gyakorlatban. Ugyanazt a feladatot fogom 3 különféle ciklussal megoldani. A már ismert dolgokat nem emelem ki újra, de a lényegi dolgokat igen.
Előre tudjuk a fájl adatsorainak számát
Adott egy 6 keresztnevet tartalmazó szövegfájl. Olvassuk be a tartalmát, tároljuk el, és írjuk ki a képernyőre!
Megoldás while ciklussal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
String[] nevek = new String[6];
try
{
raf = new RandomAccessFile("nevek.txt","r");
int db = 0;
sor = raf.readLine();
while( sor != null )
{
nevek[db] = sor;
db++;
sor = raf.readLine();
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
for( String str : nevek )
{
System.out.println(str);
}
}
}
Nézzük akkor a kiemelt részeket:
9 – Mivel előre tudjuk, hogy 6 nevet tartalmaz a fájl, ekkora tömböt hozunk létre tárolni azokat.
15 – Egy számláló, mely majd az aktuálisan beolvasott sor tömbbeli helyét adja majd meg. Nulláról indul természetesen, mint a tömbök indexelése.
16 – Beolvasunk egy sort, és eltároljuk a sor nevű String típusú változóban. Fontos, hogy a readLine() metódus, mindig egész sort olvas be (a sorvégjelig), és az úgynevezett fájlmutató (hogy éppen hol tartok a fájlban) automatikusan a következő sor elejére kerül, a sor hosszától függetlenül.
18-23 – Rögtön azzal kezdeném, hogy a ciklus futási feltétele azt jelenti, hogy a beolvasott sor nem null érték. Null értéket akkor olvashatunk, ha a fájl végén állunk. Tehát ha nem vagyunk a fájl végén, akkor mehetünk tovább. Láthatod, hogy a ciklus minden esetben azzal kezdi, hogy a nevek tömb db-odik helyére berakja a beolvasott sor. A db változó itt egy mutatóként funkcionál, ami minden esetben azt mutatja, hogy a tömbben hol található a következő üres hely. Mivel most erre az üres helyre betettünk egy elemet, a mutatót a következő üres hely indexére állítjuk (megnöveltük). Ha ez megtörtént, beolvassuk a következő sort. Persze ha ez a sor lesz a fájl vége, akkor ezt már nem tároljuk el tesszük be a tömbbe, mert a ciklus futási feltétele nem fog teljesülni. Ilyenkor a db változó egy nem létező helyre mutat (a tömbön kívül), ami valójában a tömb mérete lesz.
25-28 – Kiírjuk a tömb elemeit egymás alá.
Megoldás for ciklussal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.*;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
String[] nevek = new String[6];
try
{
raf = new RandomAccessFile("nevek.txt","r");
int db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[db] = sor;
db++;
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
for( String str : nevek )
{
System.out.println(str);
}
}
}
15 – Egy ugyanolyan számláló, mint az előző esetben.
16 – A for ciklust most elég érdekesen használom. Először is, nincs klasszikus ciklusváltozó. Oké, van egy db, de azt most nem a ciklus kezeli. Sőt, még a sor változót sem a ciklusban deklaráltam, hanem előtte. Majd később meglátod, miért. Szóval a ciklusfej inicializáló részében beolvasok egy sort. Futási feltételként megvizsgálom, hogy a sor az null érték-e. Ha nem, akkor a ciklusmagban eltárolom a beolvasott sort a while példában ismertetett módon (a db változóval jelzett üres helyre), majd a ciklus növekményes részében beolvasom a következő sort.
Megoldás do-while ciklussal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
String[] nevek = new String[6];
try
{
raf = new RandomAccessFile("nevek.txt","r");
int db = 0;
sor = raf.readLine();
do
{
nevek[db] = sor;
db++;
sor = raf.readLine();
}
while( sor != null );
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
for( String str : nevek )
{
System.out.println(str);
}
}
}
Ezt a megoldást nem is fejteném ki részletesen, hiszen csak ismert dolgokat láthatsz benne. A három megoldás közül viszont az egyik sántít, ezért nem is szeretem, ha általános megoldás típusként azt használják. Melyik akadhat ki a háromból és mikor? Gondolkodj el ezen.
A fájl sorainak számát az első sor tartalmazza
Említettem azt, hogy három alapeset van akkor, ha el szeretnénk tárolni a beolvasott fájl tartalmát. Az elsőn már túl vagyunk, vagyis valami oknál fogva pontosan tudtuk, hogy hány sorból áll a fájl, így könnyű dolgunk van.
Néha nem ennyire jó fejek, de annyira azért igen, hogy a fájl első sorába odaírják a megfejtést. Mondjuk így néz ki a fájlunk tartalma:
6
Bela
Jozsef
Anna
Peter
Eva
Jolan
Az első sorban ott van a valódi adatokat tartalmazó sorok száma. Nosza, használjuk. Az előzőleg felsorolt három lehetőség közül ez a 2. eset, vagyis itt a fájl első sorában az a szám található, ami a tárolandó adatok tömbjének méretét jelenti.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RandomAccessFile raf;
String sor;
String[] nevek = null; // még nem tudjuk hány nevünk lesz
try
{
raf = new RandomAccessFile("nevek.txt","rw");
int db = Integer.parseInt( raf.readLine() );
// most már tudjuk, fel is használjuk gyorsan
nevek = new String[db];
db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[db] = sor;
db++;
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
// elemek kiírása, stb...
for( String str : nevek )
{
System.out.println(str);
}
// ....
// ....
3 – Nagyon fontos sor! A tömböt a fájl elején deklaráltam a try-catch szerkezeten kívül, pontosabban előtte. Mindez azért fontos, mert ezt a tömböt a fájlkezelés lezárása után is szeretném, sőt, igazán akkor szeretném használni! A fájlkezelés csak azért kell, hogy legyenek adataim, amivel utána dolgozhatok. Mi lenne ott akkor, ha a tömb nem kapna kezdőértéket? Mondjuk a 3-as sor után csak ennyi szerepelne:
String[] nevek;
Mi a helyzet akkor, ha egy változónak nincs kezdőértéke? Addig nem használhatom. A 28-as sorban bizony használni szeretném a kiíratáshoz. Hibát is okoz, ráadásul szintaktikai hibát. Addig el sem indulhat a programom, amíg ez itt van. De miért van itt? Hiszen a try ágban úgyis megadom a tömb méretét! Az addig rendben, de a fájlkezelés kockázat. Semmi nem garantálja, hogy a fájl ott lesz, jó néven, éppen nem használja valaki stb. Vagyis lehet, hogy nem a try hanem a catch ág fog lefutni, és a tömbnek nem lesz kezdőértéke! Vagyis a kiíratáskor mindenképpen hibát fog jelezni! A program persze előre nem tudhatja, hogy hibás lesz-e a beolvasás, vagy sem, neki az a lényeg, hogy kezdőérték nélküli változót nem használhatunk!
9 – Mivel tudom, hogy ott van a fájl elején a valódi sorok száma (leírta a feladat, megmondták, megálmodtam, stb), ezért beolvasom az első sort. Ennek eredményét azonnal számmá alakítottam, ez jelenti majd a tömböm méretét.
11 – Rögtön be is állítom a megfelelő méretet. De azzal, hogy az első sort beolvastam, a fájlmutató máris átkerült a követező sor elejére. Miért jó ez? Mert a for ciklussal történő feldolgozás már csak a valódi adatsorokat olvassa be.
13 – A db változót most nullázom, mert ettől kezdve ez már nem megszámol (nincs is rá szükség), hanem mutatóként ismét a tömbben lévő üres helyet mutatja beolvasott adatok számára.
Ne felejtsük el ezt a momentumot, hogy a readLine() beolvasott egy sort, és a mutató a következő sor elejére került. Feljebb már emlegettem, ki is emeltem ezt! A readLine() metódus akár arra is használható, hogy sorokat ugorjunk át a fájlban feldolgozás nélkül. Ez még hasznos lesz a későbbiekben!
Nem tudom hány sorból áll a fájl
Itt semmit nem tudok a fájlról, legfeljebb annyit, hogy nem tartalmaz több sort, mint mondjuk 100. Ilyenkor mit tehetek?
Létrehozok egy 100 elemű tömböt, beolvasok mindent, és megjegyzem egy számlálóban, hogy hány valódi elemet tartalmaz a tömb. Na ne…
Megszámolom a sorokat, majd egy pont akkora tömböt hozok létre, amibe éppen belefér annyi sor, így a tömböm mérete a valódi sorok számát jelenti. Aztán beolvasok mindent és eltárolom.
Listát használok. De csak akkor, ha tömbökből már profi vagyok!
Amíg nem ismered a listákat, a megoldás menete a kiemelt algoritmus szerint történik. Lássuk ezt hogyan lehet megoldani.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
RandomAccessFile raf;
String sor;
String[] nevek = null; // még nem tudjuk hány nevünk lesz
try
{
raf = new RandomAccessFile("nevek.txt","rw");
int db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
db++;
}
nevek = new String[db];
raf.seek(0);
db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[db] = sor;
db++;
}
raf.close();
}
catch( IOException e )
// ....
// ....
3 – Már ismerős, ugye? El ne felejtsd! Bár úgyis hamar észreveszed, hogy nem működik a program 🙂
12 – Azért megyek végig a fájlon, hogy megszámoljam, hány sorból áll. Ahányszor lefut a ciklus, annyi sorból állt és növelgettem a számlálóm.
14 – Most már tudom, mekkora tömb kell. Itt a db a tömbméretet jelenti! Ezért számoltuk meg a sorokat, hogy pont akkora tömbünk legyen, amekkorára éppen szükségünk van.
15 – Megint végig akarom majd olvasni a fájlt, mert először csak a sorait számoltam. Ehhez vissza kell állnom a fájl elejére. A RandomAccessFile lehetőséget ad arra, hogy a fájlban bármilyen helyre pozicionáljak, vagyis a mutatót oda állítom be, ahova akarom. Erre szolgál a seek() metódusa, aminek meg kell adni egész számként a fájlmutató helyét bájtban megadva. Nekünk a fájl elejére van szükségünk, annak mutatója mindig 0.
17 – Ha visszaálltam az elejére, és kezdhetem elölről a beolvasást a már megismert módon, de most már a db mutatóként a tömbben lévő következő üres helyet mutatja, nem a sorok számát jelenti, hanem azt, hogy hova kell a tömbben betenni az éppen beolvasott sorban lévő adatot (lásd az előző példákat). Zavaró lehet, hogy ugyanazt a változót egyszer számlálónak, máskor mutatónak használom. Ez csak megszokás kérdése. Ha jobban belegondolsz, pontosan ugyanezt tettem meg a kiválogatásnál is.
Nézzük meg ugyanezt ArrayList segítségével:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;
import java.util.ArrayList;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
ArrayList<String> nevek = new ArrayList<String>();
try
{
raf = new RandomAccessFile("nevek.txt","r");
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek.add(sor);
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
}
}
Jóval egyszerűbb a többinél. Nem kell számolgatni, hány sorból áll, akkor sem, ha nem tudjuk mekkora a fájl. A lista úgyis akkora lesz, ameddig csak hozzáadunk valamit. És nyilván sem kell tartani, hogy hol a vége, az add() mindig a végéhez fűzi hozzá.
A beolvasással, akkor készen is vagyunk. Eldöntöd majd, melyik szerkezetet használod (while, for, do-while). Úgy gondolom, a legtöbb esetben a for ciklus a leghasználhatóbb, és mivel annak használata ismeretlen méretű fájlnál hasonlít a kiválogatásra, így akár sablonként is használható. Tárolás és beolvasás szempontjából a lista nagyon hatékony, de csak akkor ess neki a használatának, ha a tömbökkel biztosan meg tudod oldani a feladatot.
Új fájl írása
Ez sem sokkal bonyolultabb, mint az előzőek. Legalábbis abban az esetben, ha valóban új fájlt kell létrehoznunk. Tegyük fel, van egy neveket tartalmazó tömbünk, és ennek tartalmát szeretnénk egy fájlba kiírni. A részletes programokat most már nem fogom leírni, csak a try blokkon belüli részekre koncentrálok.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RandomAccessFile raf;
String[] nevek = { "Bela","Geza","Eva","Adam","Orsi" };
try
{
raf = new RandomAccessFile("nevek2.txt","rw");
for( String s: nevek )
{
raf.writeBytes(s+"\n");
}
raf.close();
}
catch( IOException e )
// ....
// ....
6 – Az új fájlt “rw” vagyis írás módban nyitjuk meg, így módosítható a tartalma. Ha a fájl nem létezik, akkor egy új üres fájl jön létre. Ha létezik, akkor megnyitja, és az elejére pozícionál.
10 – Sorokat a raf.writeBytes( String ) metódussal lehet. Vagyis csak Stringet írhatunk ki. Azonban feltűnhet az, hogy a Stringhez hozzáfűzök egy “\n” részt is. Ez a sordobás karaktere. Vagyis minden esetben neked kell új sort kezdeni! A kiíratás nem szükséges, hogy számlálóval rendelkező ciklussal történjen, hiszen a neveket tartalmazó tömb minden elemét ki akarjuk írni, ezért egy for each ciklussal ezt gond nélkül megtehetjük.
A sorokat tetszőleges dolgokat tartalmazhatnak, a lényeg, hogy minden sor végén legyen ott a sortörés karaktere. A gond akkor van, ha bele szeretnél nézni a fájlba, és mondjuk jegyzettömbbel megnyitod. Azt láthatod, hogy a neveket egymás mellé írta egy sorba. Semmi gond, itt csak arról van szó, hogy a jegyzettömb buta, és a raf.writeBytes() által használt sordobás karaktereket nem ismeri fel rendesen, ugyanis Linux és Windows környezetben más vezérlő karakterek jelentik a sorvégeket. Nyisd meg Geany vagy NetBeans szerkesztővel és látni fogod, hogy minden rendben van.
Meglévő fájl végéhez hozzáfűzés
Meglévő fájlok kezelésekor a fájl végéhez íráskor van a legkönnyebb dolgunk. Ha megnyitottuk a fájlt, egyszerűen a végére kell ugranunk, és raf.writeBytes()-szal írni ész nélkül, amit csak akarunk. Na de hogy ugrunk a végére? A RandomAccessFile rendelkezik egy seek() nevű metódussal, mellyel a fájlban tetszőleges helyre pozicionálhatunk. Már használtuk is, a raf.seek(0) a fájl elejére pozicionálta a fájlmutatót. Mivel a fájlban szöveges tartalom van, minden egyes karakter egy bájtot jelent. Akkor tudnunk kellene, hogy mennyi cucc van a fájlban, és megmondjuk, hogy ezek után állunk. Lássuk akkor hogyan is tegyük ezt meg:
1
2
3
4
5
6
7
8
9
10
11
12
13
RandomAccessFile raf;
try
{
raf = new RandomAccessFile("nevek.txt","rw");
raf.seek( raf.length() );
// innentől jöhet az írás, már a fájl végén vagyunk
raf.close();
}
catch( IOException e )
// ....
Nem sok mindent kell itt megmagyarázni. A raf.seek() a fájl adott bájtja (karaktere) elé pozicionálja a fájlmutatót, és onnantól írhatunk. A raf.length() pedig megadja, hogy egy adott fájl hány bájtból áll, így azonnal a végére ugrunk.
Adott sor kicserélése
Na, kezdődik… Érdekes feladat az, amikor egy adott sort kell kicserélni az állományban. A szöveges fájlt nem úgy kell elképzelni, mint egy különálló sorokból álló valamit, aminek mi látjuk. Ez egy karakterfolyam, melyben néha “sorvég” karakterek \n-ek találhatóak. Így valójában nagyon nehéz megoldani azt, hogy egy adott sort cseréljünk ki, hiszen a sorok nem egyforma méretűek.
A fájl tehát nem így néz ki:
Bela
Jozsef
Anna
Hanem így:
Bela\nJozsef\nAnna
Na most ide Jozsef helyere beszúrni egy Adam-ot meglehetősen érdekes eredményeket ad. Még a \n is bezavar, hiszen az is ugyanolyan karakter (bájt), mint az összes betű. Még ha pontosan pozicionálsz a második név elejére a seek(6)-tal, akkor is rossz az eredmény, hiszen ezt kapod:
Bela
Adam
f
Anna
Itt nincs mese, közvetlenül nincs csere. Be kell olvasni egy String tömbbe a fájl tartalmát, kicserélni Jozsef-et Adam-ra, visszaállsz a fájl elejére és az egészet kiírod újra. Oké, és ha a csere után a fájl hosszabb lett? Semmi gond, a fájlnak megnő a mérete is. És mi van, ha a csere után rövidebb lett? Gond. Mert a régi fájlból ottmaradt a maradék a végén. De mindjárt meglátod, ez nem akkora gond. Lássuk akkor a teljes feladatot, ami minden esetben kicserél egy adott sort egy szöveges fájlban. Az egyszerűség kedvéért tudjuk, hogy 3 nevünk van a fájlban. A másodikat akarjuk kicserélni egy újra.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RandomAccessFile raf;
String sor;
String[] nevek = new String[3];
try
{
raf = new RandomAccessFile("nevek.txt", "rw");
int i = 0;
for (sor = raf.readLine(); sor != null; sor = raf.readLine())
{
nevek[i] = sor;
i++;
}
nevek[1] = "Pal";
raf.seek(0);
for (String s : nevek)
{
raf.writeBytes(s+"\n");
}
if( raf.length() > raf.getFilePointer() )
{
raf.setLength( raf.getFilePointer() );
}
raf.close();
}
catch( IOException e )
// ....
// ....
Akkor apránként, de szerintem ha már idáig eljutottál a nagy része teljesen érthető. Amivel már találkoztál, csak címszavakban fejtem ki.
9-13 – A fájl tartalmának beolvasása.
16 – Kicseréljük a tömbben a 2. nevet.
17 – Visszaállunk a fájl elejére.
19-22 – Kiírjuk a tömbből a neveket a fájlba.
24 – Megnézzük, hogy a fájl hosszabb-e, mint az a pozíció, ahol most állunk (vagyis a tömb kiírásának befejezése után).
26 – Ha hosszabb, akkor a fájl méretét beállítjuk arra a pozícióra és ez lesz az új fájl vége, mert az előző névsor maradéka még ott van a végén!
Talán még egyszerűbb az a megoldás, hogy a fájl beolvasása után azonnal nullázzuk a méretét, és csak kiírjuk a String tömb tartalmát ész nélkül. Akkor még a seek()-et is megspórolhatjuk, mivel a fájl mérete 0, vagyis csak az elején lehetünk.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RandomAccessFile raf;
String sor;
String[] nevek = new String[3];
try
{
raf = new RandomAccessFile("nevek.txt", "rw");
int i = 0;
for (sor = raf.readLine(); sor != null; sor = raf.readLine())
{
nevek[i] = sor;
i++
}
raf.setLength(0); // fájl tartalmának törlése
nevek[1] = "Pal";
// jöhet a kiírás, stb
Sor beszúrása fájlba (nem a végére)
Na ez már tényleg érdekes. Egy kis ötlettel ez is megoldható. Nyilván itt sem lehet ész nélkül írni sehova sem. Bárhova írsz a fájlba, ha nem a végéhez fűzöd hozzá, akkor mindenképpen felülírsz valamit. Adja magát a dolog, hogy itt is tömbbe tárold el a fájl tartalmát. Igen ám, de a tömbbe beszúrni nem lehet. Egyrészt mert akkor megnőne a mérete (tömb mérete fix!), másrészt a beszúrás pozíciójától kezdődően mindenkit odébb kell pakolni eggyel hátrább (egyesével?). Van egy nem túl vészes megoldás.
Tegyük fel, hogy a 4 soros fájlunk közepére szeretnénk egy új nevet beszúrni.
Hasonlóan az előzőhöz, előbb beolvasom a fájl tartalmát. Igen ám, de 4 elemű tömbben fogom tárolni a neveket, hogy rakom be közéjük az 5.-et? Sehogy. Először kiírom az előtte lévőket, majd az új nevet, végül az utána következőket. A tömbbe nem rakom bele. Annyit kell csak tudnom, melyik után akarom beszúrni, mert ott kell megállnom a nevek kiírásakor egy pillanatra, utána meg onnan folytathatom.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RandomAccessFile raf;
String sor;
String[] nevek = new String[4];
try
{
raf = new RandomAccessFile("nevek.txt", "rw");
nevek = new String[4];
int i = 0;
for ( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[i++] = sor;
}
raf.setLength(0);
for( int j = 0; j < 2; j++ )
{
raf.writeBytes(nevek[j]+"\n");
}
raf.writeBytes("Teodor\n");
for( int j = 2; j < nevek.length; j++ )
{
raf.writeBytes(nevek[j]+"\n");
}
raf.close();
}
catch ( IOException e )
// stb...
16 – Fájl tartalmának törlése
18-21 – Beszúrás előtti részek kiírása.
23 – Új sor beszúrása a fájlba
25-28 – Beszúrás utáni részek kiírása.
Hogy ez mennyivel egyszerűbb listával…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
RandomAccessFile raf;
String sor;
ArrayList<String> nevek = new ArrayList<String>();
try
{
raf = new RandomAccessFile("nevek.txt","rw");
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek.add(sor);
}
nevek.add(2, "Teodor");
raf.setLength(0);
for ( String s : nevek)
{
raf.writeBytes(s+"\n");
}
raf.close();
}
catch ( IOException e )
14 – A beolvasott listába a megfelelő helyre beszúrok egy elemet, ami automatikusan hátrább tolja a mögötte lévőket a listában.
16 – Fájl tartalmának törlése
17-20 – Válogatás nélkül kiírom az egész listát a fájlba.
Ezek voltak a fájlkezelés alapjai, amikor egy sorban egyetlen szöveges adat szerepelt. A későbbiek sem sokkal bonyolultabbak. Ha számokkal dolgoznánk, akkor a beolvasott sorokat azonnal számmá kellene alakítani az Integer.parseInt() metódussal, és kész.
A gond akkor lesz, ha egy sor több összetartozó adatot tartalmaz. Mondjunk egy kutya menhely lakóinak adatait, és ezeket az adatokat valamivel elválasztjuk egymástól a soron belül. Erre egy példaobjektumot már láthattál kutyára az Osztályok és objektumok témakörben. A String metódusokat is kívülről kell fújnod ahhoz, hogy fájlkezelés területén tovább haladhassunk.
SAJÁT OBJEKTUM
Mint már említettem, ez az OO szemlélet abból indul ki, hogy a fejlesztés során modellezett objektumok állandóak, csak a hozzájuk kapcsolódó teendők változnak. Az objektum egyfajta önálló entitás, ami tulajdonságokkal és viselkedésekkel rendelkezik.
Objektumok, mint modellek
Objektum lehet egy egyszerű kávéfőző, ami a következő tulajdonságokkal rendelkezik:
vízmennyiség
kávémennyiség
Az objektum azonban nem csak adatokat tárol saját magáról, hanem azokat a viselkedéseket is tartalmazza, amelyekkel ezeket az adatokat kezeli. Például van egy “feltölt” utasítása, amellyel kávét vagy vizet lehet tölteni bele. Van egy “főz” utasítása, amellyel kávét lehet főzetni vele.
Az objektumok önállóan léteznek, és önmagukat kezelni tudják, de nem automatikusan, hanem kívülről kell vezérelni őket. Kell egy vezérlőprogram, amely ezt az objektumot használja és utasítja a megfelelő viselkedésre. Például én töltöm fel a kávéfőzőt, de engem nem érdekel, hogy azt ő hogyan csinálja, vagy én indítom a főzést, de továbbra sem érdekel, hogy azt hogyan oldja meg, én csak utasítok.
kavefozoTermészetesen a feltölt metódus nem csak annyit csinál, hogy megnöveli a kávé és vízmennyiséget a gépben, hanem hibaellenőrzés is kapcsolódik hozzá, hiszen nem tölthetem túl a gépet, mert kifolyik. A főzést kezelő metódusban is kell hibakezelés, hiszen nem főzhetek akkor kávét, ha nincs benne víz vagy kávé. De nekem a vezérlőprogramban ezzel sem kell foglalkozni, ott csak kiadom az utasítást: főzzél kávét. Erre maga a gép jelez majd vissza, hogy nem fog menni, mert üres.
Objektumok, mint adattárolók
Az objektumokat nem csak arra használjuk, hogy modellezzünk velük valamit. Akkor is hasznosak, amikor logikailag összetartozó adatokat egy önálló egységként szeretnénk tárolni. A fájlkezeléses feladatok során a forrásban egy sor több adatot is tartalmaz. Azonban minden sor egy önálló egységet jelent, a benne lévő adatok ugyanahhoz a dologhoz tartoznak. Olyan ez, mint amikor egy adatbázis-kezelés feladatban a forrásban egy sor egy egyed tulajdonságait tartalmazza, csak ott a sort rekordnak hívtuk. Mondjuk egy .csv kiterjesztésű fájlban a sorokban lévő pontosvesszők valójában oszlopokat választanak el egymástól, és ezeket beolvasva nagyjából egy adatbázis tábláját kapjuk. Akkor most itt álljunk meg egy pillanatra. Vegyünk egy sort, mely egy kutya adatait tartalmazza. Nevét, fajtáját, színét, tömegét, életkorát és a nemét. A nemét egy logikai változóban tároljuk majd. Ha igaz, akkor kan kutya, ha nem, akkor szuka. Ezeket az adatokat egy sorban soroljuk fel, pontosvesszővel elválasztva a következőképp:
Buksi;tacsko;fekete;11.6;5;1
Ezek az adatok mind ugyanarra a kutyára vonatkoznak. De a fájlban lehet több kutya adata is, hasonló szerkezetben. Ilyenkor minden egyes sor egy új kutyát jelent. Ezért azt tesszük, hogy írunk egy kutya osztályt, amelyben különböző kutyák adatait tartjuk nyilván, de minden kutyáét egy önálló objektumban. Így a különböző adatok nem keverednek össze, de bármelyik kutya összes adatát egyben tudjuk kezelni.
Lássunk akkor egy példakutyát. Emlékszel, minden objektum a következő részekből áll:
Változók
Metódusok
Konstrukciós műveletek
public class Kutya
{
// Változók
private String nev;
private String fajta;
private String szin;
private double suly;
private int kor;
private boolean kan;
// Metódusok
public String getNev()
{
return nev;
}
public String getFajta()
{
return fajta;
}
public String getSzin()
{
return szin;
}
public double getSuly()
{
return suly;
}
public int getKor()
{
return kor;
}
public boolean isKan()
{
return kan;
}
// Konstruktor
public Kutya(String nev, String fajta, String szin,
double suly, int kor, int kan)
{
this.nev = nev;
this.fajta = fajta;
this.szin = szin;
this.suly = suly;
this.kor = kor;
if( kan == 1 )
{
this.kan = true;
}
else
{
this.kan = false;
}
}
}
Az előző példában láthatod, hogy a kutya 6 tulajdonsággal rendelkezik. Ezek mindegyike annak megfelelő típusú, amilyen adatot tárolni szeretnénk benne. Minden változót bezártunk, vagyis privát változóvá tettünk. Ezzel azt érjük el, hogy az adott osztály változóját nem lehet közvetlenül elérni, csakis egy metóduson keresztül kaphatjuk meg az értékét. Mint az Osztályok és objektumok témakörben írtam, ennek biztonsági okai vannak.
Az adott osztálynak azon metódusait, melyeknek csak és kizárólag az a szerepe, hogy a változói felől érdeklődőknek választ adjanak, get metódusoknak nevezzük, rövidebben getter-eknek. Get metódust minden olyan változónak biztosítani kell, amelyet kívülről szeretnénk elérhetővé tenni. Ez nem jelenti azt, hogy módosítani is lehet majd, ez csak egy lekérdezés. A get metódusok elnevezése szokásjog szerint a get szóval kezdődik, és utána nagy kezdőbetűvel a változó neve szerepel. Egyetlen kivétel a boolean típusú változót kezelő getter, ahol nem “get” hanem “is” szóval kezdjük a nevet. Ezek a metódusok mindig visszatérési értékkel rendelkeznek, mely nyilván meg kell hogy egyezzen a változó típusával. Ebből a példából kigyűjtve:
public String getNev()
public String getFajta()
public String getSzin()
public double getSuly()
public int getKor()
public boolean isKan()
Az osztályunk konstruktora csak egyfajta, mert a beolvasáskor egy kutya összes adatát megtaláljuk az adott sorban, és ezeket beolvasva, szétdarabolva hívjuk meg a konstruktort, hogy új kutyát hozzunk létre:
new Kutya("Buksi","tacsko","fekete",11.6,5,1)
Ugye emlékszel, hogy ilyet így soha nem csinálunk! Így nincs eltárolva a létrehozott objektum hivatkozása, vagyis úgy hoztuk létre, hogy a kupacról azonnal el is takarítják, amit körbenéznek szemét (vagyis hivatkozás nélküli) objektumok után.
Használjuk úgy, hogy az objektum hivatkozását eltároljuk valahol:
Kutya k = new Kutya("Buksi","tacsko","fekete",11.6,5,1)
Láthatod, hogy a konstruktornak a kutya nemét nem logikai változóként adjuk oda. A fájlból 0 vagy 1-es értéket olvastunk be, majd a konstruktorban beállítjuk, hogy melyik jelenti a true-t, és melyik a false-t. Bár az ilyen szerkezetű beállítás, amit a Kutya osztályban látsz jóval egyszerűbb is lehet. Elegáns, és a legegyszerűbb megoldás:
this.kan = kan == 1;
A konstruktor paraméterei
A konstruktorban nagyon sok mindent megcsinálhatunk, hiszen a kapott értékeket fel kell dolgozni, hogy tárolhatóak legyenek a nekik megfelelő változókban. Lehet, hogy eleve nem olyan formában kapom meg a változókat, hogy azt közvetlenül használni tudjam. Fájl beolvasásakor soronként haladunk, melyeket Stringekként tudunk beolvasni. Ezeket utána szét kell darabolnunk, hogy aztán azt csináljunk, amit akarunk. Vegyük ismét a beolvasandó példasort:
Buksi;tacsko;fekete;11.6;5;1
Tudjuk, hogy ; karakterrel vannak az egyes “oszlopok” elválasztva egymástól. A beolvasást végző programnak fogalma sincs arról, hogy amit beolvas, az mit jelent. Ő csak beolvas, és odaadja az eredményt annak, aki azt értelmezni tudja. Annyit azért segíthet, hogy a beolvasott sor darabjait adja tovább, valahogy így:
String sor = raf.readLine();
Kutya k = new Kutya( sor.split(";") );
Láthatod, hogy egy új kutyát hozok létre, de a konstruktorának a beolvasott sor darabjait adom oda, melyeket a ; karakternél török szét. Ennek a kódnak más dolga nincs, a kutya megkapta az adatait, építse fel magát.
Hogy néz ki akkor a kutya konstruktora, ha egy halom Stringet kap? A kutyának tudnia kell, hogy a tömb darabjai közül melyik melyik adatát jelenti majd, vagyis úgy kell megírni a kutya konstruktorát, hogy tisztában legyünk a fájl szerkezetével, ami a forrásadatokat biztosítja. Akkor jöjjön a konstruktor:
/*
* sor: Buksi;tacsko;fekete;11.6;5;1
* tömb: { "Buksi","tacsko","fekete","11.6","5","1" }
* index: 0 1 2 3 4 5
*/
public Kutya( String[] tomb )
{
this.nev = tomb[0];
this.fajta = tomb[1];
this.szin = tomb[2];
this.suly= Double.parseDouble(tomb[3]);
this.kor = Integer.parseInt(tomb[4]);
this.kan = tomb[5].equals("1");
}
Ez ugye annyit tesz csak, hogy a beolvasott sort tömbbé darabolva a konstruktor a megfelelő darabokat a megfelelő típussá alakítja, majd eltárolja azokat. Ráadásul ez a szerkezet rendkívül rugalmas. Ha a fájlban esetleg megjelenik egy új tulajdonság a kutyánál, mondjuk testmagasság, akkor a beolvasó programon semmit nem kell módosítani. Csak a kutyába kell egy új változó, valamint a konstruktorába kell beszúrni egy új sort, ami az adott tulajdonságot az új változóban tárolja el.
1
2
3
4
5
6
7
8
9
10
11
12
private int magassag;
public Kutya( String[] tomb )
{
this.nev = tomb[0];
this.fajta = tomb[1];
this.szin = tomb[2];
this.suly= Double.parseDouble(tomb[3]);
this.kor = Integer.parseInt(tomb[4]);
this.kan = tomb[5].equals("1");
this.magassag = Integer.parseInt(tomb[6]);
}
Saját metódusok
Ide most nem a gettereket sorolnám, holott azok is metódusok, csak külön kategóriát alkotnak. Sokszor előfordul, hogy nem csak lekérdezni kell adatokat, hanem az objektumhoz kapcsolódik valamilyen tevékenység is. Tegyük fel, a kutyánkat etetni szeretnénk, és ha “ránézünk”, szeretnénk pár dolgot megtudni róla.
Ezeket a teendőket mind metódusokon keresztül tudjuk megtenni. Hogy egyszerűbb legyen a példa, amikor a kutyát megetetjük, akkor nem lesz éhes. De csak akkor etethetjük, ha valóban az. Az etetéshez bevezetek egy új változót, ehes néven. Ez egy skálán elhelyezkedve a kutya pillanatnyi állapotát jelenti. 0 jelentse azt, hogy nem éhes, a 5-ös pedig a majd éhen halt. Ezen kívül bevezetek egy olyan metódust is, amivel “rá lehet nézni a kutyára”, de hogy milyennek néz ki, az a pillanatnyi állapotától is függ.
Ezek a metódusok nemcsak arra szolgálnak, hogy a két változó értékét módosítják, hanem arra is, hogy ellenőrzött körülmények között teszik azt. Nem fog enni, ha nem éhes. Nézzük meg ezeket:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int ehes;
public void etet( int kaja )
{
if( ehes == 0 )
{
System.out.println("A kutya nem ehes.");
}
else
{
System.out.println("A kutya jóllakott.");
ehes = 0;
}
}
Lássuk a ránézést.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String leiras()
{
StringBuilder desc = new StringBuilder();
desc.append("Ez egy "+szin+" szinu "+fajta+". Jelenleg ");
String kaja;
switch( ehes )
{
case 0 : kaja = "nem"; break;
case 1 : kaja = "kicsit"; break;
case 3 : kaja = "kozepesen"; break;
case 4 : kaja = "nagyon"; break;
default : kaja = "borzasztoan"; break;
}
desc.append(kaja+" ehes.");
return desc.toString();
}
Az objektumok tehát rendkívül sokoldalúak. Valódi dolgok modelljeként is használhatjuk őket, valamint adattárolóként is működnek. Az emelt érettségi programozási feladatában ez utóbbira van szükségünk.
Pár lényeges dolog összeszedve:
A változókat mindig védd meg, tedd őket priváttá.
Írd meg a megfelelő get metódusokat, hogy elérd a változókat.
Ha a változón módosítani kell, arra is írj metódust. (setter)
Egy jó konstruktor már fél siker. Állíts be benne mindent, amit csak tudsz. Akár olyan változókat is, melyeket nem a fájlbeolvasáskor kaptál, hanem a meglévő változókból lehet kiszámítani. A konstruktort utólag is bővítheted.
Írj saját metódusokat, és használd az objektum változóit, ha szükséged van rájuk.
Mindig legyen egy aktualizált toString() metódusa az objektumnak, mely a változóit írja ki, így ellenőrizni tudod, megfelelő objektummal dolgozol-e.
FÁJLKEZELÉS
Fájl beolvasása
A listát, mint szerkezetet nagyon jól lehet használni a fájlkezelés során, de most azért, hogy értsük, miért fognak lassan korlátozni a tömbök, velük kezdjük a feladatok megoldását. A fájl sorait minden esetben mint karakterláncokat olvassuk be. Ha ezek egyébként számokat tartalmaznak, azokat át kell majd alakítanunk. Ha csak a nyers beolvasott sorokat akarjuk tárolni, akkor ehhez egy String tömbre van szükségünk.
Az első gond tehát az szokott lenni a fájlkezelés során, hogy a beolvasott állományt valahol tárolni kell. Hogyan? Soronként? A sorokat még tovább bonthatjuk? Ne szaladjunk ennyire előre, kezdjük az elején.
Különböző szerkezetű források esetén beolvasás szempontjából az alábbi esetek lehetségesek:
Előre tudjuk, hány sorból áll a fájl
Nem tudjuk, hány sorból áll a fájl, de az első sorban megtaláljuk a sorok darabszámát
Nem tudjuk, hány sorból áll a fájl
Az első eset a legegyszerűbb, hiszen azonnal létrehozhatunk egy sorok számának megfelelő méretű tömböt, és a beolvasott sorokat eltároljuk.
A második eset csak annyival bonyolultabb, hogy az első sort külön kell beolvasnunk, majd az ott kapott értéknek megfelelően kell a tömb méretét beállítani.
A harmadik esetben nincs mese, számolnunk kell, hiszen a tömb méretét előre kell beállítanunk, de fogalmunk sincs, hány sorból áll a fájlunk. Ekkor kétszer olvassuk be a fájlt. Egyszer azért, hogy megszámoljuk, hány sorból áll. Ekkor a tömbméretet beállítva beolvassuk a fájlt újra, ekkor már azért, hogy eltároljuk a tartalmát.
Lássuk, hogy néz ki ez a gyakorlatban. Ugyanazt a feladatot fogom 3 különféle ciklussal megoldani. A már ismert dolgokat nem emelem ki újra, de a lényegi dolgokat igen.
Előre tudjuk a fájl adatsorainak számát
Adott egy 6 keresztnevet tartalmazó szövegfájl. Olvassuk be a tartalmát, tároljuk el, és írjuk ki a képernyőre!
Megoldás while ciklussal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
String[] nevek = new String[6];
try
{
raf = new RandomAccessFile("nevek.txt","r");
int db = 0;
sor = raf.readLine();
while( sor != null )
{
nevek[db] = sor;
db++;
sor = raf.readLine();
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
for( String str : nevek )
{
System.out.println(str);
}
}
}
Nézzük akkor a kiemelt részeket:
9 – Mivel előre tudjuk, hogy 6 nevet tartalmaz a fájl, ekkora tömböt hozunk létre tárolni azokat.
15 – Egy számláló, mely majd az aktuálisan beolvasott sor tömbbeli helyét adja majd meg. Nulláról indul természetesen, mint a tömbök indexelése.
16 – Beolvasunk egy sort, és eltároljuk a sor nevű String típusú változóban. Fontos, hogy a readLine() metódus, mindig egész sort olvas be (a sorvégjelig), és az úgynevezett fájlmutató (hogy éppen hol tartok a fájlban) automatikusan a következő sor elejére kerül, a sor hosszától függetlenül.
18-23 – Rögtön azzal kezdeném, hogy a ciklus futási feltétele azt jelenti, hogy a beolvasott sor nem null érték. Null értéket akkor olvashatunk, ha a fájl végén állunk. Tehát ha nem vagyunk a fájl végén, akkor mehetünk tovább. Láthatod, hogy a ciklus minden esetben azzal kezdi, hogy a nevek tömb db-odik helyére berakja a beolvasott sor. A db változó itt egy mutatóként funkcionál, ami minden esetben azt mutatja, hogy a tömbben hol található a következő üres hely. Mivel most erre az üres helyre betettünk egy elemet, a mutatót a következő üres hely indexére állítjuk (megnöveltük). Ha ez megtörtént, beolvassuk a következő sort. Persze ha ez a sor lesz a fájl vége, akkor ezt már nem tároljuk el tesszük be a tömbbe, mert a ciklus futási feltétele nem fog teljesülni. Ilyenkor a db változó egy nem létező helyre mutat (a tömbön kívül), ami valójában a tömb mérete lesz.
25-28 – Kiírjuk a tömb elemeit egymás alá.
Megoldás for ciklussal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.*;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
String[] nevek = new String[6];
try
{
raf = new RandomAccessFile("nevek.txt","r");
int db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[db] = sor;
db++;
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
for( String str : nevek )
{
System.out.println(str);
}
}
}
15 – Egy ugyanolyan számláló, mint az előző esetben.
16 – A for ciklust most elég érdekesen használom. Először is, nincs klasszikus ciklusváltozó. Oké, van egy db, de azt most nem a ciklus kezeli. Sőt, még a sor változót sem a ciklusban deklaráltam, hanem előtte. Majd később meglátod, miért. Szóval a ciklusfej inicializáló részében beolvasok egy sort. Futási feltételként megvizsgálom, hogy a sor az null érték-e. Ha nem, akkor a ciklusmagban eltárolom a beolvasott sort a while példában ismertetett módon (a db változóval jelzett üres helyre), majd a ciklus növekményes részében beolvasom a következő sort.
Megoldás do-while ciklussal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
String[] nevek = new String[6];
try
{
raf = new RandomAccessFile("nevek.txt","r");
int db = 0;
sor = raf.readLine();
do
{
nevek[db] = sor;
db++;
sor = raf.readLine();
}
while( sor != null );
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
for( String str : nevek )
{
System.out.println(str);
}
}
}
Ezt a megoldást nem is fejteném ki részletesen, hiszen csak ismert dolgokat láthatsz benne. A három megoldás közül viszont az egyik sántít, ezért nem is szeretem, ha általános megoldás típusként azt használják. Melyik akadhat ki a háromból és mikor? Gondolkodj el ezen.
A fájl sorainak számát az első sor tartalmazza
Említettem azt, hogy három alapeset van akkor, ha el szeretnénk tárolni a beolvasott fájl tartalmát. Az elsőn már túl vagyunk, vagyis valami oknál fogva pontosan tudtuk, hogy hány sorból áll a fájl, így könnyű dolgunk van.
Néha nem ennyire jó fejek, de annyira azért igen, hogy a fájl első sorába odaírják a megfejtést. Mondjuk így néz ki a fájlunk tartalma:
6
Bela
Jozsef
Anna
Peter
Eva
Jolan
Az első sorban ott van a valódi adatokat tartalmazó sorok száma. Nosza, használjuk. Az előzőleg felsorolt három lehetőség közül ez a 2. eset, vagyis itt a fájl első sorában az a szám található, ami a tárolandó adatok tömbjének méretét jelenti.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RandomAccessFile raf;
String sor;
String[] nevek = null; // még nem tudjuk hány nevünk lesz
try
{
raf = new RandomAccessFile("nevek.txt","rw");
int db = Integer.parseInt( raf.readLine() );
// most már tudjuk, fel is használjuk gyorsan
nevek = new String[db];
db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[db] = sor;
db++;
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
// elemek kiírása, stb...
for( String str : nevek )
{
System.out.println(str);
}
// ....
// ....
3 – Nagyon fontos sor! A tömböt a fájl elején deklaráltam a try-catch szerkezeten kívül, pontosabban előtte. Mindez azért fontos, mert ezt a tömböt a fájlkezelés lezárása után is szeretném, sőt, igazán akkor szeretném használni! A fájlkezelés csak azért kell, hogy legyenek adataim, amivel utána dolgozhatok. Mi lenne ott akkor, ha a tömb nem kapna kezdőértéket? Mondjuk a 3-as sor után csak ennyi szerepelne:
String[] nevek;
Mi a helyzet akkor, ha egy változónak nincs kezdőértéke? Addig nem használhatom. A 28-as sorban bizony használni szeretném a kiíratáshoz. Hibát is okoz, ráadásul szintaktikai hibát. Addig el sem indulhat a programom, amíg ez itt van. De miért van itt? Hiszen a try ágban úgyis megadom a tömb méretét! Az addig rendben, de a fájlkezelés kockázat. Semmi nem garantálja, hogy a fájl ott lesz, jó néven, éppen nem használja valaki stb. Vagyis lehet, hogy nem a try hanem a catch ág fog lefutni, és a tömbnek nem lesz kezdőértéke! Vagyis a kiíratáskor mindenképpen hibát fog jelezni! A program persze előre nem tudhatja, hogy hibás lesz-e a beolvasás, vagy sem, neki az a lényeg, hogy kezdőérték nélküli változót nem használhatunk!
9 – Mivel tudom, hogy ott van a fájl elején a valódi sorok száma (leírta a feladat, megmondták, megálmodtam, stb), ezért beolvasom az első sort. Ennek eredményét azonnal számmá alakítottam, ez jelenti majd a tömböm méretét.
11 – Rögtön be is állítom a megfelelő méretet. De azzal, hogy az első sort beolvastam, a fájlmutató máris átkerült a követező sor elejére. Miért jó ez? Mert a for ciklussal történő feldolgozás már csak a valódi adatsorokat olvassa be.
13 – A db változót most nullázom, mert ettől kezdve ez már nem megszámol (nincs is rá szükség), hanem mutatóként ismét a tömbben lévő üres helyet mutatja beolvasott adatok számára.
Ne felejtsük el ezt a momentumot, hogy a readLine() beolvasott egy sort, és a mutató a következő sor elejére került. Feljebb már emlegettem, ki is emeltem ezt! A readLine() metódus akár arra is használható, hogy sorokat ugorjunk át a fájlban feldolgozás nélkül. Ez még hasznos lesz a későbbiekben!
Nem tudom hány sorból áll a fájl
Itt semmit nem tudok a fájlról, legfeljebb annyit, hogy nem tartalmaz több sort, mint mondjuk 100. Ilyenkor mit tehetek?
Létrehozok egy 100 elemű tömböt, beolvasok mindent, és megjegyzem egy számlálóban, hogy hány valódi elemet tartalmaz a tömb. Na ne…
Megszámolom a sorokat, majd egy pont akkora tömböt hozok létre, amibe éppen belefér annyi sor, így a tömböm mérete a valódi sorok számát jelenti. Aztán beolvasok mindent és eltárolom.
Listát használok. De csak akkor, ha tömbökből már profi vagyok!
Amíg nem ismered a listákat, a megoldás menete a kiemelt algoritmus szerint történik. Lássuk ezt hogyan lehet megoldani.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
RandomAccessFile raf;
String sor;
String[] nevek = null; // még nem tudjuk hány nevünk lesz
try
{
raf = new RandomAccessFile("nevek.txt","rw");
int db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
db++;
}
nevek = new String[db];
raf.seek(0);
db = 0;
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[db] = sor;
db++;
}
raf.close();
}
catch( IOException e )
// ....
// ....
3 – Már ismerős, ugye? El ne felejtsd! Bár úgyis hamar észreveszed, hogy nem működik a program 🙂
12 – Azért megyek végig a fájlon, hogy megszámoljam, hány sorból áll. Ahányszor lefut a ciklus, annyi sorból állt és növelgettem a számlálóm.
14 – Most már tudom, mekkora tömb kell. Itt a db a tömbméretet jelenti! Ezért számoltuk meg a sorokat, hogy pont akkora tömbünk legyen, amekkorára éppen szükségünk van.
15 – Megint végig akarom majd olvasni a fájlt, mert először csak a sorait számoltam. Ehhez vissza kell állnom a fájl elejére. A RandomAccessFile lehetőséget ad arra, hogy a fájlban bármilyen helyre pozicionáljak, vagyis a mutatót oda állítom be, ahova akarom. Erre szolgál a seek() metódusa, aminek meg kell adni egész számként a fájlmutató helyét bájtban megadva. Nekünk a fájl elejére van szükségünk, annak mutatója mindig 0.
17 – Ha visszaálltam az elejére, és kezdhetem elölről a beolvasást a már megismert módon, de most már a db mutatóként a tömbben lévő következő üres helyet mutatja, nem a sorok számát jelenti, hanem azt, hogy hova kell a tömbben betenni az éppen beolvasott sorban lévő adatot (lásd az előző példákat). Zavaró lehet, hogy ugyanazt a változót egyszer számlálónak, máskor mutatónak használom. Ez csak megszokás kérdése. Ha jobban belegondolsz, pontosan ugyanezt tettem meg a kiválogatásnál is.
Nézzük meg ugyanezt ArrayList segítségével:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.*;
import java.util.ArrayList;
public class Fajlkezelesalapok
{
public static void main(String[] args)
{
RandomAccessFile raf;
String sor;
ArrayList<String> nevek = new ArrayList<String>();
try
{
raf = new RandomAccessFile("nevek.txt","r");
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek.add(sor);
}
raf.close();
}
catch( IOException e )
{
System.out.println("HIBA");
}
}
}
Jóval egyszerűbb a többinél. Nem kell számolgatni, hány sorból áll, akkor sem, ha nem tudjuk mekkora a fájl. A lista úgyis akkora lesz, ameddig csak hozzáadunk valamit. És nyilván sem kell tartani, hogy hol a vége, az add() mindig a végéhez fűzi hozzá.
A beolvasással, akkor készen is vagyunk. Eldöntöd majd, melyik szerkezetet használod (while, for, do-while). Úgy gondolom, a legtöbb esetben a for ciklus a leghasználhatóbb, és mivel annak használata ismeretlen méretű fájlnál hasonlít a kiválogatásra, így akár sablonként is használható. Tárolás és beolvasás szempontjából a lista nagyon hatékony, de csak akkor ess neki a használatának, ha a tömbökkel biztosan meg tudod oldani a feladatot.
Új fájl írása
Ez sem sokkal bonyolultabb, mint az előzőek. Legalábbis abban az esetben, ha valóban új fájlt kell létrehoznunk. Tegyük fel, van egy neveket tartalmazó tömbünk, és ennek tartalmát szeretnénk egy fájlba kiírni. A részletes programokat most már nem fogom leírni, csak a try blokkon belüli részekre koncentrálok.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RandomAccessFile raf;
String[] nevek = { "Bela","Geza","Eva","Adam","Orsi" };
try
{
raf = new RandomAccessFile("nevek2.txt","rw");
for( String s: nevek )
{
raf.writeBytes(s+"\n");
}
raf.close();
}
catch( IOException e )
// ....
// ....
6 – Az új fájlt “rw” vagyis írás módban nyitjuk meg, így módosítható a tartalma. Ha a fájl nem létezik, akkor egy új üres fájl jön létre. Ha létezik, akkor megnyitja, és az elejére pozícionál.
10 – Sorokat a raf.writeBytes( String ) metódussal lehet. Vagyis csak Stringet írhatunk ki. Azonban feltűnhet az, hogy a Stringhez hozzáfűzök egy “\n” részt is. Ez a sordobás karaktere. Vagyis minden esetben neked kell új sort kezdeni! A kiíratás nem szükséges, hogy számlálóval rendelkező ciklussal történjen, hiszen a neveket tartalmazó tömb minden elemét ki akarjuk írni, ezért egy for each ciklussal ezt gond nélkül megtehetjük.
A sorokat tetszőleges dolgokat tartalmazhatnak, a lényeg, hogy minden sor végén legyen ott a sortörés karaktere. A gond akkor van, ha bele szeretnél nézni a fájlba, és mondjuk jegyzettömbbel megnyitod. Azt láthatod, hogy a neveket egymás mellé írta egy sorba. Semmi gond, itt csak arról van szó, hogy a jegyzettömb buta, és a raf.writeBytes() által használt sordobás karaktereket nem ismeri fel rendesen, ugyanis Linux és Windows környezetben más vezérlő karakterek jelentik a sorvégeket. Nyisd meg Geany vagy NetBeans szerkesztővel és látni fogod, hogy minden rendben van.
Meglévő fájl végéhez hozzáfűzés
Meglévő fájlok kezelésekor a fájl végéhez íráskor van a legkönnyebb dolgunk. Ha megnyitottuk a fájlt, egyszerűen a végére kell ugranunk, és raf.writeBytes()-szal írni ész nélkül, amit csak akarunk. Na de hogy ugrunk a végére? A RandomAccessFile rendelkezik egy seek() nevű metódussal, mellyel a fájlban tetszőleges helyre pozicionálhatunk. Már használtuk is, a raf.seek(0) a fájl elejére pozicionálta a fájlmutatót. Mivel a fájlban szöveges tartalom van, minden egyes karakter egy bájtot jelent. Akkor tudnunk kellene, hogy mennyi cucc van a fájlban, és megmondjuk, hogy ezek után állunk. Lássuk akkor hogyan is tegyük ezt meg:
1
2
3
4
5
6
7
8
9
10
11
12
13
RandomAccessFile raf;
try
{
raf = new RandomAccessFile("nevek.txt","rw");
raf.seek( raf.length() );
// innentől jöhet az írás, már a fájl végén vagyunk
raf.close();
}
catch( IOException e )
// ....
Nem sok mindent kell itt megmagyarázni. A raf.seek() a fájl adott bájtja (karaktere) elé pozicionálja a fájlmutatót, és onnantól írhatunk. A raf.length() pedig megadja, hogy egy adott fájl hány bájtból áll, így azonnal a végére ugrunk.
Adott sor kicserélése
Na, kezdődik… Érdekes feladat az, amikor egy adott sort kell kicserélni az állományban. A szöveges fájlt nem úgy kell elképzelni, mint egy különálló sorokból álló valamit, aminek mi látjuk. Ez egy karakterfolyam, melyben néha “sorvég” karakterek \n-ek találhatóak. Így valójában nagyon nehéz megoldani azt, hogy egy adott sort cseréljünk ki, hiszen a sorok nem egyforma méretűek.
A fájl tehát nem így néz ki:
Bela
Jozsef
Anna
Hanem így:
Bela\nJozsef\nAnna
Na most ide Jozsef helyere beszúrni egy Adam-ot meglehetősen érdekes eredményeket ad. Még a \n is bezavar, hiszen az is ugyanolyan karakter (bájt), mint az összes betű. Még ha pontosan pozicionálsz a második név elejére a seek(6)-tal, akkor is rossz az eredmény, hiszen ezt kapod:
Bela
Adam
f
Anna
Itt nincs mese, közvetlenül nincs csere. Be kell olvasni egy String tömbbe a fájl tartalmát, kicserélni Jozsef-et Adam-ra, visszaállsz a fájl elejére és az egészet kiírod újra. Oké, és ha a csere után a fájl hosszabb lett? Semmi gond, a fájlnak megnő a mérete is. És mi van, ha a csere után rövidebb lett? Gond. Mert a régi fájlból ottmaradt a maradék a végén. De mindjárt meglátod, ez nem akkora gond. Lássuk akkor a teljes feladatot, ami minden esetben kicserél egy adott sort egy szöveges fájlban. Az egyszerűség kedvéért tudjuk, hogy 3 nevünk van a fájlban. A másodikat akarjuk kicserélni egy újra.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RandomAccessFile raf;
String sor;
String[] nevek = new String[3];
try
{
raf = new RandomAccessFile("nevek.txt", "rw");
int i = 0;
for (sor = raf.readLine(); sor != null; sor = raf.readLine())
{
nevek[i] = sor;
i++;
}
nevek[1] = "Pal";
raf.seek(0);
for (String s : nevek)
{
raf.writeBytes(s+"\n");
}
if( raf.length() > raf.getFilePointer() )
{
raf.setLength( raf.getFilePointer() );
}
raf.close();
}
catch( IOException e )
// ....
// ....
Akkor apránként, de szerintem ha már idáig eljutottál a nagy része teljesen érthető. Amivel már találkoztál, csak címszavakban fejtem ki.
9-13 – A fájl tartalmának beolvasása.
16 – Kicseréljük a tömbben a 2. nevet.
17 – Visszaállunk a fájl elejére.
19-22 – Kiírjuk a tömbből a neveket a fájlba.
24 – Megnézzük, hogy a fájl hosszabb-e, mint az a pozíció, ahol most állunk (vagyis a tömb kiírásának befejezése után).
26 – Ha hosszabb, akkor a fájl méretét beállítjuk arra a pozícióra és ez lesz az új fájl vége, mert az előző névsor maradéka még ott van a végén!
Talán még egyszerűbb az a megoldás, hogy a fájl beolvasása után azonnal nullázzuk a méretét, és csak kiírjuk a String tömb tartalmát ész nélkül. Akkor még a seek()-et is megspórolhatjuk, mivel a fájl mérete 0, vagyis csak az elején lehetünk.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RandomAccessFile raf;
String sor;
String[] nevek = new String[3];
try
{
raf = new RandomAccessFile("nevek.txt", "rw");
int i = 0;
for (sor = raf.readLine(); sor != null; sor = raf.readLine())
{
nevek[i] = sor;
i++
}
raf.setLength(0); // fájl tartalmának törlése
nevek[1] = "Pal";
// jöhet a kiírás, stb
Sor beszúrása fájlba (nem a végére)
Na ez már tényleg érdekes. Egy kis ötlettel ez is megoldható. Nyilván itt sem lehet ész nélkül írni sehova sem. Bárhova írsz a fájlba, ha nem a végéhez fűzöd hozzá, akkor mindenképpen felülírsz valamit. Adja magát a dolog, hogy itt is tömbbe tárold el a fájl tartalmát. Igen ám, de a tömbbe beszúrni nem lehet. Egyrészt mert akkor megnőne a mérete (tömb mérete fix!), másrészt a beszúrás pozíciójától kezdődően mindenkit odébb kell pakolni eggyel hátrább (egyesével?). Van egy nem túl vészes megoldás.
Tegyük fel, hogy a 4 soros fájlunk közepére szeretnénk egy új nevet beszúrni.
Hasonlóan az előzőhöz, előbb beolvasom a fájl tartalmát. Igen ám, de 4 elemű tömbben fogom tárolni a neveket, hogy rakom be közéjük az 5.-et? Sehogy. Először kiírom az előtte lévőket, majd az új nevet, végül az utána következőket. A tömbbe nem rakom bele. Annyit kell csak tudnom, melyik után akarom beszúrni, mert ott kell megállnom a nevek kiírásakor egy pillanatra, utána meg onnan folytathatom.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
RandomAccessFile raf;
String sor;
String[] nevek = new String[4];
try
{
raf = new RandomAccessFile("nevek.txt", "rw");
nevek = new String[4];
int i = 0;
for ( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek[i++] = sor;
}
raf.setLength(0);
for( int j = 0; j < 2; j++ )
{
raf.writeBytes(nevek[j]+"\n");
}
raf.writeBytes("Teodor\n");
for( int j = 2; j < nevek.length; j++ )
{
raf.writeBytes(nevek[j]+"\n");
}
raf.close();
}
catch ( IOException e )
// stb...
16 – Fájl tartalmának törlése
18-21 – Beszúrás előtti részek kiírása.
23 – Új sor beszúrása a fájlba
25-28 – Beszúrás utáni részek kiírása.
Hogy ez mennyivel egyszerűbb listával…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
RandomAccessFile raf;
String sor;
ArrayList<String> nevek = new ArrayList<String>();
try
{
raf = new RandomAccessFile("nevek.txt","rw");
for( sor = raf.readLine(); sor != null; sor = raf.readLine() )
{
nevek.add(sor);
}
nevek.add(2, "Teodor");
raf.setLength(0);
for ( String s : nevek)
{
raf.writeBytes(s+"\n");
}
raf.close();
}
catch ( IOException e )
14 – A beolvasott listába a megfelelő helyre beszúrok egy elemet, ami automatikusan hátrább tolja a mögötte lévőket a listában.
16 – Fájl tartalmának törlése
17-20 – Válogatás nélkül kiírom az egész listát a fájlba.
Ezek voltak a fájlkezelés alapjai, amikor egy sorban egyetlen szöveges adat szerepelt. A későbbiek sem sokkal bonyolultabbak. Ha számokkal dolgoznánk, akkor a beolvasott sorokat azonnal számmá kellene alakítani az Integer.parseInt() metódussal, és kész.
A gond akkor lesz, ha egy sor több összetartozó adatot tartalmaz. Mondjunk egy kutya menhely lakóinak adatait, és ezeket az adatokat valamivel elválasztjuk egymástól a soron belül. Erre egy példaobjektumot már láthattál kutyára az Osztályok és objektumok témakörben. A String metódusokat is kívülről kell fújnod ahhoz, hogy fájlkezelés területén tovább haladhassunk.
A fájlkezelés alapeseteit (beolvasás, kiírás, hozzáfűzés, csere, beszúrás) már megismerhetted, és ha ezeket alkalmazni is tudod, akkor jöhet a mélyebb víz egy másik leckében.
Na, megvan a 3 fajta ciklusból melyiket nem szeretem a beolvasáskor? Oké, sokat nem segítettem, mert mindenhol for-t használtam. Maradt 50%. Próbálj a 3 ciklusfajtával beolvastatni egy üres fájlt.
SAJÁT OBJEKTUM VAGY METÓDUS
Mint már említettem, ez az OO szemlélet abból indul ki, hogy a fejlesztés során modellezett objektumok állandóak, csak a hozzájuk kapcsolódó teendők változnak. Az objektum egyfajta önálló entitás, ami tulajdonságokkal és viselkedésekkel rendelkezik.
Objektumok, mint modellek
Objektum lehet egy egyszerű kávéfőző, ami a következő tulajdonságokkal rendelkezik:
vízmennyiség
kávémennyiség
Az objektum azonban nem csak adatokat tárol saját magáról, hanem azokat a viselkedéseket is tartalmazza, amelyekkel ezeket az adatokat kezeli. Például van egy “feltölt” utasítása, amellyel kávét vagy vizet lehet tölteni bele. Van egy “főz” utasítása, amellyel kávét lehet főzetni vele.
Az objektumok önállóan léteznek, és önmagukat kezelni tudják, de nem automatikusan, hanem kívülről kell vezérelni őket. Kell egy vezérlőprogram, amely ezt az objektumot használja és utasítja a megfelelő viselkedésre. Például én töltöm fel a kávéfőzőt, de engem nem érdekel, hogy azt ő hogyan csinálja, vagy én indítom a főzést, de továbbra sem érdekel, hogy azt hogyan oldja meg, én csak utasítok.
kavefozoTermészetesen a feltölt metódus nem csak annyit csinál, hogy megnöveli a kávé és vízmennyiséget a gépben, hanem hibaellenőrzés is kapcsolódik hozzá, hiszen nem tölthetem túl a gépet, mert kifolyik. A főzést kezelő metódusban is kell hibakezelés, hiszen nem főzhetek akkor kávét, ha nincs benne víz vagy kávé. De nekem a vezérlőprogramban ezzel sem kell foglalkozni, ott csak kiadom az utasítást: főzzél kávét. Erre maga a gép jelez majd vissza, hogy nem fog menni, mert üres.
Objektumok, mint adattárolók
Az objektumokat nem csak arra használjuk, hogy modellezzünk velük valamit. Akkor is hasznosak, amikor logikailag összetartozó adatokat egy önálló egységként szeretnénk tárolni. A fájlkezeléses feladatok során a forrásban egy sor több adatot is tartalmaz. Azonban minden sor egy önálló egységet jelent, a benne lévő adatok ugyanahhoz a dologhoz tartoznak. Olyan ez, mint amikor egy adatbázis-kezelés feladatban a forrásban egy sor egy egyed tulajdonságait tartalmazza, csak ott a sort rekordnak hívtuk. Mondjuk egy .csv kiterjesztésű fájlban a sorokban lévő pontosvesszők valójában oszlopokat választanak el egymástól, és ezeket beolvasva nagyjából egy adatbázis tábláját kapjuk. Akkor most itt álljunk meg egy pillanatra. Vegyünk egy sort, mely egy kutya adatait tartalmazza. Nevét, fajtáját, színét, tömegét, életkorát és a nemét. A nemét egy logikai változóban tároljuk majd. Ha igaz, akkor kan kutya, ha nem, akkor szuka. Ezeket az adatokat egy sorban soroljuk fel, pontosvesszővel elválasztva a következőképp:
Buksi;tacsko;fekete;11.6;5;1
Ezek az adatok mind ugyanarra a kutyára vonatkoznak. De a fájlban lehet több kutya adata is, hasonló szerkezetben. Ilyenkor minden egyes sor egy új kutyát jelent. Ezért azt tesszük, hogy írunk egy kutya osztályt, amelyben különböző kutyák adatait tartjuk nyilván, de minden kutyáét egy önálló objektumban. Így a különböző adatok nem keverednek össze, de bármelyik kutya összes adatát egyben tudjuk kezelni.
Lássunk akkor egy példakutyát. Emlékszel, minden objektum a következő részekből áll:
Változók
Metódusok
Konstrukciós műveletek
public class Kutya
{
// Változók
private String nev;
private String fajta;
private String szin;
private double suly;
private int kor;
private boolean kan;
// Metódusok
public String getNev()
{
return nev;
}
public String getFajta()
{
return fajta;
}
public String getSzin()
{
return szin;
}
public double getSuly()
{
return suly;
}
public int getKor()
{
return kor;
}
public boolean isKan()
{
return kan;
}
// Konstruktor
public Kutya(String nev, String fajta, String szin,
double suly, int kor, int kan)
{
this.nev = nev;
this.fajta = fajta;
this.szin = szin;
this.suly = suly;
this.kor = kor;
if( kan == 1 )
{
this.kan = true;
}
else
{
this.kan = false;
}
}
}
Az előző példában láthatod, hogy a kutya 6 tulajdonsággal rendelkezik. Ezek mindegyike annak megfelelő típusú, amilyen adatot tárolni szeretnénk benne. Minden változót bezártunk, vagyis privát változóvá tettünk. Ezzel azt érjük el, hogy az adott osztály változóját nem lehet közvetlenül elérni, csakis egy metóduson keresztül kaphatjuk meg az értékét. Mint az Osztályok és objektumok témakörben írtam, ennek biztonsági okai vannak.
Az adott osztálynak azon metódusait, melyeknek csak és kizárólag az a szerepe, hogy a változói felől érdeklődőknek választ adjanak, get metódusoknak nevezzük, rövidebben getter-eknek. Get metódust minden olyan változónak biztosítani kell, amelyet kívülről szeretnénk elérhetővé tenni. Ez nem jelenti azt, hogy módosítani is lehet majd, ez csak egy lekérdezés. A get metódusok elnevezése szokásjog szerint a get szóval kezdődik, és utána nagy kezdőbetűvel a változó neve szerepel. Egyetlen kivétel a boolean típusú változót kezelő getter, ahol nem “get” hanem “is” szóval kezdjük a nevet. Ezek a metódusok mindig visszatérési értékkel rendelkeznek, mely nyilván meg kell hogy egyezzen a változó típusával. Ebből a példából kigyűjtve:
public String getNev()
public String getFajta()
public String getSzin()
public double getSuly()
public int getKor()
public boolean isKan()
Az osztályunk konstruktora csak egyfajta, mert a beolvasáskor egy kutya összes adatát megtaláljuk az adott sorban, és ezeket beolvasva, szétdarabolva hívjuk meg a konstruktort, hogy új kutyát hozzunk létre:
new Kutya("Buksi","tacsko","fekete",11.6,5,1)
Ugye emlékszel, hogy ilyet így soha nem csinálunk! Így nincs eltárolva a létrehozott objektum hivatkozása, vagyis úgy hoztuk létre, hogy a kupacról azonnal el is takarítják, amit körbenéznek szemét (vagyis hivatkozás nélküli) objektumok után.
Használjuk úgy, hogy az objektum hivatkozását eltároljuk valahol:
Kutya k = new Kutya("Buksi","tacsko","fekete",11.6,5,1)
Láthatod, hogy a konstruktornak a kutya nemét nem logikai változóként adjuk oda. A fájlból 0 vagy 1-es értéket olvastunk be, majd a konstruktorban beállítjuk, hogy melyik jelenti a true-t, és melyik a false-t. Bár az ilyen szerkezetű beállítás, amit a Kutya osztályban látsz jóval egyszerűbb is lehet. Elegáns, és a legegyszerűbb megoldás:
this.kan = kan == 1;
A konstruktor paraméterei
A konstruktorban nagyon sok mindent megcsinálhatunk, hiszen a kapott értékeket fel kell dolgozni, hogy tárolhatóak legyenek a nekik megfelelő változókban. Lehet, hogy eleve nem olyan formában kapom meg a változókat, hogy azt közvetlenül használni tudjam. Fájl beolvasásakor soronként haladunk, melyeket Stringekként tudunk beolvasni. Ezeket utána szét kell darabolnunk, hogy aztán azt csináljunk, amit akarunk. Vegyük ismét a beolvasandó példasort:
Buksi;tacsko;fekete;11.6;5;1
Tudjuk, hogy ; karakterrel vannak az egyes “oszlopok” elválasztva egymástól. A beolvasást végző programnak fogalma sincs arról, hogy amit beolvas, az mit jelent. Ő csak beolvas, és odaadja az eredményt annak, aki azt értelmezni tudja. Annyit azért segíthet, hogy a beolvasott sor darabjait adja tovább, valahogy így:
String sor = raf.readLine();
Kutya k = new Kutya( sor.split(";") );
Láthatod, hogy egy új kutyát hozok létre, de a konstruktorának a beolvasott sor darabjait adom oda, melyeket a ; karakternél török szét. Ennek a kódnak más dolga nincs, a kutya megkapta az adatait, építse fel magát.
Hogy néz ki akkor a kutya konstruktora, ha egy halom Stringet kap? A kutyának tudnia kell, hogy a tömb darabjai közül melyik melyik adatát jelenti majd, vagyis úgy kell megírni a kutya konstruktorát, hogy tisztában legyünk a fájl szerkezetével, ami a forrásadatokat biztosítja. Akkor jöjjön a konstruktor:
/*
* sor: Buksi;tacsko;fekete;11.6;5;1
* tömb: { "Buksi","tacsko","fekete","11.6","5","1" }
* index: 0 1 2 3 4 5
*/
public Kutya( String[] tomb )
{
this.nev = tomb[0];
this.fajta = tomb[1];
this.szin = tomb[2];
this.suly= Double.parseDouble(tomb[3]);
this.kor = Integer.parseInt(tomb[4]);
this.kan = tomb[5].equals("1");
}
Ez ugye annyit tesz csak, hogy a beolvasott sort tömbbé darabolva a konstruktor a megfelelő darabokat a megfelelő típussá alakítja, majd eltárolja azokat. Ráadásul ez a szerkezet rendkívül rugalmas. Ha a fájlban esetleg megjelenik egy új tulajdonság a kutyánál, mondjuk testmagasság, akkor a beolvasó programon semmit nem kell módosítani. Csak a kutyába kell egy új változó, valamint a konstruktorába kell beszúrni egy új sort, ami az adott tulajdonságot az új változóban tárolja el.
1
2
3
4
5
6
7
8
9
10
11
12
private int magassag;
public Kutya( String[] tomb )
{
this.nev = tomb[0];
this.fajta = tomb[1];
this.szin = tomb[2];
this.suly= Double.parseDouble(tomb[3]);
this.kor = Integer.parseInt(tomb[4]);
this.kan = tomb[5].equals("1");
this.magassag = Integer.parseInt(tomb[6]);
}
Saját metódusok
Ide most nem a gettereket sorolnám, holott azok is metódusok, csak külön kategóriát alkotnak. Sokszor előfordul, hogy nem csak lekérdezni kell adatokat, hanem az objektumhoz kapcsolódik valamilyen tevékenység is. Tegyük fel, a kutyánkat etetni szeretnénk, és ha “ránézünk”, szeretnénk pár dolgot megtudni róla.
Ezeket a teendőket mind metódusokon keresztül tudjuk megtenni. Hogy egyszerűbb legyen a példa, amikor a kutyát megetetjük, akkor nem lesz éhes. De csak akkor etethetjük, ha valóban az. Az etetéshez bevezetek egy új változót, ehes néven. Ez egy skálán elhelyezkedve a kutya pillanatnyi állapotát jelenti. 0 jelentse azt, hogy nem éhes, a 5-ös pedig a majd éhen halt. Ezen kívül bevezetek egy olyan metódust is, amivel “rá lehet nézni a kutyára”, de hogy milyennek néz ki, az a pillanatnyi állapotától is függ.
Ezek a metódusok nemcsak arra szolgálnak, hogy a két változó értékét módosítják, hanem arra is, hogy ellenőrzött körülmények között teszik azt. Nem fog enni, ha nem éhes. Nézzük meg ezeket:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private int ehes;
public void etet( int kaja )
{
if( ehes == 0 )
{
System.out.println("A kutya nem ehes.");
}
else
{
System.out.println("A kutya jóllakott.");
ehes = 0;
}
}
Lássuk a ránézést.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String leiras()
{
StringBuilder desc = new StringBuilder();
desc.append("Ez egy "+szin+" szinu "+fajta+". Jelenleg ");
String kaja;
switch( ehes )
{
case 0 : kaja = "nem"; break;
case 1 : kaja = "kicsit"; break;
case 3 : kaja = "kozepesen"; break;
case 4 : kaja = "nagyon"; break;
default : kaja = "borzasztoan"; break;
}
desc.append(kaja+" ehes.");
return desc.toString();
}
Az objektumok tehát rendkívül sokoldalúak. Valódi dolgok modelljeként is használhatjuk őket, valamint adattárolóként is működnek. Az emelt érettségi programozási feladatában ez utóbbira van szükségünk.
Pár lényeges dolog összeszedve:
A változókat mindig védd meg, tedd őket priváttá.
Írd meg a megfelelő get metódusokat, hogy elérd a változókat.
Ha a változón módosítani kell, arra is írj metódust. (setter)
Egy jó konstruktor már fél siker. Állíts be benne mindent, amit csak tudsz. Akár olyan változókat is, melyeket nem a fájlbeolvasáskor kaptál, hanem a meglévő változókból lehet kiszámítani. A konstruktort utólag is bővítheted.
Írj saját metódusokat, és használd az objektum változóit, ha szükséged van rájuk.
Mindig legyen egy aktualizált toString() metódusa az objektumnak, mely a változóit írja ki, így ellenőrizni tudod, megfelelő objektummal dolgozol-e.
TÖMB MINT TÁRHELY
Szerintem mindenki emlékszik arra a pillanatra, amikor megismerte a tömböket. Vagy szerelem volt első látásra, vagy ekkor esett először komolyan kétségbe. De ha túltette magát az első sokkon, akkor rájött, hogy nem is olyan bonyolultak. A tömböket nagyon szeretjük. Nagyon sok és sokfajta adatot képesek tárolni. Ezek lehetnek primitív, vagy referencia típusok is. Mi több, az elemeknek sorrendje van. A nagyon sok adatból bármikor kivehetünk egyet. Akár megvizsgálhatjuk az összeset, szigorúan sorban haladva. Akkor mi a gond vele?
A mérete
A bővítése
Keresés az elemei között
Sorolhatnám még, de bevezetésnek ennyi pont elég.
Az első problémával már biztosan találkoztál. A tömbnek elsőre jó méretet kell választani. Miért? Mert a mérete fix. Ez egy nagyon komoly döntés. Ez nem egy hajvágás. Az kinő újra. De egy tömb méretét megváltoztatni… Aztán rájössz, hogy nagyobb kell, akkor készíthetsz másikat, és abba átpakolhatod az eredeti értékeket, meg azokat, amik nem fértek el. És ha az is kicsi lesz? Vagy elsőre kiszámolhatod, hogy mekkorára van szükség, és létrehozod amekkora kell. És ha valami nem várt esemény miatt mégiscsak kicsi? Vagy épp túl nagy?
A második gond elsőre hasonlíthat az elsőhöz, valójában teljesen más. Itt bővítés alatt nem feltétlenül arra gondolok, hogy a tömb kicsi. Tételezzük fel, hogy van egy 100 elemű tömböd. Okosan ekkorát hoztál létre, mert tudtad, hogy ennél több elemet soha nem kell tárolnod. De csak 65-öt tettél bele. Akkor is felkészültél mindenre, mert az új elemeket bármikor odarakhatod a tömb végére. Az már csak apróság, hogy ha nincs tele a tömb, akkor a méretét megadó tömb.length értelmét vesztette. Neked kell külön nyilvántartanod és folyamatosan frissítened, hogy mennyi valódi elem van benne. Ráadásul olyan ügyes vagy, hogy így még ki is vehetsz elemet a tömb végéről (pontosítok, nullázod az ottani elemet), és ekkor csökkentheted a valódi elemszámot tároló változót. Profi. Hozzáadhatsz és el is vehetsz belőle. Az is apró szépséghiba, hogy a tömb végén a nem valódi elemek ugyanakkora memóriát foglalnak, mint az elején lévő valódiak. Képzeljük el, hogy gyerekek neveit tárolod annak megfelelően, hogy a tornasorban hol állnak. Érkezett egy új gyerek. Hova állítod? A sor végére? Elég ritka eset. De a tömbbe nem lehet csak úgy akárhova beszúrni egy elemet. A többit odébb kell pakolni. Neked. És ha távozik egy gyerek? Az sem feltétlenül a sor végéről fog eltűnni. És a többi üresen hagyja a helyét? Vagy pakoljunk mindenkit eggyel előrébb, aki utána állt?
A harmadik probléma akkor jött elő, amikor meg akartuk tudni, hogy egy tömbben benne van-e egy elem, akkor meg kellett keresni. Ha ügyesek voltunk, és lehetőségünk volt rá, akkor valamilyen rendezett tömbbel dolgoztunk. Abban lehet, hogy nem lineárisan, minden elemet megvizsgálva kell keresni. De milyen jó lenne, ha a kereséssel nem nekünk kell foglalkozni, hanem azonnal választ kaphatnánk arra, hogy benne van-e a keresett elem, vagy nem.
A tömbök buták. Szeretjük őket, de buták. A tanulmányaink elején muszáj megismernünk őket. Rajtuk keresztül tanulunk meg programozni. És minél jobban megtanulunk, annál jobban megismerjük a korlátait. Felismerjük azt, hogy amit mi eddig félistenként tiszteltünk, mert mindent meg tudtunk oldani vele (igaz, néha körülményesen), valójában inkább spanyolcsizma. Szűk, rugalmatlan, és ha sokat akarunk ugrálni, akkor nagyon szúr.
Ismerjük meg azt, ami minden gondunkat megoldja.
ArrayList
Ha nagyon sarkosan szeretnénk fogalmazni, mondhatnánk, hogy az ArrayList egy változtatható méretű tömb. Sőt, a méretével egyáltalán nem kell foglalkoznunk, ha nem akarunk. Megkérdezni azért szabad.
Az ArrayList valójában egy osztály, ami a motorháztető alatt szintén egy tömbbel dolgozik. De nem most mondtam, hogy a tömb mérete fix? És ha változtatni kell a tömb méretén? Akkor létrehoz egy újat és azzal dolgozik. Az ArrayList osztály tele van pakolva olyan hasznos metódusokkal, amelyek az összes előzőleg felsorolt problémát nemcsak hogy megoldják, hanem még többre is képesek. Oké, így picit becsapva érezheted magad, hiszen mégis csak tömböt használsz. Csak nem Te. És ez sok gondtól megkímél.
Lássuk akkor, hogyan használhatjuk az ArrayList-et, és mi mindenre jó. Tételezzük fel, szükségünk van egy olyan listára, mely egész számokat tárol.
Ahhoz, hogy létrehozhassunk egyet, importálni kell azt a kódot, ahol ő található, az ArrayList osztályt:
import java.util.ArrayList;
public class Lista
{
public static void main(String[] args )
{
ArrayList<Integer> szamok = new ArrayList<>();
}
}
Az első kiemelt sor mutatja az osztály importálását, ilyet már láthattál a Scanner esetén. A második egy változó deklarálás és egy példányosítás. Akkor most dekódoljuk, hogy mit is látunk:
Megadunk egy ArrayList osztályú változót.
Rögzítjük, hogy ebben Integer osztályú objektumokat szeretnénk tárolni.
A változó neve: szamok
Egy új listát hozunk létre, ahol a típust már nem kell újra megadni.
És nem adunk meg semmit sem a konstruktorának.
Miért nem <int> szerepel a típusmegadásnál, ahogy a tömböknél láttuk? Mi ez az Integer? Nagy betűvel kezdődik, akkor ez egy osztály?
Igen. Ez egy burkoló vagy csomagoló osztály. Arra való, hogy becsomagolja magába a primitív értéket, így olyan helyen is használhatjuk azokat, ahol csak Objektummal állnak szóba.
Azért van erre szükség, mert a lista csak és kizárólag referencia típusú adatokat képes tárolni, primitív típusokat nem rakhatunk bele. Ha mégis azokat szeretnénk tárolni, akkor a primitív típusok megfelelő csomagoló osztályát kell használnunk típusként:
int helyett Integer
double helyett Double
char helyett Character
boolean helyett Boolean
(valamint a többi egész és valós típus, azonos névvel)
Vegyünk egy tömb témakörrel kapcsolatos komplex feladatot, de most új barátunkat használjuk. Sorsoljunk ki 20 egész számot a [-10;40] intervallumból és tároljuk el őket. A kiemelt sorokat a példa után megmagyarázom, ezek tartalmazzák a gyakran használt ArrayList metódusokat és a lényegi részeket, melyek a lista általános használatához szükségesek.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import java.util.ArrayList;
public class Lista
{
public static void main(String[] args )
{
ArrayList<Integer> szamok = new ArrayList<>();
// töltsük fel a listát
for( int i = 0; i < 20; i++ )
{
szamok.add( (int)(Math.random() * 51) - 10 );
}
System.out.println("A lista mérete: " + szamok.size());
// írjuk ki az elemeit
for( int i = 0; i < szamok.size(); i++ )
{
System.out.print( szamok.get(i)+" " );
}
System.out.println();
// töröljünk ki a lista legkisebb elemét
// ha több legkisebb van, akkor az elsőt
int min = 0;
for( int i = 0; i < szamok.size(); i++ )
{
if( szamok.get(i) < szamok.get(min) )
{
min = i;
}
}
System.out.println("A legkisebb eleme: " + szamok.get(min));
szamok.remove(min);
System.out.println("A lista merete: " + szamok.size());
// a lista elemei
for (Integer i : szamok)
{
System.out.print(i+" ");
}
System.out.println();
// szúrjunk be egy véletlen elemet a lista elejére
szamok.add( 0, (int)(Math.random() * 51) - 10 );
for (Integer i : szamok)
{
System.out.print(i + " ");
}
System.out.println();
// nézzük meg, benne van-e az intervallum legnagyobb
// eleme a listában, és ha igen, hol?
int hely = szamok.indexOf(40);
if( hely > -1 )
{
System.out.println("A 40-es elem helye: " + hely);
}
else
{
System.out.println("Nincs 40-es elem a listában.");
}
// vizsgáljuk meg, van-e 0 érték a listában
if( szamok.contains(0) )
{
System.out.println("A lista tartalmaz 0-at.");
}
else
{
System.out.println("A lista NEM tartalmaz 0-at.");
}
// rendezzük a listában szereplő számokat növekvő sorrendbe
int csere;
for (int i = 0; i < szamok.size() - 1; i++)
{
for (int j = i + 1; j < szamok.size(); j++)
{
if( szamok.get(i) > szamok.get(j) )
{
csere = szamok.get(i);
szamok.set(i, szamok.get(j));
szamok.set(j, csere);
}
}
}
// írjuk ki a rendezett számokat
System.out.println("Rendezett sorrend:");
for (Integer i : szamok)
{
System.out.print(i + " ");
}
System.out.println();
// töröljük ki a negatív elemeket a rendezett listából
for( int i = 0; i < szamok.size(); i++ )
{
if( szamok.get(i) > -1 )
{
szamok.removeAll(szamok.subList(0, i));
break;
}
}
// írjuk ki a listában maradt elemeket
for (Integer i : szamok)
{
System.out.print( i +" ");
}
}
}
Akkor lássuk a feladat megoldását részenként, melyen keresztül az ArrayList működését is megértjük. A felsorolás elején lévő számok a kiemelt sorokat jelentik.
8 – Figyeld meg, hogy nem hivatkozok a lista méretére a feltöltésekor, mivel a mérete alaphelyzetben 0. A for ciklusban a futási feltételben számként adom meg, hogy 20x fusson le a ciklus, vagyis 20 elemet fogok eltárolni a listában. Minden elem hozzáadás után a lista mérete eggyel nő.
10 – Itt láthatod, hogyan adunk hozzá egy elemet a listához, ami mindig a lista végére kerül.
12 – A lista méretét a .size() metódussal kaphatod meg.
15 – A .size() már szerepelt, de most már a lista bejárásához használom egy for ciklus futási feltételében.
17 – A lista bármelyik eleme indexelhető, hasonlóan a tömbökhöz, csak itt a hivatkozáshoz a .get(index) metódust használjuk, és nem a tömböknél tanult tomb[index] szerkezetet.
32 – Bármilyen elemet eltávolíthatok az indexe alapján a .remove(index) metódussal. Az utána elhelyezkedő elemek eggyel balra tolódnak és a lista mérete eggyel csökken.
36-39 – Foreach ciklus használható az elemek eléréséhez, például kiíratás esetén. Ha csak az elemek számítanak és az indexük nem, akkor a foreach ciklus mindig használható a for helyett.
43 – Az add(index, elem) metódussal a lista tetszőleges helyére beszúrhatunk egy elemet. Ha nem a lista végére szúrunk be elemet, akkor a beszúrás helyén lévő és a mögötte állók eggyel jobbra tolódnak, vagyis valódi beszúrásról beszélünk, nem cseréről!
53 – Az String kezelésből már ismert .indexOf(elem) metódussal megkaphatjuk egy adott elem helyét a listában. Ha az eredmény -1, akkor nincs a listában. Az indexOf() mindig a lista elejéről indítja a keresést, és több előfordulás esetén az első találat helyét adja meg. A lastIndexOf(), hasonlóan a String témakörben tanulthoz hátulról adja meg az első előfordulás helyét, és -1-et ha nincs találat.
64 – A .contains(elem) logikai választ (boolean) ad arra a kérdésre, hogy az adott elem benne van-e a listában.
82-83 – A set(index, elem) metódus az index helyen lévő elemet cseréli fel az általunk megadottra. Ilyenkor a mögötte álló elemek a helyükön maradnak, vagyis nem beszúrás történik. Jellemzően az elemek felcserélésekor használjuk, hiszen az elemek eltávolítása és hozzáadása nem így történik.
102 – Ez egy komplexebb példa. Egy listából ki lehet törölni egy másik lista elemeit. Jelen esetben a .subList(int start, int end) metódust használom. A for ciklusban megnézem, hogy a rendezett tömbben hol található az első nem negatív elem. Ennek a helye i lesz. A szamok.subList(0, i) azt jelenti, hogy a 0 indextől az i előtti indexig tartó elemeket kiemelem a listából, majd ezt a kapott listát odaadom a removeAll metódusnak, hogy ezeket törölje a szamok listából.
Ezek a példák lefedik az ArrayList témakör nagy részét. Persze vannak még finomságok benne, de úgy gondolom indulásnak ennyi pont elég. Egy fontos dolgot viszont megemlítenék:
Az ArrayList is túlindexelhető! Nem hivatkozhatsz olyan indexű elemre, ami nem létezik!
ArrayList, de nem minden áron
A helyzet az, hogy nem minden esetben éri meg az ArrayList-et használni. Tény, hogy rengeteg mindent tud, de a tömböket nem válthatja ki teljes mértékben. Tisztázzunk akkor pár irányelvet, melyet figyelembe kell venni, hogy ha választanod kell a tömb és az ArrayList között.
Tömb:
Ha a tanulmányaid elején jársz.
Ha előre tudod, hány elemet szeretnél tárolni, és nem akarod bővíteni a számukat.
Ha csak primitív értékeket tárolsz.
Ha az alap algoritmusokat még nem alkalmazod hibátlanul.
ArrayList:
Ha már az alap algoritmusokat tetszőleges feladatokban hibátlanul alkalmazni tudod.
Ha objektumokkal dolgozol.
Ha a tárolt elemeid száma változhat.
Ha a tömbök már inkább korlátoznak, mint segítenek.
Diamond operátor
A 7-es verziójú Java-tól kezdődően bevezették az úgynevezett diamond operátort. Ez valójában nem operátor, de hivatalos Java oldalon is így nevezik, valamint rengeteg hivatkozás is ilyen névvel illeti. Arról van szó, hogy a lista deklarálása után az inicializáláskor nem kötelező a típust megadni, a szerkezetből elhagyható. A 6. sorban lévő eredetileg ismertetett deklarálást és inicializálást rövidítheted a 7. sorban látható módon. A diamond talán a típuselhagyás után ottmaradó <> jelek alakjára utal. Azért mutattam meg ezt a dolgot, mert újabb kódokban már nem találkozhatsz ilyennel, de régiekben még a megjegyzésben szerepló forma is előfordulhat. Nem hiba, csak már felesleges ismét kiírni a típust.
1
2
3
4
5
6
7
8
9
import java.util.ArrayList;
public class Lista
{
public static void main(String[] args )
{
// ArrayList<Integer> szamok = new ArrayList<Integer>();
ArrayList<Integer> szamok = new ArrayList<>();
}
}
https://www.webotlet.hu/?p=1374
Nincsenek megjegyzések:
Megjegyzés küldése