9. hét Input-output műveletek
Tartalom:
9.1. Egyszerű konzol I/O műveletek
9.2. Karakteres fájlok kezelése
9.2.1. Karakteres fájlok olvasása
9.2.2. Karakteres fájlok írása
9.3. Bináris fájlok feldolgozása
9.4. Szöveges állományok kezelése
9.5. Állományok közvetlen elérése
9.6. Tesztkérdések
9.7. Feladatok
9.1. Egyszerű konzol I/O műveletek
Minden programozási nyelv alapfeladatai közé tartozik a külvilággal való kommunikáció, amely a legtöbb esetben az adatok olvasását jelenti egy bemeneti eszközről (pl. billentyűzet), ill. az adatok írását egy kimeneti eszközre (pl. képernyő, fájl). A Java nyelv az i/o műveleteket adatfolyamokon (ún. streameken) keresztül, egységesen valósítja meg. Egy dologra kell csak figyelni, hogy bemeneti vagy kimeneti csatornát kezelünk-e. A csatornák kezelése Java osztályokon keresztül valósul meg, azonban ezek nem részei a Java alap eszközkészletének, ezért importálni kell őket a java.io csomagból.
A konzol kezelésére a Java három adatfolyamot biztosít. Ezeket az adatfolyamokat nem kell megnyitni vagy bezárni, e műveletek automatikusan történnek.
A standard adatfolyamok a következők:
standard bemenet: System.in
standard kimenet: System.out
standard hiba: System.err
A konzol I/O műveletek használata esetén a
java.util
csomagból importálnunk kell a megfelelő osztályokat.
Az alábbi kis egyszerű program a standard be- és kimeneti konzol használatát mutatja be. A program egy egész számot vár a billentyűzetről, amit utána megjelenít a képernyőn:
Forráskód: io1.java
Az sc
nevű objektum a Scanner
osztály példánya lesz, amely osztály a beolvasáshoz a
System.in
(billentyűzet input) paramétert használja. Az egész típusú
a adattag
az sc
példány nextInt()
metódusa által megkapja a beírt számot, a standard
System.out.println()
metódus pedig megjeleníti a képernyőn.
Több szám bevitele esetén a számok közé alapesetben szóközöket kell beiktatni, amely jel a Java-ban felülbírálható. Az alábbi program az előző továbbfejlesztése. Több (akárhány!) egész számot vár - vesszővel elválasztva - a billentyűzetről, majd kiírja a darabszámukat és az összegüket:
Forráskód: io_token.java
Ebben az esetben a teljes input sort beolvassuk a
sor nevű
stringbe, majd a StringTokenizer
osztály segítségével a megadott határolójel mentén adategységekre (tokenekre)
feldarabolva feldolgozzuk azt. Nézzünk egy programfutási eredményt is:

9.2. Karakteres fájlok kezelése
A Java a fájlokat is streamként kezeli, ezért most is importálni kell a java.io csomag néhány osztályát. Az i/o típusok karakteresek (a Java az UTF-8-as Unicode karakterkódolást használja) és binárisak (8 bites bájtformátum) lehetnek.
A karakteres állományok kezelését három fő részre osztjuk:
megnyitás
műveletek
lezárás
A megnyitás művelete során valamely stream osztály objektuma jön létre (példányosítással), amelyhez hozzárendeljük a megnyitandó állományt. A fájlkezelő műveleteket try-catch-finally kivételkezelő blokkszerkezetbe kell foglalni, mert sok probléma adódhat használatuk során (pl. nem létező állomány, betelt háttértár, stb.). A következő példaprogramok mindegyikében lesz ilyen szerkezet, de a jobb áttekinthetőség miatt e forráskódok csak a minimálisan kötelező kivételkezelést tartalmazzák. A kivételkezelésről bővebben a 14. heti tananyagban lesz szó.
9.2.1. Karakteres fájlok olvasása
Karakteres állományok olvasására a FileReader osztály read() metódusa áll rendelkezésre. A metódus használatához implementálni kell a FileNotFoundException (a fájl nem található) kivételkezelő osztályt, és ennek ősét, az IOException (általános i/o hiba) osztályt is.
A következő példa egy szöveges állományból egyenként beolvassa a karaktereket, majd megjeleníti őket a képernyőn. Ha nem jelölünk ki pontos elérési utat, akkor a fájlnak a Java projekt főkönyvtárában kell lennie. A fájl végét a -1-es értékkel érzékeli a read() metódus.
Forráskód: io.java
A forrásállomány (szoveg.txt,
letöltés után nevezzük át szöveg.txt-re!)
az UTF-8-as Unicode kódolású szöveg mentését is lehetővé tevő Jegyzettömb programmal
készült. A 2. és 3. sorban használt kis- és nagybetűs tesztszövegben
(árvíztűrő tükörfúrógép) minden magyar ékezetes betű szerepel, így
könnyen ellenőrizhető a fájlkezelő metódusok betűhelyes működése.
A forrásállomány Jegyzettömb által mutatott tartalma :

A képernyőn megjelenő szöveg:

9.2.2. Karakteres fájlok írása
Karakteres állományok írására a FileWriter osztály write() metódusa áll rendelkezésre. Ehhez a metódushoz az IOException (általános i/o hiba) osztályt kötelező implementálni.
Az alábbi program egy string típusú változóban tárolt kétsoros szöveget - karakterenként feldolgozva - ír ki egy szöveges állományba. Ha nem jelölünk ki pontos elérési utat, akkor a fájl a Java projekt főkönyvtárában jön létre. Mivel könnyen lekérdezhető a string hossza, a karakterek feldolgozása nem okoz gondot.
Forráskód: io2.java
Nézzük a létrejött állomány (mentes.txt,
eredeti neve mentés.txt) tartalmát! Szerencsére a fájlba írás
karakterkódolásával semmi gond, és a Jegyzettömb is helyesen jeleníti meg:

9.3. Bináris fájlok feldolgozása
A bináris adatokat tartalmazó állományok kezelését a FileInputStream és a FileOutputStream osztályok biztosítják. Mindkét osztályt importálni kell a java.io csomagból. Metódusaik a szokásos read(), write() és close(). Használatuk hasonló a karakteres állományoknál látottakhoz.
Hozzunk létre egy binary.dat nevű bináris állományt! Bájtjainak értéke decimálisan 65-től 90-ig terjedjen!
Forráskód: io_bin.java
Bővítsük előző programunk
Main
metódusát! A
keletkezett állományt olvassuk be, és írjuk ki a bájtjai által reprezentált
karaktereket a képernyőre szóközzel elválasztva! Ne feledjük, hogy az importáló utasításokat bővíteni
kell a java.io.FileInputStream
osztállyal!
Forráskód: io_bin.java
A képernyőn a várt eredmény
látható:

9.4. Szöveges állományok kezelése
Szöveges állományok feldolgozása gyakran nem karakterenként, hanem nagyobb részenként, pl. soronként történik. Ehhez a Java ún. pufferelt fájlkezelési támogatást nyújt. Külön osztályok támogatják a byte- és string-szintű műveleteket:
Byte-szintű osztályok:
BufferedInputStream (olvasás)
BufferedOutputStream (írás)
PrintStream (írás)
String-szintű osztályok:
BufferedReader (olvasás)
PrintWriter (írás)
Számunkra most a stringek kezelése fontosabb, ezért nézzünk rá egy egyszerű példát! Írjunk ki egy sima szöveges állományba (text.txt) néhány UTF-8-as kódolású sort, majd az állományt visszaolvasva jelenítsük meg azokat a képernyőn is!
Írás a PrintWriter osztály println() metódusával:
Forráskód: io_txt.java
Olvasás a BufferedReader
osztály
readLine() metódusával:
Forráskód: io_txt.java
Figyeljük meg, hogy a
BufferedReader
osztály nem tud egy fájlt közvetlenül megnyitni, hanem csak a FileReader osztály
egy példányát. Az eredmény meggyőző:

9.5. Állományok közvetlen elérése
Az előző alfejezetek adatfolyam-kezelő műveletei mind soros hozzáférésűek voltak, tehát az állományok írása ill. olvasása az elejüktől a végükig sorban történt. Azonban lehetőség van arra is, hogy egy fájl tartalmához közvetlenül is hozzáférhessünk. Ezt a java.io csomag RandomAccessFile osztálya biztosítja. Ez az osztály egyszerre alkalmas olvasásra és írásra is, csupán a példányosítás során egy paraméterrel jelezni kell, hogy milyen műveletet kívánunk alkalmazni az objektumra:
csak olvasás: RandomAccessFile("input.txt", "r")
olvasás+írás: RandomAccessFile("input.txt", "rw")
Az így megnyitott állományokon használhatók a szokásos read() és write() metódusok, valamint a közvetlen elérést támogató metódusok:
void seek(long pos): megadott pozícióra ugrás a fájl elejétől
int skipBytes(int n): aktuális pozíció mozgatása n bájttal
long getFilePointer(): aktuális pozíció lekérdezése
Fontos tudni, hogy a read() és write() metódusok meghívása a fájlmutató (aktuális pozíció a fájlon belül - file pointer) értéket automatikusan megnöveli 1 egységgel, tehát nem szükséges manuálisan léptetni. Ezek a metódusok és a close() (fájl bezárása) is megkövetelik az IOException kivételkezelő osztály használatát.
Hozzunk létre egy állományt (random.txt), amely az angol ABC nagybetűit tartalmazza!
Forráskód: io_random.java
Majd minden ötödik karaktert - a fájlban elfoglalt
pozíciójukkal együtt - jelenítsük meg a képernyőn!
Forráskód: io_random.java
A raf.length()
metódussal lehet lekérdezni a fájl méretét. Figyeljük meg, hogy a kiírás során a
getFilePointer() pozíciólekérdező metódus
megelőzi az olvasó metódust, így a helyes értéket adja, de mégis meg kell
növelni az értékét 1-gyel, mivel a pozíciók számozása 0-tól kezdődik. Az
eredmény:

A kimenet utolsó sorát az alábbi kiegészítés állítja elő. A
fájl minden ötödik karakterét cseréljük le annak kisbetűs változatára, majd
jelenítsük meg a fájl összes karakterét!
Forráskód: io_random.java
A betűcserét végrehajtó ciklus minden ötödik betűre
pozícionál, majd az ott található betű kódjához 32-t adva éri el, hogy kisbetű
lesz belőle. A kiolvasás eltolja az aktuális pozíciót, ezért szükséges
visszalépni egyet. A betűk képernyőn való megjelenítését megelőzi a fájl
kezdőpozíciójába ugrás, majd egy sima (fájlvégjel-figyeléses) feldolgozó ciklus
kiírja a karaktereket.
9.6. Tesztkérdések
9.7.1. Kérjünk be két egész számot a billentyűzetről, és írjuk ki a képernyőre a szorzatukat!
9.7.2. A FileWriter és a FileReader osztályok segítségével írjuk ki az abc.txt fáljba az angol abc kisbetűit! Utána olvassuk be a fájlt, és az abc-t - nagybetűsre alakítva - írjuk ki a képernyőre!
9.7.3. A PrintWriter és a BufferedReader osztályokat felhasználva írjuk ki a hét napjait soronként a napok.txt szöveges fájlba! Ezután olvassuk be a fájlt, és a napokat egy sorban, vesszővel elválasztva jelenítsük meg a képernyőn!
9.7.4. A RandomAccessFile osztály metódusai segítségével cseréljük le az abc.txt fájl minden 3. betűjét nagybetűsre, majd a teljes fájlt írjuk ki a képernyőre!