Skip to main content Link Menu Expand (external link) Document Search Copy Copied

Tiedostot

Tällä oppitunnilla tutustumme Javan java.nio.files-pakettiin, joka tarjoaa meille suoraviivaisen tavan lukea ja kirjoittaa tekstitiedostoja. Tiedostojen lukemisen ja kirjoittamisen yhteydessä pääsemme myös harjoittelemaan merkkijonojen käsittelyä sekä poikkeuksia, joita saattaa syntyä, mikäli tiedosto-operaatiossa tapahtuu virheitä.


Sisällysluettelo

Oppitunnin videot

Videoiden katsominen edellyttää kirjautumista MS Stream -palveluun Haaga-Helian käyttäjätunnuksellasi ja liittymistä kurssin Teams-ryhmään.

Tiedoston lukeminen ja poikkeusten käsittely 48:09

Videolla esiintyvät lähdekoodit

Tiedoston kirjoittaminen, CSV-tiedostot ja Kalenteri-esimerkki 54:22

Videolla esiintyvät lähdekoodit

CSV-käsittelyn refaktorointi 7:43

Videolla esiintyvät lähdekoodit

Tiedostojen lukeminen ja kirjoittaminen

Suurten tiedostojen käsittely ohjelmallisesti saattaa aiheuttaa merkittäviä suorituskykyhaasteita:

  • kirjoittaessa tiedostoon yksittäiset kirjoitus- ja lukuoperaatiot ovat hitaita
  • luettaessa tiedostosta koko sisällön lukeminen kerralla voi viedä paljon muistia.

Suorituskykyhaasteiden vuoksi tiedostoja käsitellään usein erilaisten puskurien avulla:

  • puskuriin voidaan kirjoittaa dataa pienissäkin palasissa, mutta puskurin sisältö tallennetaan levylle isommissa erissä
  • tiedostoa luetaan kerralla isompi erä puskuriin, josta siitä hyödynnetään esim. rivi kerrallaan.

Vastaavia haasteita ja ratkaisuja hyödynnetään myös mm. tietoliikenneyhteyksissä.

java.nio.file.Files

Jos kirjoitettavaa tai luettavaa on kohtuullinen määrä, ei puskureita ole välttämätöntä käyttää. Tiedosto voidaan kirjoittaa kerralla esimerkiksi listasta merkkijonoja tai tiedosto voidaan lukea listaksi merkkijonoja. Yksinkertainen standardikirjaston luokka tiedostojen lukemiseen ja kirjoittamiseen on java.nio.file.Files.

Files-luokan käyttämiseksi tarvitset tyypillisesti seuraavat import-komennot:

import java.nio.file.Files; // luokka tiedostojen käsittelyyn
import java.nio.file.Paths; // luokka tiedostojen polkujen määrittelemiseksi
import java.io.IOException; // poikkeusluokka virhetilanteita varten
import java.nio.charset.StandardCharsets; // merkistöluokka, jossa yleisimmät merkistöt

Tiedoston lukeminen

Tiedostoja käsiteltäessä on aina mahdollisuus siihen, että lukeminen epäonnistuu. Tiedostonkäsittelyssä yleinen IOException-poikkeus on sen vuoksi ns. tarkastettu poikkeus, eli siihen täytyy aina varautua joko try/catch-rakenteella tai heittämällä poikkeus edelleen metodista:

public static void main(String[] args) {
    Path tiedostonPolku = Paths.get("luettava_tiedosto.txt");

    try {
        // readAllLines palauttaa kaikki rivit List<String>-listana. Tiedoston polun
        // lisäksi metodille kannattaa määritellä tiedoston merkistökoodaus (UTF-8):
        List<String> rivit = Files.readAllLines(tiedostonPolku, StandardCharsets.UTF_8);

        System.out.println("Tiedostosta luettiin rivit:");
        for (String rivi : rivit) {
            System.out.println(rivi);
        }

    } catch (IOException e) {
        System.out.println(e);
    }
}

Jos poikkeustenkäsittelyä ei haluta tehdä tässä metodissa, voidaan se määritellä heittämään poikkeus edelleen lisäämällä metodin otsikkoon throws IOException:

public static void main(String[] args) throws IOException {
    Path tiedostonPolku = Paths.get("luettava_tiedosto.txt");

    List<String> rivit = Files.readAllLines(tiedostonPolku, StandardCharsets.UTF_8);

    System.out.println("Tiedostosta luettiin rivit:");
    for (String rivi : rivit) {
        System.out.println(rivi);
    }
}

Tätä ratkaisua ei kuitenkaan suositella, koska käsittelemätön poikkeus kaataa ohjelman hallitsemattomasti.

Tiedostojen kirjoittaminen

Path tiedostonPolku = Paths.get("kirjoitettava_tiedosto.txt");

try {
    // Tiedostoon kirjoitetaan listalla olevat rivit:
    List<String> rivit = Arrays.asList("Rivi 1", "Rivi 2");

    // Files.write kirjoittaa listan sisällön riveittäin annettuun tiedostoon
    Files.write(tiedostonPolku, rivit, StandardCharsets.UTF_8); // muista merkistökoodaus!

    System.out.println("Tiedosto kirjoitettiin onnistuneesti!");

} catch (IOException e) {
    System.out.println(e);
}

Yllä olevan koodin suorituksen jälkeen tiedostojärjestelmässä on uusi tiedosto “kirjoitettava_tiedosto.txt” seuraavalla sisällöllä:

Rivi 1
Rivi 2

CSV-tiedostot (comma-separated values)

Taulukkomuotoisen tiedon tallentamiseen yksinkertaisina tekstitiedostoina käytetään usein CSV-tiedostoja:

“CSV on toteutukseltaan tekstitiedosto, jonka taulukkorakenteen eri kentät on eroteltu toisistaan pilkuilla ja rivinvaihdoilla. Jos jokin kenttä sisältää erikoismerkkejä, kyseinen kenttä ympäröidään pystysuorilla lainausmerkeillä (“). Ensimmäisellä rivillä voi olla kenttien selitykset samassa muodossa kuin mitä itse tiedot ovat.”

Wikipedia. CSV. https://fi.wikipedia.org/wiki/CSV

Wikipedian esimerkissä autojen tiedot on esitetty tallennettuna seuraavassa CSV-muodossa:

Vuosi,Merkki,Malli,Pituus
1997,Ford,E350,2.34
2000,Mercury,Cougar,2.38

Sama data on esitettävissä myös taulukkomuodossa:

Vuosi Merkki Malli Pituus
1997 Ford E350 2.34
2000 Mercury Cougar 2.38

Koska CSV-tiedostot on helposti koneluettavia ja -kirjoitettavia, hyvin monet ohjelmat tukevat niitä tiedon tallennusmuotonaan.

Esimerkki CSV-tiedoston lukemisesta

Tämä CarCsvReader-luokka käyttää Javan Files-luokka yllä esitetyn csv-tiedoston lukemiseksi ja sen tietojen muuttamiseksi Java-olioiksi:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CarCsvReader {

    public static void main(String[] args) {
        // Määritellään muuttuja try-lohkon ulkopuolella, jotta sitä voidaan käyttää
        // try-lohkon jälkeen:
        List<String> lines;

        try {
            Path csvPath = Paths.get("cars.csv");
            lines = Files.readAllLines(csvPath, StandardCharsets.UTF_8);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        // Ensimmäisellä rivillä on tiedoston otsikot:
        String[] headers = lines.get(0).split(","); // "Vuosi", "Merkki" ja "Malli"

        // Muutetaan taulukko listaksi, jotta voimme käyttää 'indexOf'-metodia:
        List<String> headerList = Arrays.asList(headers);

        // Selvitetään sarakkeiden indeksit. Indeksejä ei kannata kovakoodata, koska
        // tiedoston rakenne saattaa vaihdella:
        int yearIndex = headerList.indexOf("Vuosi");
        int makeIndex = headerList.indexOf("Merkki");
        int modelIndex = headerList.indexOf("Malli");

        // Kerätään rivit Car-olioina tälle listalle:
        List<Car> cars = new ArrayList<>();

        // Hypätään otsikkorivi (0) yli ja aloitetaan riviltä 1:
        for (int i = 1; i < lines.size(); i++) {
            // Pilkotaan kukin rivi vuorollaan pilkkujen kohdista:
            String[] row = lines.get(i).split(",");

            // Luetaan riviltä halutut tiedot:
            String year = row[yearIndex];
            String make = row[makeIndex];
            String model = row[modelIndex];

            // Luodaan uusi Car-olio ja laitetaan talteen listalle:
            cars.add(new Car(year, make, model));
        }

        /*
         * Vaihtoehtoinen tapa listan läpikäyntiin ja arvojen tulostamiseen:
         *
         * Listan forEach-metodille voidaan antaa operaatio, joka suoritetaan listan
         * jokaiselle arvolle. Tässä suoritettavaksi operaatioksi annetaan
         * metodiviittaus println-metodiin:
         */
        cars.forEach(System.out::println);
    }
}

Yllä oleva koodiesimerkki käyttää tätä Car-luokkaa auton tietojen mallintamiseen:

public class Car {

    private String year;
    private String make;
    private String model;

    public Car(String year, String make, String model) {
        this.year = year;
        this.make = make;
        this.model = model;
    }

    @Override
    public String toString() {
        return "Car [year=" + year + ", make=" + make + ", model=" + model + "]";
    }
}

Suoritettaessa yllä oleva ohjelma tekee seuraavat tulosteet:

Car [year=1997, make=Ford, model=E350]
Car [year=2000, make=Mercury, model=Cougar]

MerkistÃ◦t (merkistöt)

Eri kielialueilla on perinteisesti ollut tarve hyvin erilaisille kirjainmerkeille. Siksi niissä on kehitetty erilaisia merkistöjä, joissa tietyllä bittijonolla on keskenään eri merkitys. Oletkin varmasti törmännyt erilaisissa asiayhteyksissä kummallisiin erikoismerkkeihin sanoissa, joissa kuuluisi olla ääkkösiä.

Nykypäivänä suositellaan käytettäväksi UTF-8 -nimistä merkistöä, joka sisältää merkittävän osan maailmalla käytetyistä merkeistä, kuten 請 ja ✌️. UTF-8:n suosio on noussut erittäin voimakkaasti ja myös tämä tiedosto on kirjoitettu UTF-8:lla.

Share of web pages with different character encoding

By Chris55 - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=51421096

Jotta ohjelmasi toimisi luotettavasti eri suoritusympäristöissä, kannattaa aina tiedostoja luettaessa ja kirjoitettaessa määritellä käytettävä merkistökoodaus:

// Lukeminen:
List<String> rivit = Files.readAllLines(tiedostonPolku, StandardCharsets.UTF_8);
// Kirjoittaminen:
Files.write(tiedostonPolku, rivit, StandardCharsets.UTF_8);

Koodaustehtävä

Kirjoita ohjelma WordCount, joka kysyy käyttäjältä tiedoston nimeä ja tulostaa kyseisessä tiedostossa olevien rivien, sanojen ja merkkien määrän. Luettavan tiedoston on oltava Java-projektin juuressa.

Riveksi lasketaan myös tyhjät rivit ja merkeiksi myös esimerkiksi välilyönnit. Sanojen laskemiseksi voit käyttää String-luokan split-metodia, jolla pilkot kunkin rivin välilyöntien kohdalta. Huomaa kuitenkin, että tyhjällä rivillä ei saa laskea yhtään sanaa, vaikka split-metodi palauttaakin yhden pituisen taulukon.

Anna tiedoston nimi: loremipsum.txt

Tiedostossa on:
2 riviä
8 sanaa
55 merkkiä

Yllä olevassa esimerkkisuorituksessa luettiin seuraava tekstitiedosto loremipsum.txt:

Lorem ipsum dolor sit amet,
consectetur adipiscing elit.

loremipsum.txt