Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 4.2: Säiliöitä... ja kaatuva ohjelma
Tästä sivusta:
Pääkysymyksiä: Miten kuvaan suhdetta yhdestä oliosta useaan toiseen olioon? Onko muitakin kokoelmia kuin puskureita? Mitä kaikkea puskurilla tms. kokoelmalla voi tehdä? Miten kuvaan alkiokokoelmaa, jonka sisältöä ei ole tarkoitus muuttaa?
Mitä käsitellään? Puskurit olioina. Uusi kokoelmatyyppi Vector
.
Puskurien ja vektorien metodeita. Kokoelman käyttö osana luokan
toteutusta. Uusia muuttujien rooleja: säiliö ja sopivimman
säilyttäjä. Ajonaikaiset virheilmoitukset.
Mitä tehdään? Luetaan, testaillaan kirjastometodeita REPLissä ja ohjelmoidaan.
Suuntaa antava työläysarvio:? Kolme, neljä tuntia.
Pistearvo: A115.
Oheismoduulit: GoodStuff, Miscellaneous, Football2 (uusi).
Muuta: Yhdessä kohdassa on kaiuttimista tai kuulokkeista hyötyä. Aivan pakolliset ne eivät ole.
Takaisin GoodStuffiin: Category
-luokan käyttö
Tässä luvussa palaamme taas GoodStuff-ohjelman äärelle ja alamme toteuttaa Category
-luokkaa. Tuota toteutusta laatiessa vastaan tulee useita uusia asioita.
Kertaus tarpeen?
Jos luvun 3.3 alun yhteenveto GoodStuff-ohjelman luokista ei ole enää tuoreessa muistissa, niin kertaa se nyt.
Tarkastellaan ensin, miten haluaisimme Category
-luokan toimivan. Se on esitelty alla
animaatiossa. Uusia käsitteitä animaatiossa ei muuten tule, ja voit käydä sen nopeasti
läpi, jos asia tuntuu sinusta selvältä. Varmista kuitenkin, että ymmärrät esimerkin
hyvin, ennen kuin jatkat.
Animaatiossa esiintyy pieni testiohjelma CategoryTest
, joka vain koekäyttää eräitä
Category
-luokan metodeita.
Category
-luokan versioista
Tiedoksi vain: Jos satut omin päin kokeilemaan GoodStuff-moduulista
löytyvää Category
-luokkaa (mikä ei ole tässä välttämätöntä),
niin huomaat siinä pieniä eroja tässä esitettyyn. Se johtuu siitä,
että GoodStuff-moduulissa on se lopullinen versio, johon päädytään
erinäisten vaiheiden jälkeen luvun 4.3 lopussa.
Ensimmäinen luonnos Category
-luokasta
Hahmotellaan luokalle toteutusta vähitellen tarkentaen ja parannellen. Ensimmäinen versio pseudokoodina:
class Category(val name: String, val unit: String): yksityiseen ilmentymämuuttujaan experiences luettelo lisätyistä kokemuksista, aluksi tyhjä yksityiseen ilmentymämuuttujaan fave viittaus arvosanaltaan korkeimpaan kokemukseen def favorite = this.fave def addExperience(newExperience: Experience) = Lisää annettu kokemus siihen luetteloon, johon this.experiences viittaa. Vaihda fave-muuttujan arvoa lisätyn kokemuksen ollessa kaikkein paras. def allExperiences = Palauta luettelo kaikista toistaiseksi lisätyistä kokemuksista. end Category
Jotta päästään pseudokoodista Scala-koodiin, tarvitaan ratkaisut kahteen ongelmaan.
Miten pidetään kirjaa useasta yhteen kategoriaolioon liittyvästä kokemusoliosta?
Miten päivitetään suosikkia vain uuden kokemuksen ollessa parempi?
Aloitamme ratkaisemalla ensimmäisen ongelman. Siis: Miten saamme käytettyä muuttujaa
experiences
"säiliönä", jonka kautta pääsee käsiksi useisiin toisiin olioihin?
Minkä tyyppinen experiences
-ilmentymämuuttujan pitäisi olla ja miten lisätään uusi
kokemus addExperience
-metodissa?
Rooli: Säiliö
Muuttujaa, jonka avulla käsitellään useaa tietoalkiota voi sanoa säiliöksi (container; tämä on yleinen muuttujan rooli).
Yhdellä muuttujalla voi olla vain yksi arvo kerrallaan, joten säiliömuuttujan tulee olla
jotakin sellaista tyyppiä, että sillä voi viitata alkiokokoelmaan. Olet jo kohdannut
yhden tällaisen tyypin, Buffer
in. Tätä puskurityyppiä voi käyttää osana Category
-luokan toteutusta:
class Category(val name: String, val unit: String):
private val experiences = Buffer[Experience]()
// tästä puuttuu vielä fave-ilmentymämuuttuja
def favorite = this.fave
def addExperience(newExperience: Experience) =
this.experiences += newExperience
// tästä puuttuu vielä fave-ilmentymämuuttujan päivittäminen
def allExperiences = this.experiences
end Category
Niin kuin luvussa 1.5 todettiin, tyhjää puskuria luodessa pitää ilmoittaa alkioiden tietotyyppi ns. tyyppiparametrina hakasulkeissa.
Kukin kategoriaolio käyttää experiences
-muuttujaansa säiliönä,
johon lisätään uusi kokemus aina, kun addExperience
-metodia
kutsutaan. (Kertaa puskuriin lisääminen +=
-operaattorilla
tarvittaessa luvusta 1.5.)
Tämä allExperiences
-metodin toteutus yksinkertaisesti palauttaa
viittauksen Category
-olion käyttämään puskuriin, kun pyydetään
luetteloa kaikista aiemmin lisätyistä kokemuksista. Tämä ratkaisu
toimii muttei ole ihan tyydyttävä. Palataan asiaan kohta.
Voit tutustua luokan toimintaan myös seuraavan animaation avulla.
Pikkutehtäviä
Koodinlukutehtävä: lampaiden kloonausta
Olion kloonaamisesta
Ei ole aivan yksinkertaista muodostaa oliosta kopiota, joka olisi alkuperäisestä täysin erillinen mutta alkutilaltaan samankaltainen. Ratkaisu riippuu siitä, millaisesta kopioitavasta oliosta on kyse ja millaista kopiointia täsmälleen tavoitellaan.
Tässä yksinkertainen ratkaisumalli yksinkertaisiin tilanteisiin:
class Juttu(var luku: Int, var toinen: Int):
// Luo toisen samanlaisen mutta erillisen olion ja palauttaa viittauksen siihen.
// Alkuperäisen ja kopion tietoja voi muuttaa toisistaan riippumattomasti.
def kopio = Juttu(this.luku, this.toinen)
end Juttu
Tässä eräitä huomionarvioisia seikkoja:
Äskeinen metodi palauttaa pintakopion (shallow copy) alkuperäisestä: siinä yksinkertaisesti otetaan ilmentymämuuttujien arvot ja tehdään toinen olio, jonka ilmentymämuuttujilla on samat arvot. (Pintakopion palauttaa myös lammasluokan kloonausmetodi ylempänä.)
Mutta mitä jos lukujen sijaan ilmentymämuuttujissa onkin viittauksia muunlaisiin olioihin? Haluttaisiinko niistä luoda kopiot samalla? Entä niistä olioista, joihin nuo toiset oliot edelleen viittavat? Haluttaisiinko syväkopio (deep copy)?
Periytyminen, jota käsittelemme lisää seitsemännellä kierroksella, tuo omat kommervenkkinsä kopioimiseen.
Muuttumattomuus yksinkertaistaa tässäkin asioita. Jos käytämme tilaltaan muuttumattomia olioita, onko kopioimiselle edes tarvetta? Mieti vaikkapa
String
-,Pos
- jaOdds
-luokkia, joiden oliot ovat muuttumattomia.
Heikkous Category
-toteutuksessamme
Seuraava pohdiskelu Category
-luokan julkisesta rajapinnasta ei ole välttämätöntä
luettavaa. Se voi silti herättää ajatuksia ja osin myös perustelee alempaa löytyvän
Vector
-kokoelmatyypin esittelyn. Kiireessä voit siirtyä suoraan sinne.
Puskuri Category
-luokan rajapinnassa
Otetaan lähtökohdaksi, että haluamme ehkäistä luokkiemme virheellistä käyttöä ja mahdollisuuksien mukaan kitkeä luokan julkisesta rajapinnasta toimintoja, jotka eivät ole mielekkäitä tai johtavat virhetilanteisiin (luku 3.2).
Katso vielä uudelleen tätä toteutusta:
class Category(val name: String, val unit: String):
private val experiences = Buffer[Experience]()
// tästä puuttuu vielä fave-ilmentymämuuttuja
def favorite = this.fave
def addExperience(newExperience: Experience) =
this.experiences += newExperience
// tästä puuttuu vielä fave-ilmentymämuuttujan päivittäminen
def allExperiences = this.experiences
end Category
allExperiences
-metodin tehtävänä on palauttaa luettelo
kaikista kategoriaan kuuluvista kokemuksista.
(GoodStuff-ohjelman käyttöliittymä tarvitsee tätä metodia,
jotta se saa kategoriaan kuuluvat kokemukset ruudulle
näkyviin.)
Metodi on nyt toteutettu niin, että se jakaa kutsujalleen
viittauksen yksityisesti käytettyyn puskuriin. Millaisia
virhetilanteita mahdollistuu tämän vuoksi? Mitä ikävää olisi
vaikkapa siinä, että kutsumme jokuKategoria.allExperiences
,
saamme paluuarvona puskuriviittauksen ja lisäämme jotakin
tuohon puskuriin? Vaihtoehtoisesti: Millaisia virhetilanteita
mahdollistuisi, jos experiences
-ilmentymämuuttuja olisikin
julkinen?
Vastaus: jos metodi on toteutettu noin, voi kutsuva koodi pyytää viittauksen kategorian
sisäisesti käyttämään puskuriin ja lisätä tai poistaa sieltä alkioita (kokemuksia)
kutsumatta kategorian addExperience
-metodia ja päivittämättä kategorian fave
-muuttujaa:
val wineCategory = Category("Wine", "bottle")wineCategory: Category = o1.goodstuff.Category@31ab4859 wineCategory.addExperience(Experience("Il Barco 2001", "ookoo", 6.69, 6))wineCategory.favorite.nameres0: String = Il Barco 2001 val bufferUsedByCategory = wineCategory.allExperiencesbufferUsedByCategory: Buffer[Experience] = ArrayBuffer(o1.goodstuff.Experience@2a863ca0) val superkokemus = Experience("Supermahtava", "woohoo", 10, 10)superkokemus: Experience = o1.goodstuff.Experience@4985e5b6 bufferUsedByCategory += superkokemuswineCategory.favorite.nameres1: String = Il Barco 2001 wineCategory.allExperiencesres2: Buffer[Experience] = ArrayBuffer(o1.goodstuff.Experience@2a863ca0, o1.goodstuff.Experience@4985e5b6)
Esimerkin lopuksi suosikki on sama aluksi lisätty kokemus...
... vaikka kategoriaan tuli lisättyä toinenkin kokemus, joka on arvosanaltaan ensimmäistä parempi.
Ongelma on siis se, että puskuria voi muokata. Palauttamalla luokan käyttäjälle
viittauksen tällaiseen muuttuvatilaiseen alkiokokoelmaan tarjoamme hänelle
mahdollisuuden tehdä tyhmyyksiä ja saatamme houkutellakin sellaisiin: Buffer
-tyyppinen paluuarvo ikään kuin antaa ymmärtää, että palautettua kokoelmaa olisi sopivaa
tai järkevää muokata, vaikka todellisuudessa olemme suunnitelleet, että kategoriaan
kuuluvien kokemusten luettelon päivittäminenhän on yksin Category
-olion vastuulla.
Olisi parempi, jos voisimme palauttaa allExperiences
-metodin kutsujalle kokoelman,
jonka sisältö on juuri se kategorian senhetkinen kokemusluettelo ja sillä hyvä. Tuota
kokoelmaa ei voisi muokata ja jo sen tietotyyppi tekisi tämän kutsujalle selväksi.
Asia on helppo korjata. Lisäksi sen korjatessa saamme syyn tutustua uuteen
kokoelmatyyppiin Vector
, joka tulee osoittautumaan muutoinkin hyödylliseksi
ja esiintyy kurssilla jatkossa toistuvasti.
Uusi kokoelmatyyppi: vektori
"Vektori"-sanalla on erilaisia enemmän ja vähemmän toisiinsa liittyviä merkityksiä mm. matematiikassa ja ohjelmoinnissa. Ohjelmoinnin piirissäkin riippuu yhteydestä, mitä tällä sanalla tarkoitetaan.
Scalassa vektori (vector) on alkiokokoelmatyyppi samassa mielessä kuin puskurikin.
Vector
-luokka
Luokka Vector
on käytössä kaikissa Scala-ohjelmissa ilman erillistä import
-käskyä
samoin kuin vaikkapa String
ja Int
ovat. Kukin vektori on alkiokokoelma, joka
sisältää keskenään samantyyppisiä alkioita. Vektorin luominen käy kuin puskurinkin:
val lukuvektori = Vector(4, 10, -100, 10, 15)lukuvektori: Vector[Int] = Vector(4, 10, -100, 10, 15) val sanavektori = Vector("eka", "toka", "kolmas", "neljäs")sanavektori: Vector[String] = Vector(eka, toka, kolmas, neljäs)
Kuten puskurinkin tapauksessa, myös vektorin alkioilla on nollasta alkavat indeksit, joiden perusteella voi poimia yksittäisen alkion:
lukuvektori(3)res3: Int = 10 sanavektori(2)res4: String = kolmas
Miten vektori sitten eroaa puskurista?
Keskeisin ero luokkien Buffer
ja Vector
toiminnallisuudessa on se, että siinä missä
puskuria voi muuttaa sen luomisen jälkeen, vektori on luomisen jälkeen täysin muuttumaton.
Siihen ei voi lisätä alkioita, eikä alkioita voi poistaa tai vaihtaa toisiin:
lukuvektori += 999-- Not Found Error: |lukuvektori += 999 |^^^^^^^^^^^^^^ |value += is not a member of Vector[Int] lukuvektori(2) = 999-- Not Found Error: |lukuvektori(2) = 999 |^^^^^^^^^^^ |value += is not a member of Vector[Int]
Vektorit — ja puskurit — olioina
Vektorin alkioiden lukumäärän saa selville size
-metodilla:
sanavektori.sizeres5: Int = 4 lukuvektori.sizeres6: Int = 5
Ja siinä tulikin paljastettua jotakin: Scalan vektoreilla on metodeita eli ne ovat olioita, ja niin ovat muuten puskuritkin.
Vektorin koko on aina sama, mutta puskuriolion size
-metodi voi palauttaa eri
kutsukerroilla eri luvun:
val sanapuskuri = Buffer[String]("eka")sanapuskuri: Buffer[String] = ArrayBuffer(eka) sanapuskuri.sizeres7: Int = 1 sanapuskuri += "toka"sanapuskuri += "kolmas"sanapuskuri.sizeres8: Int = 3
Muunnokset kokoelmatyyppien välillä: to
, toVector
, toBuffer
Ei ole harvinaista, että ohjelmassa halutaan viedä yhden alkiokokoelman sisältö toiseen, yleensä niin, että kohdekokoelma on eri tyyppiä kuin alkuperäinen. Tämä on helppoa, koska siihen on nimenomaisia metodeita. Tässä niistä ensin yleiskäyttöisin:
val ekaVektori = Vector(10, 50, 5)ekaVektori: Vector[Int] = Vector(10, 50, 5) val sisaltoKopioitunaPuskuriin = ekaVektori.to(Buffer)sisaltoKopioitunaPuskuriin: Buffer[Int] = ArrayBuffer(10, 50, 5)
to
-metodi siis loi uuden puskurin, kopioi vektorin sisällön sinne
ja palautti viittauksen luomaansa puskuriin. Tuota puskuria voi muokata
tutusti:
sisaltoKopioitunaPuskuriin += 100res9: Buffer[String] = ArrayBuffer(10, 50, 5, 100)
Puskurin sisällön kopioiminen uuteen vektoriin on yhtä helppoa:
val tokaVektori = sisaltoKopioitunaPuskuriin.to(Vector)tokaVektori: Vector[Int] = Vector(10, 50, 5, 100)
to
-metodia kutsuessa annetaan parametri, joka määrää halutun kokoelman tyypin,
mistä Buffer
ja Vector
yllä olivat esimerkkejä. Muutamalle usein
käytetylle kokoelmatyypille on lisäksi erilliset metodinsa, kuten toBuffer
ja toVector
. Seuraavat koodinpätkät tekevät keskenään ihan saman:
val puskuriinKopioitu = jokuKokoelma.to(Buffer)
val vektoriinKopioitu = jokuKokoelma.to(Vector)
val puskuriinKopioitu = jokuKokoelma.toBuffer
val vektoriinKopioitu = jokuKokoelma.toVector
Näin voit siis jättää yhdet sulkeet pois koodista, jos haluat.
toVector
avuksi Category
-luokassa
Tilkitään Category
-luokan vuotava rajapinta:
class Category(val name: String, val unit: String):
private val experiences = Buffer[Experience]()
// tästä puuttuu vielä fave-ilmentymämuuttuja
def favorite = this.fave
def addExperience(newExperience: Experience) =
this.experiences += newExperience
// tästä puuttuu vielä fave-ilmentymämuuttujan päivittäminen
def allExperiences = this.experiences.toVector
end Category
allExperiences
-metodi luo vektorin, joka sisältää
kaikki ne kokemukset, jotka juuri kutsuhetkellä kuuluvat
kyseiseen kategoriaan. Paluuarvon tyypiksi on valittu
Vector[Experience]
eikä Buffer[Experience]
, koska tämän
paluuarvon muuttaminen ei ole sellainen operaatio, jota
metodia kutsuneen tahon on mielekästä tehdä.
Silti: miksei aina Buffer
?
Ihmettelet ehkä edelleen, onko erillinen vektorityyppi vaivan arvoinen, kun sillä kerran voi tehdä vähemmän asioita kuin puskurilla.
Aina ei ole hyvä voida tehdä kokoelmalla mitä tahansa. Muuttumaton alkiokokoelma takaa,
että missään kohdassa ohjelmaa kyseisen kokoelman sisältö ei muutu. Tämä muun muassa
auttaa ihmislukijaa päättelemään, miten ohjelma toimii ja missä virheet sijaitsevat.
Vektorilla voi myös selkiyttää luokan rajapintaa ja osin ehkäistä sen virhekäyttöä,
mistä Category
-luokkamme toimi esimerkkinä.
Muuttumaton data on funktionaalisen ohjelmointiparadigman kulmakivi ja monen laadukkaan ohjelman taustalla vaikuttava periaate. Siitä lisää luvussa 11.2 ja jatkokursseilla.
Vektorien ja puskurien välillä on myös suoritustehokkuuseroja, jotka riippuvat tapauksesta ja joita emme käsittele tällä peruskurssilla.
Muuttumattoman ja muuttuvan kokoelman ero on samantapainen kuin val
- ja var
-muuttujan.
Luvussa 1.4 annettiin tämä nyrkkisääntö:
Tee jokaisesta muuttujasta
val
, ellei sinulla ole juuri nyt selvää syytä tehdä siitävar
.
Samaan tapaan voimme nyt todeta:
Tee jokaisesta kokoelmasta muuttumaton (kuten
Vector
), ellei sinulla ole juuri nyt selvää syytä tehdä siitä muuttuvatilaista (kutenBuffer
).
Vektorien ja puskurien metodeita
Ennen luvun päättävää isompaa ohjelmointitehtävää tutkikaamme, millaisia metodeita kokoelmilta löytyy, kun ne kerran olioita ovat. Näiden metodien joukosta löytyy työkaluja myös siihen ohjelmointitehtävään.
Kaikkia seuraavia metodeita ei tarvitse heti opetella ulkoa, mutta voit pistää mieleen, että niitä on esitelty tässä luvussa (ja myös Scalaa kootusti -sivulla). Muitakin kokoelmien metodeita tulee vastaan kurssin mittaan.
Seuraavissa esimerkeissä käytämme enimmäkseen vektoreita, mutta esitellyt vektorien metodit löytyvät myös puskureilta (ja muiltakin kokoelmatyypeiltä).
Luodaan aluksi testivektori, jossa on vaikkapa merkkijonoja:
val vektori = Vector("eka", "toka", "kolmas", "neljäs")vektori: Vector[String] = Vector(eka, toka, kolmas, neljäs)
Kokoelman kuvaileminen: mkString
Ohjelman tuottamia tekstejä muotoillessa on usein iloa mkString
-metodista (sanoista make
string), jota voi käyttää näin:
vektori.mkString(";")res10: String = eka;toka;kolmas;neljäs
Metodi siis muodostaa ja palauttaa merkkijonon, joka sisältää kuvauksen kustakin alkiosta. Erottimena käytetään parametriksi annettua merkkijonoa, yllä puolipistettä.
Tässä toinen esimerkki, jossa erottimena on käytetty rivinvaihtoa (joka merkitään \n
):
val lukuja = Vector(10, 50, 30, 100, 0)lukuja: Vector[Int] = Vector(10, 50, 30, 100, 0) println(lukuja.mkString("\n"))10 50 30 100 0
mkString
-tehtävä
Ensimmäiseltä kierrokselta tuttu soittamisfunktio osaa soittaa useaa melodiaa yhtäaikaa.
Yhtäaikaisten melodioiden väliin tulee &
-merkki:
play("cccedddfeeddc---&cdefg-g-gfedc-c-")
Tässä merkkijonossa on yksi &
-merkki. play
-funktio soittaa
sen vasemmanpuolisen melodian samaan aikaan kuin oikeanpuoleisen.
Tällä tavoin voi myös esimerkiksi koota bändin, joka soittaa useita instrumentteja.
Laadi funktio together
, jonka avulla voi muodostaa moniäänisiä kappaleita:
val ukkoNooa = "cccedddfeeddc---"ukkoNooa: String = cccedddfeeddc--- val basso = "[33]<<c-c-d-d-e-d-cdc"basso: String = [33]<<c-c-d-d-e-d-cdc val esitys = together(Vector(ukkoNooa, basso), 150)esitys: String = cccedddfeeddc---&[33]<<c-c-d-d-e-d-cdc/150 play(esitys)
Funktiolle annetaan ensimmäiseksi parametriksi ääniä kuvaavat merkkijonot vektorissa.
Toiseksi parametriksi annetaan koko kappaleen tempo kokonaislukuna.
Funktio palauttaa moniäänistä kappaletta edustavan merkkijonon,
jossa välimerkit ovat paikoillaan play
-funktiolle sopivalla
tavalla.
Tässä toinen käyttöesimerkki, jossa syöte on mutkikkaampi ja kauniimpi mutta periaate ihan sama:
val aanet = Vector( "FGHbG(GHb>D<)--.(GHb>D<)--.(FAC)---- FGHbG(FAC)--.(FAC)--.(DGHb)--AG-FGHbG(FGHb)--->C-<(AF)--GF---F-(DF>C<)---.(DGHb)------", "<< (<eb>eb)--.(<eb>eb)--.(<f>f)---- (<d>d)--.(<d>d)--.(<g>g)---- (<c>c)-----(<f>f)-----f---(da)---.(<g>g)------", "P:<< " + "c cc" * 14)aanet: Vector[String] = Vector(FGHbG(GHb>D<)--.(GHb>D<)--.(FAC)---- FGHbG(FAC)--.(FAC)--.(DGHb)--AG-FGHbG(FGHb)--->C-<(AF)-- GF---F-(DF>C<)---.(DGHb)------, << (<eb>eb)--.(<eb>eb)--.(<f>f)---- (<d>d)--.(<d>d)--.(<g>g)---- (<c>c)-----(<f>f)---- -f---(da)---.(<g>g)------, P:<< c ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc cc) play(together(aanet, 20))
Kirjoita funktio misc.scala
-tiedostoon Miscellaneous-moduulissa.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Kokoelman sisällön tutkiminen: indexOf
, contains
ja isEmpty
Alkioiden poimiminen: apply
; head
ja tail
; take
ja drop
Vektoria "muuttavista" metodeista
On helppo kuvailla metodia kuten tail
sanomalla, että "se ottaa vektorista kaikki
alkiot paitsi ensimmäisen", mikä kuulostaa siltä että vektorin sisältö muuttuisi.
Moni muukin metodi tässä mielessä näennäisesti — mutta vain näennäisesti — muuttaa
vektorin sisältöä. Vaikkapa reverse
"kääntää vektorin sisällön":
val lukuvektori = Vector(4, 10, -100, 10, 15)lukuvektori: Vector[Int] = Vector(4, 10, -100, 10, 15) lukuvektori.reverseres13: Vector[Int] = Vector(15, 10, -100, 10, 4)
Kuitenkin kyseessä on kokonaan uusi kokoelma, jossa samat alkiot ovat eri järjestyksessä. Alkuperäinen on ennallaan.
lukuvektorires14: Vector[Int] = Vector(4, 10, -100, 10, 15)
Mikään esitellyistä — tai muistakaan — vektorien metodeista ei ole vaikutuksellinen eikä siis muuta alkuperäistä vektoria. Vektorin käyttäjä voi luottaa siihen, että kerran tietynlaiseksi luotu vektori pysyy aina sellaisena kuin se on.
Puskuria muuttavista metodeista
Puskureilla on samat yllä esitellyt vaikutuksettomat metodit kuin vektoreillakin. Lisäksi niille on määritelty joukko vaikutuksellisia metodeita, joilla puskurin sisältöä voi muokata. Tässä eräitä:
Kokoelmien yhdistäminen: ++
, +:
ja diff
Alla on näytetty yksinkertainen tapa yhdistää kaksi kokoelmaa. Tarkemmin sanoen näin muodostetaan uusi kokoelma, jossa on samat alkiot kuin kahdessa olemassa olevassa kokoelmassa:
val ekat = Vector("eka", "toka")ekat: Vector[String] = Vector(eka, toka) val tokat = Vector("a", "b", "c")tokat: Vector[String] = Vector(a, b, c) val yhdistelma = ekat ++ tokatyhdistelma: Vector[String] = Vector(eka, toka, a, b, c) val toisinPain = tokat ++ ekattoisinPain: Vector[String] = Vector(a, b, c, eka, toka)
Operaattorissa on kaksi plusmerkkiä.
Tämäkin toimii myös puskureille.
Operaattori ++
siis yhdistää kaksi kokoelmaa. Jos haluat yhdistää vain yhden alkion
kokoelman alkuun, niin operaattori +:
palvelee:
val ekat = Vector("eka", "toka")ekat: Vector[String] = Vector(eka, toka) val isompi = "oikeesti eka" +: ekatisompi: Vector[String] = Vector(oikeesti eka, eka, toka)
diff
puolestaan tuottaa kokoelman, jossa yhden kokoelman alkiot on "vähennetty"
toisesta:
Vector("a", "b", "c", "d").diff(Vector("a", "c"))res15: Vector[String] = Vector(b, d)
Football2: uusi otteluluokka
Johdanto
Muutoksen maailmassa yksi asia on muuttumaton: taukoamaton muuttuminen.
—väitetty Herakleitoksen sanomaksi; alkuperä tuntematon
Successful software always gets changed.
Ohjelmankehitystyö on usein iteratiivista: laaditaan ensin yksinkertainen versio tai prototyyppi, kokeillaan ja arvioidaan sitä ja jatketaan sitten kehitystyötä eteenpäin.
On myös tyypillistä, että ohjelmalle asetetut vaatimukset muuttuvat ohjelman kehitystyön aikanakin ja uudet vaatimukset on huomioitava seuraavaa versiota kehitettäessä. Niin on jo käynyt esimerkiksi FlappyBug-ohjelmalle, ja nyt niin käy luvussa 3.5 pohjustetulle jalkapallotilastoja käsittelevälle ohjelmalle. (Eikä viimeistä kertaa.)
Tehtävänanto
Nouda moduuli Football2 ja tutustu sen Scaladoc-dokumentaatioon. Tässä ensimmäisessä
vaiheessa toteutetaan luokka Match
. Luokat Club
ja Player
on annettu valmiina, eikä
niihin tarvitse tehdä muutoksia. Dokumentaatiossa mainittu luokka Season
toteutetaan
myöhemmässä tehtävässä (luku 4.4).
Football2-spesifikaatio eroaa aiemmassa Football1-moduulin yksinkertaisemmasta
spesifikaatiosta. Uuden Match
-luokan tulee pitää kirjaa ei ainoastaan maaleista vaan
myös siitä, ketkä nuo maalit ovat tehneet. Vanhat addHomeGoal
- ja addAwayGoal
-metodit
on korvattu yhdellä addGoal
-metodilla, jonka olisi tarkoitus päätellä maalintekijän
mukaan, kummalle joukkueelle maali kirjataan. Lisäksi toivelistalla on uusia metodeita
muun muassa ottelun voittomaalin tekijän määrittämiseen. Tämä kaikki edellyttää melko
suurta remonttia luokkaan Match
.
Tiedostosta Match.scala
löytyy vaillinainen versio luokasta Match
. Siinä on ihan hyvä
alku uudelle toteutukselle: ilmentymämuuttujissa on kaksi puskuria, joihin voi tallentaa
maalien tekijöitä. Uusi tapa lisätä maaleja on luonnosteltu metodiin addGoal
. Myös uusi
winnerName
-metodi on valmiiksi tehty, ja se tuottaa virheilmoituksia vain, koska luokka
on muilta osin puutteellinen. Tehtäväsi on täydentää luokka Match
toimivaksi viimeistelemällä
addGoal
-metodi ja lisäämällä puuttuvat metodit.
Ohjeita ja vinkkejä
Pakkauksessa
o1.football2.gui
on pieni sovellusohjelmaFootballApp
, joka tarvitsee toimiakseen spesifikaatiota vastaavanMatch
-luokan. Kunhan saat luokkasi käyttökuntoon, voit kokeilla sen metodeitaFootballApp
-sovelluksen kautta käynnistämällä ohjelman ja painamalla maalintekijänappuloita tarjotussa ikkunassa:Käytä ilmentymämuuttujissa puskureita ja
Player
-olioita, kuten annettu koodi jo ehdottaakin.Poimi kokoelmankäsittelykonsteja tästä luvusta.
Älä tee muutoksia valmiisiin luokkiin
Player
jaClub
.Osa puuttuvista metodeista tehtiin jo luvussa 3.5. Voit poimia ne osat silloisesta ratkaisustasi Football2-moduuliin. Jos et tehnyt jo tuota aiempaa tehtävää, niin aloita siitä. (Voit myös katsoa sen esimerkkiratkaisun.)
FootballApp
-sovellus vain lisäilee maaleja ja tutkii voittomaalin tekijöitä. Voit myös käyttääMatch
-luokkaasi erikseen REPLissä tai laatia oman pienen testiohjelman.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Suosikkikirjanpito (ensimmäinen yritys)
Category
-luokkamme jäi kesken: miten kategoriaolion pitäisi huolehtia siitä, että se
pystyy kertomaan, mikä aiemmin lisätyistä kokemuksista on käyttäjän suosikki?
Tässä kaksi erilaista ratkaisutapaa:
Kirjataan kategoriaolioon tieto siitä, mikä on paras kategoriaan lisätty kokemus. Päivitetään tätä tietoa aina, kun kategoriaan lisätään aiempaa suosikkia parempi kokemus.
Pidetään kategoriaoliossa tallessa ainoastaan luettelo kaikista kokemuksista. Kun oliolta kysytään suosikkikokemusta, se käy koko luettelon läpi ja palauttaa siellä olleista kokemuksista arvosanaltaan parhaan.
Valitsemme nyt ensimmäisen tavan, jolle meillä onkin jo alku. Yllä suunnittelimme
käyttävämme fave
-nimistä muuttujaa niin, että se viittaa aina juuri siihen lisätyistä
kokemuksista, joka on arvosanaltaan paras.
Millainen fave
-ilmentymämuuttujan pitäisi olla ja miten sitä käsittelisimme
addExperience
-metodissa?
Hahmotelma pseudokoodina
class Category(val name: String, val unit: String): private val experiences = Buffer[Experience]() private var fave = aluksi ei vielä ole suosikkia def favorite = this.fave def addExperience(newExperience: Experience) = this.experiences += newExperience Aseta this.fave-muuttujan arvoksi parempi näistä: newExperience tai entinen this.fave. def allExperiences = this.experiences.toVector end Category
Kategoriassa, jossa ei ole vielä kokemuksia ei ole suosikkiakaan. Tarvitsemme jonkin tavan ilmoittaa, ettei muuttujalla ole aluksi arvoa tai että sen arvo tarkoittaa "ei suosikkia". Toistaiseksi tähän ei ole opittu hyviä välineitä.
Käytämme fave
-muuttujaa vähän kuin tuoreimman säilyttäjänä
mutta nirsosti: vain parempi kelpaa uudeksi arvoksi.
null
-arvo
Ehkä fave
-muuttujan voisi alustaa näin?
class Category(val name: String, val unit: String):
private val experiences = Buffer[Experience]()
private var fave: Experience = null
def favorite = this.fave
Scalan sana null
tarkoittaa "arvoa, joka ei ole mikään
olio lainkaan" tai "viittausta ei-mihinkään". null
-arvon
voi sijoittaa muuttujaan tähän tapaan.
null
-arvon voi sijoittaa monentyyppisiin muuttujiin,
ja pelkästä sijoituksesta ei nyt tule ilmi, että haluamme
fave
-muuttujan tyypiksi Experience
. Niinpä tässä
tilanteessa täytyy tämän muuttujan tyyppi kirjata erikseen
koodiin kaksoispisteen perään samaan tapaan kuin
parametrimuuttujille aina tehdään. (Tyypinhän saa
kirjoittaa muuttujan määrittelyyn vaikka aina; ks. luvut 1.8
ja 3.5.)
Ennakkovaroitus
null
-arvo on ongelmallinen, ja sen käyttöä kannattaa
yleensä välttää. Tähän palataan pian. Jatketaan nyt
addExperience
-metodia toteuttamalla.
Rooli: sopivimman säilyttäjä
Sopivimman säilyttäjä (most-wanted holder) on muuttuja, joka pitää kirjaa
toistaiseksi "parhaasta" löydetystä arvosta. Valintaperusteena voidaan käyttää mitä
erilaisimpia kriteereitä: joskus pidetään kirjaa suurimmasta tai pienimmästä
luvusta, joskus pisimmästä merkkijonosta, parhaasta urheilutuloksesta, nuorimmasta
opiskelijaoliosta. Ohjelmassamme fave
-ilmentymämuuttuja on sopivimman säilyttäjä,
jonka sopivimmuuskriteerinä on kokemukselle annettu arvosana.
Kun muokkaamme sopivimman säilyttäjän arvoa, tarkastelemme, onko uusi kandidaattiarvo
sopivampi kuin muuttujan vanha arvo. Tämän voi tehdä esimerkiksi if
-käskyllä, jossa
vertaillaan arvoja keskenään, tai tarkoitukseen sopivalla metodilla.
Tässä tapauksessa meillä on juuri tarkoitukseemme sopiva metodi tarjolla: luvussa 3.4
toteuttamamme chooseBetter
:
class Category(val name: String, val unit: String):
private val experiences = Buffer[Experience]()
private var fave: Experience = null
def favorite = this.fave
def addExperience(newExperience: Experience) =
this.experiences += newExperience
this.fave = newExperience.chooseBetter(this.fave)
def allExperiences = this.experiences.toVector
end Category
chooseBetter
palauttaa kahdesta vertailemastaan
kokemuksesta paremman.
Tässä toteutuksessa perusidea on täysin kunnossa. Se toimii juuri niin kuin aiemmissa
luvuissa on periaatteellisella tasolla nähty Category
-luokan toimivan: kategoriaolio
kysyy kokemusoliolta paremmuudesta ja käyttää vastausta päivittääkseen suosikkia.
Kuitenkin luokassa on eräs vakava puute. Tämä käy ilmi, kun sitä yrittää käyttää:
Ajonaikainen poikkeustilanne
Otetaan äskeinen versio Category
-luokasta ja luvun alun käynnistysolio CategoryTest
.
Ajetaan CategoryTest
. Ohjelma käynnistyy, mutta tekstikonsoliin ilmestyy heti
virheilmoitus:
Exception in thread "main" java.lang.ExceptionInInitializerError
at o1.goodstuff.gui.CategoryTest.main(CategoryTest.scala)
Caused by: java.lang.NullPointerException
at o1.goodstuff.Experience.isBetterThan(Experience.scala:22)
at o1.goodstuff.Experience.chooseBetter(Experience.scala:28)
at o1.goodstuff.Category.addExperience(Category.scala:31)
at o1.goodstuff.gui.CategoryTest$.<clinit>(CategoryTest.scala:7)
Kyseessä on poikkeus eli ajonaikainen virhe. Se ilmenee vasta, kun ohjelmaa käytetään (luku 1.8). Virheilmoituksen rivit luettelevat kutsupinon sillä hetkellä sisältämät kehykset, kun ongelmatilanne syntyi. Tällaista tulostetta kutsutaan stack traceksi. Se auttaa paikantamaan ongelman. Tarkastellaan virheilmoitusta:
Näemme, että CategoryTest-ohjelmaa ajaessa on syntynyt poikkeustilanne.
Poikkeustilanteen syyksi mainitaan NullPointerException
.
Tämä poikkeustyypin nimi mikä jo itsessään kertoo vähän siitä,
millaisesta ongelmasta on kyse. Lisää siitä alempana.
Ongelma syntyi kokemusluokan rivillä 22 isBetterThan
-metodissa,
jota oli kutsuttu saman luokan chooseBetter
-metodista riviltä 28,
jota oli kutsuttu kategorialuokan riviltä 31 metodista addExperience
,
jota oli kutsuttu CategoryTest.scala
-tiedostosta.
NullPointerException
-virhetilanteet
NullPointerException
-tyyppinen virhetilanne kertoo aina yrityksestä käyttää null
-arvon
(eikä olemassa olevan olion) ominaisuutta: yritetään kutsua olemattoman olion metodia tai
käyttää olemattoman olion ilmentymämuuttujaa. Tällainen toimenpide ei ole mielekäs, eikä
sitä pidä tietokoneohjelmassa tehdä (vrt. nollalla jakaminen aritmetiikassa).
NullPointerException
kertoo yleensä ohjelmoijan tekemästä virheestä. Niin nytkin.
Tarkastele virhetilanteen syntymistä seuraavan animaation kautta. Huomaa varsinkin
null
-arvo muuttujassa ja se, mitä sillä tehdään.
Miten tästä selvitään?
Yhteenvetoa
Voimme kuvata yhteyttä yhdestä oliosta useaan toiseen olioon käyttämällä ilmentymämuuttujaa, joka viittaa kokoelmaan olioita.
Alkiokokoelmia on erilaisia, esimerkiksi vektoreita ja puskureita.
Ne ovat Scalassa olioita ja tarjoavat monia metodeita.
Vektori on tilaltaan muuttumaton, puskuri muuttuvatilainen.
Muuttujaa voi käyttää sopivimman säilyttäjänä eli pitämään kirjaa siitä arvosta, joka toistaiseksi parhaiten täyttää tietyn kriteerin (esim. pienin luku, paras kokemus).
Tällaisen muuttujan alkuarvo ansaitsee erityishuomion.
Ajonaikaisten poikkeustilanteiden setvimisessä voivat kutsupinon tilasta kertovat tulosteet auttaa.
On olemassa
null
-niminen arvo, joka tarkoittaa "viittaus ei-mihinkään". Sen käyttö ei ole suositeltavaa muun muassa sen virhealttiuden vuoksi. Parempia välineitä on luvassa.Lukuun liittyviä termejä sanastosivulla: kokoelma, puskuri, vektori; säiliö; muuttumaton, muuttuvatilainen; sopivimman säilyttäjä;
null
-viittaus; ajonaikainen virhe, stack trace.
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, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, 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 ja Juha Sorva. 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. Pääkehittäjänä on nyt Markku Riekkinen, jonka lisäksi A+:aa 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
Ahmed Ahne on René Goscinnyn and Jean Tabaryn luomus.
Luvussa tehdään vääryyttä Carl Bellmanin ja jonkun muun musiikille. Kiitos ja anteeksi.
Luodaan kullekin kategoriaoliolle avuksi puskuri. Kunkin kategoriaolion muuttuja
experiences
siis sisältää viittauksen tällaiseen puskuriin, ja kullakin kategoriaoliolla on oma puskurinsa, aluksi tyhjä. (Muista: tämä koodirivi suoritetaan aina silloin, kun luodaan uutta kategoriaoliota.)