Práca so súbormi Java: ako čítať súbory rýchlo a efektívne

Práca so súbormi je základným aspektom programovania. Existuje množstvo rozličných spôsobov ako čítať súbory v Jave. Už len pri hľadaní Java príkladu pre jednoduché načítanie súboru môžeš naraziť na nasledovné Java triedy pre prácu so súbormi, ide napríklad o InputStream, FileInputStream, DataInputStream, SequenceInputStream, Reader, InputStreamReader, FileReader, BufferedReader, FileChannel, SeekableByteChannel, Scanner, StreamTokenizer, Files a podobne.

Sme si celkom istí, že tých tried bude ešte viac a niektoré z nich sa nedostali do tohto zoznamu. Samozrejme, ešte nespomíname externé knižnice tretích strán pre prácu so súbormi, ktoré sú tiež k dispozícii.

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.

Pri práci so súbormi by sme mali dopredu vedieť s akými typmi súborov budeme pracovať a teda či potrebujeme čítať binárne súbory (napr. hudba vo formáte mp3) alebo textové súbory. Rovnako či tie súbory budú menšie a teda je lepšie načítať ich celé do pamäte, alebo ideme pracovať so súbormi, ktoré sa do pamäte nezmestia a budú vyžadovať iný typ spracovania, napr. čítanie a spracovanie po riadkoch.

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 načítavanie binárnych dát používame Stream triedy a textových Reader triedy. Triedy, ktoré majú v názve obe tieto výrazy kombinujú načítavanie binárnych a textových dát. Napr. triedy InputStreamReader konzumuje InputStream, ale sama sa správa ako Reader. FileReader je v podstate kombinácia FileInputStream so InputStreamReader.

Ako môžeme z prechádzajúceho textu vidieť, načítavanie dát v Jave sa môže poriadne zamotať, preto sa v našom článku zameriame na tri základné scenáre pri načítavaní dát, ktoré pokryjú 90 percent prípadov použitia:

  • Najjednoduchší spôsob načítania celého textového súboru do premennej typu String, resp. zoznamu (a binárneho súboru do bajtového poľa).
  • Načítavanie a spracovanie veľkých súborov, ktoré sa celé nezmestia do pamäte.
  • Čítanie súborov, ktorých obsah sa da rozdeliť oddeľovačom, napr. CSV súbory.

Krátka história načítavania súborov v Jave

Načítavanie súborov pomocou Java knižníc bolo až po verziu Java 7 pomerne ťažkopádne. Najčastejšie sa na načítanie súboru používala trieda FileInputStream, pri ktorej bolo potrebné okrem ošetrenia výnimiek zabezpečiť, že sa stream zatvorí v prípade úspešného načítania súboru, ako aj v prípade chyby. Automatické uzatvorenie použitých zdrojov (ako poznáme v súčasnosti s Try-with-resources) vtedy ešte neexistovalo. Preto veľa Java programátorov uprednostňovalo knižnice tretích strán ako Apache Commons alebo Google Guava, ktoré poskytovali oveľa pohodlnejšie možnosti.

To sa zmenilo s príchodom Java 7, ktorá 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á jednou metódou načítať celý textový/binárny súbor.

Načítanie binárneho súboru do bytového poľa

Pomocou metódy Files.readAllBytes() dokážeme načítať obsah celého súboru do bytového poľa:

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

String fileName = "fileName.dat";
byte[] bytes = Files.readAllBytes(Path.of(fileName));

Trieda Path predstavuje abstrakciu súboru a obsahuje cestu k súboru v súborovom systéme.

Načítanie textového súboru do premennej typu String

Od verzie Java 11 existuje možnosť jednoducho načítať obsah celého textového súboru do premennej typu String pomocou metódy Files.readString() nasledovne:

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

String fileName = "fileName.dat";
String text = Files.readString(Path.of(fileName));

Metóda readString() využíva interne metódu readAllBytes(), a potom prekonvertuje binárne dáta do požadovaného reťazca typu String.

Načítanie textového súboru po riadkoch

Textové súbory sa väčšinou skladajú z viacerých riadkov. Ak chceme načítať a spracovať text po riadkoch môžeme využiť metódu dostupnú od verzie Java 8 – readAllLines(), ktorá to robí automaticky.

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

String fileName = "fileName.dat";
List<String> lines = Files.readAllLines(Path.of(fileName));

Potom už len klasicky iterujeme zoznamom a spracujeme každý riadok.

Načítanie textového súboru po riadkoch pomocou String streamu

Java 8 priniesla streamy ako významné vylepšenie jazyka. Rovnaká verzia rozšírila triedu Files o novú metódu lines(), ktorá vráti načítané riadky textového súboru vo forme streamu reťazcov typu String. To nám umožňuje využiť funkcionalitu streamov napr. pri filtrovaní dát.

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

String fileName = "fileName.dat";
Files.lines(Path.of(fileName))
        .filter(line -> line.contains("ERROR"))
        .forEach(System.out::println);

V tomto príklade vypíšeme na konzolu všetky riadky načítaného súboru, ktorý obsahuje reťazec “ERROR“.

Tieto metódy pokrývajú najčastejšie scenáre načítavania menších súborov a majú spoločné to, že sú načítané celé do RAM. V prípade veľkých súborov je vhodné načítavať ich po kúskoch a ihneď ich spracovať. To si nižšie predvedieme.

Načítanie veľkého binárneho súboru pomocou BufferedInputStream

Binárny súbor sa načítava cez InputStream po jednom bajte (až po koniec súboru, keď sa vráti -1), čo je v prípade veľkých súborov pomerne dlho. To sa dá urýchliť načítavaním dát cez BufferedInputStream, do ktorej sa zabalí trieda FileInputStream a načítavanie dát z operačného systému už neprebieha bajt po bajte, ale po 8 KB blokoch, ktoré sa ukladajú do pamäte. Následne čítanie súboru síce prebieha bajt po bajte, ale už je oveľa rýchlejšie, pretože prebieha priamo z pamäte.

import java.io.BufferedInputStream;
import java.io.FileInputStream;

String fileName = "fileName.dat";
try (FileInputStream is = new FileInputStream(fileName);
     BufferedInputStream bis = new BufferedInputStream(is)) {
    int b;
    while ((b = bis.read()) != -1) {
        // TODO: process b
    }
}

Načítavanie súboru po blokoch (tzv. buffering) je výrazne rýchlejšie ako čítanie po bajtoch.

Načítanie veľkého textového súboru pomocou BufferedReader

Trieda FileReader kombinuje FileInputStreamInputStreamReader. Pre rýchlejšie načítavanie súborov využijeme triedu BufferedReader, ktorá obalí triedu FileReader a umožní využiť 8 KB buffer spolu s dodatočným bufferom pre 8192 dekódovaných znakov. Výhodou triedy BufferedReader je aj to, že nám umožňuje načítavať a spracovať textový súbor riadok po riadku (miesto načítavania a spracovania po jednotlivých znakoch).

import java.io.BufferedReader;
import java.io.FileReader;

String fileName = "fileName.dat";
try (FileReader reader = new FileReader(fileName);
     BufferedReader bufferedReader = new BufferedReader((reader))) {
    String line;
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println("Line: " + line);
    }
}

Načítavanie súboru po častiach pomocou Scanner

Niekedy potrebujeme miesto čítania súboru riadok za riadkom ho čítať po častiach. Trieda Scanner funguje tak, že obsah súboru rozdeľuje na časti pomocou oddeľovača, čo môže byť akákoľvek konštantná hodnota. Bežne sa táto trieda používa pre CSV (comma-separated values) súbory, teda súbory, ktoré majú špecifický formát, kde dáta sú oddelené čiarkami a súbor sa dá používať ako tabuľka v aplikácií Excel.

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

List<String> words = new ArrayList<>();
String fileName = "fileName.csv";
Scanner scanner = new Scanner(Path.of(fileName));
scanner.useDelimiter(",");

while (scanner.hasNext()) {
    String next = scanner.next();
    words.add(next);
}
scanner.close();

V tomto príklade sme CSV súbor načítali po jednotlivých tokenoch (miesto klasického prístupu načítavania po riadkoch) oddelených čiarkou a tie si uložili do zoznamu na ďalšie spracovanie.

V tomto článku sme si ukázali najčastejšie scenáre načítavania dát zo súboru. V Jave sa dajú načítavať a uložiť do pamäte malé súbory jednoducho volaním jedinej funkcie a aj čítanie a spracovanie veľkých súborov sa dá v Jave efektívne vykonávať pomocou bufferovacích tried.

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ť