LINUXSOFT.cz
Nazwa użytkownika: Hasło:     
    CZ UK PL

> Java (16) - I/O operace II.

Zajímavých streamů je mnohem víc, než jsme si ukázali minule. Proto nyní dojde i na některé další. Přijde řada i na tvorbu vlastních streamů pro specifické účely.

23.6.2005 07:00 | Lukáš Jelínek | czytane 37212×

RELATED ARTICLES KOMENTARZE   

Další zajímavé streamy

Ve standardních knihovnách se nachází řada zajímavých streamů, na které stojí za to prozkoumat. Tedy vzhůru do toho, podívejme se na některé z nich!

PrintStream

PrintStream je výstupní stream, který nemá svůj vstupní protějšek a slouží k tisku uživatelsky srozumitelných dat různým způsobem. Může být napojen přímo na výstupní soubor nebo (protože je to filtrový stream) na libovolný jiný výstupní stream.

Pozn.: Napojení PrintStream na soubor lze používat od JDK verze 1.5, u dřívějších verzí je nutno použít přístup přes FileOutputStream.

Charakteristickou vlastností třídy PrintStream je, že nevyhazuje výjimky IOException - chyby jsou v podstatě ignorovány (lze je ale zjistit voláním metody checkError()). Proto se hodí hlavně tam, kde fungování streamu není z hlediska funkce celého programu důležité (logování, informativní výpisy apod.).

Ještě důležitější je ovšem, že PrintStream poskytuje přímou podporu převodu různých primitivních typů na textovou reprezentaci, a následně na proud bajtů. Při tomto převodu se uplatní kódová tabulka platformy nebo (pokud byl zavolán příslušný konstruktor) kódování poskytnuté vytvářené instanci streamu.

Třída samozřejmě disponuje metodami write(), které se chovají tak, jak je pro výstupní streamy obvyklé. Hlavní síla je však v metodách print() a println() - rozdíl je pouze ten, že druhá z metod navíc vloží konec řádku. Právě tyto metody provádějí výše uvedené konverze. Od verze 1.5 lze použít i volání printf() s podobným chováním, jako má stejnojmenná funkce v jazyce C (stejnou službu ale poskytne i metoda format()).

Konstruktoru lze poskytnout argument říkající, že se má provádět tzv. autoflush (automatický zápis výstupního bufferu). Pokud je tato funkce zapnuta, buffer se zapíše ihned po zavolání některé metody println(), po zápisu znaku pro odřádkování anebo po zápisu pole bajtů. Stream vytvořený přímým napojením na soubor má funkci autoflush vypnutou. Více ukáže následující příklad:

PrintStream ps = null;
try {
    ps = new PrintStream("vystup.txt");
} catch (FileNotFoundException e) {
    System.err.println("Vystupni soubor nelze otevrit");
    ps = System.out;  // použije se standardní výstup
}

ps.println("nejaky text");
ps.printf("%X", new Integer(120));  // vypíše hexadecimální číslo
...
ps.println();

ps.close();

Konstruktor je v příkladu uzavřen do bloku try - to je nezbytné kvůli výjimce FileNotFoundException, kterou konstruktor může vyhodit. Pokud k vyhození dojde (nastane problém s otevřením souboru), použije se v příkladu standardní výstupní stream. Zbylá část programu už nemusí mít (a nemá) kontrolu výjimek.

Zvláštním případem jsou dva standardní (systémové) streamy: standardní výstup (System.out) a standardní chybový výstup (System.err). Tyto streamy (v příkladu jsou použity) jsou v každém programu k dispozici a navenek (z pohledu operačního systému) se chovají úplně stejně jako jejich céčkovské obdoby. Pro úplnost uvádím, že je k dispozici i standardní vstup (System.in), na který se díváme přes rozhraní InputStream.

PipedInputStream/PipedOutputStream a PipedReader/PipedWriter

Tyto dva páry streamů představují tzv. roury (pipe), což jsou vlastně vzájemně propojené streamy. Vezmeme jeden vstupní a jeden výstupní stream, propojíme je, a s každým koncem pracujeme úplně stejně, jako by to byl normální vstupní, resp. výstupní stream. K čemu je to dobré?

Nejčastějším použitím je komunikace mezi vlákny (o vláknech samotných bude řeč někdy později), kdy se v jednom vlákně vytvářejí nějaká data a současně se ve druhém tato data zpracovávají. Je to výhodné hlavně proto, že se nemusíme starat o synchronizaci přístupu k datům, práce se streamy je velmi jednoduchá, můžeme využít veškeré možnosti nabízené streamy a při změně způsobu práce není třeba příliš do programu zasahovat.

Tyto streamy se vytvářejí tak, že vytvoříme jeden z nich a druhému ho předáme jako argument v konstruktoru. Jinou cestou je vytvořit je nezávisle a pak na některém z nich zavolat metodu connect(). Viz příklad:

// první možnost
PipedInputStream is = new PipedInputStream();
PipedOutputStream os = new PipedOutputStream(is);

// druhá možnost
PipedOutputStream os = new PipedOutputStream();
PipedInputStream is = new PipedInputStream(os);

// třetí možnost
PipedReader pr = new PipedReader();
PipedWriter pw = new PipedWriter();
pr.connect(pw);

ByteArrayInputStream/ByteArrayOutputStream

Jedná se o dvojici streamů, které pracují nad polem bajtů. Je to podobné jako u známých tříd StringReader/StringWriter (a dalších podobných, kam patří třeba StringBufferInputStream nebo CharArrayWriter). Máme nějakou oblast v paměti, kam se zapisují (resp. odkud se čtou) data streamu. S těmito daty pak můžeme naložit dle libosti.

Výstupní stream funguje tak, že si spravuje vlastní buffer, a dokud se tento nevymaže, stále se zapisováním plní. Pokud potřebujeme jeho obsah, získáme kopii dat (ne tedy přístup k původnímu bufferu) zavoláním toByteArray. To je třeba si dobře uvědomit kvůli výkonnostním úvahám! Data lze získat i ve formě textového řetězce (voláním toString()).

Vstupní stream naopak pracuje vždy s bufferem pevné velikosti. Přečíst lze jen tolik bajtů, kolik jich v bufferu je.

Serializace/deserializace dat

Velice často máme nějak uložená data (v primitivních nebo složitějších datových typech) a potřebujeme je uložit nebo přenést na jiné místo. Musíme to udělat tak, aby se v jiném čase nebo na jiném místě data správně zrekonstruovala do původní podoby. Těmto činnostem říkáme serializace a deserializace.

Serializace je konverze obecných dat (nějakým způsobem uložených) na proud bajtů tak, aby je šlo následně snadno zrekonstruovat. Naopak deserializace je právě rekonstrukce proudu bajtů na data použitelná v programu. Java k těmto činnostem poskytuje výraznou podporu.

Serializace/deserializace primitivních typů

Celý mechanismus okolo serializace/deserializace je docela složitý, proto bych se nyní chtěl zaměřit jen na to, co je důležité pro základní práci. Protože v Javě nikdy nevíme, jak jsou jednotlivé datové typy uloženy (i když třeba známe jejich číselné rozsahy), nelze jednoduše rozsekat třeba long na 8 bajtů (většinou by to sice šlo, ale ztrácíme tím plnou přenositelnost - obecně se totiž může stát, že "předpoklady" nejsou zcela naplněny), natož něco kopírovat rovnou (pořadí bajtů!). Naštěstí se zrovna o toto nemusíme starat.

Máme totiž dvě třídy, DataInputStream a DataOutputStream, které potřebné konverze bezpečně udělají za nás. Streamy mají metody pro uložení/načtení všech primitivních datových typů. Pozor samozřejmě na to, v jakém pořadí se data ukládají. Tento způsob serializace neumožňuje jednotlivé typy zpětně identifikovat! Příklad naznačí, jak se s uvedenými třídami pracuje:

int i = 165;
float f = 0.35;

try {
    DataOutputStream os = new DataOutputStream(new FileOutputStream("soubor.dat"));
    os.writeInt(i);    // bezpečné uložení hodnoty typu int
    os.writeFloat(f);  // bezpečné uložení hodnoty typu float
    os.close();
} catch (IOException e) {
    ...
}

Serializace/deserializace objektů

Trochu složitější je to s instancemi objektů. Ale i tady máme podobné prostředky - v podobě tříd ObjectInputStream a ObjectOutputStream. Ty nejenže ukládají a načítají instance objektů, ale poradí si i s primitivními typy (takže pokud je používáme, nemusíme už používat třídy DataInputStream/DataOutputStream).

Nelze ukládat všechny objekty. Nutnou podmínkou je, aby implementovaly rozhraní Serializable (pokud se pokusíme serializovat nevyhovující objekt, dočkáme se výjimky NotSerializableException). Protože se instance serializuje i se všemi odkazovanými objekty, musí být i tyto serializovatelné, anebo označené modifikátorem transient (tedy že nebudou uloženy).

Narozdíl od primitivních typů, u objektů lze při deserializaci zjistit jejich typ (a nejen to, k úspěšné deserializaci musí být k dispozici příslušná třída - jinak to skončí výjimkou ClassNotFoundException; případné poškození dat vyvolá zase jiné výjimky). Metoda readObject() sice vrací referenci na typ Object, ale třídu si můžeme zjistit voláním getClass() na vrácené instanci nebo jiným způsobem, a následně přetypovat podle potřeby. Více opět napoví příklad:

ArrayList list = new ArrayList();  // vytvoříme seznam
list.add("nejaky text");           // vložíme hodnoty
list.add(new Double(1.655));
list.add(new Integer(123));

try {
    ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("seznam.dat"));
    os.writeObject(list); // celý seznam se bezpečně uloží
    os.close();
} catch (IOException e) {
    ...
}

Zde je dobře vidět, že můžeme snadno uložit nebo přenést celý kontejner i s obsahem. Jen pozor na to, že všechny obsažené objekty musí být serializovatelné. K procesu serializace se ještě někdy později vrátíme a podíváme se na něj podrobněji - toto by jako úvod stačilo.

Vytváření vlastních streamů

Někdy potřebujeme stream, který má nějaké speciální vlastnosti. Proto si můžeme (pokud nám žádný z dostupných streamů nevyhovuje) vytvořit vlastní stream, do kterého přidáme potřebné funkce. Nejlepší je rozšířit nějaký už existující stream.

Ukážeme si to na streamu filtrového typu. Požadujeme, aby stream sledoval četnost jednotlivých bajtů (tedy hodnoty 0-255). Nový stream odvodíme od třídy FilterInputStream předefinováním potřebných metod. Mohlo by to vypadat třeba takto:

public class CounterInputStream extends FilterInputStream {
    private long cnt [] = new long[256];  // pole pro uložení četností
    
    public FilterInputStream(InputStream in) {
        super(in); // konstruktor pouze zavolá předka
    }
    
    // Metoda resetuje počitadla četností
     public void resetCounters() {
        for (int i=0; i<256; i++) {
            cnt[i] = 0;
        }
    }
    
    // Vrací četnost daného bajtu.
    // Meze indexu se netestují.
    public long getCount(int index) {
        return cnt[index];
    }
    
    // Základní metoda - přečtení bajtu
    public int read() throws IOException {
        int b = super.read(); // přečte se bajt
        if (b >= 0) cnt[b]++;  // pokud je platný, inkrementuje se počitadlo
        return b;
    }
    
    // Metoda pro čtení bloku bajtů
    public int read(byte[] b, int off, int len) throws IOException {
        int r = super.read(b, off, len);
        if (r > 0) {
            for (int i=0; i<r; i++) {
                cnt[b[i]]++; // není třeba testovat platnost
            }
        }
        return r;	
    }
}

Z metod read() předka předefinováváme pouze ty dvě uvedené, třetí může zůstat v původní podobě (volá totiž jednu z těch zbývajících). Uvedená implementace má jednu zásadní vlastnost, a sice tu, že při návratu na označenou pozici (metodou reset() - pokud to samozřejmě podřízený stream umožňuje) se znovu čtené bajty opět započítají.

Zde je jednoduchý příklad použití vytvořeného streamu:

try {
    CounterInputStream cis = new CounterInputStream(System.in);
    int b = 0;
    for (int i=0; i<100, b>=0; i++) {
        b = cis.read();
        if (b >= 0) {
            ...     // nějaká činnost
        }
    }
    cis.close();
    System.out.println("Cetnost hodnoty 54 je " + cis.getCount(54));
} catch (IOException e) {
    ...
}

Příklad ukazuje analýzu dat načítaných ze standardního vstupu. Po skončení čtení (přečte se 100 bajtů, při chybě už se dál nečte) se vypíše četnost hodnoty 54.

Práce se soubory

Dostali jsme se na konec úvodní části o výměně dat mezi programem a vnějším prostředím. Příště se vrhneme na důležitou věc, které se při psaní aplikací nikdo nevyhne, a to je práce se soubory. Prozkoumáme, jak jsou řešeny takové operace, jako je mazání nebo přejmenování souborů, jak se vytvářejí dočasné soubory, a v neposlední řadě, jak je řešena rozdílnost různých platforem, na kterých Java může běžet.


KOMENTARZE
chybka v poslednim prikladu 12.3.2006 13:12 azero
float 14.8.2006 13:33 Martin Landa
Tylko zarejestrowani użytkownicy mogą dopisywać komentarze.
> Szukanie oprogramowania
1. Pacman linux
Download: 4875x
2. FreeBSD
Download: 9064x
3. PCLinuxOS-2010
Download: 8562x
4. alcolix
Download: 10945x
5. Onebase Linux
Download: 9660x
6. Novell Linux Desktop
Download: 0x
7. KateOS
Download: 6241x

1. xinetd
Download: 2412x
2. RDGS
Download: 937x
3. spkg
Download: 4757x
4. LinPacker
Download: 9963x
5. VFU File Manager
Download: 3197x
6. LeftHand Mała Księgowość
Download: 7202x
7. MISU pyFotoResize
Download: 2807x
8. Lefthand CRM
Download: 3562x
9. MetadataExtractor
Download: 0x
10. RCP100
Download: 3119x
11. Predaj softveru
Download: 0x
12. MSH Free Autoresponder
Download: 0x
©Pavel Kysilka - 2003-2024 | mailatlinuxsoft.cz | Design: www.megadesign.cz