Osa 4

Tiedon lukeminen ja tiedostot

Merkittävä osa ohjelmistoista perustuu tavalla tai toisella tiedon käsittelyyn. Musiikin toistoon tarkoitetut ohjelmistot käsittelevät musiikkitiedostoja, kuvankäsittelyohjelmat käsittelevät kuvatiedostoja. Verkossa ja mobiililaitteissa toimivat sovellukset kuten Facebook, WhatsApp ja Telegram taas käsittelevät tiedostoihin perustuviin tietokantoihin tallennettuja henkilötietoja. Kaikissa näistä sovelluksista on yhteistä tiedon lukeminen, tiedon käsitteleminen tavalla tai toisella sekä se, että käsiteltävä tieto on loppujenlopulta tallennettu jonkinlaisessa muodossa yhteen tai useampaan tiedostoon.

Lukeminen näppäimistöltä

Olemme käyttäneet Scanner-luokkaa käyttäjän kirjoittaman syötteen lukemiseen kurssin alusta lähtien. Tiedon lukemiseen käytetty runko on while-true -toistolause, missä lukeminen lopetetaan tietynmuotoiseen syötteeseen.

Scanner lukija = new Scanner(System.in);

while (true) {
    String rivi = lukija.nextLine();

    if (rivi.equals("loppu")) {
        break;
    }

    // lisää luettu rivi listalle myöhempää käsittelyä
    // varten tai käsittele rivi heti

}

Yllä Scanner-luokan konstruktorille annetaan parametrina järjestelmän syöte (System.in). Tekstikäyttöliittymissä käyttäjän kirjoittama tieto ohjataan syötevirtaan rivi kerrallaan, eli tieto lähetetään käsiteltäväksi aina kun käyttäjä painaa rivinvaihtoa.

Loading

Käyttäjän syöttämä syöte luetaan merkkijonomuotoisena. Mikäli syöte halutaan käsitellä esimerkiksi kokonaislukuina, tulee käyttäjän syöte muuntaa ohjelmassa toiseen muotoon. Alla olevassa esimerkissä ohjelma lukee käyttäjältä syötettä kunnes käyttäjä syöttää merkkijonon "loppu". Mikäli käyttäjän syöte ei ole "loppu", käsitellään syöte lukuna — tässä tapauksessa luku vain tulostetaan.

Scanner lukija = new Scanner(System.in);

while (true) {
    String rivi = lukija.nextLine();

    if (rivi.equals("loppu")) {
        break;
    }

    int luku = Integer.valueOf(rivi);
    System.out.println(luku);
}
Loading

Tiedosto ja tiedostojärjestelmä

Tiedostot ovat tietokoneella sijaitsevia tietokokoelmia, jotka voivat sisältää vaikkapa tekstiä, kuvia, musiikkia tai niiden yhdistelmiä. Tiedoston tallennusmuoto määrittelee tiedoston sisällön sekä tallennusmuodon lukemiseen tarvittavan ohjelman. Esimerkiksi PDF-tiedostoja luetaan PDF-tiedostojen lukemiseen soveltuvalla ohjelmalla ja musiikkitiedostoja luetaan musiikkitiedostojen lukemiseen soveltuvalla ohjelmalla. Jokainen näistä ohjelmista on ihmisen luoma, ja ohjelman luoja tai luojat — eli ohjelmoijat — ovat osana työtään myös määritelleet tiedoston tallennusmuodon.

Tiedostot sijaitsevat tietokoneen kovalevyllä, joka on käytännössä iso joukko ykkösiä ja nollia eli bittejä. Tieto muodostuu näistä biteistä: esimerkiksi yksi int-tyyppinen muuttuja vie tietokoneen muistista 32 bittiä (eli 32 ykköstä tai nollaa). Nykyisiin teratavun kokoisiin kovalevyihin mahtuu noin 8 biljoonaa bittiä (auki kirjoitettuna luku on 8,000,000,000,000). Tässä mittakaavassa yksi kokonaisluku on hyvin pieni.

Tiedostot voivat sijaita käytännössä missä tahansa kovalevyn kohdassa, jopa niin, että tiedosto on pilkottuna useampaan osaan. Tietokoneen tiedostojärjestelmän vastuulla on pitää kirjaa tiedostojen sijainnista kovalevyllä sekä tarjota mahdollisuus uusien tiedostojen luomiseen sekä näiden muokkaamiseen. Tärkein tiedostojärjestelmän toiminnallisuus on kuitenkin kovalevyn todellisen rakenteen abstrahointi: tiedostoja käyttävän henkilön tai ohjelman ei tarvitse välittää siitä, miten ja minne tiedosto on oikeasti tallennettu.

Tietokoneissa on useampia ohjelmia tiedostojen selaamiseen ja nämä ohjelmistot ovat käyttöjärjestelmäkohtaisia. Kaikki tiedostojen selaamiseen käytettävistä ohjelmista käyttävät tavalla tai toisella tietokoneen tiedostojärjestelmää.

Myös käyttämämme ohjelmointiympäristö tarjoaa mahdollisuuden ohjelmointiympäristössä olevien projektien sisältämien tiedostojen selaamiseen. Voit käydä tarkastelemassa NetBeansissa kaikkia projektiin liittyviä tiedostoja valitsemalla Files-välilehden, joka löytyy Projects-välilehden kanssa samasta paikasta. Mikäli Files-välilehteä ei löydy, saa sen auki myös Window-valikosta. Klikkaamalla projektin auki, näet kaikki siihen liittyvät tiedostot.

Loading

Lukeminen tiedostosta

Tiedoston lukeminen tapahtuu Scanner-luokan avulla. Kun Scanner-luokan avulla halutaan lukea tiedosto, annetaan luokan konstruktorille parametrina luettavaa tiedostoa kuvaava File-muuttuja (new File("tiedostonnimi.paate")). Tämän jälkeen tiedostoa voi lukea kuten näppäimistöltä luettavaa syötettä. Lukeminen tapahtuu while-toistolauseella, jota jatketaan kunnes kaikki tiedoston rivit on luettu, eli kunnes tiedostossa ei ole enää luettavia rivejä. Tiedostoja lukiessa voidaan kohdata virhetilanne, joten tiedoston lukeminen vaatii erillisen "yrittämisen" (try) sekä mahdollisen virheen kiinnioton (catch). Palaamme virhetilanteiden käsittelyyn ohjelmoinnin jatkokurssilla eli Ohjelmoinnin MOOCin osan 7 jälkeen.

// alkuun
import java.util.Scanner;
import java.io.File;


// ohjelmassa:

// luodaan lukija tiedoston lukemista varten
try (Scanner tiedostonLukija = new Scanner(new File("tiedosto.txt"))) {

    // luetaan tiedostoja kunnes kaikki rivit on luettu
    while (tiedostonLukija.hasNextLine()) {
        // luetaan yksi rivi
        String rivi = tiedostonLukija.nextLine();
        // tulostetaan luettu rivi
        System.out.println(rivi);
    }
} catch (Exception e) {
    System.out.println("Virhe: " + e.getMessage());
}

Oletuksena (eli kutsuttaessa new Scanner(new File("tiedosto.txt"))) tiedosto luetaan projektin juuresta eli kansiosta, joka sisältää kansion src sekä tiedoston pom.xml (ja mahdollisesti myös muita tiedostoja). Tämän kansion sisältöä voi tarkastella NetBeansin Files-välilehdeltä.

Loading
Loading

Alla olevassa esimerkissä luetaan tiedoston "tiedosto.txt" kaikki rivit, jotka lisätään ArrayList-listaan.

ArrayList<String> rivit = new ArrayList<>();

// luodaan lukija tiedoston lukemista varten
try (Scanner tiedostonLukija = new Scanner(new File("tiedosto.txt"))) {

    // luetaan kaikki tiedoston rivit
    while (tiedostonLukija.hasNextLine()) {
        rivit.add(tiedostonLukija.nextLine());
    }
} catch (Exception e) {
    System.out.println("Virhe: " + e.getMessage());
}

// tee jotain luetuilla riveillä
Loading
Loading
Loading

Monimutkaisemman tiedon lukeminen

Edellisissä esimerkeissä ja ohjelmissa luettu tieto on ollut pääosin melko yksinkertaista. Todellisuudessa maailma on täynnä tietoa, joka liittyy muuhun tietoon — tieto muodostaa kokonaisuuksia. Esimerkiksi henkilön tietoihin kuuluu nimi, syntymäaika, puhelinnumero, osoitetietoihin kuuluu maa, kaupunki, katuosoite, postinumero ja niin edelleen.

Monimutkaisemman tiedon lukemiseen on käytännössä kaksi menetelmää. Joko tietokokonaisuuden osat kysytään osa kerrallaan tai tieto luetaan sellaisessa muodossa, missä (esimerkiksi) yksi rivi sisältää aina yksittäiseen tietokokonaisuuteen liittyvät osat. Alla tiedon lukemisesta on esitelty muoto, missä tietoa kysytään osa kerrallaan. Ohjelma kysyy käyttäjältä henkilöiden nimiä ja ikiä, ja selvittää niiden perusteella vanhimman henkilön nimen. Ohjelman suoritus loppuu kun käyttäjä syöttää nimeksi tyhjän merkkijonon.

Scanner lukija = new Scanner(System.in);

int vanhinIka = -1;
String vanhin = "";

while (true) {
    System.out.print("Syötä nimi: ");
    String nimi = lukija.nextLine();

    if (nimi.equals("")) {
        break;
    }

    System.out.print("Syötä henkilön " + nimi + " ikä: ");
    int ika = Integer.valueOf(lukija.nextLine());

    if (vanhin.equals("") || vanhinIka < ika) {
      vanhin = nimi;
      vanhinIka = ika;
    }
}

System.out.println("Vanhin henkilö oli " + vanhin + " (ikä " + vanhinIka + ")");

Yllä olevassa esimerkissä ja tehtävässä käsiteltävä tieto kysyttiin pala kerrallaan. Toinen vaihtoehto on kysyä tieto määrätynlaisessa muodossa, missä kaikki tiedon osat ovat esimerkiksi samalla rivillä. Eräs tällainen muoto on comma-separated values (CSV)-muoto, eli pilkuilla erotetut tiedot. Alla olevassa esimerkissä on toteutettuna yllä oleva ohjelma siten, että nimi ja ikä syötetään samalla rivillä pilkuilla erotettuna.

Scanner lukija = new Scanner(System.in);

int vanhinIka = -1;
String vanhin = "";

while (true) {
    System.out.print("Syötä nimi: ");
    String rivi = lukija.nextLine();

    if (rivi.equals("")) {
        break;
    }

    String[] palat = rivi.split(",");
    String nimi = palat[0];
    int ika = Integer.valueOf(palat[1]);

    if (vanhin.equals("") || vanhinIka < ika) {
      vanhin = nimi;
      vanhinIka = ika;
    }
}

System.out.println("Vanhin henkilö oli " + vanhin + " (ikä " + vanhinIka + ")");

Ohjelman toiminta on seuraava:

Esimerkkitulostus

virpi,19 jenna,21 ada,20

Vanhin henkilö oli jenna (ikä 21 vuotta)

Vastaavan ohjelman toteuttaminen siten, että henkilön tiedot luetaan tiedostosta onnistuu myös. Oletetaan, että käytössämme on tiedosto nimelta henkilot.csv, joka sisältää nimiä ja syntymävuosia. Tiedoston sisältö on seuraava:

Esimerkkidata

sauli,1948 tarja,1943 martti,1936 mauno,1923 urho,1900

Yllä kuvatun tiedoston lukeminen onnistuu samalla tavalla kuin muiden tiedostojen lukeminen. CSV-muotoiset tiedostot luetaan rivi riviltä. Kukin rivi pilkotaan osiin (taulukoksi) merkkijonon split-metodilla, jonka jälkeen tieto käsitellään. Alla olevassa esimerkissä tiedoston sisältö luetaan ja tulostetaan. Esimerkissä myös varaudutaan tilanteeseen, missä rivi on tyhjä — tällaisessa tilanteessa riville ei tehdä mitään.

// luodaan lukija tiedoston lukemista varten
try (Scanner tiedostonLukija = new Scanner(new File("henkilot.csv"))) {

    // luetaan kaikki tiedoston rivit
    while (tiedostonLukija.hasNextLine()) {
        String rivi = tiedostonLukija.nextLine();

        // mikäli rivi on tyhjä, ei käsitellä sitä
        if (rivi.trim().length() == 0) {
            continue;
        }

        // muulloin tulostetaan tiedot
        String[] osat = rivi.split(",");

        String nimi = osat[0];
        int vuosi = Integer.valueOf(osat[1]);

        System.out.println(nimi + " on syntynyt vuonna " + vuosi);

    }
} catch (Exception e) {
    System.out.println("Virhe: " + e.getMessage());
}
Esimerkkitulostus

sauli on syntynyt vuonna 1948 tarja on syntynyt vuonna 1943 martti on syntynyt vuonna 1936 mauno on syntynyt vuonna 1923 urho on syntynyt vuonna 1900

Loading
Loading
Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan:

Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!