Java (15) - I/O operace I.

Jednou z klíčových činností v programech je výměna dat s okolním světem. V Javě to jde velice snadno a elegantně.

1.6.2005 15:00 | Lukáš Jelínek | přečteno 48934×

Základní informace o streamech

Vstupně/výstupní operace lze v Javě realizovat několika způsoby. My se nyní zaměříme na ten základní způsob, tedy práci se streamy. Téměř každý, kdo programuje v nějakém jazyce, bude pojem stream znát. A právě v Javě se tomuto slovu dostává plného významu.

Stream si lze představit jako trubku, jejíž konec máme k dispozici a můžeme "čerpat" data z něho (tedy číst) nebo naopak do něho (tj. zapisovat). Streamů existuje (z hlediska implementace) celá řada, z pohledu uživatele se však všechny streamy daného směru (tedy "dovnitř" nebo "ven") chovají skoro stejně - většinu funkcionality mají společnou. Pro streamy je charakteristická především jejich sekvenčnost. I když to neplatí úplně stoprocentně (existují streamy se "zpětným chodem"), s daty se pracuje v konkrétním neměnném pořadí.

Streamy v Javě

V Javě je každý stream reprezentován jako objekt, tedy instance nějaké třídy. Balík java.io obsahuje hierarchii základních tříd, se kterými si pro běžné operace vystačíme (další streamy najdeme i v jiných standardních balících). Pokud budeme potřebovat něco speciálního, není žádný problém si vhodnou třídu vytvořit (resp. odvodit od nějaké existující).

Známe dvě hlavní kategorie streamů: binární a textové. Liší se způsobem práce se znaky, binární streamy pracují se "surovými" bajty (tak, jak jsou), zatímco streamy textové pojímají bajty, resp. skupiny bajtů způsobem, který odpovídá nastavení prostředí. Binární vstupní streamy jsou odvozeny od abstraktní třídy InputStream, výstupní od třídy OutputStream. Textové pak od třídy Reader, resp. Writer.

Základy práce se streamy

Každý stream má svůj životní cyklus - je velmi jednoduchý a sestává z těchto etap:

  1. Vytvoření (zavolání konstruktoru) - může se vytvářet přímo tam, kde se používá, anebo ho může vytvořit nějaký jiný objekt a předat.
  2. Otevření - stream se často otvírá už při vytváření, pokud však otevřen nebyl, musí se otevřít později, jinak s ním nelze pracovat. Otevření znamená, že se alokují potřebné systémové prostředky a stream se připraví pro práci.
  3. Vlastní práce - se streamem se provádějí požadované operace, tzn. volají se jeho metody.
  4. Uzavření - toto je velmi důležité, bohužel se na to často zapomíná. I když korektně finalizovaný objekt streamu zaručuje správné zavření (u standardních streamů), podobně i normální ukončení programu, nikdy se na to nesmí spoléhat. Jednak se vyčerpávají systémové prostředky (file deskriptory apod.), a za druhé může mnoho dat zůstat nezapsaných (u výstupních streamů), a to i hodně dlouho. Streamy se prostě musí uzavírat, a to ihned, když s nimi přestáváme pracovat (s jednou výjimkou, kterou za chvíli zmíním).

Ošetření chyb

Důležitým aspektem práce se streamy je ošetření chyb, které se mohou vyskytnout. Téměř všechny chybové stavy jsou řešeny výjimkami. Tou základní je IOException, kterou mohou vyhodit naprostá většina streamových metod. Tato výjimka je synchronní (musí se tedy povinně zachytávat nebo deklarovat k předání výše), zahrnuje do sebe celou škálu podtříd (výjimek signalizujících konkrétní chybové stavy) a je společná všem streamům bez ohledu na implementaci. Kromě IOException mohou streamy produkovat i jiné výjimky, to ale závisí na jejich určení a implementaci.

Jak jsem již uvedl, výjimku IOException může vyhodit prakticky kterákoli metoda streamu. Musíme to mít na zřeteli, ani takové uzavírání streamu není "bezpečná" operace. Když se na to zapomene, připomene to rázně kompilátor.

Základní druhy streamů

Se streamy pracujeme prakticky stejně, ať už se jedná o data v souborech na disku, o síťovou komunikaci, komunikaci mezi vlákny apod. I když je tato práce téměř shodná, existují podstatné rozdíly zejména v přípravě streamů před komunikací. Podíváme se tedy blíže na jednotlivé druhy streamů.

Souborové streamy

Asi nejčastějším způsobem komunikace s vnějším prostředím je čtení a zápis souborů. Z hlediska Javy se nerozlišují vlastnosti souborového systému, se soubory se pracuje vždy stejně, jen nás zajímá, zda soubor existuje, lze z něj číst nebo do něj zapisovat.

try {
  InputStream is = new FileInputStream("soubor.dat"); // stream se hned otevře
  int i = 0;
  while ((i = is.read()) >= 0) {      // čte se, dokud není konec souboru
      ...
  }
  is.close();   // zavření souboru
} catch (IOException e) {
    ...         // zpracování výjimky
}

Příklad ukazuje základní způsob čtení ze streamu, v tomto případě souborového. Stream se otevře, v cyklu se z něho čte po bajtech (pozor - i když se čtou bajty, hodnota je typu int; je to z více důvodů, ale důležité je, že pokud je přečtena hodnota -1, bylo dosaženo konce souboru). Všimněte si, že všechny operace jsou uzavřeny do bloku try k zachycení výjimek.

Řetězcové streamy

Podobně jako třeba v C++, i v Javě lze snadno číst z řetězce a zapisovat do něj streamovým způsobem. Stream pracuje nad objektem typu StringBuffer, ke kterému můžeme získat přímý přístup - lze ale také ze streamu "vytáhnout" také instanci třídy String, ta se ale samozřejmě musí vždy vytvořit, neboť je neměnná.

Následující příklad ukazuje, jak se streamově zapisuje do řetězce. Stream je samozřejmě textově orientovaný, což je naprosto v souladu s daným účelem. Pro výstupní řetězcové streamy je charakteristické, že operace nevyhazují výjimku IOException - a to ani při pokusu dělat nějaké operace po uzavření streamu (operace uzavření totiž nic nedělá).

StringWriter sw = new StringWriter();
sw.write("abcd");
sw.write(sw.toString());   // obsahu streamu se zapíše zpět do streamu
System.out.println(sw);

Copak asi uvedený příklad dělá? Zapíše uvedený řetězec dvakrát za sebou (nejprve přímo, potom prostřednictvím metody toString() zavolané na streamu) a celý obsah vypíše na standardní výstup. Zbývá si ještě zodpovědět jednoduchou otázku, k čemu je to vlastně dobré - samozřejmě hlavně k tomu, že vytvořený stream můžeme předat k nějakému vnějšímu použití, kde nezáleží na tom, kam se data zapisují (resp. odkud se čtou).

Mezi základní streamy patří ještě některé další druhy, ale o těch si řekneme až později. Nejdřív by se totiž hodilo znát něco jiného...

Filtrové streamy

Největší množství streamů patří do obrovské množiny, které se říká filtrové streamy. Takový stream si lze představit skutečně jako nějaký kus trubky s filtrem. Nejobecněji to vypadá tak, že tento stream napasujeme na nějaký jiný stream. Data, která přes filtrový stream procházejí, mohou být různě pozměněna, stream je může všelijak zkoumat a něco počítat atd. Filtrové streamy lze prakticky libovolně řetězit za sebe (pokud na sebe navazují stejné kategorie, ve smyslu binární a textové).

Filtrových streamů je celá řada, řekneme si tedy nejprve o těch nejdůležitějších.

Bufferované streamy

Protože často pracujeme s malými objemy dat, nebývají vstupně/výstupní operace příliš operačně výkonné. Záleží na prostředcích operačního systému, druhu streamu atd., a většinou nemůžeme na nic spoléhat (píšeme platformově nezávislé programy!). Proto existují streamy, které obsahují vlastní buffer a optimalizují přístupy k datům.

Představme si, že zapisujeme data třeba po jednom bajtu - to by za normálních okolností mohlo znamenat třeba mnoho zbytečných přístupů na disk, posílání "prázdných" paketů po síti apod. Přitom obvykle není žádný důvod, aby se data okamžitě sunula někam dál. Pro optimalizaci tedy použijeme bufferovaný stream.

try {
    BufferedReader br = new BufferedReader(new FileReader("soubor.txt"));
    String s = "";
    while ((s = br.readLine()) != null) {
        ...
    }
    br.close();
} catch (IOException e) {
    ...
}

Příklad ukazuje hned několik aspektů práce s bufferovaným (textovým) streamem. Filtrové streamy obvykle vytváříme tak, že předáme jejich konstruktoru jako parametr podřízený stream, v daném případě textový souborový.

Dále je vidět, že zde můžeme číst celé řádky - to je věc specifická právě pro tento stream, ale i zde můžeme stále číst jednotlivé znaky, toto je jen usnadnění. Po skončení práce uzavřeme "nejvrchnější" stream, ten už zajistí kaskádovitě uzavření všech ostatních.

Ještě důležitá poznámka - při použití bufferovaných výstupních streamů není zaručeno, kdy se data z bufferu přesunou do navazujícího streamu. K zajištění zápisu dat z bufferu proto v případě potřeby voláme metodu flush().

Konverzní streamy

Název není úplně přesný, řeč bude pouze o konverzi mezi textovými a binárními streamy. V řadě případů totiž odněkud získáme binární stream, a přitom potřebujeme textový. Vřadíme tedy mezičlánek, který nám konverzi zajistí. Viz příklad:

try {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    int i = 0;
    while (i >= 0) {
        i = br.read();
        if (i >= 0) {
            char c = (char) i;
            ...
        }
    }
    br.close();
} catch (IOException e) {
    ...
}

V příkladu je použit standardní vstup (čili obvykle klávesnice), který však je binární. Protože potřebujeme znaky, musíme provést převod, a to nejlépe hned "na cestě", pomocí konverzního streamu.

Streamy pro kompresi/dekompresi dat

Jako takovou třešničku na dortu, která ukáže, co se také se streamy dá dělat, si nyní vyzkoušíme dekompresi dat a zároveň výpočet kontrolního součtu. Na samém konci budeme číst po řádcích textová data. Stačí jen streamy zřetězit za sebe...

try {
    CheckedInputStream cis = 
            new CheckedInputStream(new GZipInputStream(new FileInputStream("data.gz")));
    BufferedReader br = new BufferedReader(new InputStreamReader(cis));
    String s = "";
    while ((s = br.readLine()) != null) {
        ...
    }
    System.out.println("Kontrolni soucet je: " + cis.getValue());
    br.close();
} catch (IOException e) {
    ...
}

Jak snadné... A tohle zdaleka není všechno, co streamy dovedou. Příště se podíváme na některé další druhy streamů (těch zajímavých je ještě řada), řekneme si něco o tom, jak efektivně a bezpečně přenášet různá data, a také jak si vyrobit vlastní stream pro specifické účely.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=836