20. Fájlkezelés

A legtöbb programnak szüksége van arra, hogy külső forrásból olvasson adatokat, vagy külső célba írja a működése (rész)eredményeit. A forrás és a cél sokféle lehet: konzol, háttértár, másik program, vagy egyéb speciális eszközök. Az adatok is különbözők lehetnek: számok, szövegek, vagy akár tetszőleges objektumok. Ez a fejezet bemutatja, hogy hogyan lehet Java nyelven kezelni az adatállományainkat.

20.1. Adatfolyamok

Információ kinyeréséhez a program megnyit egy adatfolyamot az információforráshoz (fájl, memória, socket), és sorban kiolvassa az információt, amint az ábrán látszik:

Olvasás fájlból

Hasonlóképpen, egy program külső célba is küldheti az információkat adatfolyam megnyitásával, és információkat írhat ki sorban, mint itt:

Fájlba írás

Nem számít, mekkora adat jön be, vagy megy ki, és nem számít a típusa, az algoritmus sorban olvassa és írja, a következőköz hasonlóan:

Olvasás

adatfolyam megnyitása
amíg van újabb információ
    olvasás
adatfolyam bezárása

Írás

adatfolyam megnyitása
amíg van újabb információ
    írás
adatfolyam bezárása

A java.io csomag tartalmazza azon osztályokat, melyek lehetővé teszik az írást és olvasást. Az osztályok használatához importálni kell a java.io csomagot.

Az adatfolyam osztályok két hierarchiára oszthatók az adattípusuk alapján (karakter vagy bájt):

Adattípusok

Karakter folyamok

Reader és Writer a java.io absztrakt ősosztályai. 16 bites karakterekkel való műveleteket teszik lehetővé. A Reader és Writer leszármazott osztályai speciális műveleteket végeznek, és két külön kategóriára oszthatók: az adat olvasás és írás (szürkével jelezve) és feldolgozások végrehajtása (fehérek). Az utóbbiakat szűrő folyamoknak is nevezzük. A Reader és Writer osztályok osztályhierarchiáját mutatja az ábra.

A Reader osztály

A Writer osztály

A legtöbb programnak szüksége van a Reader és Writer osztályokra szöveges információk olvasásához és írásához. Ezek bármilyen karaktert képes lekezelni Unicode karakterként.

Bináris folyamok

A 8-bites bájt írásához és olvasásához a programnak használnia kell a bináris folyamokat, melyek az InputStream és OutputStream leszármazottai. Ezek a folyamok jellemzően bináris adatok olvasására és írására alkalmasak, mint egy kép vagy hang. A bájt folyam két osztálya, ObjectInputStream és ObjectOutputStream használható objektumok soros folyamon keresztüli kezeléshez.

Az InputStream és OutputStream osztályok a Reader és Writer osztályokhoz hasonlóan két kategóriára oszthatók, a következő osztályhierarchia alapján: adatkezelő folyamok (szürke) és feldolgozó vagy más néven szűrő folyamok (fehér).

InputStream osztály

OutputStream osztály

Az I/O szülőosztályok

Reader és InputStream azonos API-kat határoznak meg, de különböző adattípusra. A Reader metódusai karakterek és karaktertömbök olvasására alkalmasak:

int read()
int read(char cbuf[])
int read(char cbuf[], int offset, int length)

Az InputStream hasonló metódusai bináris adatok és tömbök olvasására alkalmasak:
int read()
int read(byte cbuf[])
int read(byte cbuf[], int offset, int length)

A Reader és InputStream is megvalósítja az adatfolyamon belüli állapot jelölését, ugrálást, és az aktuális pozíció visszaállítását.

A Writer és OutputStream hasonló szolgáltatásokkal rendelkeznek. A Writer karakterek, és karaktertömbök írását valósítja meg:

int write(int c)
int write(char cbuf[])
int write(char cbuf[], int offset, int length)

Az OutputStream azonos metódusú de bináris adatra:
int write(int c)
int write(byte cbuf[])
int write(byte cbuf[], int offset, int length)

Mind a négy osztály esetén igaz, hogy automatikusan megnyílik az adatfolyam, ha létre van hozva az objektum. Minden adatfolyam bezárható a close meghívásával. A szemétgyűjtő is bezárja, ha már nincs rá hivatkozás, de célszerű inkább direkt módon meghívni a close metódust.

Megjegyezés: A program „kötelessége”, hogy minden erőforrást csak a ténylegesen szükséges ideig foglaljon le. Éppen ezért az állomány megnyitását olyan későn kell megtenni, amikor csak lehet, és ugyanilyen hamar be is kell zárni.

Az adatfolyam osztályok használata

A táblázat a java.io adatfolyamokat és feladatuk leírását tartalmazza.

I/O típus Osztályok Leírás
Memória CharArrayReader
CharArrayWriter

ByteArrayInputStream
ByteArrayOutputStream

Az adatfolyamok kiolvasásra és a memóriába írására használható. Az adatfolyamok létrehozhatók egy meglévő tömbbel, és az olvasást és írást használva kiolvas és a tömbbe ír.
StringReader
StringWriter

StringBufferInputStream

A StringReader karaktereket olvas a memóriából. A StringWriter egy String-be ír. A StringWriter kiírandó karaktereket gyűjt egy StringBuffer-be, ami String-be konvertál.

StringBufferInputStream hasonló a StringReader-hez, kivéve, hogy egy StringBuffer-ből karaktereket olvas.

Cső PipedReader
PipedWriter

PipedInputStream
PipedOutputStream

A bemenet és kimenet között valósít meg egy összekötő csövet. A csövek a kimenettől a másik bemenetig tartó folyamot valósítanak meg.
Fájl FileReader
FileWriter

FileInputStream
FileOutputStream

Együttesen fájl folyamoknak hívják, melyekkel fájlból olvashatunk, vagy fájlba írhatunk.
Lánc SequenceInputStream Több bemenet adatfolyamainak összefűzése egy bemeneti adatfolyamba
Objektum sorosítása ObjectInputStream
ObjectOutputStream
Objektumok soros szervezésű tárolásához és visszaállításához.
Adat konverzió DataInputStream
DataOutputStream
Primitív adattípusok írása és olvasá-sa gépfüggetlen formátumban
Számláló LineNumberReader
LineNumberInputStream
Nyomon követhető a sorok száma olvasáskor.
Előre olvasás PushbackReader

PushbackInputStream

A bemeneti adatfolyamok egy vissza-tehető pufferbe kerülnek. Az adatok olvasásakor néha hasznos a következő néhány bájt vagy karakter ismerete a következő lépés eldöntéséhez.
Kinyomtatás PrintWriter

PrintStream

Nyomtatási metódusokat tartalmaz. Az adatfolyamok tartalmának kiíratására használható.
Pufferelés BufferedReader
BufferedWriter

BufferedInputStream
BufferedOutputStream

Olvasáskor és íráskor az adatok pufferelődnek, ezáltal csökkentve a forrás adat eléréseinek számát. A pufferelt adatfolyamok hatékonyabbak, mint a nem pufferelt adatfolyamok, és gyakran használhatók más adatfolyamokkal is.
Szűrés FilterReader
FilterWriter

FilterInputStream
FilterOutputStream

Ezek az absztrakt osztályok a szűrő adatfolyamok vázát adják meg, milyen szűrés használható íráskor vagy olvasáskor.
Bájtok és karakterek közti konverzió InputStreamReader

OutputStreamWriter

Az olvasó és író páros hidat képez a bájt és a karakterfolyamok között.

Az InputStreamReader bájtokat olvas egy InputStream-ből és karakterbe konvertálja, az alapértelmezett karakterkódolást vagy egy megnevezett karakterkódolást használva.

Az OutputStreamWriter karaktereket konvertál bájtba, az alapértelmezett karakterkódolást vagy egy meg-nevezett karakterkódolást használva és a bájtokat egy OutputStream-be írja.

20.1.1 Fájl adatfolyamok használata

A fájl adatfolyamok talán a legkönnyebben megérthető adatfolyamok. A fájl adatfolyamok (FileReader, FileWriter, FileInputStream, és FileOutputStream) a natív fájlrendszer fájljait olvassák, vagy írják. Létrehozhatunk fájl adatfolyamot fájlnév String-ből, File objektummal, vagy FileDescriptor objektummal.

A következő Copy program FileReader és FileWriter segítségével az input.txt tartalmát átírja (átmásolja) az ouput.txt fájlba:

import java.io.*;
public class Copy {
    public static void main(String[] args) throws IOException{
        File inputFile = new File("input.txt");
        File outputFile = new File("output.txt");
        FileReader in = new FileReader(inputFile);
        FileWriter out = new FileWriter(outputFile);
        int c;
        while ((c = in.read()) != -1)
           out.write(c);
        in.close();
        out.close();
    }
}

A program nagyon egyszerű. A FileReader megnyitja a farrago.txt fájlt, a FileWriter pedig az outagain.txt fájlt. A program az in objektummal beolvas minden karaktert a bemeneten a bemeneti fájlból, és az out objektummal kiírja a karaktereket. Ha a bemenet elfogyott (-1), a program bezárja az olvasót és az írót is.

Megjegyzés: Érdemes megfigyelni, a read és write metódusok nem char, hanem int típussal dolgoz-nak. Ennek mindössze az az oka, hogy egy speciális, a char megengedett értéktartományán kívüli visszatérési értéket (a -1-et) is kell nyújtani abban az esetben, ha nincs több olvasható karakter, így a char nem lenne alkalmazható.

A Copy program ezt a kódot használja a FileReader létrehozásához:

File inputFile = new File("input.txt");
FileReader in = new FileReader(inputFile);

A kód létrehoz egy File objektumot, ami a fájl logikai leírására szolgál. A File segédosztályt a java.io tartalmazza. A Copy program ezt az objektumot használja a FileReader felépítéséhez. Azonban a program az inputFile-t akkor is használhatja, ha információt szeretnénk szerezni a fájl egészéről (pl. olvasható-e, mekkora a mérete stb).

A program futása után ugyanannak kell szerepelnie az input.txt és az output.txt fájlban is.

Ne feledjük, hogy a FileReader és a FileWriter 16-bites karaktert olvas és ír. Azonban, a legtöbb natív fájlrendszer 8-biten alapszik Az adatfolyamok kódolják a karaktereket, az alapértelmezett karakterkódolási séma alapján. A karakterkódolás megkapható a System.getProperty("file.encoding") használatával. Új alapértelmezett kódolás megadásához létre kell hozni egy OutputStreamWriter vagy egy FileOutputStream objektumot, és megadni a kódolást.

20.1.2 Szűrő adatfolyamok

A java.io csomag tartalmazza az absztrakt osztályok speciális beállításainak lehetőségét, mellyel definiálhatunk, és részlegesen implementálhatunk szűrőket, olvasás, illetve írás céljából. A használatos filter adatfolyamok a FilterInputStream, FilterOutputStream. Egy filter adatfolyam egy másik alapadatfolyamra épül, melybe a write metódus fog adatokat menteni, de csak a szűrés után. Vannak adatfolyamok, melyek speciális feladatokat látnak el, mint például a konvertálás vagy számlálás folyamata.

A legtöbb filter adatfolyamot a java.io csomag által szolgáltatott származtatott osztályok nyújtják, ezek a következők:

  • DataInputStream és DataOutputStream
  • BufferedInputStream és BufferedOutputStream
  • LineNumberInputStream
  • PushbackInputStream
  • PrintStream

A java.io csomag csak egy származtatott osztályát tartalmazza a FilterReader-nek, ez nem más, mint a PushbackReader. A következő fejezet bemutatja egy példán szemléltetve, hogyan használjuk a szűrő adatfolyamokat a DataInputStream és a DataOutStream használatával. Ugyancsak tárgyalja, hogyan hozhatunk létre származtatott osztályokat a FilterInputStream és FilterOutputStream-ből, melyek segítségével saját szűrő adatfolyamunkat valósíthatjuk meg.

Szűrő adatfolyamok használata

A szűrő objektumokat csatolni kell egy másik I/O adatfolyamhoz, példaként csatolhatunk szűrő adatfolyamot a standard input adatfolyamra:

BufferedReader d = new BufferedReader(new
    DataInputStream(System.in));
String input;

while ((input = d.readLine()) != null) {
    ... //do something interesting here
}

Megjegyzés: A readLine metódus nem használható a DataInputStream-ben, ezért hoztuk létre a BufferedReader típusú d szűrő objektumot.

A következőkben egy példán szemléltetjük a DataInputStream és DataOutputStream használatát. Ezek olyan szűrők melyek képesek írni/olvasni elemi adattípusokat.

Előfordul, hogy a feldolgozás független a kezelt adat formátumától, de néha szorosan összefügg az illető adatokkal vagy annak formátumaival, úgymint adatok írása, olvasása sorokból vagy cellákból.

DataInputStream és DataOutputStream használata

Ez a fejezet a DataInutStream és DataOutputStream osztályok használatát mutatja be egy DataIODemo példaprogramon keresztül, mely olvas és ír táblázatba (cellákba) foglalt adatokat. A cellák tartalmazzák az eladási árat, a rendelt áru mennyiségét, és a leírást. Elméletileg az adat a következőképpen néz ki, habár a gép ezt bináris adatként kezeli:

19.99   12      Java T-shirt
9.99    8       Java Mug

DataOutputStream-et, mint ahogyan más kimeneti adatfolyamokat, csatolni kell egy másik OutputStream-hez. Ebben az esetben a FileOutputStream-hez csatoltuk. Példa:
DataOutputStream out = new DataOutputStream(
                        new FileOutputStream("invoice1.txt"));

A DataIODemo a DataOutputStream speciális write metódusait használja írásra, amennyiben az megfelelő típusú.
for (int i = 0; i < prices.length; i ++) {
    out.writeDouble(prices[i]);
    out.writeChar('\t');
    out.writeInt(units[i]);
    out.writeChar('\t');
    out.writeChars(descs[i]);
    out.writeChar('\n');
}
out.close();

A DataIODemo program beolvassa az adatokat a DataInputStream speciális metódusai segítségével:
try {
    while (true) {
        price = in.readDouble();
        in.readChar();       //throws out the tab
        unit = in.readInt();
        in.readChar();       //throws out the tab
        char chr;
        desc = new StringBuffer(20);
        char lineSep =
            System.getProperty("line.separator").charAt(0);
        while ((chr = in.readChar() != lineSep) {
            desc.append(chr);
        }
        System.out.println("You've ordered " + unit
            +  " units of "
                           + desc + " at $" + price);
        total = total + unit * price;
    }   
} catch (EOFException e) { }
System.out.println("For a TOTAL of: $" + total);
in.close();

Mihelyst beolvasta az adatokat, a DataIODemo megjeleníti az összegzést, a rendelt és az összes mennyiséget, majd bezárul.

Megjegyzés: a DataIODemo által használt ciklus a DataInputStream-ből olvassa az adatokat:

while ((input = in.read()) != null) {
    . . .
}

A read metódus null értékkel tér vissza, mely a fájl végét jelenti.

Számos alkalommal azonban ezt nem tehetjük meg, a szabálytalanul megadott értékek esetén hibajelzést kapunk. Tegyük fel, hogy -1 értéket szeretnénk ugyanerre a célra felhasználni. Ezt nem tehetjük meg, ugyanis a -1 nem szabályos érték, kiolvasása a readDoublevagy readInt használatával történik. Tehát DataInputStream-ek esetén nem kapunk EOFException kivételt.

A program futása a következő kimenetet eredményezi:

You've ordered 12 units of Java T-shirt at $19.99
You've ordered 8 units of Java Mug at $9.99
You've ordered 13 units of Duke Juggling Dolls at $15.99
You've ordered 29 units of Java Pin at $3.99
You've ordered 50 units of Java Key Chain at $4.99
For a TOTAL of: $892.8800000000001

20.2. Objektum szerializáció

A java.io két adatfolyama az ObjectInputStream és az ObjectOutputStream abban más az átlagos bájtfolyamoktól, hogy objektumokat tudnak írni és olvasni.

Az objektumok írásának az alap feltétele, hogy egy olyan szerializált formára hozzuk az objektumot, ami elegendő adatot tartalmaz a későbbi visszaalakításhoz. Ezért hívjuk az objektumok írását és olvasását objektum szerializációnak.

Objektumokat a következő két módon lehet szerializálni:

  • Távoli eljáráshívás (Remote Method Invocation, RMI)
    Kliens és szerver közötti objektumátvitel esetén
  • Könnyűsúlyú perzisztencia (Lightweight persistence)
    Egy objektum archiválása későbbi használatra

20.2.1 Objektumok szerializálása

Az ObjectOutputStream írása

A következő kódrészlet megkapja a pontos időt a Date objektum konstruktorának a meghívásával, és aztán szerializálja az objektumot:

FileOutputStream out = new FileOutputStream("theTime");
ObjectOutputStream s = new ObjectOutputStream(out);
s.writeObject("Today");
s.writeObject(new Date());
s.flush();

Az ObjectOutputStream konstruktorát egy másik adatfolyamra kell meghívni. Az előző példában az ObjectOutputStream konstruktora egy FileOutputStream-re hívódik, így egy 'theTime' nevű fájlba szerializálja az objektumot. Ezek után egy String („Today”) és egy Date objektum íródik az adatfolyamra az ObjectOutputStream writeObject nevű metódusával.

A writeObject metódus szerializálja az adott objektumot, rekurzívan átmenti a más objektumokra való hivatkozásait, így megtartja az objektumok közötti kapcsolatot.

Az ObjectOutputStream implementálja a DataOutput interfészt, ami primitív adattípusok írására való metódusokat tartalmaz (writeInt, writeFloat, writeUTF…). Ezeket a metódusokat használjuk primitív adattípusok ObjectOutputStream-re való írásához.

A writeObject metódus NotSerializableException-t dob, ha a megadott objektum nem szerializálható. Egy objektum csak akkor szerializálható, ha implementálja a Serializable interfészt.

Az ObjectInputStream olvasása

A következő példaprogram kiolvassa a theTime fájlba előzőleg kiírt String és Date objektumokat:

FileInputStream in = new FileInputStream("theTime");
ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();

Az ObjectOutputStream-hez hasonlóan az ObjectInputStream konstruktorát is egy másik adatfolyamra kell hívni. Mivel egy fájlba írtuk az objektumokat, itt is egy FileInputStream-re kell hívni az ObjectInputStream-et. Az olvasást az ObjectInputStream readObject metódusa végzi. Az objektumokat mindenképpen abban a sorrendben kell kiolvasni, ahogy az adatfolyamra lettek írva.

A readObject metódus deszerializálja a soron következő objektumot. Rekurzívan vizsgálja a hivatkozásokat, hogy az összes hivatkozott objektumot is deszerializálja, így megtartva az objektumok közötti kapcsolatot.

Az ObjectInputStream adatfolyam implementálja a DataInput interfészt, ami primitív adattípusokat olvasó metódusokat tartalmaz. A DataInput metódusai a DataOutput metódusainak olvasó megfelelői (readInt, readFloat, readUTF...).

20.2.2 Objektum szerializáció a gyakorlatban

Egy objektum csak akkor szerializálható, ha az osztálya implementálja a Serializable interfészt. Ezt könnyen megtehetjük:

public class MySerializableClass implements Serializable {
   ...
}

A Serializable egy üres interfész (vagyis nincs egyetlen tagja sem), csak a szerializálható osztályok elkülönítésére való. Az osztály példányainak a szerializálását az ObjectOutputStream osztály defaultWriteObject nevű metódusa végzi. A deszerializáció az ObjectInputStream osztály defaultReadObject metódusával végezhető el. A legtöbb objektumhoz ez a módszer elég is. Néhány esetben viszont lassú lehet, és az osztálynak is szüksége lehet a szerializáció feletti határozottabb vezérlésre.

A szerializáció testreszabása

Az objektum szerializáció testreszabásához két metódus felülírására van szükségünk: a writeObject metódus (írásra) és a readObject (olvasásra vagy adatfrissítésre).

A writeObject deklarálása:

private void writeObject(ObjectOutputStream s) throws IOException {
    s.defaultWriteObject();
    // testreszabott szerializáló kód
}

A readObject metódust deklarálása:

private void readObject(ObjectInputStream s) throws IOException  {
    s.defaultReadObject();
    //  testreszabott deszerializáló kód
    ...
    // objektumokat frissítő kód (ha szükséges)
}

Mindkét metódust pontosan a bemutatott módon kell deklarálni.

A writeObject és a readObject metódusok csak az adott osztály szerializálásáért felelősek, a szülő osztályok esetleges szerializálása automatikus. Ha egy osztálynak a szülőosztályaival teljesen összhangban kell lennie a szerializációhoz, akkor az Externalizable interfészt kell implementálni.

20.2.3 Az Externalizable interfész implementálása

Hogy egy osztály teljes mértékben kontrollálni tudja a szerializációs folyamatot, implementálnia kell az Externalizable interfészt. Externalizable objektumoknál csak az objektum egy 'reprezentációja' kerül az adatfolyamra. Az objektum maga felelős a tartalom írásáért, olvasásáért és a szülőobjektumokkal való együttműködésért.

Az Externalizable interfész definíciója:

package java.io;
public interface Externalizable extends Serializable {
    public void writeExternal(ObjectOutput out)
        throws IOException;
    public void readExternal(ObjectInput in)
        throws IOException, java.lang.ClassNotFoundException;
}

Egy Externalizable osztálynál a következőket kell szem előtt tartani:
  • implementálnia kell a java.io.Externalizable interfészt.
  • implementálnia kell a writeExternal metódust az objektum állapotának az elmentéséhez.
  • implementálnia kell a readExternal metódust a writeExternal által az adatfolyamra írt tartalom visszaállításához.
  • külső adatformátum írásáért és olvasásáért kizárólag a writeExternal és a readExternal metódusok felelősek.

A writeExternal és a readExternal metódusok publikusak, így lehetőséget nyújtanak arra, hogy egy kliens az objektum metódusai nélkül is tudja olvasni/írni az objektum adatait, ezért csak kevésbé biztonságigényes esetekben alkalmazhatóak.

20.2.4 Az érzékeny információ megvédése

Ha egy osztályunk elérést biztosít bizonyos forrásokhoz, gondoskodnunk kell a kényes információk és függvények védelméről. A deszerializáció alatt az objektum private állapota is helyreáll. Például egy fájlleíró eljárás hozzáférést biztosít egy operációs rendszerforráshoz. Ha ez egy adatfolyamon keresztül áll helyre, a sebezhetősége miatt visszaélésekre ad lehetőséget. Ezért a valós időben történő szerializáció esetében nem szabad feltétlenül megbízni az objektum-reprezentációk valódiságában. Két módon oldhatjuk meg a problémát: vagy nem szabad az objektum érzékeny tulajdonságait az adatfolyamról visszaállítani, vagy ellenőrizni kell azokat.

A védelem legegyszerűbb módja az érzékeny adatok private transient-ként való feltüntetése. A transistent és a static adattagok nem kerülnek szerializálásra, így nem is jelennek meg az adatfolyamon. Mivel a private adattagok olvasását és írását nem lehet az osztályon kívülről helyettesíteni, az osztály transient tagjai így biztonságban vannak.

A különösen érzékeny osztályokat egyáltalán nem szabad szerializálni. Ez esetben egyszerűen nem szabad implementálni sem a Serializable, sem az Externalizable interfészt.

Néhány osztálynál különösen előnyös az írás és az olvasás arra való használata, hogy a deszerializálás közben kezelni és ellenőrizni tudjuk az érzékeny adatot. A writeObject és a readObject metódusok itt csak a helyes adatot menthetik, vagy állíthatják helyre. Ha a hozzáférés tiltására van szükség, egy NotSerializableException dobása elég, hogy megtagadjuk a hozzáférést.

20.3. Közvetlen elérésű állományok

Az ki és bemenő adatfolyamok eddig a leckéig soros hozzáférésűek voltak, melyek írása és olvasása az elejétől a végéig sorban történik. Bár az ilyen adatfolyamok hihetetlenül hasznosak, de ezek csak a soros hozzáférésű médiumok (papír és mágnesszalag) következtében jöttek létre. Másrészről a közvetlen hozzáférésű fájlok, nem-soros, hanem véletlenszerű (közvetlen) hozzáférést biztosítanak a fájl tartalmához.

Tehát miért van szükség közvetlen elérésű fájlokra? Gondoljunk csak a tömörített ZIP formátumra. A ZIP állományok fájlokat tartalmaznak, tömörítve a helytakarékosság miatt. A ZIP állományok tartalmaznak egy az állomány végén elhelyezett dir bejegyzést, mely megmutatja, hogy a különböző fájlok hol találhatók a ZIP belsejében.

Fájlszerkezet

Feltéve, hogy egy meghatározott fájlt akarunk kicsomagolni a ZIP-ből, soros hozzáférés esetén a következőket kell tennünk:

  • Megnyitjuk a ZIP fájlt
  • Végigkeressük a ZIP fájlt a keresett állományért
  • Kicsomagoljuk az állományt
  • Bezárjuk a ZIP-et

Ezt az algoritmust használva, átlagban legalább a ZIP állomány felét végig kell olvasni a keresett állományért. Kitömöríthetjük ugyanezt a fájlt a ZIP-ből sokkal hatékonyabban, ha a közvetlen elérésű fájl seek, azaz keresési funkcióját használjuk a következő lépések szerint:

  • Megnyitjuk a ZIP fájlt
  • Megkeressük a dir bejegyzést és azon belül a kitömörítendő fájl helyét
  • Megkeressük (visszafelé) a fájl pozícióját a ZIP-en belül
  • Kicsomagoljuk az állományt
  • Bezárjuk a ZIP-et

Ez a módszer sokkal hatékonyabb, mert csak a dir bejegyzést és kicsomagolandó fájlt kell beolvasni. A java.io csomagban található RandomAccessFile osztály implementálja közvetlen elérésű fájlokat.

20.3.1 A közvetlen elérésű állományok használata

A RandomAccessFile osztály mind írásra, mind olvasásra alkalmas, ellentétben a java.io csomag által tartalmazott ki-, és bemeneti adatfolyamokkal. Egy RandomAccessFile objektumot a szerint hozunk létre különböző paraméterekkel, hogy írni vagy olvasni akarunk vele. A RandomAccessFile nincs kapcsolatban a java.io csomag ki-, és bemeneti adatfolyamaival, és ez az, amiért nem az InputStream és OutputStream leszármazottja. Ennek az a hátránya, hogy a RandomAccessFile-ra nem alkalmazhatók azok a szűrők, amelyek az adatfolyamokra igen. Habár a RandomAccessFile implementálja a DataInput és DataOutput interfészt, tehát ha készítünk egy olyan filtert, mely vagy az DataInput-tal vagy a DataOutput-tal együttműködik, ez működni fog néhány soros elérésű állománnyal valamint az összes közvetlen elérésűvel.

Ha meg akarjuk nyitni olvasásra az input.txt állományt, akkor a következő kóddal tehetjük meg:
new RandomAccessFile("input.txt", "r");

Ha olvasni és írni is akarjuk:

new RandomAccessFile("input.txt", "rw");

Ezek után a read és write metódusok a szokásos módon állnak rendelkezésre.

Fájlmutató

Fontos további szolgáltatás, hogy ezen kívül az aktuális állomány pozíció közvetlenül is manipulálható a következő metódusokkal:

int skipBytes(int) előre mozgatja az aktuális pozíciót
void seek(long) a megadott pozícióra mozgatja az aktuális pozíciót
long getFilePointer() lekérdezi a pillanatnyi pozíciót

20.4. További osztályok és interfészek

Az előzőekben bemutatott osztályokon és interfészeken kívül a java.io csomag többek között a következő osztályokat és interfészeket tartalmazza:

File

Egy fájlt képvisel a natív fájlrendszerben. Létrehozhatunk a File objektumot egy állományhoz a natív fájlrendszerben, és adatokat kérdezhetünk le az állományról. Például a teljes elérési útját.

FileDescriptor

Egy fájlkezelőt valósít meg nyitott állományok és portok esetén. Nem sokszor használt.

StreamTokenizer

Adatfolyam tartalmát tördeli részekre (tokenekre). A tokenek a legkisebb egységek (szavak, szimbólumok), melyeket a szövegfelismerő algoritmusok képesek felismerni. A StreamTokenizer objektumok bármely szöveges fájl elemzésére használhatók. Például használható a forrásállományoktól a változónevek, operátorok, illetve HTML fájlokból HTML tagok kigyűjtésére. (A StringTokenizer-hez hasonló feladatot lát el.)

FilenameFilter

A File osztály list metódusa használja, hogy mely állományok vannak egy listázandó könyvtárban. A FilenameFilter használható az egyszerű kifejezés stílusú állomány kereső minták implementálására, mint például *.doc, vagy akár sokkal bonyolultabb szűrésekre is.

Találhatóak még egyéb input osztályok a java.util.zip csomagban:

CheckedInputStream és CheckedOutputStream

Egy ki- és bementi adatfolyam pár, mely egy ellenőrző összeget számít az adatok írásakor és olvasásakor.

DeflaterOutputStream és InflaterInputStream

Betömöríti és kitömöríti az adatokat íráskor és olvasáskor.

GZIPInputStream és GZIPOutputStream

GZIP formában tömörített adatokat ír és olvas.

ZipInputStream és ZipOutputStream

ZIP formában tömörített adatokat ír és olvas.

20.5. Ellenőrző kérdések

  • Mi az író karaktercsatornák absztrakt őse?
  • Mi az olvasó karaktercsatornák absztrakt őse?
  • Mi az író bájtcsatornák absztrakt őse?
  • Mi az olvasó bájtcsatornák absztrakt őse?
  • Melyik metódus használható az összes olvasó csatorna esetén?
  • Melyik metódus használható az összes író csatorna esetén?
  • Mondjon példát memória-csatornára!
  • Írjon példát a File osztály szolgáltatásaira!
  • Mik a szűrő folyamok?

Igaz vagy hamis? Indokolja!

  • A RandomAccessFile egy szűrő folyam.
  • A véletlen elérésű állomány mutatóját lehet állítani.
  • A véletlen elérésű állomány segítségével egy fájlnak bármelyik bájtja módosítható.
  • Az objektumfolyamra írható primitív adat is.
  • Az objektumfolyamra írt objektumnak implementálnia kell a Serializable interfészt.
  • A pufferező folyamhoz mindig csatolni kell egy másik folyamatot.

A következők közül melyik hoz létre InputStreamReader objektumot?

(Minden helyes választ jelöljön meg!)

  • new InputStreamReader(new FileInputStream("data"));
  • new InputStreamReader(new FileReader("data"));
  • new InputStreamReader(new BufferedReader("data"));
  • new InputStreamReader("data");
  • new InputStreamReader(System.in);

A következők közül melyik korrekt?

  • RandomAccessFile("data", "r");
  • RandomAccessFile("r", "data");
  • RandomAccessFile("data", "read");
  • RandomAccessFile("read", "data");

Melyikre nem képes a File osztály?

  • Aktuális könyvtár beállítása
  • Szülő könyvtár nevének előállítása
  • Állomány törlése
  • Állomány(ok) keresése