Java programozás 21. – Fájlkezelés alapjai

Fájlkezelés, avagy dolgozzunk is már valamit

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:

  1. Standard kimenet: System.out
  2. Standard bemenet: System.in
  3. 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.

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:

  1. Előre tudjuk, hány sorból áll a fájl
  2. Nem tudjuk, hány sorból áll a fájl, de az első sorban megtaláljuk a sorok darabszámát
  3. 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
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
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
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.

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?

  1. 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…
  2. 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.
  3. 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.

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:

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.

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:

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.

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.

  1. 9-13 – A fájl tartalmának beolvasása.
  2. 16 – Kicseréljük a tömbben a 2. nevet.
  3. 17 – Visszaállunk a fájl elejére.
  4. 19-22 – Kiírjuk a tömbből a neveket a fájlba.
  5. 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).
  6. 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.

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.

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…

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. Melyiknek lesz gondja vele?

(A tematika szerint a saját metódusok lesz a következő lecke, ez még nincs kész, addig átugorjuk)

Következő lecke: Saját objektumok

3 Replies to “Java programozás 21. – Fájlkezelés alapjai”

  1. Pingback: Java programozás 20. – ArrayList |

  2. Üdv ismét!

    “\n”, avagy “A jegyzettömb buta”.
    Ez lehet, de vannak még butábbak. Például az a cég, aki (valószínűleg pont emiatt) átláthatatlan és nehezen feldolgozható txt fájlokban küldi át a megrendeléseket a partnercégnek (szerettük is nagyon az ilyen fájljaikat:). Nekünk leendő programozóknak pedig épp az lenne a feladatunk, hogy olyan produktumokat állítsunk elő, ami kiküszöböli az ilyen butaságokat.
    Ez csak egy észrevétel volt.

    A sorvég jelző:
    Eredetileg valóban azt tapasztaltam, ami a leckében le van írva.
    Keresgéltem egy kicsit.
    Találtam:
    Windows, Linux és Apple operációs rendszerek alatt más kombinációban működik.
    Az egyiknél \n, a másiknál \r a harmadiknál \r\n (a sorrendre nem emlékszek).
    Windows alatt kipróbáltam és azt tapasztaltam, hogy az új infók nem voltak pontosak.
    A lényeg, hogy nekem \r\n-nel a jegyzettömb is jól nyitja meg a txt-t és a leckében leírt példáknál sem okozott később problémát.

    Más:
    Árvíztűrő tükörfúrógép…
    Hosszas keresgélés után annyit ki tudtam bogozni, hogy az “ő, Ő, ű, Ű” betűk globális ( 🙂 )
    problémát jelentenek, és hogy a karakterkódolások adhatják a megoldást.
    A bajom az, hogy sehogy sem működik.
    Az Eclipse-t úgy állítottam be, hogy mindent UTF-8-ban mentsen.
    Ennek ellenére ennek a kódnak:
    RandomAccessFile raf10;
    String[] nevek10 = { “ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP”, “árvíztűrtő tükörfúrógép” };

    try {
    raf10 = new RandomAccessFile(“csakNevek2.txt”, “rw”);

    for (String nev : nevek10) {
    raf10.writeBytes(nev + “\r\n”);
    }

    raf10.close();

    } catch (Exception e) {
    System.err.println(“HIBA! ” + e);
    }

    ez az eredménye az ellenőrző kiíratáskor (és a fájlban is):
    ÁRVÍZTpRP TÜKÖRFÚRÓGÉP
    árvíztqrtQ tükörfúrógép

    További keresgélés után odáig jutottam, hogy kipróbálom az ISO-8859-2 kódolást PrintWriter-en.
    Kód:
    PrintWriter pw;
    RandomAccessFile raf12;
    String[] teszt = { “ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP”, “árvíztűrtő tükörfúrógép” };

    try {
    pw = new PrintWriter(“teszt.txt”, “ISO-8859-2”);
    raf12 = new RandomAccessFile(“teszt.txt”, “r”);

    for (String string : teszt) {
    pw.println(string);
    }

    pw.close();
    raf12.close();

    } catch (Exception e) {
    System.err.println(“HIBA! ” + e);
    }

    Fájl tartalma jó:
    ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP
    árvíztűrtő tükörfúrógép

    Ellenőrző kiíratás:
    ÁRVÍZTÛRÕ TÜKÖRFÚRÓGÉP
    árvíztûrtõ tükörfúrógép

    Ez nyilván jobb, mint az eredeti, de mégsem az elvárt.
    Mi a megoldás?

    Az oldal továbbra is tetszik!
    Szerintem végig fogom csinálni.
    Köszönet érte!

Vélemény, hozzászólás?

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük

*

Ez az oldal az Akismet szolgáltatást használja a spam csökkentésére. Ismerje meg a hozzászólás adatainak feldolgozását .