Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 4.1: 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 tuntia.
Pistearvo: A110.
Oheisprojektit: 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-projektin äärelle ja alamme toteuttaa
Category
-luokkaa. Tuota toteutusta laatiessa vastaan tulee useita uusia asioita.
Kertaus tarpeen?
Jos luvun 3.2 alussa oleva yhteenveto GoodStuff-projektin luokista ei ole enää tuoreessa muistissa, niin kertaa se nyt.
Tarkastellaan ensin, miten haluaisimme Category
-luokan toimivan. Se on esitelty alla
olevassa 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-projektista
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-projektissa on se lopullinen versio, johon päädytään
erinäisten vaiheiden jälkeen luvun 4.2 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. }
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, jota avulla käsitellään useaa tietoalkiota voi sanoa säiliöksi (container; tämä on yksi muuttujan rooli; vrt. luku 2.6).
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
}
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.)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 alla olevan animaation avulla.
Pikkutehtäviä
Koodinlukutehtävä: lampaiden kloonausta
Heikkous Category
-toteutuksessamme
Seuraava pohdiskelu Category
-luokan julkisesta rajapinnasta ei ole välttämätöntä
luettavaa. Kuitenkin paitsi että se voi itsessään herättää ajatuksia, se myös osin
perustelee alempana olevan 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.1).
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
}
allExperiences
-metodin tehtävänä on palauttaa luettelo
kaikista kategoriaan kuuluvista kokemuksista.
(GoodStuff-projektin käyttöliittymä tarvitsee tätä metodia,
jotta se saa kategoriaan kuuluvat kokemukset ruudulle
näkyviin.)jokuKategoria.allExperiences
,
saamme palautusarvona 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 = new Category("Wine", "bottle")wineCategory.addExperience(new 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 = new 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)
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 palautusarvo 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 samaan tapaan kuin
puskurinkin (ilman new
-sanaa):
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<console>:9: error: value += is not a member of scala.collection.immutable.Vector[Int] lukuvektori += 999 ^ lukuvektori(2) = 999<console>:9: error: value update is not a member of scala.collection.immutable.Vector[Int] lukuvektori(2) = 999 ^
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ä: 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 nimenomaiset metodit:
val ekaVektori = Vector(10, 50, 5)ekaVektori: Vector[Int] = Vector(10, 50, 5) val sisaltoKopioitunaPuskuriin = ekaVektori.toBuffersisaltoKopioitunaPuskuriin: Buffer[Int] = ArrayBuffer(10, 50, 5)
toBuffer
-metodi siis loi uuden puskurin, kopioi vektorin sisällön sinne ja palautti
viittauksen luomaansa puskuriin. 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.toVectortokaVektori: Vector[Int] = Vector(10, 50, 5, 100)
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
}
allExperiences
-metodi luo vektorin, joka sisältää
kaikki ne kokemukset, jotka juuri kutsuhetkellä kuuluvat
kyseiseen kategoriaan. Palautusarvon tyypiksi on valittu
Vector[Experience]
eikä Buffer[Experience]
, koska tämän
palautusarvon 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. Muuttumattoman alkiokokoelman
käyttö 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. Siitä lisää luvussa 10.2 ja jatkokursseilla.
Vektorien ja puskurien välillä on myös suoritustehokkuuseroja, jotka riippuvat käyttökontekstista 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 muuttujastaval
, ellei sinulla ole juuri nyt selvää syytä tehdä siitävar
.
Samaan tapaan voimme nyt todeta:
Tee jokaisesta kokoelmasta muuttumaton (kutenVector
), 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, 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 laitetaan &
-merkki:
o1.play("cccedddfeeddc---&cdefg-g-gfedc-c-")
&
-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:
import o1.misc._import o1.misc._ 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 o1.play(esitys)
o1.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 ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc ccc cc" )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) o1.play(together(aanet, 20))
Kirjoita funktio misc.scala
-tiedostoon Miscellaneous-projektissa.
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)
Tämäkin toimii myös puskureille.
Operaattori ++
siis yhdistää kaksi kokoelmaa. Jos haluat yhdistää vain yhden alkion
puskurin 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 nyt luvussa 3.4 pohjustetulle jalkapallotilastoja käsittelevälle ohjelmalle. (Eikä viimeistä kertaa.)
Tehtävänanto
Nouda projekti 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.3).
Football2-spesifikaatio eroaa aiemmassa Football1-projektissa olleesta 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 winner
-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.4. Voit poimia ne osat silloisesta ratkaisustasi Football2-projektiin.
- 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.
Palauttaminen
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 siten, 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 }
fave
-muuttujaa vähän kuin tuoreimman
säilyttäjänä (luku 2.6) 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
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 olevan tyyppiä 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.4.)Ennakkovaroitus
null
-arvo on ongelmallinen, ja sen käyttämistä kannattaa
yleensä välttää. Tähän palataan pian. Jatketaan nyt kuitenkin
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.3
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
}
chooseBetter
palauttaa kahdesta vertailemastaan kokemuksesta
paremman. Käytetään sitä.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 yllä oleva versio Category
-luokasta ja edellisen luvun alussa käytetty käynnistysolio
CategoryTest
. Ajetaan CategoryTest
. Tekstikonsoliin ilmestyy välittömästi virheilmoitus:
Exception in thread "main" java.lang.NullPointerException
at o1.goodstuff.Experience.isBetterThan(Experience.scala:23)
at o1.goodstuff.Experience.chooseBetter(Experience.scala:29)
at o1.goodstuff.Category.addExperience(Category.scala:35)
at o1.goodstuff.CategoryTest$delayedInit$body.apply(CategoryTest.scala:7)
at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.App$$anonfun$main$1.apply(App.scala:71)
at scala.collection.immutable.List.foreach(List.scala:309)
at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32)
at scala.App$class.main(App.scala:71)
at o1.goodstuff.CategoryTest$.main(CategoryTest.scala:3)
at o1.goodstuff.CategoryTest.main(CategoryTest.scala)
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:
NullPointerException
, mikä jo itsessään kertoo
vähän siitä, millaisesta ongelmasta on kyse. Lisää siitä alempana.isBetterThan
-metodissa,
jota oli kutsuttu saman luokan chooseBetter
-metodista
riviltä 29, jota oli kutsuttu kategorialuokan riviltä 35
metodista addExperience
.CategoryTest.scala
tiedoston rivillä numero 3.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 erityisesti
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!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
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 Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa 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+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.
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.)