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/>.