Helsingin yliopisto / Tietojenkäsittelytieteen laitos
Copyright © 2005 Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

5.2 Tiedostoista

(Muutettu viimeksi 30.11.2009)

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ä.

Luokka File

File-luokan avulla tiedostoja pääsee hallitsemaan: "onko olemassa", "uudelleennimetään", "poistetaan", "mikä on pituus", ... Luokan ilmentymät eivät itse ole tiedostoja. Luokan ilmentymien avulla voidaan mm. saada Java-ohjelman käyttöön käyttöjärjestelmän tuntemia tiedostonimiä, hakemistopolkuja ja sen semmoisia.

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.

Tekstitiedoston kirjoittaminen

Tekstitiedoston tulostamiseen luokka PrintWriter on mukava, koska siellä on tarjolla System.out-kentän käytössä tutuksi tulleet metodit print ja println kuormitettuina monentyyppisille parametreille.

Luokan ilmentymiä eli tulostustiedostoja voidaan luoda mm. konstruktoreilla

   public PrintWriter(String fileName) throws FileNotFoundException
   public PrintWriter(File file)       throws FileNotFoundException
Tiedostonimi 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:

  1. Jos tulostustiedosto on jo olemassa, uusi kirjoitetaan varoittamatta sen "päälle", ts. vanha tiedosto hävitetään kyselemättä ja varoittelematta.
  2. On kömpelöä, että voidaan tulostaa vain tiedostoon "gradu.txt".

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");
    
  }
}

Tekstitiedoston lukeminen

Scanner-luokan avulla tekstitiedostojen lukeminen on helppoa. Kaikki, mitä on jo opittu standardisyöttövirran lukemisesta, on sellaisenaan sovellettavissa tekstitiedostojen lukemiseen!

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.!!
  }
}

Sarjallistaminen - binääritiedostoja

Olioiden sarjallistamiseksi kutsutaan olioarvojen kirjoittamista tulosvirtaan, vaikkapa tiedostoon. Sarjallistetut oliot voidaan sitten lukea tiedostosta oliomuuttujien arvoksi. Sarjallistetuista olioista rakentuvat tiedostot vastaavat muiden ohjelmointikielten ns. binääritiedostoja. Sarjallistamista käytetään myös olioita tietoliikenneyhteyksin koneelta koneelle siirrettäessä.

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 3 
Jos 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!


Takaisin luvun 5 sisällysluetteloon.