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.
01.
//Z tohoho souboru budeme cist
02.
InputStream is =
new
FileInputStream(
"/tmp/file.txt"
);
03.
//Ale soubor je sifrovan, predame (de)sifrovacimu proudu instanci puvodniho vstupniho proudu
04.
//NullCipher nedela nic :-), je to identita
05.
CipherInputStream cis =
new
CipherInputStream(is,
new
NullCipher());
06.
//precteme jeden byte desifrovanych dat
07.
int
i = cis.read();
08.
09.
//uzavreme proud (uvolnime zdroje systemu)
10.
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é).
001.
/**
002.
* Demonstrace serializacnich mechanismu
003.
* @author Pavel Micka
004.
*/
005.
public
class
Main {
006.
007.
/**
008.
* @param args the command line arguments
009.
*/
010.
public
static
void
main(String[] args)
throws
FileNotFoundException, IOException, ClassNotFoundException {
011.
ObjectOutputStream s =
new
ObjectOutputStream(
new
FileOutputStream(
"c:/temp/object.txt"
));
012.
A a =
new
A();
013.
a.setBar(
"bar"
);
014.
a.setFoo(
"foo"
);
015.
s.writeObject(a);
//zapiseme a
016.
017.
018.
B b =
new
B();
019.
b.setFoo(
"fooB"
);
020.
b.setBar(
"barB"
);
021.
s.writeObject(b);
//zapiseme b - vypise "Serializating"
022.
023.
C c =
new
C();
024.
c.setFoo(
"fooC"
);
025.
c.setBar(
"barC"
);
026.
027.
s.writeObject(c);
028.
029.
s.close();
//zavreme vystupni stream
030.
031.
032.
ObjectInputStream inputStream =
new
ObjectInputStream(
new
FileInputStream(
"c:/temp/object.txt"
));
033.
A readA = (A)inputStream.readObject();
034.
System.out.println(readA.getFoo());
//foo
035.
System.out.println(readA.getBar());
//null
036.
037.
B readB = (B) inputStream.readObject();
//vypise "Deserializating"
038.
System.out.println(readB.getFoo());
//fooB
039.
System.out.println(readB.getBar());
//null
040.
041.
C readC = (C) inputStream.readObject();
042.
System.out.println(readC.getFoo());
//fooC
043.
System.out.println(readC.getBar());
//barC
044.
045.
inputStream.close();
//zavreme vstupni stream
046.
047.
}
048.
}
049.
050.
/**
051.
* Demonstrace automaticke serializace
052.
* @author Pavel Micka
053.
*/
054.
class
A
implements
Serializable {
055.
056.
protected
String foo;
057.
/**
058.
*
059.
*/
060.
protected
transient
String bar;
061.
062.
/**
063.
* @return the foo
064.
*/
065.
public
String getFoo() {
066.
return
foo;
067.
}
068.
069.
/**
070.
* @param foo the foo to set
071.
*/
072.
public
void
setFoo(String foo) {
073.
this
.foo = foo;
074.
}
075.
076.
/**
077.
* @return the bar
078.
*/
079.
public
String getBar() {
080.
return
bar;
081.
}
082.
083.
/**
084.
* @param bar the bar to set
085.
*/
086.
public
void
setBar(String bar) {
087.
this
.bar = bar;
088.
}
089.
}
090.
091.
/**
092.
* Demonstrace explicitniho pouziti metod readObject a writeObject
093.
* @author Pavel Micka
094.
*/
095.
class
B
extends
A {
096.
097.
private
void
writeObject(ObjectOutputStream out)
throws
IOException {
098.
//zapise automaticky, co lze automaticky zapsat
099.
out.defaultWriteObject();
100.
System.out.println(
"Serializating"
);
101.
}
102.
103.
private
void
readObject(ObjectInputStream in)
throws
IOException, ClassNotFoundException {
104.
// our "pseudo-constructor"
105.
in.defaultReadObject();
106.
System.out.println(
"Deserializating"
);
107.
108.
}
109.
}
110.
111.
/**
112.
* Pouziti rozhrani Externalizable
113.
* @author Pavel Micka
114.
*/
115.
class
C
extends
A
implements
Externalizable {
116.
117.
public
C() {
118.
}
119.
120.
121.
public
void
writeExternal(ObjectOutput out)
throws
IOException {
122.
out.writeObject(
this
.foo);
123.
out.writeObject(
this
.bar);
124.
}
125.
126.
public
void
readExternal(ObjectInput in)
throws
IOException, ClassNotFoundException {
127.
this
.foo = (String) in.readObject();
128.
this
.bar = (String) in.readObject();
129.
}
130.
}
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.
1.
class
A
implements
Serializable {
2.
private
static
final
long
serialVersionUID = 10275539472837495L;
3.
.
4.
.
5.
.
6.
}
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.
01.
/**
02.
* Radkova utilita, ktera spocita pocet znaku, slov a radku v zadanem souboru
03.
* @author Pavel Micka
04.
*/
05.
public
class
Main {
06.
07.
/**
08.
* @param args the command line arguments
09.
*/
10.
public
static
void
main(String[] args)
throws
FileNotFoundException, UnsupportedEncodingException, IOException {
11.
//dva parametry prikazove radky -> 1) cesta k souboru 2) kodovani souboru
12.
if
(args.length !=
2
){
13.
System.out.println(
"Usage: program file_name file_encoding"
);
14.
System.exit(
1
);
//ukonci beh programu (virtualniho stroje) s kodem 1, kod jiny nez 0 == chyba
15.
}
16.
String filePath = args[
0
];
17.
String fileEncoding = args[
1
];
18.
19.
int
wordCount =
0
;
20.
int
lineCount =
0
;
21.
int
characterCount =
0
;
22.
23.
//vytvorime novou bufferovanou ctecku s danym kodovanim z FileInputStreamu
24.
BufferedReader reader =
new
BufferedReader(
new
InputStreamReader(
new
FileInputStream(filePath), fileEncoding));
25.
String line =
null
;
26.
while
((line = reader.readLine()) !=
null
){
//dokud mame k dispozici dalsi radek
27.
characterCount += line.length();
28.
lineCount++;
29.
30.
//tokenizer rozlozi text na jednolive symboly (tokeny) dle uvedenych oddelovacu (delimiter)
31.
StringTokenizer tokenizer =
new
StringTokenizer(line,
" \\u0009"
);
//oddelovace jsou mezera a \\u0009 == tabulator
32.
wordCount += tokenizer.countTokens();
33.
}
34.
35.
reader.close();
//i Readery (a Writery) musime zavrit
36.
37.
System.out.println(
"Pocet znaku: "
+ characterCount);
38.
System.out.println(
"Pocet slov: "
+ wordCount);
39.
System.out.println(
"Pocet radku: "
+ lineCount);
40.
}
41.
}
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/>.