Vstupno-výstupné metódy v jazyku Java

2007/01/12

Úvod

Programovací jazyk, ktorý by neponúkal dostatočný repertoár prostriedkov na zabezpečenie vstupu a výstupu (napr. načítavanie z klávesnice, čítanie a zápis do súborov) by bol asi veľmi rýchlo odsúdený na neúspech. Veď čo už s takým programom, s ktorým nemôžete interagovať.

Java poskytuje tento repertoár v plnej miere. Na rozdiel od klasických procedurálnych jazykov sú prostriedky na zabezpečenie vstupu a výstupu reprezentované pomocou viacerých tried a ich metód (združených hlavne v balíčku java.io). Výhodou je možnosť vytvárať mnohoraké kombinácie prístupov, ktorými je možné pokryť značné množstvo prípadov použitia (chcete načítavať komprimované dáta z Internetu?). Nevýhodou sa však môže zdať relatívna komplikovanosť niektorých postupov - tam, kde Cčkar napíše scanf(), sa musí začiatočník v Jave vysomáriť z toho, ktoré objekty je nutné vytvoriť a čo zavolať na dosiahnutie cieľa.

Triedy v balíčku java.io však majú logické usporiadanie a po pochopení niektorých základných myšlienkových pochodov je práca s nimi bezproblematická.

Súbory

Práca so súbormi je veľmi častá (príklady si ukážeme nižšíe). Pojmu súbor (i adresár) zodpovedá trieda java.io.File. Pomocou takéhoto objektu môžeme testovať existenciu súboru, či získavať zoznam podadresárov (resp. súborov), vytvárať nové súbory a pod. alebo zisťovať absolútne a relatívne cesety. Samotné načítavanie dát zo súboru však rieši iná trieda, o ktorej sa zmienime hneď.

Nasledovný príklad vypíše mená všetkých adresárov v danom adresári.

File file = new File("D:\\Projects");
if(file.exists()) {
  File[] childFiles = file.listFiles();
  for (File childFile: childFiles) {
    if(childFile.isDirectory()) {
      System.out.println(childFile.getName());
    }
  }
}

Metódou exists() overíme existenciu súboru či adresára. Metóda listFiles() vráti zoznam súborov a adresárov v danom adresári, z ktorých vypíšeme len mená (metóda getName()) vráti len meno súboru bez adresárov).

Všimnime si, že na Windowse musíme zdvojiť spätné lomky v ceste k adresáru. Alternatívne môžeme používať aj obyčajné lomky (teda /), Java si s tým poradí a to aj na Windowse.

Vstupné prúdy

Výlet po triedach balíčka java.io začneme vstupnými prúdmi. Vstupný prúd je objekt, ktorý dokáže odniekiaľ načítavať bajty. Zdrojom bajtov môže byť naozaj hocičo, napr. súbor, rúra či internetové pripojenie.

Vstupnému prúdu zodpovedá trieda java.io.InputStream. Táto trieda je abstraktná (čiže si nevyrobíte novú inštanciu). Jej jednotliví dedičia reprezentujú konkrétne zdroje bajtov. Pri pohľade na dokumentáciu sa zamerajme na dve najdôležitejšie metódy:

Všetky metódy hádžu výnimku java.io.IOException. Vstupný prúd je nutné uzavrieť vždy a to i v prípade, že nastane výnimka. Vhodným miestom je teda finally blok.

Súborový vstupný prúd

Jedným z konkrétnych vstupných prúdov je súborový vstupný prúd, java.io.FileInputStream, ktorý umožňuje načítavať bajty zo súboru. Príklad použitia, kde zo súboru načítame prvé štyri bajty je nasledovný:

InputStream in = null;
try {
  in = new FileInputStream(
    "d:\\Projects\\io\\bin\\InputStreamTest.class");
  for (int i = 0; i < 4; i++) {
    int aByte = in.read();
    System.out.println(Integer.toHexString(aByte));
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try { 
    in.close();
  } catch (Exception e) {
    /*
     ak nastala chyba pri zatváraní súboru, už je to jedno, 
     ignorujeme ju
    */
  }
}

Objekt FileInputStream bol vytvorený nad daným súborom, pričom sme v konštruktore špecifikovali rovno cestu. Jednotlivé bajty sú pred výpisom na konzolu konvertované na šestnástkový zápis – mali by sme vidieť text CAFEBABE, čo je hlavička .class súborov v Jave.

Alternatívne je možné vytvoriť FileInputStream nad objektom typu File, čiže spôsobom:

File f = new File("d:\\Projects\\io\\bin\\InputStreamTest.class");
InputStream in = new FileInputStream(f);

Vstupný prúd pre URL adresy

Ďalším príkladom vstupného prúdu sú bajty prichádzajúce zo zdroja reprezentovaného URL adresou, typicky z internetovej stránky. Na rozdiel od súborového vstupného prúdu, kde používame FileInputStream však nemôžeme použiť URLInputStream (taká trieda totiž nejestvuje). Musíme vytvoriť objekt pre URL adresu, teda inštanciu triedy java.net.URL, a z neho získať InputStream.

To je možno nekonzistentné s prácou so súbormi (možno by sme očakávali, že aj súbor File by nám dokázal poskytnúť InputStream, ale nie je to tak), musíme sa však s tým zmieriť.

Príklad získania vstupného prúdu z URL adresy je uvedený nižšie.

InputStream in = null;
try {
  URL url = new URL("http://www.google.com");
  in = url.openStream();
  int i = 0;
  while((i = in.read()) != -1) {
    System.out.print((char) i);
  }
} catch (MalformedURLException e) {
  System.err.println("Neplatná adresa.");
} catch (IOException e) {
  System.err.println("Vstupno-výstupná chyba.");
} finally {
  try {
    in.close();
  } catch (Exception e) {
    // do nothing
  }
}

Trieda URL nie je obmedzená len na internetové adresy. Pomocou nej možno získavať vstupné prúdy z obyčajných súborov, JAR archívov a pod. Ak vytvoríme objekt URL nasledovne, získame tým vstupný prúd nad obyčajným súborom.

URL url = new URL("file:///c:/autoexec.bat");

Všimnime si, že konštruktor URL hádže výnimku java.net.MalformedUrlException a to v prípade, že adresa používa nepodporovaný protokol. Ak vytvoríme adresu http://ždiebik.sk, tak výnimka nenastane (napriek tomu, že taká adresa určite nejestvuje). Na druhej strane, vytvorenie adresy nad Magnet linkom by výnimku vyvolalo.

Výstupné prúdy

Ukázali sme si spôsob, ktorým je možné načítavať bajty. Čo však so zápisom? Existuje niečo ako OutputStream? Náhľad do dokumentácie ukáže, že áno. Ku vstupným prúdom existujú ich protipóly – výstupné prúdy, ktoré dokážu zapísať „niekam" jeden alebo viac bajtov.

Trieda java.io.OutputStream má opäť niekoľko najdôležitejších metód:

Všetky metódy tiež hádžu výnimku java.io.IOException.

Súborový výstupný prúd

OutputStream je opäť abstraktná trieda a až jej podtriedy špecifikujú konkrétny cieľ, do ktorého sa budú zapisovať dáta. K dispozícii je napr. súborový vstupný prúd, java.io.FileOutputStream demonštrovaný nižšie. Do príslušného súboru zapíšeme štyri bajty:

OutputStream out = null;
try {
  out = new FileOutputStream(
    "d:\\Projects\\paz-pisomka\\InputStreamTest.bin");
  for (int i = 0; i < 4; i++) {
    int aByte = 65 + i;
    out.write(aByte);
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try { 
    out.close();
  } catch (Exception e) {
    // do nothing
  }
}

Namiesto cyklu by sme mohli použiť metódu zapisujúcu pole bajtov:

out.write(new byte[] {65, 66, 67, 68});

FileOutputStream štandardne súbory prepisuje. Môžeme však použiť alternatívny konštruktor:

OutputStream out 
  = new FileOutputStream("d:\\Projects\\io\\InputStreamTest.bin", 
                         true);

kde true v druhom parametri nastaví pripájanie dát k existujúcim (append mód).

Načítavanie reťazcov pomocou readerov

V predošlých častiach sme demonštrovali triedy slúžiace na načítavanie a zápis bajtov. Veľmi často sa však namiesto bajtov pracuje so znakmi a reťazcami, napr. pri práci s textovým súborom.

Na načítavanie znakov z vhodného zdroja je k dispozícii trieda java.io.Reader. Jej metódy sú veľmi podobné metódam InputStreamu, ibaže pracujú priamo so znakmi.

Metódy vracajú výnimku IOException a opäť poznamenávame, že uzatváranie readerov je skoro povinné.

Reader je všeobecná abstraktná trieda a preto treba vytvoriť inštanciu z niektorej jeho dediacej triedy.

Načítavanie znakov zo súboru

Trieda java.io.FileReader umožňuje načítavať znaky zo súboru, pričom sa použije kódovanie nastavené v operačnom systéme (napr. na Windowse je to cp1250). Nasledovný príklad načíta zo súboru všetky znaky a vypíše ich na konzolu:

Reader in = null;
try {
  in = new FileReader("d:\\adresa.txt");
  int aByte;
  while((aByte = in.read()) != -1) {
    System.out.print((char) aByte);
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try { 
    in.close();
  } catch (Exception e) {
    // do nothing
  }
}

Alternatívne môžeme použiť aj konštruktor nad objektom File

Reader in = new FileReader("d:\\adresa.txt");

Poznamenajme, že uvedený príklad vypíše diakritické a podobné znaky správne len v prípade, ak kódovanie súboru je zhodné s kódovaním používaným v operačnom systéme. Ak by sme napr. chceli načítať znaky zo súboru v kódovaní UTF-8 na Windowse (kde je štandardné kódovanie cp1250), slovenské znaky by sa zobrazili ako otázniky. Aj tento problém sa dá vyriešiť (ak poznáme kódovanie súboru), zmienime sa o tom nižšie.

Načítavanie riadkov pomocou readerov s buffrom

Načítavanie riadkov pomocou predošlej metódy je síce možné, ale pomerne náročné. Idea by bola zrejme taká, že by sme kumulovali znaky v reťazci/StringBuilderi až do chvíle, kým by sme nenačítali koniec riadka, prípadne koniec súboru. Na tento účel by sme si dokonca mohli spraviť vlastnú triedu LineSupportingFileReader (FileReader s podporou načítavania riadkov).

S tým sa však vôbec nemusíme trápiť, pretože máme k dispozícii triedu java.io.BufferedReader. Tá je reprezentantom filozofie založenej na návrhovom vzore wrapper. BufferedReader je reader, ktorý dodá inému readeru schopnosť načítavať reťazce po riadkoch. Inak povedané, je to reader, ktorý načítava znaky z iného readera a tieto znaky kumuluje do riadkov. Ľubovoľný reader teda môže byť obalený BufferedReaderom a tým získať schopnosť riadkového čítania.

Všimnime si flexibilitu tohto návrhu. Ak by sme chceli navrhnúť reader načítavajúci riadky zo súboru, mohli by sme vytvoriť triedu dediacu z FileReadera a dorobiť do nej príslušnú metódu. Lenže čo v prípade, keby sme chceli načítavať riadky z readera nad internetovým pripojením? Museli by sme vytvoriť triedu LineSupportingInternetConnectionReader a v nej opäť dopracovať metódu. Ak by sme mali veľa readerov nad rôznymi zdrojmi, nastala by explózia počtu dediacich tried.

Prístup založený na návrhovom vzoru wrapper je elegantnejší – použijeme totiž len jednu triedu poskytujúcu danú schopnosť a zaobalíme ňou ľubovoľný reader z ľubovoľného zdroja.

Požadovaný riadkovo orientovaný reader nad súborom vytvoríme nasledovne:

FileReader fileReader = new FileReader("C:\\autoexec.bat");
BufferedReader in = new BufferedReader(fileReader);

Hlavnou metódou BufferedReadera je metóda String readLine(), ktorá vráti ďalší načítaný riadok alebo null, ak sa dosiahol koniec vstupného prúdu.

Kompletný príklad, kde sa vypíše na konzolu obsah súboru je nasledovný:

BufferedReader in = null;
try {
  in = new BufferedReader(new FileReader("c:\\autoexec.bat"));
  String line = null;
  while((line = in.readLine()) != null) {
    System.out.println(line);
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try { 
    in.close();
  } catch (Exception e) {
    // do nothing
  }
}

Ak zavoláme metódu close() na BufferedReaderi, tak sa zároveň zatvorí aj obalený reader, teda sa zatvorí aj FileReader.

Zápis znakov pomocou writerov

Tak ako InputStream slúži na načítavanie bajtov a jeho proťajškom OutputStream umožňuje ich zápis, k Readeru existuje Writer slúžiaci na zápis znakov.

Trieda java.io.Writer má nasledovné významné metódy:

Všetky metódy tiež hádžu výnimku java.io.IOException.

Zápis znakov pomocou do súboru

Writer zapisujúci do súboru sa volá, prekvapivo, java.io.FileWriter a jeho použitie je skoro také isté, ako FileOutputStreamu. Príklad, ktorý zapíše do súboru päťkrát daný text je uvedený nižšie:

Writer out = null;
try {
  out = new FileWriter("du.txt");
  String message = "Budem si písať domácu úlohu.\n";
  for (int i = 0; i < 5; i++) {
    out.write(message);
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try { 
    out.close();
  } catch (Exception e) {
    // do nothing
  }
}

Všimnite si, že ak chceme zapísať reťazce po riadkoch, musíme ich ukončiť znakom \n. Tento znak zodpovedá UNIXovému koncu riadku. V prípade Windowsu to však nie je veľmi korektné, keďže riadky by mali byť ukončené znakmi CR a LF (\r\n). Program, ktorý je platformovo nezávislý, by mal vyzerať lepšie:

String EOL = System.getProperty("line.separator");
...
String message = "Budem si písať domácu úlohu." + EOL;

Tento postup však budeme používať málokedy, pretože na zápis celých riadkov existuje trieda BufferedWriter.

Zápis riadkov pomocou writerov s buffrom

Ukázali sme si, že na riadkové načítavanie jestvuje BufferedReader, ktorým možno obaliť ľubovoľný reader a tým mu dodať túto schopnosť. Na zápis riadkov je k dispozícii opäť protipól a to java.io.BufferedWriter. Jeho hlavná výhoda spočíva v možnosti buffrovať výstup. Obyčajný writer totiž zapisuje znaky na výstup ihneď, čo môže byť niekedy neefektívne. Zrejme je lepšie kumulovať znaky určené na zápis do nejakej medzipamäte, teda buffra a až po jej naplnení ich odoslať na výstup. Dokumentácia odporúča používať BufferedWriter vždy, keď je operácia zápisu relatívne náročná (spomína sa špeciálne prípad FileWritera).

BufferedWriter poskytuje oproti klasickému Writeru jedinú novú metódu void newLine(), ktorou sa na výstup zapíše znak konca riadka. Vytvoriť inštanciu je možné napr. nasledovne

BufferedWriter out = new BufferedWriter(new FileWriter("D:\\data.txt"));

Zápis riadku je potom možný pomocou

out.write("Ahoj");
out.newLine();

To však stále nie je úplne ideálny stav. Našťastie je k dispozícii pomocná trieda PrintWriter.

Zápis textových dát pomocou PrintWritera

Trieda java.io.PrintWriter je veľmi užitočný writer, ktorý dokáže obaliť ľubovoľný iný Writer alebo OutputStream a dodať mu schopnosť zapisovať textové reprezentácie mnohých dátových typov. Popri metódach zdedených od klasického Writera poskytuje metódy ako:

Nasledovný príklad zapíše do súboru desať riadkov. Na nepárnych riadkoch je text "Line:", na párnych sú čísla.

PrintWriter out = null;
try {
  out = new PrintWriter(
          new BufferedWriter(new FileWriter("cisla.txt")));
  for (int i = 0; i < 5; i++) {
    out.println("Line:");
    out.println(i);
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try { 
    out.close();
  } catch (Exception e) {
    // do nothing
  }
}

V príklade PrintWriter obaľuje BufferedWriter (aby sme získali väčšiu efektivitu pri zapisovaní, v opačnom prípade by sa každý zápis odoslal ihneď do súboru, čo nemusí byť efektívne) a ten obaľuje writer nad súborom.

Zatvorenie PrintWritera pomocou close() kaskádne zavrie BufferedWriter a následne FileWriter.

Čítanie a zápis byteov s buffrovaním

Dosiaľ sme spomenuli možnosť buffrovania pri čítaní resp. zapisovaní reťazcov a znakov. Ale i obyčajné vstupno-výstupné prúdy je možné obohatiť o podporu buffrovania.

Trieda java.io.BufferedInputStream používa pri načítavaní bajtov zo vstupu buffer a teda následné volania read() pristupujú vo veľkej miere k buffru a nie priamo k vstupnému zdroju. Naviac táto trieda poskytuje podporu pre metódy mark() a reset(), o ktorých budeme písať v ďalšej sekcii.

Jej zapisovací kamarát java.io.BufferedOutputStream ukladá bajty z metódy write() do buffra, ktorý zapíše na výstup až po jeho naplnení. Tým znižuje počet volaní zápisu na obaľovanom výstupnom prúde.

Obe triedy sú založené na filozofii obaľovača. Príklad použitia je napr.:

BufferedOutputStream out 
  = new BufferedOutputStream(new FileOutputStream("D:\\data.txt"));

Metódy mark() a reset()

Metódy read() na vstupnom prúde sa po vstupnom prúde posúvajú smerom „dopredu". Niekedy však môže nastať situácia, keď sa chceme vrátiť v prúde späť a teda napr. načítať niektoré dáta z prúdu ešte raz. Na to môžeme použiť dvojicu metód mark() a reset().

Metóda mark() si umožňuje poznačiť aktuálnu pozíciu vo vstupnom prúde (na dané miesto umiestnime „záložku"). Metódou reset() sa zase môžeme vrátiť v prúde naspäť na poznačenú pozíciu. Ak načítame z prúdu dva bajty, položíme záložku pred tretí bajt cez mark(), načítame štvrtý a následne piaty bajt a zavoláme reset(), ďalšie volanie metódy read() načíta opäť štvrtý bajt (prípadné ďalšie volania budú pokračovať piatym, šiestym atď bajtom).

Takáto záložka môže byť v prúde len jedna a je treba poznamenať, že nie všetky vstupné prúdy podporujú túto funkcionalitu. Predstavme si, že televízny signál prichádzajúci do nášho televízora sú bajty. Ak si chceme zopakovať zaujímavý gól, zrejme nemôžeme požiadať vysielateľa, aby nám to spravil na požiadanie. To isté sa týka niektorých vstupných prúdov (napr. bajtov prúdiacich zo sieťového pripojenia). Vstupný prúd InputStream má metódu boolean markAvailable(), ktorá vráti true, ak prúd podporuje značkovanie a návrat na označkovanú pozíciu.

Ak používaný vstupný prúd nepodporuje značkovanie, netreba zúfať. V prípade televízneho signálu by sme mohli získať možnosť púšťať zaujímavé výseky nanovo zapojením DVD rekordéra, ktorý môže slúžiť ako medzipamäť. V prípade potreby prezerania zaujímavých častí budeme čítať dáta z DVD, na ktoré sa bude ukladať prichádzajúce dáta; DVD rekordér teda slúži ako buffer.

Túto analógiu môžeme použiť aj v prípade vstupných prúdov. Ak obalíme vstupný prúd už spomínaným BufferedInputStreamom, získame možnosť vracať sa na označkované miesta aj v prípade, že obalený vstupný prúd túto funkciu priamo neponúka.

Poznamenajme ešte, že metóda mark() má jeden celočíselný parameter. Po načítaní daného počtu bajtov sa príslušná značka v prúde zneplatní, teda zabudne. Ak si označkujeme vstupný prúd 16timi bajtmi, po načítaní šestnástich bajtov sa značka v prúde zruší.

Premostenie bytov a znakov – InputStreamReader a OutputStreamWriter

Ak sa spätne pozrieme na spôsoby, ktorými je možné načítavať dát z vhodného zdroja, zistíme, že ich môžeme logicky rozdeliť na:

Toto všetko Java uľahčuje a dáva k dispozícii triedy, ktoré reprezentuju premostenie medzi svetom bajtov a svetom znakov.

Premostenie vstupných prúdov a readerov

Trieda java.io.InputStreamReader umožňuje obaliť ľubovoľný InputStream, načítavať z neho bajty a prevádzať ich na znaky s použitím zadaného kódovania.

Ak chceme načítavať znaky zo súboru, ktorý je v kódovaní utf-8, môžeme použiť nasledovný kód:

FileInputStream fileInputStream = new FileInputStream("D:\\utf8.txt");
// súborovému vstupnému prúdu dodáme podporu buffrovania kvôli
// väčšej efektivite
BufferedInputStream bufferedIn = new BufferedInputStream(fileInputStream);
InputStreamReader in = new InputStreamReader(bufferedIn, "utf-8");

InputStreamReader má metódy Readera a umožňuje vrátiť používané kódovanie znakov pomocou metódy String getEncoding().

Načítavanie reťazcových riadkov z InputStreamu získame vhodným skombinovaním viacerých tried: FileInputStream bude načítavať bajty zo súboru, InputStreamReader ich prevedie na bajty a BufferedReader zabezpečí podporu buffrovania a metódu na načítavanie reťazcov.

FileInputStream fileInputStream = new FileInputStream("d:\\utf8.txt");
InputStreamReader inReader = new InputStreamReader(fileInputStream);
BufferedReader reader = new BufferedReader(inReader);

Alebo na jeden riadok:

BufferedReader reader 
  = new BufferedReader(
      new InputStreamReader(
        new FileInputStream("d:\utf8.txt")));

Možno máte pocit deja-vu – veď to isté sme mohli dosiahnuť pomocou kombinácie FileReadera a BufferedReadera. Nuž, je to tak. Ak si pozriete dokumentáciu k triede java.io.FileReader, zistíte, že je to vlastne pomocná trieda dediaca od InputStreamReadera, ktorá vo svojich vnútornostiach používa otvára FileInputStream a bajty načítavané zo súboru konvertuje na znaky s použitím implicitného kódovania v operačnom systéme. Ak však potrebujeme špecifikovať iné kódovanie než implicitné, FileReader nám už postačovať nebude a musíme použiť kombináciu z vyššieuvedeného príkladu.

Premostenie výstupných prúdov a writerov

Tak ako InputStreamReader zabezpečuje prevod bajtov na znaky, java.io.OutputStreamWriter zodpovedá za opačný proces: znaky konvertuje na bajty podľa príslušného kódovania.

Filozofia je podobná ako v prípade vstupu, OutputStreamWriter obalí ľubovoľný OutputStream a obohatí ho o schopnosť zapisovať doň znaky. Príkladom zápisu znakov do súboru je:

FileOutputStream fileOutputStream 
  = new FileOutputStream("utf8-out.txt");
// súborovému výstupnému prúdu dodáme podporu buffrovania kvôli
// väčšej efektivite
BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOutputStream);
OutputStreamWriter outWriter 
  = new OutputStreamWriter(bufferedOut, "utf-8");

OutputStreamWriter má analogické metódy ako Writer a umožňuje nastaviť a vrátiť používané kódovanie.

Pomocným proťajškom k FileReaderu je FileWriter, ktorý nie je ničím iným, ako zabalením FileOutputStreamu do OutputStreamWritera s použitím štandardného kódovania v operačnom systéme.

Serializácia – zápis a načítavanie celých objektov

Dosiaľ sme pracovali len bajtmi, znakmi a reťazcami. Java však umožňuje odosielať do výstupných prúdov a načítavať zo vstupných prúdov celé objekty. Typickým príkladom je situácia, keď chceme niekam uložiť stav kompletného objektu a neskôr (napr. pri ďalšom spustení aplikácie) si ho obnoviť. Tento proces sa nazýva serializácia a Java ho do značnej miery uľahčuje. Serializácia umožňuje previesť ľubovoľný objekt na postupnosť bajtov, s ktorou môžeme spraviť to, čo uznáme za vhodné – uložiť ho do súboru, poslať po sieti a pod.

Serializovať možno ľubovoľný objekt, ktorý implementuje interfejs java.io.Serializable. Tento interfejs nemá žiadne metódy, indikuje len schopnosť objektu byť serializovaným. Mapovanie objektu na bajty (a prípadný spätný proces) sa deje automaticky.

Majme napríklad jednoduchú triedu osoby:

import java.io.Serializable;

public class Person implements Serializable {
  private String name;
  
  private int age;

  public Person(String name, int age) {
    super();
    this.name = name;
    this.age = age;
  }

  // gettre a settre    
}

Na ukladanie inštancie tejto triedy do výstupného prúdu jestvuje užitočná trieda java.io.ObjectOutputStream. Tá reprezentuje OutputStream, ktorý dokáže dodať ľubovoľnému inému OutputStreamu schopnosť ukladať objekty. Protipólom slúžiacim na čítanie je java.io.ObjectInputStream, ktorý dodá inému InputStreamu schopnosť načítavať z neho objekty.

ObjectOutputStream má množstvo zaujímavých metód začínajúcich sa na write (napr. void writeBoolean(boolean b). Zvyčajne zrejme budeme používať metódu void writeObject(Object o), ktorá zapíše na výstup ľubovoľný objekt implementujúci interfejs Serializable.

Nasledovný príklad odserializuje do výstupného prúdu postupne jedno číslo, jeden reťazec a jednu inštanciu triedy Person:

ObjectOutputStream oos = null;
try {
  ByteArrayOutputStream byteArrayOut = new ByteArrayOutputStream();
  oos = new ObjectOutputStream(byteArrayOut);

  oos.writeInt(12345);
  oos.writeObject("Today");
  oos.writeObject(new Person("Johnny Walker", 25));

  System.out.println(Arrays.toString(byteArrayOut.toByteArray()));
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try {
    oos.close();
  } catch (Exception e) {
    //do nothing
  }
}

Výstupným prúdom je v tomto prípade java.io.ByteArrayOutputStream, ktorá zapisuje do poľa bajtov. Obalením tohto výstupného prúdu schopnosťou zapisovať objekty získame možnosť získavať binárnu reprezentáciu inštancií a obsahov premenných. Výsledné pole bajtov získame z ByteArrayOutputStreamu pomocou metódy toByteArray().

Trieda ObjectInputStream slúžiaca na načítanie objektov zo vstupu má užitočné metódy začínajúce sa na read. Zvyčajnou je metódy Object readObject(), ktorá načíta z prúdu objekt. V príklade máme binárne dáta uložené v poli bajtov. Nad týmto poľom postavíme java.io.ByteArrayInputStream a ten obalíme ObjectInputStream, ktorý bude interpretovať tieto bajty a deserializovať ich do objektu.

//v poli bajtov máme dáta 
byte[] data = {
    -84, -19, 0, 5, 119, 4, 0, 0, 48, 57, 116, 0, 5, 84, 111, 100, 
    97, 121, 115, 114, 0, 6, 80, 101, 114, 115, 111, 110, 42, -104,
    21, -71, 92, 46, -63, 108, 2, 0, 2, 73, 0, 3, 97, 103, 101, 76, 
    0, 4, 110, 97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 
    108, 97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 120, 
    112, 0, 0, 0, 25, 116, 0, 13, 74, 111, 104, 110, 110, 121, 32, 
    87, 97, 108, 107, 101, 114
};

ObjectInputStream ois = null;
try {
  ByteArrayInputStream byteArrayIn = new ByteArrayInputStream(data);
  ois = new ObjectInputStream(byteArrayIn);
  //načítame jeden int
  System.out.println(ois.readInt());
  //načítame jeden Object (v skutočnosti je to reťazec)
  System.out.println(ois.readObject());
  //načítame jeden objekt Person
  Person person = (Person) ois.readObject();

  System.out.println(person.getName());
  System.out.println(person.getAge());

} catch (IOException e) {
  e.printStackTrace();
} catch (ClassNotFoundException e) {
  // pokúšame sa vytvoriť inštanciu triedy, ktorú
  // nemáme v systéme k dispozícii
  e.printStackTrace();
} finally {
  try {
    ois.close();
  } catch (Exception e) {
    //do nothing
  }
}

Treba poznamenať, že trieda readObject() hádže výnimku ClassNotFoundException. Môže sa stať, že sa budeme snažiť načítavať triedu, ku ktorej neexistuje v systéme binárny kód a teda Java nebude vedieť vytvoriť inštanciu tejto triedy.

Serializovať a deserializovať možno ľubovoľné komplexné objektové prepojenia (napr. Person, ktorý obsahuje odkaz na rodiča). Java ich korektne uloží a to vrátane všetkých prepojení a asociácii. Niekedy sa môže stať, že niektorá z asociovaných tried neimplementuje java.io.Serializable. V tom prípade sa pri pokuse o serializáciu vyhodí výnimka java.io.NotSerializableException. Príkladom môže byť osoba Person, ktorá má adresu Address, čo je neserializovateľná trieda. Pri pokuse o serializáciu inštancie osoby nastane chyba.

Ostatné užitočné triedy balíčka java.io

Balíček java.io obsahuje aj niektoré iné užitočné triedy, ktoré sa trochu vymykajú uvedenej hierarchii.

System.out (a PrintStream)

Premennú System.out používal zrejme každý už od čias prvého Java programu, ktorý vypisoval "Ahoj svet!". Ak sa pozrieme na dátový typ tejto premennej, zistíme, že ide o java.io.PrintStream. Táto trieda je OutputStreamom, do ktorého je možné zapisovať znaky, reťazce a ostatné primitívne dátové typy. Nie je to však divné? Spomínali sme totiž, že do výstupných prúdov sa zapisujú len bajty. Za zápis znakov (a ostatných primitívnych typov) má byť predsa zodpovedný Writer (resp. PrintWriter)! Pravda je taká, že táto trieda je v Jave len z historických dôvodov (už od verzie 1.0). V staršej dokumentácii sa dokonca uvádzalo, že PrintStream je už zastaralá (deprecated) trieda a namiesto nej je lepšie používať PrintWriter (iróniou je, že v novej dokumentácii už táto zmienka nie je a ani trieda už nie je zastaralá…). Táto trieda totiž prevádza zapisované znaky a reťazce na bajty s použitím kódovania používaného operačným systémom, čo môže niekedy spôsobiť stratu medzinárodných znakov.

Metódy PrintStreamu, podobne ako PrintWritera, nehádžu výnimky IOException. Chybový stav je možné zistiť zavolaním booleovskej metódy checkError.

Podotknime, že ak by nás napadlo zatvoriť System.out, tak to nemusí byť práve najšťastnejším nápadom. Ak náhodou potrebujeme presmerovať štandardný výstup, môžeme použiť statickú metódu System.setOut(PrintStream out), ktorej môžeme nastaviť nový PrintStream.

System.in

Táto premenná reprezentuje štandardný vstupný prúd InputStream, z ktorého je možné čítať dáta prichádzajúce zo štandardného vstupu (typicky z klávesnice). S týmto InputStreamom pracujeme ako s každým iným vstupným prúdom.

Ak chceme načítavať riadky z klávesnice, môžeme použiť tradičnú kombináciu InputStreamReadera (obalí System.in schopnosťou načítavať znaky a reťazce) a BufferedReadera (dodá schopnosť načítavať riadky).

BufferedReader console = null;
try {
  console = new BufferedReader(new InputStreamReader(System.in));
  String line = null;
  while((line = console.readLine()) != null) {
    System.out.println(line);
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  /* 
   * zatvárať štandardný vstup nie je múdre,
   * vynecháme preto close()
   */
}

Tu si nemožno neodpustiť ironickú poznámku, že kým v Pascale stačí zavolať readln(), v Jave je vytvorenie objektu konzoly pomerne nepríjemnou záležitosťou. Začiatočníci zrejme neocenia nutnosť vytvoriť tri inštancie a odchytávať výnimky.

Našťastie, v každej novej verzii Javy prišlo k zlepšeniu.

Skener java.util.Scanner (od JDK 5.0)

Skener java.util.Scanner je trieda, ktorá umožňuje načítavať z ľubovoľného textového zdroja reťazce, znaky a ostatné primitívne typy a to i v prípade, že vstupný textový zdroj je formátovaný. Túto triedu možno považovať za analógiou a rozšírenie funkcie scanf() z Cčka. Textovým zdrojom môže byť hocičo: InputStream (bajty sa prevedú na znaky s použitím kódovania operačného systému), readery, reťazce a pod.

Užitočným príkladom je skener nad štandardným vstupom. Ak chceme získať analógiou pascalovského readln(), použijeme metódu String nextLine()

Scanner s = new Scanner(System.in);		
while(s.hasNextLine()) {
  System.out.println(s.nextLine());
}

Skener sme vytvorili nad štandardným vstupom. Riadky načítavame pomocou dvoch metód: hasNextLine() vráti true, ak je možné načítať ďalší riadok (false sa vráti v prípade, že nastal koniec súboru). Metóda nextLine() zase vráti načítaný riadok.

Skener má popri tom ďalšie dvojice metód hasNextXXX() a nextXXX() (pre každý primitívny typ jednu). Skener, ktorý načíta z reťazca postupne podreťazec, boolean, celé číslo a byte je nasledovný. Skener vytvoríme nad vstupným reťazcom a nastavíme medzeru ako oddeľovač (použitím metódy useDelimiter()).

Scanner s = new Scanner("25 25 true 25");
s.useDelimiter(" ");
System.out.println(s.next());
System.out.println(s.nextInt());
System.out.println(s.nextBoolean());
System.out.println(s.nextByte());

Skener nad súborom vyrobíme jednoducho: do konštruktora dodáme inštanciu File. Treba dať pozor na to, že ak by sme do konštruktora dali len reťazec s cestou, bude to chybou, skener sa totiž pokúsi spracovávať samotný reťazec, čo zrejme nie je to, čo chceme.

Scanner scanner = null;
try {
  scanner = new Scanner(new File("input.txt"));   
  while(scanner.hasNextLine()) {
    System.out.println(scanner.nextLine());
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  scanner.close();
}

Podobne ako v prípade vstupných a výstupných prúdov je slušné po skončení práce skener zavrieť, najlepšie vo finally bloku.

Konzola java.io.Console (od JDK 6.0)

Po mnohých rokoch a bedákaniach sa do JDK dodala možnosť jednoduchého načítavania textu z konzoly a to v podobe triedy java.io.Console. Konzola má niekoľko významných metód:

System.console().printf("Zadajte riadok:");
String line = System.console().readLine();
System.out.println(line);

int numberCount = 3;
String numberLine = System.console()
   .readLine("Zadajte %s reťazcov oddelených medzerami:", 
             numberCount);
String[] numbers = numberLine.split(" ");
if(numbers != null && numbers.length == 3) {
  for (Object object : numbers) {
    System.console().printf("%s %s\n", object.getClass(), object);
  }
}

Súbor s náhodným prístupom (random access file)

Idey tried pre vstupno-výstupné operácie doteraz operovali hlavne s prúdmi dát. V prípade súborov sme mohli dáta zapisovať sekvenčne a načítavať rovnako len „po prúde". Drobnou výnimkou boli vstupné prúdy, kde sme sa mohli na jedno miesto vrátiť viackrát a to po použití metód mark() a reset.

Súbor s náhodným prístupom je skôr bližší pojmu poľa bytov, po ktorom sa môžeme hýbať „kurzorom" v ľubovoľnom smere – dopredu i dozadu a to v ľubovoľnej chvíli. Do takéhoto súboru možno podľa potreby čítať a zapisovať a to v ľubovoľnom poradí. Na tento účel existuje trieda java.io.RandomAccessFile. Jej najdôležitejšie metódy sú:

RandomAccessFile raf = null;
try {
  raf = new RandomAccessFile("binary.dat", "rw");
  raf.writeChars("a"); //char zaberá dva bajty
  raf.writeByte(128);  //jeden bajt
  raf.seek(0);         //posun na začiatok
  char c = raf.readChar(); //načíta znak (dva bajty)
  System.out.println(c);   //vypíše znak
  System.out.println(raf.getFilePointer()); // sme na pozícii 2
} catch (FileNotFoundException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
} finally {
  try {
    raf.close();
  } catch (Exception e) {
    //do nothing
  }
}

Literatúra a odkazy

>> Home