- CS-A1110
- Kierros 11
- Luku 11.3: Vähän tiedostojen käsittelystä
Luku 11.3: Vähän tiedostojen käsittelystä
Johdanto
Tiedostojen käyttö ohjelmissa
Tähän asti kurssilla et ole itse kirjoittanut koodia, joka käyttäisi tietokoneen kiintolevylle tallennettuja tietoja tai tallentaisi tietoja levyn tiedostoihin. Eräät valmiina annetut ohjelmakomponentit tosin ovat näin tehneet: esimerkiksi DNA-tietoja käsitellyt ohjelma luvusta 5.6 osasi lukea syötteensä tekstitiedostosta, samoin Stars ja RobotTribes.
Tiedostojen käsitteleminen on todellisissa sovelluksissa yleistä. Syitä on monia; tässä eräitä:
Halutaan, että ohjelma voi tallentaa dokumentteja (esim. tekstidokumentti, PowerPoint-esitys, kokemuspäiväkirjaan tehdyt merkinnät) niin, että ne säilyvät kiintolevyllä tms. massamuistissa myös ohjelman käyttökertojen välissä ja niitä voi palata muokkaamaan.
Halutaan, että ohjelma osaa ottaa tiedostoon kirjatut tiedot syötteekseen (esim. tallennettu mittausdata) ja laskea tuloksia niiden perusteella.
Halutaan, että käyttäjä voi tehdä sovelluksen käyttämiseen liittyviä asetuksia (esim. kieliasetus). Asetukset tallennetaan tiedostoon ja ladataan aina, kun sama käyttäjä käynnistää sovelluksen.
Halutaan tallentaa tietokonepelin tilanne ja ladata se käyttäjän pyytäessä.
Tässä luvussa tutustut tiedostojen käsittelemiseen vain lyhyesti. Tarkastelu on käytännöllinen: näet muutaman perustyökalun, jota tiedostojen käsittelemiseen voi Scala-ohjelmissa käyttää. Lisää aiheesta opit muilla ohjelmointikursseilla tai omin toimin vaikkapa nettilähteiden avulla.
I/O-kirjastoista
On tapana sanoa, että kun tiedostoa käytetään ohjelman syötteenä, ohjelma lukee (read) tiedostoa. Vastaavasti tietoa tallennettaessa ohjelma kirjoittaa tiedostoon (write, joissakin yhteyksissä myös print). Lukemiseen ja kirjoittamiseen viitataan usein termillä I/O (sanoista input/output). Yleistermi I/O kattaa tiedostojen käsittelyn lisäksi myös muunlaisen vuorovaikutuksen ohjelman ja muiden tahojen välillä, kuten ruudulle tulostamisen ja tietoverkon käytön.
Ohjelmointikielten peruskirjastoista löytyy käytännössä aina jonkinlainen I/O-ohjelmakirjasto,
jolla voi käsitellä tiedostoja. Scalan I/O-kirjasto löytyy arvattavan nimisestä pakkauksesta
scala.io
.
Koska Scalasta voi helposti käyttää myös Java-kielen kirjastoja (luku 5.4), joihin lukeutuu
kelvollinen I/O-kirjasto, ei samankaltaisen Scala-kirjaston laatiminen ole toistaiseksi
noussut kovin korkealle Scalan standardi-APIn kehittäjien asialistalla. Pakkauksessa
scala.io
on ainoastaan muutama suhteellisen yksinkertainen työkalu muutamaan suhteellisen
yksinkertaiseen perustarpeeseen kuten tekstitiedostojen lukemiseen. Muihin tarpeisiin
Scala-ohjelmissa voi käyttää esimerkiksi pakkausta java.io
tai jonkun kolmannen osapuolen
tarjoamaa kirjastoa perus-APIn ulkopuolelta.
Tekstitiedostoista
Tiedostoihin, kuten tietokoneen muistiin muutenkin, voi tallentaa dataa monessa eri muodossa (luku 5.4). Tässä luvussa pitäydymme ns. plain text -tekstitiedostoissa, joihin tallennetut bitit on tarkoitus tulkita kirjoitusmerkkeinä.
Tekstitiedostojen hyviin puoliin lukeutuu se, että niitä voi muokata erilaisissa editoriohjelmissa (esim. Emacs, Notepad tai IntelliJ). Monissa I/O-kirjastoissa on välineitä, jotka sopivat nimenomaan tekstitiedostojen käsittelyyn.
Tiedoston lukeminen, esimerkki 1/7
Tavoite: luetaan muutama merkki
Olkoon meillä olemassa tekstitiedosto nimeltä example.txt
, jolla on seuraava sisältö:
This is the first line from the file.
This is the second one.
This third line is the last.
Tällainen esimerkkitiedosto löytyy moduulista Files, josta myös löytyvät kaikki tämän luvun esimerkkiohjelmat.
Laaditaan aluksi ohjelma, joka lukee tiedostoa merkki kerrallaan ja raportoi muutaman ensimmäisen lukemansa merkin. Ohjelman tuloste on tällainen:
The first character in the file is T.
The second character in the file is h.
The third character in the file is i.
The fourth character in the file is s.
The fifth character in the file is .
The sixth character in the file is i.
Ratkaisu: fromFile
ja next
scala.io
-pakkauksessa on luokka Source
, joka kuvaa dataa, jonka ohjelma lukee jostakin
lähteestä, esimerkiksi tiedostosta. Luokalla on myös kumppaniolio, joka tarjoaa erilaisia
metodeita Source
-olioiden luomiseen; yksi näistä on nimeltään fromFile
. Tämä metodi luo
Source
-olion, joka saa datansa tiedostosta. Käytetään sitä:
import scala.io.Source
@main def readingExample1() =
val file = Source.fromFile("Files/example.txt")
println("The first character in the file is " + file.next() + ".")
println("The second character in the file is " + file.next() + ".")
println("The third character in the file is " + file.next() + ".")
println("The fourth character in the file is " + file.next() + ".")
println("The fifth character in the file is " + file.next() + ".")
println("The sixth character in the file is " + file.next() + ".")
file.close()
Muuttuja file
viittaa ohjelmassamme läpikäyjäolioon, jollaista
sanotaan iteraattoriksi (iterator). Iteraattori liittyy
johonkin tietolähteeseen (tiedostoon, alkiokokoelmaan tms.) ja
osaa käydä sen sisällön kerran läpi.
Luodulta iteraattorilta voi pyytää seuraavan merkin next
-metodilla.
Tässä siis ensin saadaan ensimmäinen tiedostoon tallennetuista
merkeistä (Char
-tyyppinen arvo).
Iteraattori pitää sisällään kirjaa läpikäynnin etenemisestä.
Sen tila muuttuu aina next
-metodia kutsuttaessa. Uudet
next
-kutsut palauttavat seuraavia merkkejä tiedostosta.
Tiedoston lukemiseen sisältyy, että ohjelma pyytää käyttöjärjestelmältä
yhteyden kyseiseen tiedostoon. Hyvään ohjelmointitapaan kuuluu
vapauttaa tällaiset käyttöönotetut resurssit, kun niitä ei enää
tarvita. Tämä hoituu close
-metodikutsulla.
Tässä käytettiin suhteellista tiedostopolkua. Tämä ohjelma toimii,
kunhan kansio Files
ja sen tiedosto example.txt
löytyvät ohjelman
työhakemistosta. IntelliJ’ssä työhakemisto oletusarvoisesti koko
projektin juurikansio, jonka alla Files
-moduulikansio sijaitsee.
Tiedoston lukeminen, esimerkki 2/7: virhetilanteet
I/O-ohjelmointiin liittyy kiinteästi ajonaikaisten virhetilanteiden mahdollisuus.
Esimerkiksi mikä tahansa äskeisen ohjelman file.next()
-kutsuista epäonnistuu, jos
tiedoston sisältämään dataan ei päästäkään käsiksi. Näin voi käydä vaikkapa silloin, jos
tiedosto on muistitikulla ja joku vetää tikun irti juuri, kun pitäisi lukea lisää dataa.
Tällöin next
-metodikutsu tuottaa IOException
-tyyppisen ajonaikaisen virhetilanteen,
joka katkaisee esimerkkikoodimme suorituksen. Jäljelläolevia rivejä ei suoriteta lainkaan.
Tavoite: suljetaan tiedosto aina
Yllä jo todettiin, että I/O-ohjelmoinnin "hygieniaan" kuuluu vapauttaa varatut resurssit,
kun niitä ei enää tarvita. Esimerkissämme tämä tarkoittaa close
-metodin kutsumista.
Äskeisessä ohjelmassa on close
-kutsu. Se kuitenkin jää suorittamatta, jos jokin next
-kutsuista katkaisee koodin suorittamisen poikkeustilanteeseen.
On hyvä opetella alusta pitäen huolehtimaan tiedoston sulkemisesta aina siinäkin
tapauksessa, että lukemisen aikana tapahtuu virhe. Siihen on montakin tapaa; tässä
käsittelemme niistä try
-rakenteen.
Ratkaisu: try
ja finally
Seuraava ohjelma toimii muuten samoin kuin edellinen mutta huolehtii tiedoston sulkemisesta myös virheen sattuessa.
import scala.io.Source
@main def readingExample2() =
val file = Source.fromFile("Files/example.txt")
try
println("The first character in the file is " + file.next() + ".")
println("The second character in the file is " + file.next() + ".")
println("The third character in the file is " + file.next() + ".")
println("The fourth character in the file is " + file.next() + ".")
println("The fifth character in the file is " + file.next() + ".")
println("The sixth character in the file is " + file.next() + ".")
finally
file.close()
try
-sana aloittaa koodilohkon, jonka koko ohjelmakoodi suoritetaan
— tai ainakin yritetään suorittaa — ihan tavalliseen tapaan.
Lohkon suorittaminen päättyy kesken, jos syntyy ajonaikainen
poikkeustilanne. Tällöin katkeaisi koko tämän ohjelmamme suoritus
siihen paikkaan, mutta...
... try
-lohkon perään voi kirjoittaa finally
-lohkon.
Se määrittelee, mitä tehdään try
-lohkon jälkeen kaikissa
tapauksissa, myös silloin, jos try
-lohkon
suorittaminen katkesi poikkeustilanteeseen.
Tässä siis määritellään, että: "Lopuksi sulje yhteys tiedostoon,
oli try
-lohkon sisällön suorittaminen onnistunut tai ei."
Entä virheisiin reagoiminen?
finally
-lohko siis mahdollistaa sen, että tietyt käskyt suoritetaan kaikissa tapauksissa,
sattui virhe tai ei. Vaan entä jos haluat suorittaa tietyt käskyt vain siinä tapauksessa,
että tiettyä koodinpätkää suoritettaessa tapahtui ajonaikainen virhe? Tällöin voisi
esimerkiksi tulostaa käyttäjän näkyviin jonkin virheilmoituksen.
Käyttämämme rakenne ei myöskään estä ohjelmaa kaatumasta virheeseen finally
-lohkon
suoritettuaan. Entä jos haluaisit virheen sattuessa jatkaa ohjelman suoritusta tavalla
tai toisella? (Tämä voi sovelluksesta ja virheestä riippuen olla mahdollista.)
Näihin on toki konstinsa, joista yksi on se, että try
-rakenteeseen voi liittää
catch
-lohkon, joka määrää, mitä virheen sattuessa tehdään. (Tuota aihetta käsitellään
toisilla ohjelmointikursseilla kuten Ohjelmointistudiot 1 ja
2. Löytänet lisätietoja myös verkkohaulla.)
Tiedoston lukeminen, esimerkki 3/7
Tavoite: luetaan kukin merkki
Edellinen esimerkkiohjelma tulosti vain tiedoston kuusi ensimmäistä merkkiä. Laaditaan nyt ohjelma, joka tulostaa jokaisen merkin. Kun tiedoston sisältö on kuten edellä, näyttää nyt laadittavan ohjelman tuloste tältä:
Character #1 is T.
Character #2 is h.
Character #3 is i.
Character #4 is s.
Character #5 is .
Character #6 is i.
Character #7 is s.
Character #8 is .
Character #9 is t.
[Tulostetta lyhennetty tästä välistä.]
Character #82 is s.
Character #83 is .
Character #84 is t.
Character #85 is h.
Character #86 is e.
Character #87 is .
Character #88 is l.
Character #89 is a.
Character #90 is s.
Character #91 is t.
Character #92 is ..
Ratkaisu: Source
-olio for
-silmukassa
import scala.io.Source
@main def readingExample3() =
val file = Source.fromFile("Files/example.txt")
try
var charCount = 1 // askeltaja: 1, 2, ...
for char <- file do
println("Character #" + charCount + " is " + char + ".")
charCount += 1
finally
file.close()
Iteraattorin voi käydä läpi for
-silmukalla. Tämä vastaa sitä,
että toistuvasti pyydettäisiin iteraattorilta seuraavaa arvoa
next
-metodilla, kunnes läpikäytäviä arvoja ei enää ole jäljellä.
Tässä siis toistetaan tulostamista ja merkkinumeron kasvattamista, kunnes tiedostosta on jokainen merkki käyty läpi.
Iteraattoreista ja for
-silmukoista
Yllä kerrottiin, että iteraattori on olio, joka osaa käydä jonkin datan kertaalleen läpi.
Myös alkiokokoelmalta voi pyytää kokoelman sisältöä läpikäyvän iteraattorin. Kulissien takana Scalan kokoelmat käyttävätkin iteraattoriolioita tarjotakseen tutut toimintonsa.
Sen lisäksi, että iteraattorioliolla on pääsy läpikäytävään dataan, sen tehtävänä on pitää kirjaa yhdestä tuon datan läpikäyntikerrasta eli iteraatiosta. Tapa, jolla tästä pidetään kirjaa, riippuu kokoelmasta. Vaikkapa puskuria läpikäytäessä iteraattori voi pitää sisällään kirjaa seuraavaksi käsiteltävästä indeksistä.
Alkiokokoelmaa läpikäydessä sovellusohjelmoijan ei yleensä tarvitse luoda iteraattorioliota erillisellä käskyllä. Tämä ei johdu siitä, etteikö iteraattorioliosta olisi tällöin hyötyä, vaan siitä, että esimerkiksi Scala-työkalut huolehtivat tällöin iteraattorin luomisesta automaattisesti. Voimme tarkastella asiaa lyhyen yleissivistävästi.
Tässä ensin tutunlainen for
-silmukka, joka tulostaa vektorin
sisällön.
// Versio 1
val lukuja = Vector(10, 5, 2, 4)
for luku <- lukuja do
println(luku)
Vektorilla on iterator
-metodi, joka luo uuden kyseistä vektoria
läpikäyvän iteraattoriolion (vrt. Source.fromFile
). Iteraattori
osaa antaa vektorin sisällöstä alkion kerrallaan sekä pitää kirjaa
siitä, missä alkiossa ollaan menossa. Seuraava koodi tekee saman
kuin edellinenkin.
// Versio 2
val lukuja = Vector(10, 5, 2, 4)
val iteraattori = lukuja.iterator
for luku <- iteraattori do
println(luku)
Saman tekee tämäkin koodinpätkä, jossa iteraattorin next
ja
hasNext
-metodeita käytetään suoraan sen sijaan, että tuo työ
jätettäisiin for
-silmukan huoleksi:
// Versio 3
val lukuja = Vector(10, 5, 2, 4)
val iteraattori = lukuja.iterator
while iteraattori.hasNext do
println(iteraattori.next())
Luvusta 6.3 tiedämme, että Scalan for
-silmukan toteutuksessa
käytetään foreach
-metodia apuna. Esimerkiksi yllä olevan version 1
for
-silmukka on vain erilainen tapa kirjoittaa foreach
-käsky.
Scala-kääntäjä huolehtii siitä, että versio 1 itse asiassa tarkoittaa
käytännössä tätä:
// Versio 4
val lukuja = Vector(10, 5, 2, 4)
lukuja.foreach(println)
Tässä siis kutsutaan vektoriolion foreach
-metodia, mutta miten se
on toteutettu? Metodin kuuluu käydä läpi vektorin oma sisältö, ja
siihen tarkoitukseen vektori luo itseään läpikäyvän iteraattorin
ja kutsuu sen foreach
-metodia; iteraattori huolehtii läpikäynnin
etenemisestä. Versio 4 yllä toimii siis oleellisesti samoin kuin
seuraava viitosversio:
// Versio 5
val lukuja = Vector(10, 5, 2, 4)
lukuja.iterator.foreach(println)
Syvemmälle iteraattorien toteutukseen emme nyt mene. Nämä esimerkit
kuitenkin valottavat hieman sitä, mitä tapahtuu, kun käytät
ohjelmassasi for
-silmukkaa kokoelman läpikäymiseen.
Tiedoston lukeminen, esimerkki 4/7
Tavoite: kerätään koko tiedoston sisältö muuttujaan
Entä jos haluaisimme kerätä koko tiedoston sisällön ja välittää sen parametriksi jollekin
funktiolle? Ehkä esimerkiksi lukisimme tiedostoon tallennetut nuotit ja välittäisimme
kappaleen play
-funktiolle.
Tehdään juuri niin. Yksi tarkoitukseen sopiva esimerkkitiedosto on Files-moduulin
running_up_that_hill.txt
.
Ratkaisu: mkString
import scala.io.Source
import o1.play
@main def readingExample4() =
val file = Source.fromFile("Files/running_up_that_hill.txt")
try
val entireContents = file.mkString
play(entireContents)
finally
file.close()
Iteraattorillakin on vastaava mkString
-metodi (luku 4.2) kuin
alkiokokoelmilla. Tässä sen paluuarvona saadaan merkkijono,
jossa on tiedoston koko sisältö rivinvaihtomerkkeineen kaikkineen.
Tiedoston lukeminen, esimerkki 5/7
Tavoite: numeroidaan tiedoston rivit
Laaditaan ohjelma, joka tulostaa tiedoston sisältämät rivit numeroituina:
1: This is the first line from the file.
2: This is the second one.
3: This third line is the last.
Kohti ratkaisua: getLines
Voisimme käsitellä tiedoston sisältöä merkki kerrallaan (kuten esimerkeissä 1–3). Tai voisimme muodostaa monirivisen merkkijono, jota sitten jatkokäsittelisimme (esimerkkiä 4 mukaillen). Kuitenkin tekstitiedostoa on luonnollista lukea ja käsitellä rivi kerrallaan, mikä onnistuukin hyvin.
Tehdään ennen varsinaista ratkaisua pieni kokeilu. Koetetaan tulostaa tiedoston kaksi ensimmäistä riviä tähän tapaan:
This is the first line from the file.
This is the second one.
Tällaisen tulosteen saa seuraavalla koodinpätkällä:
val tiedosto = Source.fromFile("Files/example.txt")
val tiedostoRiveittain = tiedosto.getLines
println(tiedostoRiveittain.next())
println(tiedostoRiveittain.next())
getLines
-metodi palauttaa toisen iteraattoriolion, joka
tukeutuu alkuperäiseen "merkki kerrallaan" -iteraattoriolioon
(johon tiedosto
-muuttuja viittaa) ja komentaa sitä. Tällä toisella
iteraattorioliolla sisällön voi käydä läpi riveittäin...
... sillä sen next
-metodi pyytää tiedosto
-iteraattorilta
merkkejä aina rivinvaihtomerkkiin asti. Paluuarvona saadaan
kaikki nämä merkit eli yksi rivi. (Paluuarvo on tyyppiä String
eikä Char
kuten alkuperäisellä iteraattorilla.)
Seuraava next
-käsky palauttaa seuraavan rivin.
Ratkaisu
Tässä kokonainen ohjelma, joka tulostaa kaikki rivit numeroituina, kuten haluttiin:
import scala.io.Source
@main def readingExample5() =
val file = Source.fromFile("Files/example.txt")
try
var lineNumber = 1 // askeltaja
for line <- file.getLines do
println(s"$lineNumber: $line")
lineNumber += 1
finally
file.close()
Silmukalla käydään läpi kukin niistä riveistä, jotka
getLines
ia kutsumalla saadaan.
Tässä toinenkin tapa toteuttaa äskeisen try
-lohkon sisältö. Alla on käytetty
askeltajamuuttujan sijaan zipWithIndex
-metodia (luku 9.2).
for (line, index) <- file.getLines.zipWithIndex do
println(s"${index + 1}: $line")
Paritetaan kukin riveistä (nollasta alkavan) indeksin kanssa.
Käydään parit läpi.
Tiedoston lukeminen, esimerkki 6/7
Tavoite: ensin rivit, sitten niiden pituudet
Otetaan tavoitteeksi tuottaa tiedoston perusteella seuraavanlainen tuloste:
LINES OF TEXT:
1: This is the first line from the file.
2: This is the second one.
3: This third line is the last.
LENGTHS OF EACH LINE:
37 characters
23 characters
28 characters
THE END
Ratkaisuyritys: kaksi kertaa getLines
Tässä eräs ratkaisumalli:
Käydään läpi tiedoston kaikki rivit tulostaen ne numeroituina.
Sitten käydään rivit uudestaan läpi tulostaen kunkin pituus.
Yritetään muuttaa tämä ajatus suoraan Scalaksi. Laaditaan kaksi peräkkäistä silmukkaa.
Kummassakin kutsutaan getLines
-metodia ja käydään rivit läpi yksitellen:
import scala.io.Source
@main def readingExample6() =
val file = Source.fromFile("Files/example.txt")
try
println("LINES OF TEXT:")
var lineNumber = 1 // askeltaja
for line <- file.getLines do
println(s"$lineNumber: $line")
lineNumber += 1
end for
println("LENGTHS OF EACH LINE:")
for line <- file.getLines do
println(s"${line.length} characters")
end for
println("THE END")
finally
file.close()
end readingExample6
Kuitenkaan tämän ohjelman tuloste ei miellytä:
LINES OF TEXT:
1: This is the first line from the file.
2: This is the second one.
3: This third line is the last.
LENGTHS OF EACH LINE:
THE END
Pituudet jäivät kokonaan tulostumatta!
Tiedoston lukeminen, esimerkki 7/7
Tavoite: selvitetään edellisen esimerkin bugi
Edellisen esimerkin toimimattomuus juontuu siitä, että iteraattori osaa käydä käsittelemänsä datan läpi ainoastaan yhden kerran. Iteraattori pitää kirjaa siitä, missä ollaan menossa, mutta ei palaa taaksepäin.
Esimerkiksi fromFile
-metodin palauttama iteraattoriolio (jota myös getLines
in kautta
epäsuorasti käytämme) osaa käydä tiedoston sisällön läpi alusta loppuun vain kertaalleen.
Kun jälkimmäisessä silmukassa kutsutaan getLines
-funktiota, saadaan rivit vain siitä
kohdasta eteenpäin, johon aiemmin jäätiin. Koska ensimmäinen silmukka oli jo ehtinyt
tiedoston loppuun, ei toiselle silmukalle jäänyt käsiteltäviä rivejä lainkaan.
Yksi ratkaisutapa olisi pyytää iteraattori käskyllä Source.fromFile
kahteen kertaan,
kerran kummankin silmukan alussa. Kumpikin silmukka siis lukisi koneelle tallennetun
tiedoston erikseen, toisistaan riippumattomasti. Sellaisen ratkaisun heikkoutena
on, että ohjelmamme lukisi tiedoston kahdesti aivan turhaan. Tiedostojen lukeminen
massamuistista on huomattavan hidasta verrattuna tyypillisten ohjelmointikielen
käskyjen suorittamiseen.
Toinen tapa on ottaa koko tiedoston sisältö talteen alkiokokoelmaan, jonka voi sitten käydä läpi niin monta kertaa kuin on tarvis. Tämän ratkaisumallin heikkoutena on se, että tiedoston koko sisältö on otettava talteen keskusmuistiin.
Alla on esimerkki jälkimmäisestä ratkaisutavasta.
Ratkaisu: toVector
import scala.io.Source
@main def readingExample7() =
val file = Source.fromFile("Files/example.txt")
try
val vectorOfLines = file.getLines.toVector
println("LINES OF TEXT:")
var lineNumber = 1 // askeltaja
for line <- vectorOfLines do
println(s"$lineNumber: $line")
lineNumber += 1
println("LENGTHS OF EACH LINE:")
for line <- vectorOfLines do
println(s"${line.length} characters")
println("THE END")
finally
file.close()
end readingExample7
Rivi-iteraattorilta voi pyytää kaikki rivit vektoriin (vrt.
mkString
esimerkissä 4 yllä). Nyt tiedoston sisältöön pääsee
käsiksi Vector[String]
-tyyppisen muuttujan avulla ja sitä voi
käsitellä ohjelmassa kuin mitä tahansa muutakin vektoria.
Vektorin voi käydä läpi kuinka monta kertaa vain. Tämä toteutus toimii ja tuottaa halutun tulosteen.
Tiedoston kirjoittaminen
Tiedostojen kirjoittaminen onnistuu tarkoitukseen laadittua luokkaa käyttämällä.
Tekstitiedostojen kirjoittamiseen sopii esimerkiksi java.io
-pakkauksen luokka
PrintWriter
.
Alla on pieni esimerkkiohjelma, joka käyttää PrintWriter
-luokkaa tulostaakseen
tekstitiedostoon allekkain kymmenentuhatta satunnaislukua väliltä 0–99.
import java.io.PrintWriter
import scala.util.Random
@main def writingExample() =
val fileName = "Files/random.txt"
val file = PrintWriter(fileName)
try
for n <- 1 to 10000 do
file.println(Random.nextInt(100))
println("Created a file " + fileName + " that contains pseudorandom numbers.")
println("In case the file already existed, its old contents were replaced with new numbers.")
finally
file.close()
PrintWriter
-olion voi luoda näin. Parametriksi annetaan
kirjoitettavan tiedoston nimi. Huomio! Tämän niminen tiedosto
luodaan, jos sitä ei ennestään ollut olemassa. Jos taas oli, niin
tiedoston koko vanha sisältö tuhoutuu, ja ohjelman kirjoittama
uusi sisältö korvaa sen.
println
-metodilla voi kirjoittaa tiedostoon yhden rivin
tekstiä.
Yhteyden sulkemisesta on syytä huolehtia aina myös kirjoittaessa. Itse asiassa se on erityisen tärkeää, kun tiedostoon kirjoitetaan, koska vasta yhteyttä suljettaessa vahvistuu viimeisten merkkien tallennus levylle. (Tehokkuussyistä tiedostonkirjoittamiskäskyt eivät aina toimi välittömästi, vaan keräävät kirjoitettavaksi määrättyä dataa keskusmuistissa olevaan puskuriin. Puskurista viedään kerralla runsaasti dataa tiedostoon, kunhan sitä on ensin puskuriin kertynyt — tai kun yhteys suljetaan.)
Kokeile
Tämäkin ohjelma löytyy Files-moduulin pakkauksesta o1.io
. Kokeile ajaa se.
Voit kokeilla ajaa ohjelman uudestaan. Vanhat luvut korvautuvat uusilla.
Tiedostonkäsittelyharjoitus
Laadi ohjelma, joka pyytää käyttäjältä tiedoston nimen ja lukee nimetystä tiedostosta desimaalilukuja ja laskee niiden perusteella muutaman tilastotiedon: lukujen lukumäärä, keskiarvo, mediaani sekä yleisimmän luvun esiintymien lukumäärä.
Syötetiedostossa on oltava desimaalilukuja (myös kokonaisluvut kyllä kelpaavat) omilla riveillään. Esimerkki:
10.4
5
4.12
10.9
10.9
Jos moduulissa on tiedosto kokeilu.txt
, jossa on nuo viisi riviä,
niin ohjelman tulisi toimia tähän tapaan. Huomaa, että tiedoston
nimi on käyttäjän antama syöte, ei ohjelman tuloste.
Please enter the name of the input file: kokeilu.txt Count: 5 Average: 8.264 Median: 10.4 Highest number of occurrences: 2
Pohja ohjelmalle löytyy Files-moduulin tiedostosta printFileStatistics.scala
.
Kirjoita koodisi sinne.
Käytä try
–finally
-rakennetta ja muista sulkea tiedosto
close
-käskyllä.
Voit käyttää syötteenä writingExample
-ohjelman tuottamaa
satunnaislukutiedostoa. Kuinka asianmukaisilta näennäissatunnaislukujen
tilastotiedot näyttävät?
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Yhteenvetoa
Ohjelmointikielten peruskirjastoista löytyy välineitä syötteen lukemiseen ja tulosteen kirjoittamiseen eli I/O:hon. Eräs usein tarvittu I/O:n muoto on tiedostojen käsittely.
Pakkaus
scala.io
tarjoaa välineitä muun muassa tekstitiedostojen lukemiseen.Tiedostoja käsitellessä on aina syytä huomioida poikkeustilanteiden mahdollisuus.
Lisää I/O:sta, tiedostoista ja poikkeustenkäsittelystä muilla ohjelmointikursseilla ja muista lähteistä.
Lukuun liittyviä termejä sanastosivulla: I/O eli input/output; tiedosto, tekstitiedosto; ajonaikainen virhe, poikkeustenkäsittely; keskusmuisti, massamuisti, kiintolevy.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Lisäkiitokset tähän lukuun
Luvussa tehdään vääryyttä Kate Bushin musiikille. Kiitos ja anteeksi.
fromFile
-metodi ottaa parametriksi tiedoston nimen ja palauttaaSource
-tyyppisen olion, joka pääsee käsiksi tuon tiedoston sisältöön.