Doposud naše aplikace pracovaly vesměs s daty, která jsme jim interně zadali. V dnešním dílu si představíme datové proudy (streamy), pomocí kterých může naše aplikace komunikovat se svým okolím.
Proudy pro nás ale nejsou žádnou novinkou, již jsme se s nimi mnohokrát setkali. Pro výpis na konzoli jsme volali metody proudu System.out, a když jsme ve 14. dílu získávali číslo ze standardního vstupu, tak jsme jej četli z proudu System.in.
Bylo by ovšem mylné se domnívat, že vstupem a výstupem programů je pouze klávesnice a konzole. Proudy mohou stejně dobře pracovat s výstupními zařízeními (tiskárnou), obrázky, soubory a mnoha dalšími sériovými zdroji/cíly dat.
Typy proudů
Proudy můžeme dělit dle jejich směru na vstupní (přenášejí data do aplikace) a na výstupní (přenášejí data z aplikace). Proudy také rozlišujeme na binární a znakové. Jak již názvy napovídají, tak zatímco binární proudy využijeme pro libovolná binární data (tj. data vkládáme je po bajtech), tak znakové proudy jsou určeny pouze pro text (znaky).
Decorator
S proudy je v Javě neodmyslitelně spojen návrhový vzor decorator (dekorátor). Jak jsme si již naznačili, existuje velké množství proudů pro různé situace a zařízení. Tyto proudy ale neobsahují veškerou možnou funkcionalitu, primárně protože kombinace jednotlivých vlastností nemusí být kompatibilní, a ve většině případů s sebou nese i určitou režii.
Pokud bychom zároveň vytvářeli zvláštní třídu pro každý různě obohacený proud, tak bychom jich brzy měli tisíce. Příkladem by pak mohla být třída Proud pro zápis do souboru s vyrovnávací pamětí, bez kontrolního součtu, šifrovaný, bez hashe (a pětatřiceti dalšími přívlastky).
Základní myšlenka vzoru decorator je velmi jednoduchá – ať je proud obohacený jakýmkoliv způsobem, tak musí mít pořád stejné rozhraní.
Příklad
Pro případ vstupního (de)šifrujícího souborového proudu proto stačí vytvořit nejprve proud pro přístup k souboru (základní, nešifrovaný) a ten vložit jako zdroj do (de)šifrujícího proudu. Veškerá volání dešifrujícho proudu nejprve zpracujeme a poté delegujeme patřičným metodám obaleného proudu.
Pokud tedy budeme chtít použít metodu, která se šifrování nijak netýká – například uzavření proudu – tak (de)šifrující proud volání pouze přepošle. Pokud budeme chtít přečíst nějaká data, tak je (de)šifrující proud přečte z obaleného proudu a nám navrátí výstup interního dešifrovacího procesu.
        //Z tohoho souboru budeme cist
        InputStream is = new FileInputStream("/tmp/file.txt");
        //Ale soubor je sifrovan, predame (de)sifrovacimu proudu instanci puvodniho vstupniho proudu
        //NullCipher nedela nic :-), je to identita
        CipherInputStream cis = new CipherInputStream(is, new NullCipher());
        //precteme jeden byte desifrovanych dat
        int i = cis.read();
        
        //uzavreme proud (uvolnime zdroje systemu)
        cis.close();
Binární proudy
Binární proudy umožňují přenést libovolná data. Vstupní proudy dědí z třídy InputStream (dokumentace), výstupní z OutputStream (dokumentace). Základní operací definovanou v InputStreamu je metoda read, pomocí které můžeme z proudu přečíst jeden bajt. Analogicky výstupní proud definuje metodu write.
Čtení a zápis po jednotlivých bajtech je velmi pomalé, zvláště pokud uvážíme, že na druhé straně proudu může být disk – a každý dotaz může velmi snadno znamenat nutnost nového vystavení hlaviček.
BufferedStream
Třídy BufferedInputStream (dokumentace) a BufferedOutputStream (dokumentace) za nás tento nedostatek řeší. Tyto proudy obsahují pole, které slouží jako vyrovnávací paměť. V případě čtení z disku bufferovaný proud načte celý blok dat a uloží jej do pole, ze kterého data dále posílá našemu programu. V okamžiku, kdy se pole vyprázdní, učiní dotaz na další blok. Tímto způsobem dochází k eliminaci velkého množství zbytečných a drahých volání.
Obdobně pro zápis na disk použijeme BufferedOutputStream. Zde se ovšem můžeme dostat do situace, kdy zapíšeme do bufferu méně dat, než je potřebné k jeho vyprázdnění (zápisu do obaleného proudu). V tomto případě můžeme zavolat metodu flush, která vyprázdní všechny buffery (tj. zajistí okamžitý zápis všech dat). Před uzavíráním proudu – uzavření dealokuje veškeré zdroje nutné pro udržení spojení – není nutné flush použít, metoda close to udělá sama.
Serializace
Velmi zajímavou možností využití proudů je serializace (ukládání objektů do formátu, který může být uložen nebo poslán po síti). K serializování objektů můžeme použít standardní mechanismus využívající třídy ObjectOutputStream (dokumentace) a rozhraní Serializable (dokumentace).
Pro serializaci objektu ovšem musí být splněno několik podmínek:
- Třída objektu implementuje rozhraní Serializable (nemá metody, slouží pouze jako marker).
 - Každá nadtřída, která neimplementuje Serializable musí mít veřejný bezparametrický konstruktor. Serialiyovaná třída musí zajistit serializaci datových složek všech nadtříd, které nejsou serializovatelné.
 - Všechny datové složky objektu musí být buď primitivního typu, serializovatelné nebo přechodné – klíčové slovo transient (značí, že daná položka nebude serializována).
 
První podmínku jsme schopni zaručit téměř vždy – stačí pouze objekt oddědit a implementovat rozhraní (výjimkou jsou konečné třídy (mají v hlavičce modifikátor final)). Zbylé výjimky můžeme obejít implementací rozhraní Externalizable (dokumentace), pomocí kterého si nadefinujeme vlastní serializační protokol.
Pokud chceme při serializaci a deserializaci pouze provést nějaké dodatečné akce, ale vystačíme si s automatickým mechanismem, tak musíme na serializovatelném objektu pouze implementovat metody readObject a writeObject (musí být soukromé).
/**
 * Demonstrace serializacnich mechanismu
 * @author Pavel Micka
 */
public class Main {
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
        ObjectOutputStream s = new ObjectOutputStream(new FileOutputStream("c:/temp/object.txt"));
        A a = new A();
        a.setBar("bar");
        a.setFoo("foo");
        s.writeObject(a); //zapiseme a
        B b = new B();
        b.setFoo("fooB");
        b.setBar("barB");
        s.writeObject(b); //zapiseme b - vypise "Serializating"
        C c = new C();
        c.setFoo("fooC");
        c.setBar("barC");
        s.writeObject(c);
        s.close(); //zavreme vystupni stream
        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("c:/temp/object.txt"));
        A readA = (A)inputStream.readObject();
        System.out.println(readA.getFoo()); //foo
        System.out.println(readA.getBar()); //null
        B readB = (B) inputStream.readObject(); //vypise "Deserializating"
        System.out.println(readB.getFoo()); //fooB
        System.out.println(readB.getBar()); //null
        C readC = (C) inputStream.readObject();
        System.out.println(readC.getFoo()); //fooC
        System.out.println(readC.getBar()); //barC
        inputStream.close(); //zavreme vstupni stream
    }
}
/**
 * Demonstrace automaticke serializace
 * @author Pavel Micka
 */
class A implements Serializable {
    protected String foo;
    /**
     *
     */
    protected transient String bar;
    /**
     * @return the foo
     */
    public String getFoo() {
        return foo;
    }
    /**
     * @param foo the foo to set
     */
    public void setFoo(String foo) {
        this.foo = foo;
    }
    /**
     * @return the bar
     */
    public String getBar() {
        return bar;
    }
    /**
     * @param bar the bar to set
     */
    public void setBar(String bar) {
        this.bar = bar;
    }
}
/**
 * Demonstrace explicitniho pouziti metod readObject a writeObject
 * @author Pavel Micka
 */
class B extends A {
    private void writeObject(ObjectOutputStream out) throws IOException {
        //zapise automaticky, co lze automaticky zapsat
        out.defaultWriteObject();
        System.out.println("Serializating");
    }
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // our "pseudo-constructor"
        in.defaultReadObject();
        System.out.println("Deserializating");
    }
}
/**
 * Pouziti rozhrani Externalizable
 * @author Pavel Micka
 */
class C extends A implements Externalizable {
    public C() {
    }
    
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(this.foo);
        out.writeObject(this.bar);
    }
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.foo = (String) in.readObject();
        this.bar = (String) in.readObject();
    }
}
Metoda reset
Kdybychom chtěli do výstupního proudu ukládat jeden a ten samý objekt vícekrát (byť s pozměněnými datovými složkami), tak by nám k tomu prosté opakované volání writeObject nestačilo. Výstupní proud si totiž interně pamatuje, že daný objekt již byl zapsán a podruhé jej neuloží. Tuto vlastnost ale můžeme obejít vyčištěním interní paměti proudu pomocí volání metody reset.
Serial version UID
Java si v rámci serializovatelných tříd udržuje konstantu serialVersionUID, která udává verzi objektu. Pokud změníme jeho definici, tak se změní i zmíněná konstanta a původní objekt již nepůjde deserializovat.
Existují ale i operace, které uložený objekt nezneplatní – například přidání členské proměnné, její odmazání, přidání či odebrání metody objektu. V tento okamžik se hodí mít u původní i zastaralé (uložené) entity stejné, explicitně zadané, serialVersionUID, aby deserializace proběhla bez problémů (přidaná/odebraná proměnná byla ignorována).
Mezi změny, které nejsou kompatibilní, patří odebrání rozhraní Serializable a obecně jakákoliv změna hierarchie předků dané třídy.
class A implements Serializable {
    private static final long serialVersionUID = 10275539472837495L;
    .
    .
    .
}
Znakové proudy
Znakové proudy fungují stejným způsobem jako ty binární, pouze operují s textem (a jsou odvozeny od tříd Reader (dokumentace) a Writer (dokumentace)).
Poměrně důležitou poznámkou je, že Java interně uchovává řetězce ve znakové sadě Unicode. Z toho plyne, že při každém zápisu a čtení ze znakového proudu dochází k překódování daného řetězce (znaků).
Příklad
Abychom si demonstrovali použití znakových proudů, tak vytvoříme řádkový nástroj, který v zadaném textovém souboru spočítá počet řádků, slov a písmen.
/**
 * Radkova utilita, ktera spocita pocet znaku, slov a radku v zadanem souboru
 * @author Pavel Micka
 */
public class Main {
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException, IOException {
        //dva parametry prikazove radky -> 1) cesta k souboru 2) kodovani souboru
        if(args.length != 2){
            System.out.println("Usage: program file_name file_encoding");
            System.exit(1); //ukonci beh programu (virtualniho stroje) s kodem 1, kod jiny nez 0 == chyba
        }
        String filePath = args[0];
        String fileEncoding = args[1];
        int wordCount = 0;
        int lineCount = 0;
        int characterCount = 0;
        //vytvorime novou bufferovanou ctecku s danym kodovanim z FileInputStreamu
        BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), fileEncoding));
        String line = null;
        while((line = reader.readLine()) != null){ //dokud mame k dispozici dalsi radek
            characterCount += line.length();
            lineCount++;
            //tokenizer rozlozi text na jednolive symboly (tokeny) dle uvedenych oddelovacu (delimiter)
            StringTokenizer tokenizer = new StringTokenizer(line, " \\u0009"); //oddelovace jsou mezera a \\u0009 == tabulator
            wordCount += tokenizer.countTokens();
        }
        
        reader.close(); //i Readery (a Writery) musime zavrit
        
        System.out.println("Pocet znaku: " + characterCount);
        System.out.println("Pocet slov: " + wordCount);
        System.out.println("Pocet radku: " + lineCount);
    }
}
Abychom mohli program otestovat z vývojového prostředí Netbeans, musíme vybrat kontextovém menu projektu volbu Properties, zvolit položku Run a vyplnit pole Arguments (tj. umístit zde cestu ke zvolenému textovému souboru a název jeho kódování).
Také si můžeme zkusit program spustit z příkazové řádky. Nejprve vstupíme do složky projektu a podsložky build/classes. Zde program spustíme příkazem "java balicek_tridy.jmeno_tridy parametry_prikazove_radky".
Zjištění kódování textového souboru
Pokud nevíte, jakým způsobem zjistit kódování textového souboru, tak si stáhněte textový editor PSPad (aktuálně ve verzi 4.5.4). Otevřete v něm zvolený soubor a podívejte se na dolní lištu (zde uvidíte napsáno například Kódování UTF-8). Pokud byste chtěli znakovou sadu změnit, tak můžete zvolit jinou v menu Formát.
Literatura
- HORTON, Ivor. Java 5. Praha : Neocortex spol s.r.o., 2005. 1443 s. ISBN 80-86330-12-5.
 - GREANIER, Todd. Sun Developer Network (SDN) [online]. July 2000 [cit. 2010-12-09]. Discover the secrets of the Java Serialization API. Dostupné z WWW: <http://java.sun.com/developer/technicalArticles/Programming/serialization/>.