FAV-ZCU/KIV PPA1/17. Souborový vstup a výstup.md

258 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Souborový vstup a výstup
- Dosud jsme vstupní data četli z klávesnice (standardní vstup) a vypisovali výstupní data na obrazovku (standardní výstup)
- Je však běžné, že programy čtou a zapisují vstupní a výstupní data z a do souborů
### Proudový vstup a výstup dat
- Na vstupní a výstupní operace lze nahlížet tak, že se jedná o proud dat (stream)
- To má tu výhodu, že zdroj dat (source v případě vstupu) nebo cíl dat (target v případě výstupu) mohou být různých typů (např. klávesnice/obrazovka nebo vstupní/výstupní soubor) a program tento proud dat zpracovává stejným způsobem
- Tj. můžeme pracovat stejně s různými zdroji a cíli dat, liší se pouze počáteční inicializace zdroje a/nebo cíle
#### Vlastnosti proudu dat
- Základní vlastnost „teče spojitě vpřed“
- Není možné se v něm vracet, nebo přeskakovat dopředu
- Tyto možnosti ale prakticky nejsou potřeba příliš často
- Typické zdroje dat jsou
- Klávesnice, soubor a síťové spojení
- Typické cíle dat jsou
- Obrazovka, soubor a síťové spojení
- Klávesnici a obrazovku jsme používali ve všech příkladech uvedených doposud
- Na soubory se zaměříme nyní
- Síťové spojení bude probráno v navazujících předmětech
### Typy souborů
- **Soubory** se dají rozdělit na **textové** a **binární**
- Oba typy souborů **obsahují pouze binární čísla**
- V textovém souboru jsou jednotlivé byty interpretovány jako znaky v pevně
daném kódování
- V binárním souboru je význam jednotlivých bytů dán aplikací, která soubor zapisuje a/nebo čte
- I binární soubor lze zobrazit jako textový, ale typicky uvidíme jen změť znaků, ze které se nic užitečného nedozvíme
#### Textové soubory
- Připomínají vstup z klávesnice
- Jsou **čitelné pro člověka** (obsahují prostý text)
- POZOR!
- Nejde o soubory textových procesorů (např. MS Word), které sice obsahují mj. text, ale kromě něj i jeho formátování, styly, obrázky, atd.
- Jde o soubory čitelné (zobrazitelné) obecným textovým editorem (např. PSPad, Notepad++, Poznámkový blok)
- Jsou organizovány po řádcích
- Ve skutečnosti je v souboru jen posloupnost znaků (do důsledku binárních čísel), která do řádků organizovaná není => je potřeba konce řádků vyznačit
- Pro vyznačení se používají značky konce řádku
- Značky se skládají z jednoho či dvou znaků (bytů) označovaných ```<CR>``` (znak '\r' v Javě, dekadické číslo znaku 13) a ```<LF>``` (znak '\n' v Javě, dekadické číslo znaku 10)
- Konkrétní značka závisí na systému
- Windows
- ```<CR><LF>```
- Pořadí je důležité, ```<LF><CR>``` není chápána jako značka konce řádku
- Linux
- ```<LF>```
- Mac OS/OS X
- Dříve ```<CR>``` (do Mac OS verze 9)
- Novější verze ```<LF>```
- V Javě stačí zapsat znak '\n' a Java doplní konec řádku podle toho, na jakém operačním systému je spuštěna
- Textový soubor zapsaný v jednom operačním systému nemusí mít správně zobrazené řádky v jiném operačním systému
- Záleží na konkrétním programu, který otevírá textový soubor, většina textových editorů umí správně zobrazit konce řádků ze všech běžných operačních systémů
- Typická univerzální přípona je ```.txt```
- Pouze dodržovaná konvence
- Textové soubory mohou mít i mnoho jiných specifických přípon podle toho, o jaký soubor se jedná
- Zdrojové soubory programovacích jazyků
- ```.java, .cpp, .c, .h, .pas, .js, .php``` a další
#### Binární soubory
- Pro běžného uživatele jsou mnohem častější
- Běžné binární soubory
- Obrázky
- ```.jpg, .png, .gif, .bmp``` a další
- Hudba
- ```.mp3, .ogg, .wav``` a další
- Video
- ```.avi, .mp4, .mkv, .mov``` a další
- Komprimované archivy
- ```.zip, .rar, .7z``` a další
- Dokument MS Word, MS Excel
- ```.doc, .docx, .xls, .xlsx```
- Soubory ```.docx``` a ```.xlsx``` jsou ve skutečnosti komprimované složky obsahující množství ```xml``` souborů, obrázky a další data
- Nejsou čitelné pro člověka a nejsou uspořádané po řádcích
- Pro jejich prohlížení a editaci jsou potřeba specializované programy (např. přehrávače médií pro hudbu a video)
- Jejich vnitřní organizace je daná formátem souboru
- Formát souboru je indikován příponou
- Tu ale lze libovolně měnit (je to jen součást názvu souboru), proto nemusí odpovídat skutečnému obsahu souboru
- Nesoulad přípony a skutečného obsahu souboru někdy působí potíže prohlížečům daného formátu (soubor nemusí jít otevřít)
- Jak formát vypadá tj. co který byte znamená a jaké jsou povolené hodnoty závisí na tvůrci formátu
#### Výhody a nevýhody textových a binárních souborů
- Výhody textových souborů
- Jsou čitelné pro člověka
- Člověk dokáže informaci přečíst, ale to nutně nemusí znamenat, že jí porozumí
- Např. řádka ```21, 37, 54, diference 0.3888``` je sice čitelná, ale těžko říci, co znamená
- Je možné je číst a upravovat obecným textovým editorem
- Nevýhody textových souborů
- Stejné množství informace většinou zabírá více místa
- Pomalejší zpracování (čtení/zápis kvůli převodu z/do čitelné formy)
- Výhody binárních souborů
- Paměťově úspornější
- Rychlejší zpracování
- Nevýhody binárních souborů
- Pro každý formát či skupinu formátů je potřeba speciální prohlížeč
- Nečitelnost v případě nedokumentovaných proprietárních formátů
- Tuto vlastnost někteří výrobci považují za výhodu
- V dalším textu se budeme zabývat proudovým čtením a zápisem textových souborů, pokud nebude řečeno jinak
### Využití prostředků pro standardní vstup/výstup
- Již dříve jsme využívali přesměrování standardního vstupu a/nebo výstupu pomocí prostředků operačního systému
- Programy vytvořené pro práci se standardním vstupem a výstupem fungovaly i po přesměrování vstupu a/nebo výstupu bez problémů
- Prostředky použité pro čtení standardního vstupu a zápis do standardního výstupu lze tedy použít i pro práci se soubory
- Pro práci se soubory obecně platí (bez ohledu na to, jaký prostředek použijeme)
- Soubor (vstupní i výstupní) je nutné před použitím otevřít
- Soubor se připraví pro čtení a/nebo zápis, konkrétní soubor na disku se „spojí“ s prostředkem pro práci se souborem
- Většinou se provede už při vytvoření instance prostředku pro práci se souborem
- Soubor (vstupní i výstupní) je vhodné po posledním použití uzavřít
- Konkrétní soubor na disku se „odpojí“ od prostředku pro práci se souborem
- Provede se voláním metody ```close()``` nebo automaticky při využití konstrukce ```try```-with-resources
- Všechny otevřené soubory jsou automaticky uzavřeny JVM po ukončení programu, takže nehrozí, že by zůstaly otevřené
- Při čtení ze souboru se z hlediska obsahu souboru nic nestane, pokud soubor neuzavřeme
- Souborů, které mohou být najednou otevřeny, je však omezený počet
- Pokud je soubor otevřen, typicky do něj nelze zapisovat jinou aplikací
- Při zápisu do souboru se může stát, že se data nezapíší všechna, pokud soubor neuzavřeme
- Téměř vždy je nutné ošetřit výjimky (typicky odvozené od ```IOException```)
- Čtení a zápis do souboru je operace, která může často selhat z nejrůznějších příčin, které často nejsou ovlivnitelné naším programem
- Proto je ošetření nutné (```IOException``` je kontrolovaná výjimka a je vyhazována většinou metod pro práci se soubory)
#### Použití třídy Scanner pro čtení ze souboru
- Třídu ```Scanner``` lze přímo použít pro čtení ze souboru
- Stačí do konstruktoru uvést soubor místo standardního vstupu (```System.in```)
- Nelze uvést pouze název souboru, je potřeba vytvořit instanci rozhraní ```Path```
- ```Scanner s = new Scanner(Paths.get("vstup.txt"));```
- POZOR!
- Instance rozhraní ```Path``` se vytváří metodou třídy ```get()``` třídy ```Paths```
- Je nutné ošetřit ```IOException```
- Test konce souboru
- Protože se často může stát, že nevíme, kolik hodnot či řádek je ve čteném souboru uvedeno, lze využít metody pro testování, zda se v souboru nachází další hodnota daného typu
- Pokud ne, lze skončit
- Metody ```hasNextTyp()```
- Např. metoda ```hasNextInt()```
- Vrací ```true```, pokud je ve vstupním souboru další celé číslo
- Např. metoda ```hasNextDouble()```
- Vrací ```true```, pokud je ve vstupním souboru další reálné číslo
- Např. metoda ```hasNext()```
- Vrací ```true```, pokud je ve vstupním souboru další řetězec
- Např. metoda ```hasNextLine()```
- Vrací ```true```, pokud je ve vstupním souboru další řádka
#### Použití třídy ```PrintStream``` pro zápis do souboru
- Třída ```PrintStream``` poskytuje známé metody ```println()```, ```print()``` a ```format()``` pro zápis formátovaných dat do souboru
- V referenční proměnné ```System.out``` se skrývá instance třídy ```PrintStream```
- Jeden z konstruktorů umožňuje vytvořit novou instanci třídy ```PrintStream``` ze zadaného názvu souboru
- Místo souboru můžeme použít i standardní výstup
- Stačí použít ```System.out``` (viz první zakontovaná řádka v metodě ```main()```) a odstranit konstrukci ```try catch```
### Využití prostředků balíku ```java.nio.file```
- Prostředky pro jednoduché čtení a zápis z/do souborů poskytuje třída ```Files``` z balíku ```java.nio.file```
- Nahrazuje funkcionalitu z balíku ```java.io```, který je v Javě od začátku
- Mnoho tříd z balíku ```java.io``` však stále využívá
- Obsahuje snadno použitelné statické metody (podobně jako třída ```Math```)
#### Rychlé čtení s využitím třídy BufferedReader
- Scanner funguje dobře, ale pokud by byl vstupní soubor větší, trvalo by jeho načítání nezanedbatelnou dobu
- Je třeba si uvědomit, že práce (tj. čtení a zápis) s vnější pamětí (tj. typicky s pevným diskem, kde jsou soubory uloženy) je řádově pomalejší než práce s vnitřní pamětí => rychlost čtení a zápisu je vhodné řešit ve většině případů, nejen při práci s extrémně velkými soubory
- Využití třídy ```BufferedReader```
- Tzv. bufferovaný vstup
- V podstatě to znamená, že se z disku načte najednou větší část obsahu souboru do paměti a odtud se následně čtou data => podstatně rychlejší než číst data z disku po jednotlivých číslech nebo řádcích
- Instanci třídy ```BufferedReader``` lze snadno získat ze třídy ```Files``` na základě zadání souboru jako instance ```Path```
- Metoda ```Files.newBufferedReader(soubor)```
- Třída ```BufferedReader``` nabízí metodu ```readLine()```, která umožňuje načíst jednu řádku ze souboru
- Metoda vrací ```null```, pokud dosáhne konce souboru využívá se, pokud nevíme, kolik řádek v souboru je (častý případ)
- Nenabízí však metody pro načtení jiných datových typů jako třída ```Scanner```
Načtenou řádku je tedy nutné zpracovat (rozdělit podle nějakého znaku, převést na číslo apod.) ručně
- Čtení pomocí třídy ```BufferedReader``` je výrazně rychlejší než čtení pomocí třídy ```Scanner```
#### Rychlý zápis s využitím třídy BufferedWriter
- Podobně jako lze čtení ze souboru urychlit použitím třídy ```BufferedReader```, je možné zápis do souboru urychlit použitím třídy ```BufferedWriter```
- Tzv. bufferovaný výstup
- V podstatě to znamená, že se data zapisují nejprve do paměti a následně se větší blok dat zapíše najednou na disk
- Ze třídy ```Files``` lze snadno získat instanci ```BufferedWriter``` pro rychlý zápis do souboru pouze na základě zadání souboru jako instance ```Path```
- Třída ```BufferedWriter``` ale neobsahuje metody pro pohodlný zápis výstupu (```println()```, ```print()```, ```format()```)
- Proto je vhodné zkombinovat ji se třídou ```PrintWriter```, která zmíněné metody obsahuje
- Třída ```Files``` bohužel neobsahuje metodu, která by vrátila rovnou instanci třídy ```PrintWriter```
- POZOR!
- Při použití třídy ```BufferedWriter``` je obzvláště důležité soubor po zapsání všech dat uzavřít
- ```BufferedWriter``` zapisuje data nejprve do paměti a následně zapíše celý blok dat do souboru
- Pokud soubor neuzavřeme, velmi často se stává, že poslední blok se do souboru nezapíše a soubor tak není kompletní
- Pokud je dat, které zapisujeme do souboru, málo, může se stát, že soubor bude úplně prázdný
- Okamžité zapsání do souboru lze vynutit metodou ```flush()```
- Příklad použití tříd ```BufferedWriter``` a ```PrintWriter```
- Rozdíl mezi ```PrintWriter``` a ```PrintStream```
- V Javě je několik tříd pro práci se vstupy a výstupy končící na …(```Input/Output```)```Stream``` a několik tříd pro práci se vstupy výstupy končící na …```Reader/Writer```
- Třídy ```…Reader/Writer``` pracují se znaky
- Znak v Javě zabírá dva byty, ale většina textových souborů takto uložena není
- Znaky se do souborů typicky ukládají jako jeden či více bytů podle použitého kódování
- Při čtení nebo zápisu provádějí třídy ```…Reader/Writer``` konverzi
- Třídy …(```Input/Output```) ```Stream``` pracují s byty
- Jsou určeny pro práci s byty, které však také mohou představovat znaky
- Metody určené pro formátovaný výstup (např. metody ```println()```, ```print()``` a ```format()``` třídy ```PrintStream```) rovněž provádějí konverzi, ostatní metody ne
- Umožňují i zápis a čtení jednoho či více bytů přímo (bez konverze)
- Proč je standardní výstup instance třídy ```PrintStream``` (určená pro práci s byty), i když standardní výstup evidentně pracuje se znaky
- Historické důvody, v Javě 1.0 třídy ```…Reader/Writer``` neexistovaly
- Zachováno kvůli zpětné kompatibilitě
- Jsou i situace, kdy je vhodné, aby standardní vstup a/nebo výstup pracoval s byty a ne se znaky
#### Načtení všech řádek souboru
- Určeno pro malé soubory
- Rychlost načítání je řádově srovnatelná s třídou ```BufferedReader```
- Metoda ```Files.readAllLines(soubor)```
- Metoda zajistí otevření i uzavření souboru samostatně, je potřeba pouze odchytit případnou ```IOException```
- Metoda vrací seznam řádek (```List<String>``` ne pole) souboru zadaného jako instance ```Path```
- Jednotlivé řádky v seznamu jsou přístupné pomocí metody instance seznamu ```get(index)```
- Indexy jsou stejné jako u pole 0 až délka seznamu - 1
- Délku seznamu vrátí metoda instance seznamu ```size()```
- Seznam se dá procházet, stejně jako pole, cyklem ```for each```
### Práce s binárními soubory
- Práce s binárními soubory je v Javě velice podobná práci s textovými soubory
- Používáme třídy končící na …(```Input/Output```) ```Stream```
- Naprostá většina metod těchto tříd neprovádí konverzi na znaky (s výjimkou metod ```println()```, ```print()``` a ```format()``` třídy ```PrintStream```)
- Byty se načtou/zapíšou tak, jak jsou (na rozdíl od tříd končících na ```…Reader/Writer```)
#### Zápis do binárního souboru
- Je možné zapisovat přímo jednotlivé byty
- Základní třída ```OutputStream```
- Její instanci lze získat metodou ```Files.newOutputStream(soubor)```
- Je možné zapisovat hodnoty základních datových typů
- Třída ```DataOutputStream```
- Obsahuje metody ```writeTyp(hodnota)``` pro zápis všech základních datových typů
- Např. ```writeDouble(5.0)```
- Do jednoho souboru je tak možné zapsat hodnoty různých datových typů
- Pořadí a počty jednotlivých hodnot je třeba řádně zdokumentovat, aby bylo možné takový binární soubor číst
- Pro urychlení je možné použít třídu ```BufferedOutputStream``` stejným způsobem jako ```BufferedWriter```
- Třída ```Files``` však neposkytuje metodu, která by vracela instanci ```BufferedOutputStream```
- Je potřeba vytvořit ```BufferedOutputStream``` ručně
#### Čtení z binárního souboru
- Je možné číst přímo jednotlivé byty
- Základní třída ```InputStream```
- Její instanci lze získat metodou ```Files.newInputStream(soubor)```
- Je možné číst hodnoty základních datových typů
- Třída ```DataInputStream```
- Obsahuje metody ```readTyp()``` pro čtení všech základních datových typů
- Např. ```readDouble()```
- Odpovídají metodám ```writeTyp()``` třídy ```DataOutputStream```
- Pro urychlení je možné použít třídu ```BufferedInputStream```
- Třída ```Files``` však neposkytuje metodu, která by vracela instanci ```BufferedInputStream```
- Je potřeba vytvořit ```BufferedInputStream``` ručně