Luvussa tutustutaan joihinkin tapoihin kirjoittaa ja lukea nimettyjä tiedostoja. Pääpaino on tekstitiedostojen käsittelyssä, mutta myös sarjallistamisesta ja binääritiedostoista nähdään esimerkki. Luku alkaa tiedostojen hallinnan yleisen välineen, luokan File, esittelyllä.
Luokalla on mm. konstruktori:
public File(String pathname)Parametri on tiedostonimi, tarkemmin sanottuna ns. polkunimi. Ainoa virhe File-oliota luotaessa voi olla se, että todellisena parametrina on null. Siitä seuraa NullPointerException. File-olion konstruointi ei vielä ole missään tekemisissä todellisten tiedostojen kanssa, olio on tavallaan vain konkreettisen tiedosto- tai hakemistonimen abstrakti vastine Java-ohjelmassa! Kerran luotu File-olio on myöskin muuttumaton, "immutaabeli".
Polkunimi (pathname) tarkoittaa tiedoston (tai hakemiston) nimeä mahdollisine hakemistopolkuineen. Esimerkiksi Unix/Linux-järjestelmissä ne voivat olla mm. seuraavanlaisia:
tiedosto td.sto hakemisto/alihakemisto/td.sto (nykyhakemiston alihakemisto ...) /hakemisto/alihakemisto/td.sto (juurihakemiston alihakemisto ...) ../td.sto (ylemmän tason hakemiston td.sto) ../../alihak/td.sto (.. jne...)
File-oliolle on käytettävissä monia metodeita, mm.
public boolean exists() // palauttaa true, jos tiedosto on olemassa public long length() // antaa tiedoston pituuden public boolean renameTo(File dest) // uudelleennimeää, true jos onnistui public boolean delete() // poistaa tiedoston, true jos onnistui public String getName() // antaa tiedoston nimen ... ...Näiden käytöstä saadaan joitakin esimerkkejä jatkossa.
Luokan ilmentymiä eli tulostustiedostoja voidaan luoda mm. konstruktoreilla
public PrintWriter(String fileName) throws FileNotFoundException public PrintWriter(File file) throws FileNotFoundExceptionTiedostonimi siis voidaan antaa merkkijonona tai tai File-oliona.
Poikkeus FileNotFoundException kuuluu tarkistettuihin poikkeuksiin, joten se on tavalla tai toisella käsiteltävä. Asia saattaa tosin uuden kirjoitettavan tekstitiedoston tapauksessa vaikuttaa hieman omituiselta... API selittääkin: "If the given file object does not denote an existing, writable regular file and a new regular file of that name cannot be created, or if some other error occurs while opening or creating the file".
PrintWriter-oliolla voidaan kirjoittaa tiedostoon aivan samaan tapaan kuin hyvin tutuksi tulleella System.out-kentän PrintStream-oliolla kirjoitetaan standarditulosvirtaan. Oma tulostustiedosto on kuitenkin muistettava sulkea PrintWriter-aksessorilla close().
Esimerkki 1: Laaditaan sovellus, jolla voi kirjoittaa näppäimistöltä tekstiä tiedostoon "gradu.txt" (Talleta1.java):
import java.io.*; import java.util.Scanner; public class Talleta1 { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) throws FileNotFoundException { PrintWriter tulos = new PrintWriter("gradu.txt"); System.out.println("Kirjoittamasi teksti menee tiedostoon gradu.txt"); System.out.println("(ctrl-d lopettaa!)\n"); while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); tulos.println(rivi); } tulos.close(); // !! System.out.println("Työt tehty!\n"); } }Esimerkissä annetaan main-metodille lupa kaatua, jos konstruktorin kutsu aihettaa tarkistetun poikkeuksen FileNotFoundException.
Ohjelma on ainakin kahdesta syystä vähän huono:
Esimerkki 2: Laaditaan kehittyneempi versio, jossa käytetään hyväksi luokan File tiedostokäsittelyvälineitä (Talleta2.java):
import java.io.*; import java.util.Scanner; public class Talleta2 { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) throws IOException { System.out.println("Minne tulostetaan?"); String tulNimi=lukija.nextLine(); File tulTd = new File(tulNimi); if (tulTd.exists()) { System.out.println("Tiedosto "+tulNimi+" on jo olemassa!"); System.out.println("Haluatko korvata sen uudella (enter = ei)?"); if (lukija.nextLine().length() == 0) { System.out.println("Vanhaa tiedostoa ei tuhottu."); return; // keskeytetään kaikki! } } PrintWriter tulos = new PrintWriter(tulTd); System.out.println("Kirjoittamasi teksti menee tiedostoon "+tulNimi); System.out.println("(ctrl-d lopettaa!)\n"); while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); tulos.println(rivi); } tulos.close(); System.out.println("Työt tehty!\n"); } }
Luokassa on mm. konstruktori:
public Scanner(File source) throws FileNotFoundException
Huom: Scanner-luokan String-parametrinen konstruktori luo olion, jolla selataan yksittäistä merkkijonoa, kyseessä siis ei ole tiedoston nimi kuten File-luokalla!
Esimerkki 3: Laaditaan sovellus, joka listaa kuvaruudulle tiedoston "gradu.txt" (Listaa1.java):
import java.io.*; import java.util.Scanner; class Listaa1 { public static void main(String[] args) throws FileNotFoundException { Scanner syöttötiedosto = new Scanner(new File("gradu.txt")); while (syöttötiedosto.hasNextLine()) { String rivi = syöttötiedosto.nextLine(); System.out.println(rivi); } syöttötiedosto.close(); // Huom.!! } }
Kehitellään tätäkin ohjelmaa; Nyt ohjelma lukee aina saman tiedoston "gradu.txt". Ja jos tiedosto puuttuu, saadaan systeemin antama "käyttäjävihamielinen" virheilmoitus:
Exception in thread "main" java.io.FileNotFoundException: gradu.txt (No such file or directory) at java.io.FileInputStream.open(Native Method) at java.io.FileInputStream.(FileInputStream.java:106) at java.util.Scanner. (Scanner.java:621) at Listaa1.main(Listaa1.java:8)
Esimerkki 4: Käytetään hyväksi File-luokkaa (Listaa2.java):
import java.io.*; import java.util.Scanner; class Listaa2 { private static Scanner lukija = new Scanner(System.in); public static void main(String[] args) throws FileNotFoundException { System.out.println("Minkä tiedoston haluat nähdä?"); String tiedostonNimi=lukija.nextLine(); File tiedosto_olio = new File(tiedostonNimi); if (!tiedosto_olio.exists()) { System.out.println("Tiedostoa "+ tiedostonNimi +" ei löydy!"); return; // keskeytetään kaikki! } Scanner syöttötiedosto = new Scanner(tiedosto_olio); while (syöttötiedosto.hasNextLine()) { String rivi = syöttötiedosto.nextLine(); System.out.println(rivi); } syöttötiedosto.close(); // Huom.!! } }
Olioarvojen lisäksi myös alkeistyyppisiä arvoja voidaan sarjallistaa.
Arvoja ja olioita talteen
Jos luokan ilmentymien halutaan sarjallistuvan, luokan on toteutettava rajapintaluokka Serializable, joka löytyy pakkauksesta java.io. Toteuttaminen on vaivatonta, koska tuo rajapintaluokka ei määrittele ensimmäistäkään metodia! Kyseessä onkin vain keino ilmaista Java-järjestelmälle, että olioita aiotaan sarjallistaa. Luvun 2.6 esimerkkiluokkaa Kuulaskuri voidaan muokata sarjallistuvaksi seuraavasti:
import java.io.*; public class Kuulaskuri implements Serializable { private int kuu; // sallitut arvot 1,..12 // konstruktori: public Kuulaskuri() { kuu = 1; } // aksessorit: public int moneskoKuu() { return kuu; } public void seuraavaKuu() { ++kuu; if (kuu == 13) kuu = 1; } }
Sarjallistettavia olioita voidaan kirjoittaa luokan ObjectOutputStream ilmentymään (tulostettava binääritiedosto), jonka konstruktorin parametriksi kelpaa luokan FileOutputStream ilmentymä. Viimeksimainitulla taas on mm. String- ja File-parametrinen konstruktori.
Taas kerran tätä "raskasta kalustoa": ObjectOutputStream-luokan yhden konstruktorin muodollinen parametri on abstrakti luokka OutputStream, jonka aliluokka FileOutputStream tarjoaa mukavat konstruointimahdollisuudet: File-olion ja tiedostonimen.
Tulostiedosto voidaan siis määritellä vaikkapa:
FileOutputStream ulosF = new FileOutputStream("turva.tmp"); ObjectOutputStream ulos = new ObjectOutputStream(ulosF);Luokka ObjectOutputStream tarjoaa metodeita mm. alkeistyyppien binääritulostamiseen: writeDouble, writeInt, ym., ja metodin writeObject olioarvojen tulostamiseen. ObjectOutputStream-tiedostolle on lopussa tehtävä flush()-operaatio, FileOutputStream-oliolle close()-operaatio.
Tarkistetut poikkeukset on tavalla tai toisella käsiteltävä. Tiedostojen luonnissa voi tulla IOException ja FileNotFoundException.
Sarjallistettuja oliota voidaan lukea hyvin symmetrisesti samaan tapaan kuin niitä kirjoitetaan: (ObjectInputStream, FileInputStream)
FileInputStream sisäänF = new FileInputStream("turva.tmp"); ObjectInputStream sisään = new ObjectInputStream(sisäänF);
Lukemiseen löytyy vastaavia metodeja kuin tulostamiseen: readDouble, readInt, ... readObject. Viimeksimainittua käytettäessä on tehtävä eksplisiittinen tyyppimuunnos, jos arvon saaja ei ole Object-tyyppinen. FileInputStream-oliolle on syytä lopuksi tehdä close()-operaatio. Myös lukemisen tarkistettuihin poikkeuksiin on varauduttava.
Esimerkki: Ohjelma KaikkeaSarjaan.java pyrkii vain tiiviisti havainnollistamaan binääritiedoston lukemista ja kirjoittamista. Mutta muuten ohjelma ei edusta hyvää ohjelmointityyliä: 1) Poikkeuksiin on varauduttu kaikkein laiskimmalla mahdollisella tavalla. 2) Normaalisti ei tietenkään ole tapana kirjoittaa yhteen tiedostoon keskenään eri tyyppisiä arvoja. Jne.
import java.io.*; public class KaikkeaSarjaan { public static void main(String[] args) throws Exception { FileOutputStream ulosF = new FileOutputStream("turva.tmp"); ObjectOutputStream ulos = new ObjectOutputStream(ulosF); FileInputStream sisäänF = new FileInputStream("turva.tmp"); ObjectInputStream sisään = new ObjectInputStream(sisäänF); // Alkuarvoja: int i = 100; // lukuja String s = "kissa"; // String-olioita Kuulaskuri k = new Kuulaskuri(); // omia olioita k.seuraavaKuu(); k.seuraavaKuu(); int[] t ={1,2,3}; // taulukkoja System.out.println("Talletettavat alkuarvot:"); System.out.println(i + " " +s + " " + k.moneskoKuu()); for (int j=0; j<t.length; ++j) System.out.print(t[j]+" "); System.out.println(); // Tiedostoon: ulos.writeInt(i); ulos.writeObject(s); ulos.writeObject(k); ulos.writeObject(t); ulos.flush(); ulosF.close(); // Muutetaan: i = -1; s = "hiiop"; k = new Kuulaskuri(); t[1] = -123; System.out.println("Muutetut arvot:"); System.out.println(i + " " + s + " " + k.moneskoKuu()); for (int j=0; j<t.length; ++j) System.out.print(t[j]+" "); System.out.println(); // Luetaan vanhat: i = sisään.readInt(); s = (String)sisään.readObject(); k = (Kuulaskuri)sisään.readObject(); t = (int[])sisään.readObject(); sisäänF.close(); System.out.println("Tiedostosta luetut vanhat arvot:"); System.out.println(i + " " + s + " " + k.moneskoKuu()); for (int j=0; j<t.length; ++j) System.out.print(t[j]+" "); System.out.println(); } }Ohjelma tulostaa:
Talletettavat alkuarvot: 100 kissa 3 1 2 3 Muutetut arvot: -1 hiiop 1 1 -123 3 Tiedostosta luetut vanhat arvot: 100 kissa 3 1 2 3Jos vaikkapa Kuulaskuriksi yritetään lukea String, saadaan virheilmoitus:
Exception in thread "main" java.lang.ClassCastException: java.lang.String at KaikkeaSarjaanRikki.main(KaikkeaSarjaan.java:60)
Linkitettyjä rakenteita talteen
Olioiden sarjallistus on Javassa toteutettu siten, että myös olion viittamat oliot talletetaan. Niinpä esimerkiksi linkitetyn listan saa talletettua kokonaisuudessaan tallettamalla pelkästään listan ensimmäisen alkion!