Helsingin yliopisto / Tietojenkäsittelytieteen laitos / 581258-1 Johdatus ohjelmointiin
Copyright © 1997 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 Tekstitiedostojen lukemista ja kirjoittamista

(Muutettu viimeksi 3.12.1997)

Luvussa tutustutaan yhteen tapaan kirjoittaa ja lukea nimettyjä tekstitiedostoja. Luku alkaa tiedostojen hallinnan yleisen välineen, luokan File, esittelyllä.

Edellisen luvun tapaan tämäkin luku antaa esimerkkejä valmiiden välineiden käyttötapojen etsimisestä.

Luokka File

File-luokan avulla tiedostoja pääsee hallitsemaan: "onko olemassa", "uudelleennimetään", "poistetaan", "mikä on pituus", ...

Luokalla on mm. konstruktori:

public File(String path)
joka luo File-olion. Parametri on tiedostonimi, tarkemmin sanottuna ns. polkunimi.

Polkunimi (pathname) tarkoittaa tiedoston 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: siellä on tarjolla System.out-kentän käytössä tutuksi tulleet metodit print ja println kuormitettuina monentyyppisille parametreille.

Luokan ilmentymiä, tulostustiedostoja(!), luodaan mm. konstruktorilla:

   public PrintWriter(OutputStream out, boolean autoFlush)

Huom: Jälkimmäinen parametri ohjaa ns. tulostuspuskurin tietojen siirtämistä varsinaiseen tiedostoon. Kun parametrin asettaa trueksi, tiedostoa ei tarvitse itse sulkea (close()).

Luokassa PrintWriter ei ole konstruktoria, jolle voisi antaa tiedostonimen. Niinpä seuraava ongelma on löytää jokin tapa kertoa, minne kirjoitetaan. Parametrin tyyppi OutputStream on abstrakti luokka, siitä ei siis voi luoda edes ilmentymiä! Mutta sillä on aliluokka FileOutputStream, jolla on mm. konstruktorit:

  public FileOutputStream(String name) 
                              throws IOException

  public FileOutputStream(String name, boolean append) 
                              throws IOException

  public FileOutputStream(File file) 
                              throws IOException

Kaksi ensimmäistä saa tiedostonimen parametrina, kolmas File-olion. (Toinen konstruktoreista tarjoaa mahdollisuuden kirjoittaa olemassaolevan tiedoston loppuun: kun paramertiksi append annetaan true, kirjoitettava liitetään olemassaolevaan tiedostoon.)

Jos halutaan kirjoittaa tiedosto "tulos.txt", voidaan siis ohjelmoida:

    import java.io.*; 
      ...
    PrintWriter tulos = 
          new PrintWriter(
                   new FileOutputStream("tulos.txt"),
                   true // !
              );
      ...
    tulos.println("Hip hei!");
      ...

Tässä siis luodaan nimetystä tiedostosta FileOutputStream-olio, joka annetaan samantien parametriksi PrintWriter-luokan konstruktorille.

Huom: Tämäntapainen menettely on Javan valmiiden välineiden käyttämisessä tavallista: halutaan tietynlainen käyttötapa (PrintWriter-luokan mukavat metodit) ja käytössä on tietynlainen lähtökohta (tiedoston nimi String-oliona). Ratkaisuja etsitään luokkien suhteita ja konstruktoreita tutkimalla.

Esimerkki 1: Laaditaan sovellus, jolla voi kirjoittaa näppäimistöltä tekstiä tiedostoon "juttu.txt" (Talleta1.java):

import java.io.*; 

public class Talleta1 { 

  public static void main(String[] args) 
                        throws IOException {

    PrintWriter tulos = 
          new PrintWriter(
                   new FileOutputStream("juttu.txt"),
                   true // !
              );

    System.out.println("Kirjoittamasi teksti menee tiedostoon juttu.txt");
    System.out.println("(ctrl-d lopettaa!)\n");

    String rivi;

    while ((rivi = Lue.rivi()) != null) 
       tulos.println(rivi);

    System.out.println("Työt tehty!\n");
  }
}

Esimerkissä annetaan main-metodille lupa kaatua, koska konstruktorin kutsu new FileOutputStream("juttu.txt") voi aihettaa poikkeuksen!

Ohjelma on ainakin kahdesta syystä vähän huono:

  1. Jos tulostustiedosto on jo olemassa, uusi kirjoitetaan varoittamatta sen "päälle".
  2. On kömpelöä, että voidaan tulostaa vain tiedostoon "juttu.txt".
Esimerkki 2: Laaditaan kehittyneempi versio, jossa käytetään hyväksi luokan File tiedostokäsittelyvälineitä (Talleta2.java):
import java.io.*;

public class Talleta2 { 

  public static void main(String[] args) 
                        throws IOException {

    
    System.out.println("Minne tulostetaan?");
    String tulNimi=Lue.rivi();

    File tulTd = new File(tulNimi);

    if (tulTd.exists()) {
      System.out.println("Tiedosto "+tulNimi+" on jo olemassa!");
      System.out.println("Haluatko korvata sen uudella (k = kyllä)?");
      if (Lue.merkki() != 'k') 
        return; // keskeytetään kaikki!
    }

    PrintWriter tulos = 
          new PrintWriter(
                   new FileOutputStream(tulTd),
                   true // !
              );

    System.out.println("Kirjoittamasi teksti menee tiedostoon "+tulNimi);
    System.out.println("(ctrl-d lopettaa!)\n");

    String rivi;

    while ((rivi = Lue.rivi()) != null) 
       tulos.println(rivi);

    System.out.println("Työt tehty!\n");
    
  }
}


Tekstitiedoston lukeminen

Tekstitiedoston lukemisen välineet etsitään samaan tapaan kuin tulostamisen välineet edellä: tutkitaan luokkia ja konstruktoreita. Nyt perustelut jäävät harjoitustehtäväksi!.

Tekstitiedostoa voidaan lukea riveittäin esimerkiksi seuraavasti:

  import java.io.*; 
     ...
  BufferedReader syotto =  
       new BufferedReader(
              new InputStreamReader(
                    new FileInputStream(?????)
                  )
           );
     ...

   String rivi = syotto.readLine();
     ...
Luokan FileInputStream konstruktorin parametriksi kelpaa tiedostopolku tai File-olio.

Esimerkki 3: Laaditaan sovellus, joka listaa kuvaruudulle tiedoston "juttu.txt" (Listaa1.java):

import java.io.*;

class Listaa1 {

  public static void main(String[] args) 
                       throws IOException {

    BufferedReader syotto =  
         new BufferedReader(
                new InputStreamReader(
                      new FileInputStream("juttu.txt")
                    )
             );

    String rivi;
 
    while ((rivi = syotto.readLine()) != null)
      System.out.println(rivi);
  }
}

Tätäkin ohjelmaa voi vähän kehitellä. Ohjelma osaa tulostaa aina vain tiedoston "juttu.txt". Ja jos tiedosto puuttuu, saadaan systeemin antama "käyttäjävihamielinen" virheilmoitus.

Esimerkki 4: Käytetään hyväksi File-luokkaa (Listaa2.java):

import java.io.*;

class Listaa2 {

  public static void main(String[] args) 
                        throws IOException {

    System.out.println("Minkä tiedoston haluat nähdä?");
    String syoNimi=Lue.rivi();

    File syoTd = new File(syoNimi);

    if (!syoTd.exists()) {
      System.out.println("Tiedostoa "+syoNimi+" ei löydy!");
      return; // keskeytetään kaikki!
    }


    BufferedReader syotto =  
         new BufferedReader(
                new InputStreamReader(
                      new FileInputStream(syoTd)
                    )
             );

    String rivi;
 
    while ((rivi = syotto.readLine()) != null)
      System.out.println(rivi);
  }
}


Omia luokkia syöttöön ja tulostukseen

Edellä laadittiin pelkkiä "pääohjelmaluokkia", jotka sisälsivät kaiken tiedostonkäsittelykaluston. Todellisia ohjelmia laadittaessa välineet tietenkin ohjelmoidaan luokkina!

Jos käsiteltäviä tiedostoja on aina vain yksi, voidaan laatia luokan Lue kaltaisia kirjastoluokkia.

"Oikeat" luokat, joista luodaan ilmentymiä, antavat enemmän mahdollisuuksia: ohjelmassa voi luoda useampia olioita, tiedostoja, luettaviksi ja kirjoitettaviksi.

Tiedostojenkäsittelyluokkia suunnitellessa joutuu tekemään useita valintoja, jotka vaikuttavat luokkien käyttötapaan (olkoon tiedostoluokka nimeltään Tiedosto ja sovellus Sovellus):

Näihin kysymyksiin ei ole mitään ainoata oikeata vastausta. Eri tilanteissa voi olla erilaiset tarpeet.


Takaisin luvun 5 sisällysluetteloon.