Sissejuhatus failioperatsioonidesse
Sissejuhatus
Iga muutuja, objekt ja andmestruktuur, mida programm loob, asub arvuti operatiivmälus (RAM). Mälu on kiire ja mugav, kuid hävinev (volatile) ehk kui programm oma töö lõpetab, siis kõik andmed mis selle mälus olid hävinevad. See tähendab, iga kord kui programm uuesti käivitatakse alustatakse puhtalt lehelt.
Salvestusvõimalused
Selleks, et andmed säiliksid ka peale programmi sulgemist tuleb luua mõni püsitalletuse funktsionaalsus. Failid on kõige lihtsam vorm sellest, kuid need ei ole ainsad valikuvõimalused. Õige valik sõltub sellest, kuidas andmed on struktureeritud ja kuidas neile ligi peab pääsema:
| Talitusetüüp | Parim kasutus | Näited |
|---|---|---|
| Lihttekstifail | Lihtsad, fikseeritud struktuuriga andmed. | Raamatute pealkirjade loend, logifail |
| Struktureeritud fail (CSV, JSON) | Mitme väljaga kirjed | Raamatukataloog autorite ja hindadega |
| Konfiguratsioonifailid (.properties, .ini) | Seadistuse väärtused (võti-väärtus paarid) | Serveri host, port, failiteed |
| Andmebaas | Suured andmehulgad, keerukad päringud, samaaegne ligipääs | Täielik raamatukogusüsteem kasutajate, laenutuste ja trahvidega |
See ja järgnevad peatükid keskenduvad failipõhisele salvestusele. Andmebaasid on omaette valdkond, kuid ka nende puhul on võimalik kasutada juba teadaolevaid arhitektuurimustried, milleks on DAO ja Repository.
Vood
Java käsitleb sisendit ja väljundit voo (stream) kontseptsiooni kaudu - see on järjestikune andmevoog allika ja sihtkoha vahel.
Allikas ja sihtkoht võib tähendada mitut erinevat asja, olgu selleks siis kettal olev fail, võrguühendus või mõni teine programm. Voo abstraktsioon võimaldab koodil töötada kõigiga neist ühtemoodi, sõltumata sellest, kust andmed tegelikult pärinevad või kuhu need liiguvad.
Mõningad näited:
Ärge ajage segamini siin mainitud vooge ja Stream API-t. Need on täiesti erinevad asjad!
Byte streams ja character streams
Java eristab kahte tüüpi voogusid:
Byte streams (baitivood) edastavad tooretest baitidest (raw byte) koosnevaid jadasid, mis on kõikide andmete põhiühik.
Neid kasutatakse binaarfailide jaoks, milleks on näiteks pildid, heli kompileeritud kood jne.
Nende voogudega seotud põhiklassid on InputStream ja OutputStream.
Character streams (märgivood) edastavad teksti, tehes ka automaatselt teisendust toorbaitide ja inimloetavate märkide vahel.
Nende voogudega seotud põhiklassid on Reader ja Writer
Tekstifailide puhul - millega enamik harjutusi ja rakendusi tegeleb - kasutatakse tavaliselt kõrgema taseme API-sid nagu Files.readString, Files.writeString või klasse nagu Scanner ja BufferedReader, mis on ehitatud nende voogude abstraktsioonide peale.
Puhverdamine
Ilma puhverdamiseta tehakse iga üksiku märgi lugemisel või kirjutamisel eraldi pöördumine ketta poole. Kettatoimingud on mitutuhat korda aeglasemad võrreldes samade toimingutega operatiivmälus, seega on need äärmiselt ebaefektiivsed.
Puhverdatud voog paikneb tavalise voo ees ja lisab sellele vahemälu ehk puhvri. Lugemisel täidetakse puhver ühe kettatoiminguga ning programm loeb seejärel andmeid puhvrist. Kirjutamisel kogutakse andmed puhvrisse, puhvri sisu kirjutatakse kettale ühe operatsioonina siis, kui see täitub või voog suletakse.
Seetõttu kasutatakse tihtipeale pigem BufferedReader ja BufferedWriter klasse FileReader ja FileWriter asemel.
Voogude sulgemine
Vood hoiavad enda käes avatud faili pidet ehk ühendust programmi ja kettal oleva faili vahel. Operatsioonisüsteem piirab, kui palju neid pidemeid üks protsess korraga saab enda käes hoida. Voog, mida kunagi ei suleta, hoiab seda pidet alles ka siis, kui programm enam seda ei kasuta.
Selliseid pidemeid nimetatakse ressursilekkeks. Pikaajaliselt töötavas rakenduses võivad need ammendada saadaolevad pidemed ja põhjustada rakenduse töö katkemise.
Seetõttu tuleb kõik vood pärast kasutamist sulgeda ning just sellepörast on olemas ka try-with-resources - eriline try/catch-plokk, mis sulgeb vood automaatselt peale kasutust.
Tähemärkide kodeerimine
Tekstifailid salvestavad märke baitidena. Kodeerimise eesmärk on ära kaardistada, milline baitide jada vastab millisele märgile.
ASCII ja selle piirangud
ASCII on üks vanimaid keelemärkide tabeleid, mis määratles ära 128 märki: ladina tähestik, numbrid, kirjavahemärgid ja mõned juhtmärgid. Üks märk tähendas ühte baiti ehk see oli suhteliselt lihtne ja tõhus.
ASCII töötas hästi inglise keele jaoks, kuid 128 märki pole kaugeltki piisav enamiku maailma kirjaviiside jaoks.
Näiteks eesti keel omaette vajab tähemärku nagu ä, ö, ü ja õ - tähed, mida ASCII ei sisalda.
UTF-8
Tänapäeval on laialdaselt kasutusel UTF-8. See suudab esitada kõiki Unicode standardis olevaid märke (üle 140,000 märgi), mis katavad ära praktilised kõik võimalikud kirjaviisid. Samuti on see ühilduv ka ASCII-ga, kuna esimesed 128 märki on kodeeritud samamoodi mõlema puhul.
UTF-8 on kodeering, mida tuleks kasutada kõigi tekstifailide puhul, kui puudub konkreetne põhjus kasutada midagi muud.
Miks see Java puhul tähtis on
Vanemad Java API-d kasutavad platvormi põhist vaikekodeeringut ehk seda, mida programmi käivitav operatsioonisüsteem parasjagu kasutab. See tekitab probleemi, kus ühel masinal toimib programm teisiti võrreldes teistega:
// Uses the platform default encoding — different result on different machines
BufferedReader reader = new BufferedReader(new FileReader("books.txt"));
// Always uses UTF-8 — consistent everywhere
BufferedReader reader = Files.newBufferedReader(Path.of("books.txt"));
Kui fail kirjutatakse ühes masinas ühe vaikekodeeringuga ja loetakse teises masinas teise vaikekodeeringuga, võivad ASCII vahemikust väljaspool olevad märgid rikutuks muutuda.
Näiteks ä muutub ä-ks või millekski sarnaseks.
Kaasaegsed Files.newBufferedReader ja Files.newBufferedWriter meetodid kasutavad vaikimisi UTF-8 kodeeringut, mistõttu neid eelistatakse failist lugemiseks ja faili kirjutamiseks.
Samas, kui tekib vajadus käsitsi kodeeringu määramiseks, on seda võimalik teha sedasi:
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("books.txt"), StandardCharsets.UTF_8)
);
StandardCharsets.UTF_8 on konstant java.nio.charset pakis, mis väldib vigu kodeeringu nime käsitsi kirjutamisel.