VII. Fájlok kezelése

Bevezetés

A grafikus felületek programozásánál már megtapasztaltuk, hogy a NetREXX-bôl néha hiányoznak bizonyos építôelemek, így ezeket a Java nyelvben található megfelelôjükkel kell helyettesíteni. Nincs ez másként a fájlkezeléssel sem. A NetREXX (a say és az ask utasítás kivételével) nem definiál adat be- és kivitelre alkalmas utasításokat, így csak a Java nyelv által megvalósított I/O osztályokra hagyatkozhatunk.

Adatfolyamok

A Java-ban beviteli adatfolyamnak (input stream) azokat az objektumokat, amelyek adatot szolgáltatnak. Azokat az objektumokat, amelyekbe adatot írhatunk, kiviteli adatfolyamnak (output stream) nevezzük. Ezen két fô típuson túlmenôen a Java nyelv több mint 20 alváltozatot különböztet meg. Az adatfolyamokra egyszerre több forrás vagy nyelô is rácsatlakozhat. Egy adatokat szolgáltató objektumra (input stream) például egyszerre csatlakozhat egy nyomtató, egy fájl és egy hálózati port. A streamek szempontjából mindegy, hogy milyen objektumok csatlakoznak hozzájuk, mivel azok általános I/O metódusokon keresztül használják ôket.

A fájl osztály

A fájlok és könyvtárak használata során a fájl (File) osztályból képzett objektumokkal kell dolgoznunk. A fájl osztály számtalan metódust tartalmaz, amellyel információt gyûjthetünk az állományokról, vagy éppen kedvünkre manipulálhatjuk ôket. Elsô példaprogramunk a fájl osztály metódusainak használatát mutatja be:

/* lecke07a.nrx - fájl és könyvtár-információk megjelenítése */

parse arg fileName .
if fileName = "" then do
 	say "Adja meg a vizsgálandó fájl vagy könyvtár nevét!"
 	filename = ask
end

f1 = File(fileName) 		-- elkészítjük a fájlobjektumot
if f1.exists() = 0 then do
 	say 'A megadott fájl ('filename') nem létezik!'
 	exit 8
end

say "-- A rendszerrel kapcsolatos információk:"
say "Path elválasztó :" f1.pathSeparator
say "Path elválasztó karakter :" f1.pathSeparatorChar
say "Elválasztó :" f1.separator
say "Elválasztó karakter :" f1.SeparatorChar
say

say "-- Az állománnyal kapcsolatos információk:"
say "Olvasható:" f1.canRead()
say "Irható   :" f1.canWrite()
say "Könyvtár :" f1.isDirectory()
say "Fájl     :" f1.isFile()
say "Méret    :" f1.length()
say "Módosítva:" f1.lastModified() "=" Date(f1.lastModified())
say "Abszolút :" f1.isAbsolute()
say "Teljes elérési út:" f1.getAbsolutePath()
say "Kanonikus elérési út:" f1.getCanonicalPath()
say "Elérési út:" f1.getPath()
parent1 = f1.getParent()
if parent1 = null then parent1 = "nincs szülô (null)"
say "Szülô    :" parent1
say "Név      :" f1.getName()
say "ToString :" f1.toString()
say "Hash kód :" f1.hashCode()

if f1.isDirectory() then do
 	say
 	say "-- A könyvtár tartalma:\n"
 	list1 = f1.list()
 	if list1.length = 0 then say "A könyvtár üres"
 	else
 		loop i = 0 to list1.length -1
 			f2 = File(f1.getAbsolutePath()' 'f1.separator' 'list1[i])
 			if f2.isDirectory() then say "Könyvtár:" list1[i]
			else say " Fájl:" list1[i]
 		end
end

A példaprogram bekéri a vizsgálandó állomány nevét és létrehozza a hozzátartozó fájlobjektumot. Amennyiben az objektum sikeresen létrejön, kilistázzuk az összes, az objektummal kapcsolatos változó értékét, illetve a használható metódusok által visszaadott értékeket. Könyvtárak esetén azok tartalmát is kilistázza a program. Az elôzô példában bemutatott metódusok mellett a további metódusok állnak még rendelkezésre:

Metódus:Feladat:
delete()Törli a fájlobjektumot
equals(object)Összehasonlítja a referált és a paraméterként megadott objektumot
list(fájlszûrô)Visszaadja mindazon fájlok listáját, amelyek eleget tesznek a fájlszûrônek
mkdir()Elkészíti a referált objektum nevének megfelelô könyvtárat
mkdirs()Elkészíti a referált objektum nevének megfelelô könyvtárat, beleértve a fôkönyvtárakat is
renameTo(File)Átnevezi az objektumot a paraméterben megadott névnek megfelelôen

Soronkénti olvasás és írás

Gyakran van szükségünk arra, hogy egy adatfolyamot soronként olvassunk vagy írjunk. Az olvasásra használhatjuk a BufferedReader osztály readLine metódusát. Az írásra többek között a PrintWriter osztály println metódusa is alkalmas, amely a platformtól függôen automatikusan a helyes sorlezárást használja. UNIX rendszereken az új sor karaktert, Windows vagy OS/2 alatt pedig az új sor és a kocsi vissza karaktereket használja az automatikus lezárásra. Az alábbi példaprogram az említett metódusokat alkalmazza arra, hogy kikeresse valamint kiírja a CONFIG.DEV fájlba és a képernyôre a C:\CONFIG.SYS fájlban található DEVICE kezdetû sorokat:

/* lecke07b.nrx - DEVICE kezdetû sorok kiválogatása */

filename = "C:/CONFIG.SYS"
output = "CONFIG.DEV"
say "Fájl:" filename "->" output
inFile = FileReader(filename)		-- a bemeneti fájl
source = BufferedReader(inFile) 	-- az olvasási osztály
outFile = FileWriter(output) 		-- a kimeneti fájl
dest = PrintWriter(outFile)		-- az írási osztály

loop forever
 	textline = source.readLine()	-- beolvassuk a bemeneti fájl sorait
 	if textline = null then leave 	-- elértük a fájl végét?
 	parse textline word1 "=" .
 	if word1 = "device" then do	-- ez egy DEVICE sor?
 		dest.println(textline) 	-- kiírjuk a találatot
 	end
end
source.close() 			-- bezárjuk a fájlokat
dest.close()

say "A következô DEVICE sorokat tartalmazza a C:\\CONFIG.SYS:"
source = BufferedReader( FileReader(output) ) -- kiolvassuk az eredményt
loop until textline = null
 	textline = source.readLine()
 	if textline \= null then say textline 		
end

Mazochisták használhatják a BufferedWriter osztályt is a PrintWriter helyett, ekkor viszont nekik kell gondoskodni a sorok helyes lezárásáról, amelyet a newline metódussal tehetnek meg.

Bájtorientált fájlkezelés

A bájtorientált fájlkezelés azt jelenti, hogy nem soronként, hanem bájtonként olvassuk (és esetenként írjuk is) az adatfolyamokat. A lassúsága miatt nem nagyon találkozunk ezzel a megoldással, de talán mégis érdemes néhány percet szentelni erre a témára is. A bájtonkénti beolvasásra a DataInputStream osztály readUnsignedByte metódusát használhatjuk. A DataInputStream objektumot a FileInputStream objektumból, ez utóbbit pedig a File objektumból állíthatjuk elô:

parse arg inFileName . 				  -- bekérünk egy fájlnevet
infile = File(inFileName) 			  -- ebbôl lesz egy bemeneti fájlobjektum
source = DataInputStream(FileInputStream(inFile)) -- ebbôl lesz az adatforrás objektuma
ch = Rexx source.readUnsignedByte() 		  -- beolvassuk az elsô bájtot

Adatorientált fájlkezelés

Gyakran van szükség olyan változók beolvasására vagy kiírására, amelyek valamilyen alap-adattípust (integer, float, stb.) képviselnek. A DataInputStream és DataOutputStream osztályok a következô metódusokat valósítják meg, amelyekkel ezek az adatok is eredményesen kezelhetôk:

Metódus:Feladat:
writeIntInteger (egész szám) kiíratása
writeFloatFloat (lebegôpontos szám) kiíratása
writeUTFUnicode karakter kiíratása
readIntInteger (egész szám) beolvasása
readFloatFloat (lebegôpontos szám) beolvasása
readUTFUnicode karakter beolvasása

Ezen metódusok elônye, hogy platformfüggetlenek és kompakt adatfájlokat lehet velük megvalósítani. Hátrányuk, hogy a képzett adatfájl szemmel nem értelmezhetô (bináris), így a hibakeresés nehezebb, mint a szöveges fájlokat használó metódusok esetében.

Egy másik - REXX programozók számára sokkal kézenfekvôbb megoldás - a REXX karakterláncok használata az adatok ki- és bevitelére. Ennek a megközelítésnek megvan az az elônye, hogy a különbözô adattípusokat nem kell külön kezelnünk, hiszen egy REXX karakterlánc bármelyik adattípust képes befogadni. A másik kedvezô tulajdonság az, hogy a REXX karakterláncok kiíratása után keletkezô adatállomány olvasható (szöveges) formátumú lesz. Az alábbi példaprogramban felhasználók (customer) adatait adminisztráljuk. A különbözô adatmezôket a "\t" karakterrel választjuk el egymástól. Az egyszerûség kedvéért a programba kódolt négy vevô adatait íratjuk ki a lecke07c.dat fájlba. A második lépésben visszaolvassuk a kiírt adatokat, majd pedig megjelenítjük a képernyôn:

/* lecke07c.nrx -- adat ki- és bevitel REXX sztringekkel */

class lecke07c
Properties constant
yes = boolean 1
no = boolean 0

method main(args = String[]) static
custDB = Customer[4] -- helyet foglalunk 4 vevô számára

-- a vevô objektumok megadása
custDB[0] = Customer(101, "Ueli Wahli",          "U.S.A.",  500.5, 25, yes)
custDB[1] = Customer(102, "Peter Heuchert",      "Germany", 400.4, 30, yes)
custDB[2] = Customer(103, "Frederik Haesbrouck", "Belgium", 350.9, 24, no)
custDB[3] = Customer(104, "Norio Furukawa",      "Japan",   250.5, 39, no) 

-- kiíratjuk az objektumok paramétereit a lecke07c.dat fájlba
os = PrintWriter(FileWriter("lecke07c.dat"))
os.println(custDB.length) -- az objektumok száma
loop i = 0 to custDB.length - 1
	custdata = custDB[i].getCustNo() || " \t" | | custDB[i].getName() || " \t" -
 	custDB[i].getAddress() || " \t" | | custDB[i].getHourly() || " \t" -
  	custDB[i].getWork() || " \t" | | custDB[i].getBool()
 	os.println(custdata)
end
os.close()

-- visszaolvassuk az objektumok értékeit az adatfájlból
is = BufferedReader(FileReader("lecke07c.dat"))
n = is.readLine() -- beolvassuk a feldolgozandó objektumokat
loop i = 1 to n
 	parse is.readLine() xcustno " \t" xname " \t" xaddress " \t" xhourly " \t" xwork " \t" xbool
 	say xcustno.left(4) xname.left(20) xaddress.left(10) (xhourly*xwork).right(10) xbool
end
is.close()

-- a Customer osztály definiálása
class Customer
properties private
custNo = String
name = String
address = Rexx
hourly = float
work = int
bool = boolean

method Customer(aCustNo=String, aName=String, aAddress=rexx, aHourly=float, aWork=int, aBool=boolean)
custNo = aCustNo; name = aName; address = aAddress
hourly = aHourly; work = aWork; bool = aBool

method getCustNo() returns String
return custNo

method getName() returns String
return name

method getAddress() returns Rexx
return address

method getHourly() returns float
return hourly

method getWork() returns int
return work

method getBool() returns boolean
return bool

Objektum-orientált fájlkezelés

A JDK 1.1-ben bevezetett szérializáció segítségével egyetlen adatobjektumot olvashatunk be vagy írhatunk ki a readObject és a writeObject metódusokkal. Az objektumok szérializálását az ObjectOutputStream és ObjectInputStream adatfolyammá történô transzformálással érhetjük el. Ezt azonban csak akkor tehetjük meg, ha osztályunkba elôzôleg beépítettük a java.io.Serializable interfészt. Az alap-adattípusok esetében ezzel készen is vagyunk. Speciális adattípusok esetében viszont implementálnunk kell még a writeObject és readObject metódusokat is. Mivel a NetREXX 1.0-s kiadásában a REXX karakterlánc osztály még nem volt szérializálható (az 1.1-ben már az), ezért ezt a módszert nem alkalmazhatjuk az elôzô program Customer osztályának átalakítására. A következô példaprogramban ezért Customer2 néven kissé újraírtuk ezt az osztályt. A példaprogram futása után vizsgáljuk meg egy hexa editorral a köztes fájl (lecke07d.dat) tartalmát! Azt fogjuk tapasztalni, hogy a fájl most már bináris formátumú és információt tartalmaz az osztályról (Customer2) is!

/* lecke07d.nrx */

class lecke07d
Properties constant
yes = boolean 1
no = boolean 0

method main(args=String[]) static
custDB = Customer2[4] -- lefoglaljuk a helyet 4 vevô számára
custRD = Customer2[]  -- visszaolvasunk "x" vevôt

-- definiáljuk a vevôket
custDB[0] = Customer2(101, "Ueli Wahli",          "U.S.A.",  500.5, 25, yes)
custDB[1] = Customer2(102, "Peter Heuchert",      "Germany", 400.4, 30, yes)
custDB[2] = Customer2(103, "Frederik Haesbrouck", "Belgium", 350.9, 24, no)
custDB[3] = Customer2(104, "Norio Furukawa",      "Japan",   250.5, 39, no)

-- kiíratjuk az objektumot fájlba
say "A "custDB.length "vevô adatainak kiíratása folyik..."
os = ObjectOutputStream(FileOutputStream("lecke07d.dat"))
os.writeInt(custDB.length) -- a vevôk száma
os.writeObject(custDB) -- egyetlen függvényhívással kiíratjuk az összes vevôt
os.flush() 
os.close()

-- visszaolvassuk az objektumot
say "A visszaolvasás folyik..."
is = ObjectInputStream(FileInputStream("lecke07d.dat"))
n = is.readInt() -- a vevôk száma
say "A visszaolvasott "n" vevô adatai:"
custRD = Customer2[] is.readObject() -- egyetlen hívással olvassuk be az összes vevôt
loop i = 0 to custRD.length - 1
 	say custRD[i].getCustNo() (Rexx custRD[i].getName()).left(20) -
 	(Rexx custRD[i].getAddress()).left(10) -
 	(Rexx custRD[i].getHourly() * custRD[i].getWork()).right(10) -
 	custRD[i].getBool()
end
is.close()

-- A Customer osztály szérializált változata
class Customer2 implements Serializable
properties private 
custNo = String
name = String
address = String -- Rexx string nem megengedett!
hourly = float
work = int
bool = boolean

method Customer2(aCustNo=String, aName=String, aAddress=rexx, aHourly=float, aWork=int, aBool=boolean)
custNo = aCustNo; name = aName; address = aAddress
hourly = aHourly; work = aWork; bool = aBool

method getCustNo() returns String
return custNo

method getName() returns String
return name

method getAddress() returns Rexx
return address

method getHourly() returns float
return hourly

method getWork() returns int
return work

method getBool() returns boolean
return bool

Különleges helyzetek lekezelése

Az adatfolyamok olvasása és írása közben érhetnek bennünket kellemetlen meglepetések. Elôfordulhat például, hogy olvasás közben az adatfolyam végére érünk. Fájlok esetében ezt nevezik fájlvég (EOF) kondíciónak, s ezt kétféleképpen is lekezelhetjük. A legkézenfekvôbb eljárás az olvasáshoz használt metódus visszatérési értékének ellenôrzése. Ha például a BufferedReader vagy DataInputStream osztály readLine metódusát használjuk, akkor a null visszatérési értéket kapjuk EOF kondíció esetén. A BufferedReader osztály read metódusa viszont már -1-et ad vissza ebben az esetben. Az alábbi kódrészlet azt mutatja, hogy hogyan lehet az EOF eseményre reagálni a visszatérési érték ellenôrzésével:

/* eof.nrx */
source = BufferedReader(FileReader("test.dat"))
loop forever
	textline = source.readLine()
	if textline = null then leave -- <=== itt hagyjuk el a hurkot EOF esetén
	say textline
end

A másik lehetséges módszer a Java kivételkezelô mechanizmusán (exception handling) alapszik. A DataInputStream szinte mindegyik metódusa kivételes eseményt (exception) jelez ha egy REXX változóhoz a null értéket rendeljük. Ezt a következôképpen használhatjuk ki a hurokból való kiszállásra:

/* eof2.nrx */
source = DataInputStream(FileInputStream("test.dat"))
loop forever
	textLine = Rexx source.readLine()
 	say textLine

 	catch NullPointerException -- <=== itt szállunk ki a hurokból
end

REXX GYÍK:

K1. Mi a célja az unicode karaktereknek?
V1. Az unicode karakter nem más, mint egy olyan karakter, amelyet nem 8 hanem 16 bájton tárolnak. Az unicode karakterekkel már el lehet tárolni az összes európai nyelv által használt karaktert, így elvileg nincs szükség a jelenleg még oly sok bosszúságot okozó kódtáblázatokra. Sajnos a 16 bájt még mindig nem ad annyi kombinációt, hogy a keleti nyelvek (pl. kínai) által használt összes karaktert is el lehessen tárolni.


Gyakorlat:

1. Készítsen egy NetREXX programot, amelyik beolvas egy fájlt és kiírja a fájl hexadecimális reprezentációját!

2. Készítsen egy NetREXX programot, amely kikeresi az összes sort egy fájlból, amelyek egy elôre megadott szót tartalmaznak!

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