Práca so súbormi Java 2. diel: Ako zapisovať súbory rýchlo a efektívne

Ak si čítal náš predchádzajúci článok Ako čítať súbory v Jave rýchlo a efektívne, už vieš, že existuje množstvo rozličných spôsobov, ako čítať súbory v Jave. V závislosti od typu súboru a jeho veľkosti treba zvoliť správny prístup k riešeniu, pretože sa môže stať, že načítavanie súborov bude trvať akosi pridlho.

Podobne existuje mnoho spôsobov, ako môžeš zapisovať dáta do súboru v Jave. Určité implementácie môžu byť pre dané riešenie vhodnejšie ako iné, v závislosti od typu a množstva dát, ktoré zapisuješ do súboru. Pri zapisovaní do súboru musíme dopredu vedieť, či budeme naše dáta zapisovať do textového alebo binárneho súboru. V prípade textového súboru sa budeme sústrediť na použitie tried ako sú: Files, Writer, OutputStreamWriter, FileWriter, PrintWriter, BufferedWriter. Pri binárnych súboroch použijeme pri zápise skôr triedy ako sú: OutputStream, FileOutputStream, BufferedOutputStream, ByteOutputStream, DataOutputStream. K dispozícií sú aj externé knižnice tretích strán pre prácu so súbormi, s ktorými sa dnes nebudeme zaoberať.

Každá z vyššie uvedených tried pre prácu so súbormi má svoje použitie pre špecifické prípady. Vo všeobecnosti ale platí, že na zapisovanie binárnych dát používame OutputStream triedy a textových Writer triedy.

Väčšina predpripravených tried na čítanie a zápis súborov v jazyku Java sa nachádza v balíčkoch java.io  a java.nio.file. Po príchode nového API rozhrania pre súbory Java NIO.2 (New I/O) sa situácia ešte viac skomplikovala a ako vlastne pracovať so súbormi rýchlo a efektívne sa často na programátorských fórach pýtajú aj skúsenejší programátori.

Zapisovanie dát v Jave sa môže tiež skomplikovať, preto sa v našom článku zameriame na 2 základné scenáre pri zapisovaní dát, ktoré pokryjú 90 percent prípadov použitia a to sú:

  • Najjednoduchší spôsob jednorazového zápisu do binárneho aj textového súboru.
  • Kontinuálny zápis do súboru po častiach

Java 7 priniesla dlho očakávané NIO.2 File API, ktoré okrem množstva funkcionality priniesla aj pomocnú triedu java.nio.file.Files, pomocou ktorej sa dá jediným príkazom zapísať bajtové pole, reťazec (String), alebo zoznam reťazcov do textového/binárneho súboru.

Zapísanie bajtového poľa do binárneho súboru

Bajtové pole zapísať do súboru vieš nasledovne:

import java.nio.file.Files;
import java.nio.file.Path;

String fileName = "fileName.dat";
byte[] data = {72, 101, 108, 108, 111};
Files.write(Path.of(fileName), data);

Trieda Path (k dispozícii od Java 11) predstavuje abstrakciu súboru a obsahuje cestu k súboru v súborovom systéme.

Zapísanie premennej typu String do textového súboru

Zapísanie premennej typu String do súboru od verzie Java 11 je veľmi jednoduché:

import java.nio.file.Files;
import java.nio.file.Path;

String fileName = "fileName.txt";
String text = "Hello";
Files.writeString(Path.of(fileName), text);

Zapísanie zoznamu premenných String do textového súboru

Často do textového súboru nezapisujeme jeden riadok, ale niekoľko riadkov naraz. S nasledovným príkazom vieš zapísať zoznam (resp. presnejšie Iterable<? extends CharSequence>) do textového súboru.

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

String fileName = "fileName.txt";
List<String> lines = new ArrayList<>(Arrays.asList("msg", "life"));
Files.write(Path.of(fileName), lines);

Zapisovanie do textového súboru pomocou String streamu

Neexistuje žiadna metóda, ktorá zapisuje priamo zo String-streamu do súboru, ale existuje menší trik, pomocou ktorého sa to dá dosiahnuť:

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;

String fileName = "fileName.txt";
Stream<String> lines = new ArrayList<>(Arrays.asList("msg", "life")).stream();
Files.write(Path.of(fileName), (Iterable<String>) lines::iterator);

Používame metódu Files.write() podobne ako v predchádzajúcom príklade, tá však akceptuje iba argument Iterable<String>. Samotný stream v Java 8 nie je iterovateľný, pretože sa dá zavolať iba raz (opakované volanie by skončilo výnimkou IllegalStateException). Nedá sa tak použiť ako parameter. Je tu však funkčné rozhranie Iterable, ktorého jediná metóda vracia Iterator. Preto môžeme odovzdať odkaz na metódu lines.iterator(), ktorá tiež vráti iterátor ako Iterable.

V tejto časti sme sa zoznámili s pomocnými metódami triedy java.nio.file.Files. Tieto metódy sú vhodné pre všetky prípady použitia, kedy sú dáta, ktoré chceme zapísať do súboru, kompletne uložené v pamäti a ich rozsah dát je len ​​niekoľko kilobajtov.

V ostatných prípadoch a hlavne, keď do súboru zapisujeme opakovane po častiach, je vhodnejšie využiť jednu z predpripravených OutputStream tried. V nasledujúcej časti si vysvetlíme, ako to urobiť.

Zapisovanie rýchlo do binárnych súborov pomocou BufferedOutputStream

Hlavná trieda na zapisovanie binárnych dát do súboru je FileOutputStream, ktorá umožňuje zapisovať do súboru bajt to bajte, alebo pole bajtov. Zapisovanie individuálnych bajtov je veľmi drahá operácia, preto odporúčame využívať vyrovnávaciu pamäť a zapisovanie súboru robiť po blokoch (tzv. buffering), ktorý je výrazne rýchlejší. Na väčšine systémov je optimálna veľkosť buffera 8 KB, preto Java používa túto hodnotu ako default. Pre bufferovaný zápis použijeme triedu BufferedOutputStream nasledovne.

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;

String fileName = "fileName.dat";
byte[] bytes;
try (FileOutputStream out = new FileOutputStream(fileName);
     BufferedOutputStream bout = new BufferedOutputStream(out)) {
    while((bytes = getOutputData()) != -1) {
        bout.write(bytes);
    }
}

V tomto príklade metóda getOuputData() (jej implementácia nie je dôležitá) vracia časti súboru, ktorý je postupne zapisovaný.

Zapisovanie rýchlo do textových súborov pomocou BufferedWriter

Pre zapísanie textu do súboru sa najskôr musí skonvertovať text do binárnych dát. Za konverziu znak do bajtu je zodpovedná trieda OutputStreamWriter. Trieda FileWriter kombinuje FileOutputStreamOutputStreamWriter. Pre rýchlejšie zapisovanie súborov využijeme triedu BufferedWriter, ktorá obalí triedu FileWriter a umožní využiť 8 KB buffer spolu s dodatočným bufferom pre zakódované znaky.

import java.io.BufferedWriter;
import java.io.FileWriter;

String fileName = "fileName.txt";
try (FileWriter writer = new FileWriter(fileName);
     BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
    String line;
    while ((line = getLine()) != -1) {
        bufferedWriter.write(line);
    }
}

V tomto príklade metóda getLine() (jej implementácia nie je dôležitá) vracia riadky súboru, ktorý je postupne zapisovaný.

V predchádzajúcich príkladoch sme nezatvárali vytvorené streamy. Po opustení try bloku sa automaticky zavolá close() metóda na uzatvorenie oboch streamov, preto táto nie je explicitne uvedená, ani potrebná.

Záver

V tomto článku sme si ukázali najčastejšie scenáre zapisovanie dát do súboru. Menšie súbory, ktoré máme pripravené v pamäti môžeme zapisovať pomocou metód triedy Files a väčšie súbory zapisujeme po častiach pomocou vyrovnávacej pamäte.

Ak si Java programátor a hľadáš prácu, pozri si naše benefity pre zamestnancov a reaguj na najnovšie ponuky práce.

O autorovi

Jozef Wagner

Java Developer Senior

Viac ako 10 rokov programujem v Jave, momentálne pracujem v msg life Slovakia ako Java programátor senior a pomáham zákazníkom implementovať ich požiadavky do poistného softvéru Life Factory. Vo voľnom čase si rád oddýchnem v lese, prípadne si zahrám nejakú dobrú počítačovú hru.

Daj nám o sebe vedieť