Textové súbory

<< Práca so súbormi a adresármi operačného systému | Obsah | Vytvárame prvé triedy >>

Doteraz sme pracovali iba s malým počtom vstupných dát. Keď už budeme písať väčšie programy, tie už budú pravdepodobne komunikovať s okolím aj inak ako čítaním textových políčok z vyskakovacieho okna. Najčastejšie prebieha výmena dát so súbormi v operačnom systéme, databázou alebo s inými počítačmi po sieti. Niekedy programy komunikujú aj s perifériami ako tlačiareň a skener, špeciálnymi prístrojmi v nejakom podniku alebo robotmi.

Zápis do textového súboru - trieda PrintWriter

Trieda File nám umožňuje identifikovať adresár alebo súbor na disku. Ale v prípade, že chceme pracovať s obsahom súborov, potrebujeme využiť niečo iné. Najjednoduchší zápis do textového súboru je s použitím triedy PrintWriter.

Práca s textovými súbormi sa vždy skladá z 3 krokov.

  1. otvorenie súboru, ktoré je súčasťou konštruktora nejakého čítača (napr. Scanner) alebo zapisovača (napr. PrintWriter).
  2. práca s obsahom súboru, teda čítanie alebo zapisovanie
  3. zatvorenie súboru

Na zapisovanie do súboru cez PrintWriter budeme používať metódy print(...) a println(...). Ich fungovanie je totožné s výpisom do konzoly cez System.out.print(...) a System.out.println(...), akurát že výpis nie je do konzoly ale do otvoreného súboru na disku.

Uveďme si jednoduchý príklad aj s výsledným obsahom súboru.

File subor = new File("C:/text1.txt");
PrintWriter pw = null;
try {
  pw = new PrintWriter(subor); // otvoríme súbor C:/text1.txt na zápis. Ak v ňom niečo bolo, tak sa to vymaže.
  pw.print("Jano"); // napíšeme do súboru "Jano" a čakáme na ďalší zápis konci tohto riadku
  pw.println("Javák"); // napíšeme do súboru "Javák", ideme do nového riadku a čakáme
  pw.println(); // ideme do nového riadku a čakáme
  pw.println("0903 888 222"); // napíšeme do súboru "0903 888 222", ideme do nového riadku a čakáme
} catch (FileNotFoundException e) {
  System.out.println("Súbor " + subor.getName() + " neexistuje");
} finally {
  if(pw!=null)
     pw.close(); // uložíme a zavrieme súbor C:/text1.txt
}

Ak bol zápis úspešný výsledný súbor vyzerá nasledovne:

JanoJavák

0903 888 222

Určite ste zbadali bloky try a catch. V tomto prípade je ich použitie nutné, inak vám java váš kód neskompiluje. Výnimka java.io.FileNotFoundException, teda výnimka nenájdeného súboru, je totiž považovaná za bežnú výnimku, ktorá musí byť odchytená.

Ak súbor, ktorý otvárame cez PrintWriter, neexistuje, ale existuje adresár v ktorom má byť uložený, tak sa tento súbor vytvorí. Pokiaľ súbor už existoval, premaže sa a PrintWriter doňho zapisuje od prvého znaku.

Poznámka: Aby ste sa neupísali k smrti, tak Eclipse vám vie odchytávanie výnimiek napísať za vás. Označte si oblasť, ktorú chcete mať v try bloku, kliknite prvým tlačidlom myši a vyberte možnosť Surround With > Try/catch Block.

Čítanie textového vstupu - trieda Scanner

Trieda Scanner slúži na čítanie textových súborov, vstupu z konzoly alebo z reťazca. To, čo z toho bude čítať, mu povieme pri vytváraní novej inštancie Scannera. Uveďme si používané príklady otvárania textového vstupu Scannerom:

File subor = new File("C:\\vstup.txt");
Scanner scanerZoSuboru = new Scanner(subor);
Scanner scanerZKonzoly = new Scanner(System.in);
Scanner scanerZReťazca = new Scanner("Táto trieda sa mi páči.");
Scanner scanerZReťazca2 = new Scanner("C:\\vstup.txt"); // POZOR - ČASTÁ CHYBA!!! toto nebude čítať súbor C:\vstup.txt ale reťazec "C:\vstup.txt".

System.in je štandardný vstup, teda obyčajne vstup z klávesnice. Naproti tomu existuje ešte štandardný výstup System.out, ktorý už poznáte v spojení s vypisovaním na konzolu cez metódy print(...) a println(). Java pozná ešte štandardný chybový výstup System.err, do ktorého sa dá písať rovnako cez metódy print(...) a println(). Eclipse konzola zobrazuje výstup na štandardný chybový výstup červeným písmom.

Po vytvorení už všetky inštancie fungujú rovnako, bez ohľadu na zdroj textu, ktorý čítajú. Jediný rozdiel je v Scanneri zo súboru, ktorý by sa mal po použití zatvoriť metódou close(), aby sa zatvoril aj súbor z ktorého sa čítalo.

Filozofia fungovania Scannera je cez použitie spríbuzdnených metód:

  • boolean hasNextXXX() - vráti true ak je možné zo vstupu prečítať hodnotu typu XXX, inak vráti false
  • XXX getXXX() - vráti hodnotu typu XXX zo vstupu, ak to je možné

Za XXX si môžete dosadiť primitívne typy ako napríklad int,double, boolean alebo ak nedosadíte nič a ide o String. Scanner metódou getXXX() číta po najbližší oddeľovač. Prednastaveným oddeľovačom je ľubovoľný znak, ktorý je takzvaným whitespace znakom, teda znakom, ktorý nie je vidieť. Typické whitespace znaky sú medzera ' ' a tabulátor '\t'.

Špeciálnym prípadom spríbuznených metód je boolean hasNextLine() a String nextLine(), ktoré za oddeľovač považujú (neviditeľný) znak konca riadku. Výsledkom metódy String nextLine() je teda jeden riadok vstupu od aktuálnej pozície.

Keď Scanner prečíta hodnotu cez niektorú getXXX() aktuálna pozícia sa presunie za túto hodnotu.

Prehľad niektorých metód Scannera ukazuje nasledujúca tabuľka:

metódačinnosť
boolean hasNextLine()vráti true ak je možné čítať ďalší riadok (t.j. nie sme na konci vstupu/súboru)
String nextLine()vráti ďalší riadok od aktuálnej pozície
boolean hasNextInt()vráti true ak je možné čítať ďalšie číslo typu int (t.j. dá sa skonvertovať na int)
int nextInt()vráti ďalšiu hodnotu typu int od aktuálnej pozície
boolean hasNextDouble()vráti true ak je možné čítať ďalšie číslo typu double (t.j. dá sa skonvertovať na double)
double nextDouble()vráti ďalšiu hodnotu typu double od aktuálnej pozície
boolean hasNext()vráti true ak je možné čítať ďalší reťazec
String next()vráti reťazec od katuálnej pozície až po najbližší oddeľovač
useDelimiter(String)nastaví nový oddeľovač - ak napríklad nastavíme oddeľovač na tabulátor "\t" tak medzera sa pri použití metódy next() bude brať ako bežný znak

Načítanie 5 čísiel z klávesnice môžeme spraviť nasledovne:

int[] pole = new int[5];
System.out.print("Zadajte 5 celých čísiel: ");
Scanner scanner = new Scanner(System.in);
for(int i = 0; i < 5; i++) {
  pole[i] = scanner.nextInt();
}
... // práca s naplneným poľom

Tieto čísla môžete zadať napríklad tak, že ich napíšete v jednom riadku oddelené medzerami alebo každé do nového riadku (aj nový riadok je whitespace znakom).

Načítanie 5 riadkov zo súboru môžeme spraviť nasledovne:

File súbor = new File("C:/vstup.txt");
String[] riadky = new String[5];
Scanner scanner = null;
try {
    scanner = new Scanner(súbor);
    for(int i = 0; i < 5; i++) {
      if(scanner.hasNextLine()) {
        riadky[i] = scanner.nextLine();
      } else {
        System.out.println("Chýba riadok!");
      }
    }
} catch (FileNotFoundException e) {
  System.out.println("Súbor " + subor.getName() + " neexistuje");
} finally {
  if(scanner!=null)
    scanner.close(); //nech sa stane čokoľvek, korektne uzavrieme súbor
}

Poďme si ešte ukázať načítanie do dvojrozmerného poľa zo súboru. Majme nasledovný súbor, ktorý obsahuje čísla ktoré by sme chceli dostať do políčok dvojrozmerného poľa:

5 84 -8 6
12 561 5 0
0 1 -1 0

Na prvý pohľad vidíme, že pôjde o dvojrozmerné pole celých čísiel veľkosti 3x4 ktoré si nazveme matica. Čítať zo súboru budeme po riadkoch, a každý riadok, ktorý získame metódou nextLine(), ktorý je typu String, budeme novým Scannerom čítať ako čísla oddelené whitespace oddeľovačom.

File súbor = new File("C:/matica.txt");
int[][] matica = new int[3][4];
Scanner scannerSuboru = null;
try {
    scannerSuboru = new Scanner(súbor);
    for(int i = 0; i < 3; i++) { // čítame 3 riadky
      String riadok = scannerSuboru.nextLine();
      Scanner scannerRiadku = new Scanner(riadok);
      for(int j = 0; j < 4; j++) { // čítame 4 čísla v každom riadku
         matica[i][j] = scannerRiadku.nextInt(); // na i-ty riadok a j-ty stĺpec do dvojrozmerného poľa matica vložíme j-te číslo i-teho riadku zo súboru
      }
    }
} catch (FileNotFoundException e) {
  System.out.println("Súbor " + subor.getName() + " neexistuje");
} finally {
  if(scannerSuboru!=null)
    scannerSuboru.close(); //nech sa stane čokoľvek, korektne uzavrieme súbor
}

Čo však urobíme v prípade, že sa do toho súboru nepozrieme a nevieme, že koľko má matica v súbore riadkov a stĺpcov? Najprv si odhadneme maximálnu možnú veľkosť matice (v našom príklade to odhadneme na 6x6). Potom v tomto poli vyplníme iba toľko riadkov a stĺpcov, koľko ich bolo vo vstupnom súbore. Zároveň si musíme po načítaní uložiť to, koľko riadkov a stĺpcov reálne obsahuje dáta. Uložíme si to v premenných pocetRiadkov a pocetStlpcov. Počas napĺňania dvojrozmerného poľa si budeme spravovať premenné r a s, ktoré nám budú kopírovať našu aktuálnu pozíciu riadku a stĺpca.

File súbor = new File("C:/matica.txt");
int[][] matica = new int[6][6]; //horný odhad veľkosti matice v súbore
Scanner scannerSuboru = null;
int r = -1; // zatiaľ nie sme ani na nultom riadku, až potom keď ho pôjdeme naozaj čítať zvýšime pozíciu na nulu.
int s = -1;
try {
    scannerSuboru = new Scanner(súbor);
    while(scannerSuboru.hasNextLine()) { // čítame pokiaľ ešte sú nejaké riadky v súbore
      r++; // ideme čítať ďalší riadok, zvýšime teda číslo riadku o 1
      String riadok = scannerSuboru.nextLine();
      Scanner scannerRiadku = new Scanner(riadok);
      s = -1; // zatiaľ nie sme ani na nultom stĺpci, až potom keď ho pôjdeme naozaj čítať zvýšime pozíciu na nulu.
      while(scannerRiadku.hasNextInt()) { // čítame pokiaľ ešte sú nejaké čísla v tomto riadku
         s++;  // ideme čítať ďalší stĺpec, zvýšime teda číslo stĺpca o 1
         matica[r][s] = scannerRiadku.nextInt(); // na pozíciu v danom riadku a stĺpci vložíme do dvojrozmerného poľa matica číslo z rovnakej pozície v súbore
      }
    }
    pocetRiadkov = r + 1; // naposledy sme napĺňali r-tý riadok teda riadkov máme o 1 viac (máme totiž aj nultý riadok)
    pocetStlpcov = s + 1; // naposledy sme napĺňali s-tý stĺpec teda stĺpcov máme o 1 viac
} catch (FileNotFoundException e) {
  System.out.println("Súbor " + subor.getName() + " neexistuje");
} finally {
  if(scannerSuboru!=null)
    scannerSuboru.close(); //nech sa stane čokoľvek, korektne uzavrieme súbor
}

Výsledkom tohto postupu v prípade rovnakého vstupného súboru ako v predchádzajúcom príklade budú:

  • v premennej pocetRiadkov číslo 3
  • v premennej pocetStlpcov číslo 4
  • v dvojrozmernom poli matica tieto hodnoty:
584-8600
125615000
01-1000
000000
000000
000000

Urobme si ešte výpis používanej časti tejto matice na konzolu:

for(int i = 0; i < pocetRiadkov; i++) {
  for(int j = 0; j < pocetStlpcov; j++) {
     System.out.print(matica[i][j]+ " ");
  }
  System.out.println(); // po výpise všetkých stĺpcov jedného riadku skočíme na nový riadok v konzole
}

Výsledkom je v našom príklade takýto výpis:

5 84 -8 6
12 561 5 0
0 1 -1 0

Práca s rôznymi znakovými kódovaniami súborov

Zaujímavými a niekedy užitočnými sú konštruktory tried PrintWriter a Scanner s dvoma parametrami, kde prvým parametrom je otváraný súbor a druhým kódovanie znakovej sady. Keď sa použije konštruktor iba s jedným parametrom nastaví sa znaková sada projektu. Pozrieť si ju môžete kliknutím pravým tlačidlom myši na projekt a vybratie možnosti Properties. Uvidíte podobné okno:

Bežný Slovák sa stretáva najmä so znakovou sadou windows-1250 a UTF-8. Po otvorení súboru je už práca s PrintWriterom alebo Scannerom úplne rovnaká. Použitie si ukážeme na príklade s PrintWriterom.

File subor = new File("C:/textUTF8.txt");
PrintWriter pw = null;
try {
  pw = new PrintWriter(subor,"UTF-8"); //otvoríme súbor a zapisovať budeme v kódovaní UTF-8
  pw.print("ľščťžýáíéúôä"); // niečo do súboru napíšeme
} catch (FileNotFoundException e) {
  System.out.println("Súbor " + subor.getName() + " neexistuje");
} finally {
  if (pw!=null)
    pw.close(); // uložíme a zavrieme súbor
}

Ak máte chuť použiť iné znakové sady kliknite si na stránku so zoznamom podporovaných kódovaní.