11. gyakorlat
A gyakorlat anyaga¶
Stringek feldarabolása¶
Stringek split() metódusa¶
Szövegek feldarabolására két módot fogunk nézni, az első: a String
objektumok rendelkeznek egy split()
nevű metódussal, ami egy reguláris kifejezést vár paraméterül, amely mentén tagolja a szöveget, és egy String tömbbel tér vissza, amely tartalmazza a szövegdarabokat. Bővebben a String#split() metódusáról.
String mondat = "Ez a mondat hat szóból áll.";
String[] szavak = mondat.split(" ");
for(int i=0; i < szavak.length; i++) {
System.out.println("Az " + i + ". szo: " + szavak[i]);
}
Ennek kimenete:
Az 0. szo: Ez
Az 1. szo: a
Az 2. szo: mondat
Az 3. szo: hat
Az 4. szo: szóból
Az 5. szo: áll.
StringTokenizer¶
Egy másik lehetőség a StringTokenizer
nevű osztály használata, amely a java.util
csomagban található, így előbb be kell importálnunk ezt az osztályt. Ezek után használhatjuk a kódunkban az osztályt, amelyet példányosítanunk kell, a konstruktorban egy szöveget vár, és opcionálisan egy szöveget, amelynek minden karaktere szóhatárt jelöl. Ezek után használhatjuk a StringTokenizer
objektumunkat, ennek van egy hasMoreTokens()
nevű metódusa, ami azt mondja meg, hogy van-e még elem a darabolt szövegben: igazzal tér vissza ha van, hamissal különben. Egy szó darabkát lekérni a nextToken()
metódussal lehet, amely az aktuális szövegtördelékkel tér vissza.
A StringTokenizer alapértelmezett szeparátorai a következő karakterek: " \t\n\r\f"
- szóköz
- tab karakter
- újsor karakter
- kocsi vissza (carriage return) karakter
- line feed
Ha ezektől eltérő karakterekkel szeretnénk darabolni a szövegünket, akkor a konstruktorban második paraméterként megadhatjuk azokat a karaktereket, amelyek mentén szeretnénk darabolni (a karakterlánc bármely karakterére darabol).
Bővebben a StringTokenizer osztályról
import java.util.StringTokenizer; //Fontos, hogy beimportáljuk használat előtt
public class Main {
public static void main(String[] args) {
String str = "abcd, szoveg,valami kiscica;kiskutya;medve hóember péklapát";
// StringTokenizer létrehozása alapértelmezett szeparátorral
StringTokenizer st = new StringTokenizer(str);
System.out.println("StringTokenizer első futás az str-en: (alapértelmezett szeparátorral)");
while (st.hasMoreTokens()) {
String tmp = st.nextToken();
System.out.println(tmp);
}
System.out.println();
System.out.println("StringTokenizer második futás az str-en: (; , . szeparátorokkal)");
st = new StringTokenizer(str, ";.,");
while (st.hasMoreTokens()) {
String tmp = st.nextToken();
System.out.println(tmp);
}
}
}
Ennek kimenete:
StringTokenizer első futás az str-en: (alapértelmezett szeparátorral)
abcd,
szoveg,valami
kiscica;kiskutya;medve
hóember
péklapát
StringTokenizer második futás az str-en: (; , . szeparátorokkal)
abcd
szoveg
valami kiscica
kiskutya
medve hóember péklapát
Beolvasás standard inputról¶
Ahogy az 1. gyakorlaton láttuk, a beolvasáshoz egy új Scanner
objektumot hozunk létre, aminek átadjuk a System
osztály in
adattagját. A Scanner
sokféle bemenetről tud beolvasni (például fájlokból is), ezért vár a konstruktora egy InputStream
objektumot. Ez lesz esetünkben a System.in
.
import java.util.Scanner;
public class Beolvasas {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Hello! Hogy hívnak?");
String nev = sc.nextLine();
System.out.println("Hello " + nev + "! Hany eves vagy?");
int kor = sc.nextInt();
System.out.println("Hello " + nev + ", aki " + kor + " eves.");
}
}
Ha egy osztályon belül több metódusban is használni szeretnénk a standard inputról olvasó Scanner
-t, akkor érdemes egy static
adattagban eltárolni, felesleges minden használatkor új példányt létrehozni belőle.
import java.util.Scanner;
public class Main {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
scanner.nextLine();
method1();
}
private static void method1() {
scanner.nextLine();
}
}
Fájlkezelés¶
Javaban a fájlkezelés is objektumokon keresztül történik, azonban mivel a fájlok programok között megosztott erőforrások, kicsit eltérően kell velük bánni, mint a "hagyományos" objektumokkal. Amikor egy fájlkezelő objektum létrejön, akkor az az operációs rendszeren keresztül megnyitja az adott fájlt írásra vagy olvasásra. Amíg a programunk nyitva tart egy fájlt, addig annak az elérése más programból korlátozott lehet. Például ha egy fájlt olvasásra nyitunk meg, akkor más program nem tudja törölni, vagy akár módosítani sem azt a fájlt. Ezért fontos, hogy amint nincs szükségünk egy fájlra, rögtön "becsukjuk" azt, minél rövidebb ideig foglalva azt. Természetesen, amikor a Garbage Collector felszabadítja a fájlkezelő objektumunkat, akkor az automatikusan becsukja a fájlt, ezt azonban nem tudjuk előre, hogy mikor fog megtörténni, akár jóval később, mint amikor ténylegesen utoljára használtuk a fájlt. Ha írtunk a fájlba, előfordulhat, hogy eddig a pillanatig a tartalma nem is kerül kiírásra a lemezre, bent marad a memóriában (pufferben).
Fájlkezelés során különböző hibák is előfordulhatnak, melyek kezelésére szintén oda kell figyelnünk. Ilyen lehet például, ha nem létezik az olvasásra megnyitandó fájl, nincs jogunk írni egy fájlba, betelik a lemez, stb. Ezekben az esetekben természetesen kivételeket dobnak a fájlkezelő objektumok metódusai, amiket a mi dolgunk kezelni.
A fájlkezeléssel kapcsolatos osztályok a java.io
package-ben találhatóak, mivel I/O, azaz input/output műveleteket valósítanak meg.
A régi módszer¶
Fájl olvasása¶
Az alábbi példában a már ismert java.util.Scanner
osztály segítségével olvasunk be egy komplett fájlt soronként, és írjuk ki a tartalmát az alapértelmezett kimenetre.
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = null;
try {
scanner = new Scanner(new File(args[0]));
while(scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (IOException e) {
System.err.println("Hiba történt: " + e.getMessage());
} finally {
if (scanner != null) {
scanner.close();
}
}
}
}
Elrettentőnek tűnhet, kicsit talán az is, azonban a példán keresztül érthetjük meg, hogy hányféle helyen keletkezhet hiba, amikor fájlokkal dolgozunk. Gyakorlatilag bárhol. Menjünk végig sorról-sorra a kódon!
Először létrehozunk egy Scanner
típusú referenciát, aminek azonban a null
kezdőértéket adjuk, ugyanis a példányosítást már egy try
blokkba kell ágyazzuk, hiszen maga a konstruktor is dobhat kivételt (például ha a fájl nem létezik), és később a finally
blokkban szeretnénk használni a referenciát. A példában az első parancssori paramétert használtuk fel, aminek egy fájl elérési útjának kell lennie, ebből egy java.io.File
objektumot hozunk létre, amit közvetlenül átadunk a Scanner
konstruktorának. Ha sikerült megnyitni a fájlt (a konstruktor nem dobott kivételt), tovább haladunk. A while
ciklus feltételében felhasználjuk a Scanner.hasNextLine()
metódust, ami egy boolean
értékkel tér vissza (értelemszerűen true
ha van még a fájlból, false
ha a végére értünk), majd a ciklismagban System.out.println()
metódussal kiírjuk a kimenetre a nextLine()
hívással beolvasott sort.
Megjegyzés: A
File
osztály egy fájlrendszerbeli objektumot képvisel az elérési útjának ismeretében. Tud róla információkat adni (pl. mérete, módosítás ideje, jogosultságok, stb.), illetve bizonyos műveleteket végezni rajta (előbbi tulajdonságok módosítása, átnevezés, üres fájl létrehozása, mappa létrehozása, törlés, stb.).
A catch
blokkban lekezeljük a példányosítás vagy olvasás során esetlegesen keletkezett hibákat, amelyek a java.io.IOException
osztály leszármazottai lesznek (pl. FileNotFoundException
, AccessDeniedException
). Azt hihetnénk, hogy kész vagyunk, azonban (látva a kódot is) sajnos koránt sem. Ugyanis ha valamely olvasás során kapunk hibát (például a fájl közepén járunk amikor hirtelen elveszítjük a fájl olvasásának jogát), akkor a close()
metódus nem kerülne meghívásra. Az ilyen esetek miatt írunk egy finally
blokkot is a try
-hoz, amelyben amennyiben egyáltalán sikerült példányosítani a Scanner
objektumot, lezárjuk azt, így felszabadítva a lefoglalt erőforrást, amint arra nincs szükségünk.
Fájl írása¶
Fájl írása nagyon hasonlóan történik mint a beolvasás. A java.io.PrintStream
osztályt fogjuk használni, ami már ismerős lehet, hiszen a System.out
adattag is ilyen típusú. A példában az első argumentumként kapott fájlba fogjuk kiírni a többi argumentumot.
import java.io.*;
public class Main {
public static void main(String[] args) {
PrintStream printStream = null;
try {
printStream = new PrintStream(args[0]);
for (int i = 1; i < args.length; ++i) {
printStream.println(args[i]);
}
} catch (IOException e) {
System.err.println("Hiba történt " + e.getMessage());
} finally {
if (printStream != null) {
printStream.close();
}
}
}
}
A szerkezet gyakorlatilag megegyezik a beolvasásnál látottal.
Try-with-resources¶
A fenti két példát Java 7 óta tömörebben is le tudjuk írni a try-with-resources
szerkezet segítségével. Ez egy "tuningolt" try
blokk, ami egy vagy több erőforrást is kezel.
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File(args[0]))) {
while(scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (IOException e) {
System.err.println("Hiba történt: " + e.getMessage());
}
}
}
Lényege, a hogy a zárójelben deklarált változó(k) csak a scope-jában lesznek elérhetők (szemben a fentebbi kóddal, ahol a metódus scope-ba került), illetve automatikusan le is zárja őket, amint elhagyjuk a blokkot (a háttérben egy olyan finally
blokkot generál a try
végére, amilyet mi is írtunk fentebb). Működéséhez az erőforrásnak implementálnia kell az AutoCloseable
interfészt (ami egyetlen close()
metódust vár el), ahogy azt az összes beépített IO osztály meg is teszi.
Lássunk egy példát több erőforrást is kezelő try-with-resources
-re. A következő kód egy fájl tartalmát másolja egy másikba soronként.
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try (
Scanner scanner = new Scanner(new File(args[0]));
PrintStream printStream = new PrintStream(args[1])
) {
while(scanner.hasNextLine()) {
printStream.println(scanner.nextLine());
}
} catch (IOException e) {
System.err.println("Hiba történt: " + e.getMessage());
}
}
}
Ahogy láthatjuk, a try
zárójelében tetszőleges számú erőforrás-deklarációt tehetünk pontosvesszővel elválasztva. Ha bármelyik megnyitásakor kivétel dobódik, a már megnyitottak automatikusan lezárásra kerülnek, ami a régi módszerrel különösen macerás és csúnya volt. A blokkban a már látott while
ciklus dolgozik, ezúttal a System.out
helyett az általunk megnyitott printStream
objektumot használva kiírása.
Lambda kifejezések¶
Egy grafikus felülettel ellátott alkalmazás esetében, amikor egy gombra eseménykezelőt írunk (erre példa, a 08-Programozas-I.pdf
fájlban, egy másik kód itt), akkor egy névtelen interfész-implementációt készítünk, ami nagyban nehezíti a kód olvashatóságát, átláthatóságát. Mindemellet rengeteg felesleges kódrészlet is bekerül a kódunkba, amelyet Java 8-tól elkerülhetünk könnyedén, lambda kifejezések használatával.
A lambda függvények gyakorlatilag olyan névtelen metódusok, amelyet ott írunk meg, ahol használni szeretnénk. Gyakorlatilag akkor lehet haszna, ha például egy olyan interfészt szeretnénk helyben implementálni, aminek csak egy metódusa van, vagy például kollekciók hatékony, gyors, átlátható bejárásakor. Szóval egy interfész-implementációt tömörebben, gyorsabban, átláthatóbban írhatunk meg, mint eddig.
Mivel jelen gyakorlaton nem foglalkozunk Java GUI-val, így egy másik példán keresztül ismerjük meg őket, mégpedig a kollekciók segítségével. Először egy listát (de halmazon is ugyanígy működne) járunk be, majd pedig egy kulcs-érték párokból álló Map objektumot.
Egy lambda kifejezés szintaxisa: (paraméter1, paraméter2) -> utasítás, vagy utasítás blokk
. A paraméterek típusát nem kell kiírnunk (de kiírhatjuk őket, ha szeretnénk). Egy paraméter esetén elhagyhatjuk a paraméterek körüli zárójelet.
public class Main {
public static void main(String[] args) {
List<String> szinek = new ArrayList<>();
szinek.add("Kék");
szinek.add("Zöld");
szinek.add("Piros");
szinek.add("Fekete");
szinek.add("Sárga");
szinek.add("Narancs");
szinek.forEach(szin -> System.out.println(szin));
}
}
Láthatjuk, hogy mennyivel egyszerűbb használni, mint például egy hagyományos for ciklust. Amennyiben több utasítást használunk, akkor a megszokott módon kapcsos-zárójelek közé kell tenni az utasításokat a nyíl(->
) után.
public class Main {
public static void main(String[] args) {
List<String> szinek = new ArrayList<>();
szinek.add("Kék");
szinek.add("Zöld");
szinek.add("Piros");
szinek.add("Fekete");
szinek.add("Sárga");
szinek.add("Narancs");
szinek.forEach(szin -> {
if (szin.charAt(0) > 'O') {
System.out.println(szin);
}
});
}
}
A fenti példában végigmegyünk a listán, és megnézzük, melyik szín kezdődik egy 'O' után következő betűvel, és azokat írjuk ki az alapértelmezett kimenetre. Jelen helyzetünkbe talán ez nem tűnik nagy dolognak, mert sima iterátorral, vagy for ciklussal is bejárhattuk volna a listát, körülbelül ugyanennyi lenne kódban.
Azonban nézzük meg ezt a bejárást egy Map esetében, ahol már érezhetően egyszerűsödik a helyzetünk. (Csak hogy az előadáson látott GUI elemek eseménykezelőjéről ne is beszéljünk.)
public class Main {
public static void main(String[] args) {
Map<String, Integer> szinek = new HashMap<>();
// Megkérdeztünk 1000 embert, kinek mi a kedvenc színe, ezt tároljuk le
// ebben a mapben.
szinek.put("Kék", 320);
szinek.put("Zöld", 200);
szinek.put("Sárga", 80);
szinek.put("Barna", 95);
szinek.put("Citrom", 105);
szinek.put("Piros", 75);
szinek.put("Lila", 125);
szinek.forEach((szin, ertek) -> System.out.println(szin + " szín " + ertek + " ember kedvence."));
}
}
public class Main {
public static void main(String[] args) {
Map<String, Integer> szinek = new HashMap<>();
// Megkérdeztünk 1000 embert, kinek mi a kedvenc színe, ezt tároljuk le
// ebben a map-ben.
szinek.put("Kék", 320);
szinek.put("Zöld", 200);
szinek.put("Sárga", 80);
szinek.put("Barna", 95);
szinek.put("Citrom", 105);
szinek.put("Piros", 75);
szinek.put("Lila", 125);
szinek.forEach((szin, ertek) -> {
if (ertek > 100) {
System.out.println(szin + " szín " + ertek + " ember kedvence.");
} else {
System.out.println(szin + " szín nem túl sok ember kedvence.");
}
});
}
}
Látszik, hogy a fent ismertetettekkel ellentétben lambda kifejezéssel nagyon egyszerűen, átláthatóan járhatunk be egy map-et is. Remélhetőleg mindenki kedvet kapott a lambdák további megismeréséhez, nekik ajánljuk a következő linkeket:
Videók¶
- Egy OOP gyakorlófeladat (a fájlos példa alapja) (EAIJ): https://youtu.be/A1R_xDFqofg, https://youtu.be/yfWSMVfSKE4
- Fájlok beolvasása (EAIJ): https://youtu.be/FG7xqQC6vk4
- Fájlok írása (EAIJ): https://youtu.be/GxS-jAuTDzY
- Java a kurzus után (EAIJ): https://youtu.be/SbWZGVrdosY
Feladatok¶
- Írj egy programot, amelyben pgm fájlokat tudsz beolvasni és elmenteni, illetve ezeken mindenféle átalakításokat tudsz végezni! Próbáld a feladatot részekre szedni, azaz ne mindent egyetlen osztályban próbálj megvalósítani!
- haladó extra: Készíts a programodba olyan osztályt, ami meg is jelentíti a pgm fájlokat!
Példa input fájlok: torony-o.pgm, rektori.pgm.