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

17 KiB
Raw Permalink Blame History

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ě