Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 2.4: Luokan koodi ja ilmentymämuuttujat

Tästä sivusta:

Pääkysymyksiä: Miten luokkia määritellään ohjelmakoodissa? Miten ohjelmakoodi määrää, mitä tapahtuu, kun luokasta luodaan uusi ilmentymä? Miten luokan ohjelmakoodia sovelletaan yksittäiselle ilmentymälle? Voiko ilmentymän toiminta poiketa luokan määrittelystä?

Mitä käsitellään? Luokan ohjelmakoodi, ja erityisesti: luontiparametrien määrittelyt ja ilmentymämuuttujat. Luokan metodien räätälöinti yksittäiselle oliolle. Teräsmies.

Mitä tehdään? Sekä luetaan että ohjelmoidaan.

Suuntaa antava työläysarvio:? Puolitoista tuntia? Yli? Alle? Vaarin vanhat villahousut? Luku edellyttää kaikkien aiempien asioiden osaamista. Ilmentymämuuttujien ja luontiparametrien toiminta voi joka tapauksessa aiheuttaa aluksi päänvaivaa. Älä epäröi kysyä assarilta neuvoa!

Pistearvo: A65.

Oheismoduulit: Oliointro, Odds (uusi).

../_images/person06.png

Kertausta

Tässä luvussa tutustutaan ohjelmakoodiin, jossa määritellään luokkia. Näin yhdistyy se, mitä tiedät yksittäisolioiden ohjelmakoodin laatimisesta (luku 2.2) siihen, mitä opit luokista ja niiden suhteesta olioihin (luku 2.3). Aloitetaan pikakertauksella niistä asioista, jotka ovat kaikkein tärkeimpiä tämän luvun ymmärtämiseksi.

Luokista ja olioista tiedetään jo:

  • Luokat ovat määritelmiä niistä tietotyypeistä, joita ohjelma käsittelee. Luokka määrittelee yleiskäsitteen, josta voidaan luoda ilmentymiä; luokka voi kuvata esimerkiksi opiskelijan käsitettä.

  • Luokan voi instantioida. Toisin sanoen: luokasta voi luoda ilmentymän eli olion, jolla on luokan kuvaama tietotyyppi. Tällainen olio on yksittäistapaus yleiskäsitteestä, esimerkiksi yksittäinen opiskelija.

  • Kun luomme ilmentymän luokasta, on usein tarpeen kirjata luontiparametreja. Näitä parametreja voidaan käyttää ilmentymäkohtaisten tietojen (esim. luotavan opiskelijaolion nimen ja opiskelijanumeron) alustamiseen.

  • Saman luokan eri olioilla on siis keskenään eri tiedot (esim. opiskelijaolioilla voi olla eri nimet). Näillä tiedoilla on kuitenkin samat tietotyypit (esim. kunkin opiskelijaolion nimi on merkkijono).

  • Samantyyppisillä olioilla on myös keskenään samat metodit. Esimerkiksi kaikilla opiskelijaolioilla on ilmoittautumismetodi, jos sellainen on niitä kuvaavaan luokkaan määritelty.

Yksittäisolioista tiedetään jo:

  • Scalalla voi määritellä yksittäisolioita, joille ei erikseen laadita luokkaa.

  • Yksittäisolion ohjelmakoodiin kirjoitetaan muuttujat, joiden arvoiksi kirjataan olion tietoja. Voimme myös toteuttaa oliolle metodeita.

  • Metodien toteutuksessa sana this viittaa olioon itseensä. this.jokuMuuttuja-tyyppisellä ilmaisulla voimme näin viitata olion metodeista olion omiin muuttujiin.

Luokka vs. yksittäisolio

Luvussa 2.2 toteutimme yksittäisolion, joka kuvasi yhtä työntekijää, ja käytimme sitä. Toisaalta äskeisessä luvussa 2.3 käytimme luokkaa Tyontekija, joka kuvasi työntekijöitä yleisenä tietotyyppinä ja oli siksi yksittäisoliota hyödyllisempi.

Alla on vertailun vuoksi sekä luvun 2.2 yksittäisolion että (nyt ensimmäistä kertaa) Tyontekija-luokan ohjelmakoodi. Ne ovat monilta osin aivan samanlaiset, mutta myös tärkeitä eroja löytyy.

Seuraavien kahden koodilaatikon väliset vihreätaustaiset kommentit korostavat eroavat kohdat sekä niiden yllä että alla olevasta koodista, kun viet kursorin kommentin päälle.

object tyontekija:
  var nimi = "Matti Mikälienen"
  val syntynyt = 1965
  var kkpalkka = 5000.0
  var tyoaika = 1.0

  def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt

  def kuukausikulut(kulukerroin: Double) = this.kkpalkka * this.tyoaika * kulukerroin

  def korotaPalkkaa(kerroin: Double) =
    this.kkpalkka = this.kkpalkka * kerroin

  def kuvaus =
    this.nimi + " (s. " + syntynyt + "), palkka " + this.tyoaika + " * " + this.kkpalkka + " euroa"

end tyontekija

Yksittäisolion määrittely alkaa sanalla object, luokan sanalla class. Molemmille voi vapaaehtoisesti määritellä end-loppumerkin.

Luokan nimen perässä ilmoitetaan, mitä luontiparametreja luokan ilmentymää luodessa tulee antaa. Yksittäisoliolla vastaavassa kohdassa ei ole mitään.

Parametrimuuttujia voi hyödyntää, kun olion tietoja asetetaan. Luotavalle Tyontekija-ilmentymälle asetetaan tiedoiksi parametreina vastaanotettuja arvoja. (Tästä lisää alempana.)

Metodien määrittelyissä ei ole mitään eroa yksittäisolion ja luokan välillä!

class Tyontekija(annettuNimi: String, annettuSyntymavuosi: Int, annettuPalkka: Double):
  var nimi = annettuNimi
  val syntynyt = annettuSyntymavuosi
  var kkpalkka = annettuPalkka
  var tyoaika = 1.0

  def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt

  def kuukausikulut(kulukerroin: Double) = this.kkpalkka * this.tyoaika * kulukerroin

  def korotaPalkkaa(kerroin: Double) =
    this.kkpalkka = this.kkpalkka * kerroin

  def kuvaus =
    this.nimi + " (s. " + syntynyt + "), palkka " + this.tyoaika + " * " + this.kkpalkka + " euroa"

end Tyontekija

Katsotaan nyt tarkemmin Tyontekija-luokan toimintaa.

Ilmentymämuuttujat

Miten luokan ohjelmakoodi toimii, kun luomme uuden ilmentymän?

Tarkastellaan asiaa animaation avulla. Animaatio esittelee tärkeän ilmentymämuuttujan (instance variable) käsitteen.

Voit ajatella olion luomisprosessia myös aiemmin käytetyn lomakevertauksen kautta. Alla on päivitetty versio luvun 2.3 esityksestä; se on höystetty ilmentymämuuttujan käsitteellä.

Tarkennus ilmentymän luomisesta ja luokan ohjelmakoodista

Yllä mainittiin, että luokan sisään kirjoitetut käskyt suoritetaan, kun uusi ilmentymä luodaan. Esimerkiksi uuden työntekijäluokasta instantioidun olion ilmentymämuuttujille sijoitetaan tällöin (alku)arvot.

Huomaa kuitenkin, että tällä tarkoitettiin suoraan luokkamäärittelyn sisään kirjoitettuja käskyjä, ei metodien määrittelyjen sisältämiä. Metodien koodi suoritetaan vain silloin, kun erikseen kyseistä metodia kutsumalla käsketään.

Esimerkiksi työntekijäluokan ilmentymää luotaessa ei suinkaan tule kutsutuksi mikään sen metodeista ikaVuonna, kuukausikulut, korotaPalkkaa tai kuvaus, vaikka nämä metodit on luokan ohjelmakoodin sisällä määritelty. Mainittuja metodeita voi luokan käyttäjä määrätä suoritettaviksi olion ensin luotuaan, kuten aiemmissa esimerkeissä on tehty.

Pikkutehtäviä ilmentymämuuttujista

class Lemmikki(annettuNimi: String, annettuLaji: String):

  val laji = annettuLaji
  var nimi = annettuNimi

  // tänne metodeita

end Lemmikki

Tarkastele tuota pikkuluokkaa ja erityisesti sen riviä val laji = annettuLaji. Pohdi myös aiemmin näkemääsi animaatiota työntekijäolioiden luomisesta. Mitkä seuraavista väitteistä pitävät paikkansa?

Oletetaan suoritetuiksi seuraavat käskyt:

import o1.luokkia.Tyontekija
val a = Tyontekija("Eugenia Enkeli", 1965, 5000)
val b = Tyontekija("Teija Tonkeli", a.syntynyt, a.kkpalkka - 1000)

Mitkä seuraavista väitteistä pitävät paikkansa?

Luontiparametrit vs. ilmentymämuuttujat

Metodin parametrimuuttujat ovat olemassa vain metodin suorituksen ajan. Vastaavasti luontiparametrit ovat olemassa ja niiden arvot talletettuina tietokoneen muistiin vain olion luomisprosessin ajan. Ne sijaitsevat kutsupinon kehyksessä. Tämä muistialue vapautetaan muuhun käyttöön heti, kun olio on saatu luotua.

Ilmentymämuuttujat puolestaan kuuluvat olion yhteyteen pysyvämmin. Niiden arvot säilyvät olion luomisen jälkeen ja myös metodikutsujen välillä.

Käskyt kuten val nimi = annettuNimi siis pistävät parametreina saatuja arvoja talteen myöhempää käyttöä varten.

Työntekijä- ja lemmikkiluokkien tapauksessa luontiparametreilla ei tehty mitään muuta kuin sijoitettiin niiden arvoja suoraan ilmentymämuuttujiin. Voikin herätä kysymys, miksi oliota luotaessa edes tarvitaan erikseen muuttujia luontiparametreille, jos niiden arvot vain sijoitetaan ilmentymämuuttujiin.

On järkevää, että luontiparametrit voi määritellä erikseen, koska luontiparametreilla voi tehdä myös muuta kuin kopioida ne ilmentymämuuttujiin; näet tästä esimerkkejä myöhemmin. Toisaalta "kopiointisijoitukset", joita noissa esimerkkiluokissamme esiintyy, ovat yleisiä, ja ne tosiaan voi ilmaista yksinkertaisemminkin. Scala tarjoaa mahdollisuuden tiivistää koodia:

Kätevämpi tapa kirjoittaa luokkia

Seuraavat kaksi versiota Tyontekija-luokasta tekevät käytännössä täsmälleen saman asian. Ensimmäinen on jo nähty alkuperäinen ja toinen "tiivistetty" versio.

class Tyontekija(annettuNimi: String, annettuSyntymavuosi: Int, annettuPalkka: Double):

  var nimi = annettuNimi
  val syntynyt = annettuSyntymavuosi
  var kkpalkka = annettuPalkka
  var tyoaika = 1.0

  // metodien koodi tähän

end Tyontekija

var- ja val-sanat voi kirjoittaa myös heti luokkamäärittelyn alkuun kuten alla. Tämä tarkoittaa, että sekä edellytetään näiden kolmen tiedon antamista luontiparametreiksi, kun Tyontekija-tyyppistä oliota luodaan, että tallennetaan annetut tiedot olion ilmentymämuuttujiin.

Yllä olevassa pidemmässä versiossa tehtiin annettu-alkuisia parametrimuuttujia ilmentymämuuttujien lisäksi. Niitä ei tiiviissä versiossa ole lainkaan. Tiiviissä versiossa luontiparametrien nimet ovat samat kuin ilmentymämuuttujien ja niiden arvot kopioituvat ilmentymämuuttujiin ilman erillistä käskyä.

Neljännen ilmentymämuuttujan määrittelyssä ei tässä esimerkissä käytetä parametriarvoja. Se on määritelty molemmissa versioissa samalla tavalla.

class Tyontekija(var nimi: String, val syntynyt: Int, var kkpalkka: Double):

  var tyoaika = 1.0

  // metodien koodi tähän

end Tyontekija

Voit halutessasi tarkastella jälkimmäisen version toimintaa myös tästä animaatiosta.

Tässä käytetty tiivimpi tapa tekee koodista lyhyemmän ja kenties helpomman lukea. Joka tapauksessa tämä tyyli on Scala-ohjelmoinnissa yleinen, ja siihen on hyvä totutella heti. Tiiviimpää tyyliä tulemme käyttämään jatkuvasti.

Ilmentymän metodien toiminta

Metodikutsut toimivat luokasta instantioiduille olioille aivan samalla tavalla kuin yksittäisolioille. Kuten luvussa 2.2 nähtiin, sanaa this voi käyttää metodien ohjelmakoodissa viittaamaan siihen olioon, jolle metodia on kutsuttu. Luokan ilmentymäksi luotu olio pääsee sekin käsiksi esimerkiksi omiin ilmentymämuuttujiinsa ilmaisulla, joka on muotoa this.muuttuja.

Jos tämä asia tuntuu ihan selvältä, ohita surutta seuraava animaatio; siinä ei ole muuta uutta opittavaa. Muutoin katso se.

Pikkutehtävä: suorakaidetiiviste

Oletetaan, että meillä on käytössä luokka Suorakaide, jota voi käyttää näin:

val testi = Suorakaide(10.0, 15.5)testi: o1.Suorakaide = o1.Suorakaide@152a308
testi.sivu1res0: Double = 10.0
testi.sivu2res1: Double = 15.5
testi.alares2: Double = 155.0

Suorakaideoliolta voi siis kysyä ainakin sen sivujen pituudet ja pinta-alan.

Tässä yksi tapa määritellä noin käytettävä luokka:

class Suorakaide(annettuSivunPituus: Double, annettuToinenSivunPituus: Double):

  val sivu1 = annettuSivunPituus
  val sivu2 = annettuToinenSivunPituus

  def ala = this.sivu1 * this.sivu2

  // jne. (Täällä voisi olla muita metodeita.)

end Suorakaide

Tämä tapa on tarpeettoman monisanainen. Määrittele luokka lyhyemmin määrittelemällä ilmentymämuuttujat jo luokan otsikkorivillä kuten edellä opetettiin.

Kirjoita ratkaisusi Oliointro-moduulin tiedostoon Suorakaide.scala, josta löytyy annettuna tuo pidempi ohjelmakoodi. Muokkaa annettu koodi pyydetynlaiseksi.

Varmista REPLissä kokeilemalla, että uusittu luokkasi toimii esimerkin mukaisesti.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Pikkutehtävä: suorakaiteen kuvaus

Täydennetään suorakaideluokkaa kuvauksen muodostavalla metodilla mitat. Kuvaus on muotoa "X kertaa Y". Alla on neljä yritystä. Mitkä niistä toimivat?

Voit halutessasi kokeilla mitä tahansa näistä toteutuksista lisäämällä sen suorakaideluokkaasi.

def mitat = this.sivu1 + " kertaa " + this.sivu2
def mitat = s"$this.sivu1 kertaa $this.sivu2"
def mitat = s"$sivu1 kertaa $sivu2"
def mitat = s"${this.sivu1} kertaa ${this.sivu2}"

Seuraavista kuudesta väitteestä neljä pitää paikkansa. Mitkä?

Kokoava pikkuharjoitus: tililuokka

Ota esiin luvussa 2.2 laatimasi tiliolion määrittely. Pohdi, miten tämän koodin voisi muokata tililuokaksi, josta voi instantioida erilaisia tiliolioita. Kukin näistä olioista toimisi samaan tapaan kuin se yksittäisolio, jonka aiemmin toteutit.

Mitkä seuraavista ideoista ovat mielekkäitä? Voit myös toteuttaa tarvittavat muutokset olioon, jos haluat.

Lisätreeniä: suorakaiteita grafiikaksi

Kuten sanottu, yllä laadittu suorakaideluokka kuvaa eräitä suorakaiteitten matemaattisia ominaisuuksia. On toki mahdollista lisätä luokkaan myös toimintoja, joissa suorakaidetta käsitellään graafisena elementtinä.

Lisää Suorakaide-luokkaan metodi kuvaksi, joka

  • ottaa yhden Color-tyyppisen parametrin, ja

  • palauttaa suorakaiteen kuvan (tyyppiä Pic), jossa leveytenä on sivu1, korkeutena sivu2 ja värinä metodin saama parametriarvo.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Äskeisessä tehtävässä väri miellettiin lisätiedoksi, joka välitetään suorakaideoliolle parametriksi aina, kun halutaan suorakaideolion tuottavan itsestään uuden kuvan.

Vaihtoehtoisesti voimme mallintaa väriä kunkin suorakaiteen ominaisuutena.

Laadi samaan Suorakaide.scala-tiedostoon toinen luokka nimeltä VarillinenSuorakaide. Kopioi Suorakaide-luokan koodi pohjaksi siihen alle ja vaihda kopion nimi. Muokkaa sitten VarillinenSuorakaide-luokkaa:

  • Lisää kolmanneksi luontiparametriksi Color-tyyppinen väriarvo.

  • Huolehdi siitä, että tuohon väriin pääsee käsiksi myös vari-nimisen ilmentymämuuttujan kautta.

  • Muokkaa metodia kuvaksi niin, että se on parametriton ja käyttää kyseisen suorakaiteen omaa väriä palauttamassaan kuvassa.

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Kumpi laadituista luokista on parempi? Ero ei ole suuri, ja vastaus riippuu joka tapauksessa siitä, millaisen tai millaisten laajempien ohjelmakokonaisuuksien osaksi luokka olisi tarkoitettu.

Kiinnostavampi ja yleisluontoisempi kysymys on: kannattaako yleensäkään kirjoittaa mallinnettavaa käsitettä (kuten suorakaide) kuvaavaan luokkaan sellaista koodia, joka koskee ohjelman ilmiasuun liittyviä asioita (kuten värejä ja kuvia)? Palaamme tähän vielä kurssin aikana.

Monsteritehtävä

Laaditaan luokka, jolla voisi kuvata hirviöitä jossakin yksinkertaisessa, muutoin kuvitteellisessa pelissä.

Hirviöillä on hirviötyyppi ("örkki", "vampyyri", tms.) sekä kuntopisteet. Luokkaa tulisi voida käyttää seuraavan esimerkin kaltaisesti.

val otus = Hirvio("louhikäärme", 200)otus: Hirvio = Hirvio@597f1753
otus.tyyppires3: String = louhikäärme
otus.taysiKuntores4: Int = 200
otus.nykykuntores5: Int = 200
otus.kuvausres6: String = louhikäärme (200/200)

Luodaan uusi ilmentymä luokasta, yksi hirviöolio. Luontiparametrit määräävät hirviön tyypin sekä sen kuntopisteet täysissä voimissa.

Miltä vain hirviöoliolta voi tiedustella sen tyyppiä sekä täysiä ja nykyisiä kuntopisteitä. Kuntopisteet ovat aluksi täysillä.

Oliolta voi myös pyytää kuvauksen, joka muodostuu hirviötyypistä sekä nykyisistä ja täysistä kuntopisteistä.

Hirviön tila muuttuu, kun se vahingoittuu vaikkapa sankarin hyökkäyksestä:

otus.vahingoitu(30)otus.nykykuntores7: Int = 170
otus.vahingoitu(40)otus.nykykuntores8: Int = 130
otus.kuvausres9: String = louhikäärme (130/200)

vahingoitu-metodille ilmoitetaan, paljonko kuntopisteitä hirviö menettää. Tämä vaikutuksellinen metodi vain muuttaa hirviön tilaa eikä palauta mitään.

Muutos näkyy kysyttäessä hirviöltä sen nykykuntoa tai kuvausta.

Tutkitaan luokan toteutusta:

class Hirvio(val tyyppi: String, val taysiKunto: Int):

  var nykykunto = taysiKunto

  val kuvaus = this.tyyppi + " (" + this.nykykunto + "/" + this.taysiKunto + ")"

  def vahingoitu(paljonko: Int) =
    this.nykykunto = this.nykykunto - paljonko

end Hirvio

Tämäkin koodi löytyy moduulista Oliointro; voit kokeilla luokan käyttöä REPLissä.

Perehdy koodiin huolellisesti. Mitkä seuraavista väitteistä pitävät paikkansa?

Konstruktori-termi

Tässä luvussa on puhuttu paljon useasti luontiparametreista; toinen nimi samalle asialle on "konstruktoriparametri". Jälkimmäinen termi juontuu konstruktorin (constructor; suomeksi myös rakentaja) käsitteestä: konstruktori on aliohjelma, joka suoritetaan oliota luotaessa ja jolla alustetaan olion tila. Käskyt, joilla sijoitetaan ilmentymämuuttujille arvoja, ovat konstruktoreissa tyypillisiä.

Scala-ohjelman tapauksessa konstruktorina toimivat luokkamäärittelyyn metodien ulkopuolelle kirjatut käskyt. Monissa muissa ohjelmointikielissä konstruktori on kirjoitettava erilliseksi määrittelyksi luokan sisään muiden metodien tapaan.

Odds-tehtävä (osa 1/9)

Todennäköisyyksiä (engl. odds) voidaan kuvata eri tavoin. Esimerkiksi kuusisivuista noppaa heitettäessä voimme sanoa, että kuutosen heittämiseen on "yksi kuudesta" -mahdollisuus. Eri ilmaisu samalle on "5/1", mikä kertoo, että on viisi tapaa olla heittämättä kuutosta ja yksi tapa heittää se. Toisin sanoen: on viisi kertaa niin todennäköistä, että kuutosta ei heitetä kuin että se heitetään. Vielä yksi ilmaisutapa on 16,67 %, joka voidaan myös kirjoittaa muotoon 0.1667.

Laaditaan luokka, jolla voidaan kuvata tapahtumien toteutumismahdollisuuksia ja esittää niitä eräissä muodoissa ja lopulta myös tehdä tiettyjä todennäköisyyslaskuja. Tässä tehtävässä aloitamme luokan toteuttamisen; luokkaa jatkokehitetään useassa pienessä tehtävässä myöhemmissä luvuissa.

Pohjustus: Odds-luokan haluttu toiminnallisuus

Tarkoitus olisi, että yhtä Odds-luokasta luotua oliota voitaisiin käyttää esimerkiksi kuvaamaan todennäköisyyttä heittää kuutonen tai vedonlyöntitoimiston tarjoamaa arviota Norjan todennäköisyydestä voittaa Euroviisut. Olion voisi luoda näin:

val rollingSix = Odds(5, 1)rollingSix: Odds = o1.odds.Odds@1c60524

Nyt rollingSix-muuttuja sisältää viittauksen Odds-tyyppiseen olioon, joka kuvaa kuutosen heittämisen todennäköisyyttä. Luontiparametriksi annettiin "epäonnistuvien tapausten määrä" (5) ja "onnistuvien tapausten määrä" (1), jotka määräävät todennäköisyyden.

Voimme nyt pyytää oliolta todennäköisyystietoja erilaisissa muodoissa. Metodilla probability saadaan kuutosen heiton todennäköisyys eli laskutoimituksen 1.0 ∕ (5 + 1) tulos desimaalilukuna:

rollingSix.probabilityres10: Double = 0.16666666666666666

Metodilla fractional todennäköisyys saadaan suhteellisessa muodossa:

rollingSix.fractionalres11: String = 5/1

Kuten näet, tämän metodin paluuarvo on merkkijono, jossa luontiparametreiksi annettujen lukujen välissä on kauttamerkki.

decimal-metodilla saadaan probability-metodin palauttaman luvun käänteisluku, joka kertoo, "yksi monestako" on tapahtuman todennäköisyys. Tässä tapauksessa yksi kuudesta:

rollingSix.decimalres12: Double = 6.0

Otetaan toinen esimerkki. Monet kansainväliset vedonlyöntitoimistot ilmoittavat kohteensa murtolukuina (kuten fractional-metodin paluuarvossa). Odds-olio voi toimia vedonlyöntikohteen kuvaamisessa. Oletetaan nyt vaikkapa, että vedonlyöntitoimisto tarjoaa Norjan euroviisuvoitolle luvut 5/2, ja mallinnetaan tämä Odds-oliona:

val norwayWin = Odds(5, 2)norwayWin: Odds = o1.odds.Odds@1e75d66
norwayWin.probabilityres13: Double = 0.2857142857142857
norwayWin.fractionalres14: String = 5/2
norwayWin.decimalres15: Double = 3.5

Yllä näkyvä decimal-metodin paluuarvo ilmoittaa kyseisen kohteen voittokertoimen. Esimerkiksi 5/2-kohteesta onnekas vedonlyöjä saa panoksensa 3,5-kertaisena: rahansa takaisin ja 2,5 kertaa panoksen verran lisää.

Suunnitellaan Odds-luokkaamme vielä voittopotin suuruuden laskemiseenkin sopiva metodi winnings. Sillä voi laskea vaikkapa, paljonko voittaa, jos sijoittaa 20 rahaa Norjan voitolle ja osuu oikeaan:

norwayWin.winnings(20.0)res16: Double = 70.0

(Tulos on siis panos 20 € kertaa decimal-metodinkin palauttama kerroin 3.5.)

Viimeinen esimerkkimme alla osoittaa, että ensimmäinen luontiparametri voi toki mainiosti olla toista pienempikin, jos tapahtuma on todennäköinen:

val threeOutOfFive = Odds(2, 3)threeOutOfFive: Odds = o1.odds.Odds@dfdd0c
threeOutOfFive.probabilityres17: Double = 0.6
threeOutOfFive.fractionalres18: String = 2/3
threeOutOfFive.decimalres19: Double = 1.6666666666666667
threeOutOfFive.winnings(123.45)res20: Double = 205.75

Tehtävänanto

Nouda moduuli Odds ja tutustu siellä olevaan Odds-luokan määrittelyyn. (Projektin muuhun sisältöön ei vielä tarvitse.)

Huomaat, että luokka on vajavainen: vähän alkua löytyy, ja metodi probability on tehty, mutta yllä esitellyt metodit fractional, decimal ja winnings puuttuvat. Toteuta ne.

Ohjeita ja vinkkejä

  • Sinulle on valmiiksi määritelty pari ilmentymämuuttujaa: wont ja will. Ne saavat arvonsa suoraan luontiparametreista (tavalla, joka esiteltiin ylempänä kohdassa Kätevämpi tapa kirjoittaa luokkia).

    • Voit hyödyntää näitä ilmentymämuuttujia metodeita laatiessasi.

    • Et tarvitse tässä muita ilmentymämuuttujia.

  • probability-metodia ei siis tarvitse muuttaa; se toimii. Voit silti katsoa, miten se on tehty.

    • Huomaa 1.0:lla kertominen, jotta jakolaskun tulos saadaan desimaalilukuna; ilman tulisi käytännössä aina nolla (vrt. luku 1.3). Jos tämä tuntuu ihmeelliseltä kikkailulta, niin lohduttaudu sillä, että luvussa 5.2 vastaan tulee nätimpikin tapa tehdä sama asia.

  • Kunhan varmistat, että ymmärrät mitä kunkin metodin pitäisi saada aikaan, niin niiden toteuttaminen yksinkertaisia laskutoimituksia yhdistelemällä ei liene suuri homma.

  • Varmista REPLissä kokeilemalla, että metodisi toimivat.

    • Huomaa valita Odds-moduuli REPLiin.

    • Muista REPLin uudelleenkäynnistys muutosten jälkeen.

    • Palauta ratkaisusi vasta, kun olet toimintaan tyytyväinen.

  • fractional-metodissa ei kuulu tehdä mitään sievennyksiä, vaan esimerkiksi luontiparametrien ollessa 6 ja 2 palautetaan 6/2 eikä 3/1.

    • Jos merkkijonon muodostamisessa tulee vaikeuksia, muista suorakaidetehtävän kuvaus-metodi.

  • Osaatko toteuttaa decimal ja winnings-metodit niin, että kutsut näiden metodien sisältä jotakin toista saman luokan metodia? (Ei pakollista, mutta kätevää.)

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Metodi vai muuttuja?

Männävuosien opiskelijat pohtivat:

Ei auennut että millon kannattaa tehdä def ja milloin val?

Jäin miettimään, miksi halusimme Odds-tehtävässä, että probability, fractional ja decimal olivat metodeja. Kun nämä metodit eivät kerran tarvitse mitään parametreja, eikö ainakin tämä esimerkki toimisi yhtä hyvin, jos kirjoittaisimme ne ilmentymämuuttujiksi? Tai siis toimii se, kokeilin. Miksi siis teimme näin?

Oikein hyvää pohdintaa.

Esimerkiksi seuraavasta koodista voisi kolme ensimmäistä def-sanaa vaihtaa val-sanoiksi, ja koodi toimisi edelleen yhtä hyvin:

class Odds(…):

  def probability = …

  def fractional = …

  def decimal = …

  def winnings(bet: Double) = …

end Odds

Kun probability, fractional ja decimal on määritelty metodeiksi def-sanalla, kunkin metodin sisältämä koodi suoritetaan vasta kun — tai siis vain jos — kyseistä metodia todella kutsutaan ohjelmassa kyseiselle oliolle. Toisaalta metodin sisältämät laskutoimitukset tulevat suoritetuiksi joka kerta, kun metodia kutsutaan, joten toistuva saman metodin kutsuminen saa tietokoneen laskemaan saman asian moneen kertaan.

Jos nämä metodit olisi määritelty val-sanalla ilmentymämuuttujiksi, niiden alustaminen olisi osa olion alustusprosessia ja kyseiset laskutoimitukset suoritettaisiin heti oliota luotaessa. Laskujen tulokset jäisivät muuttujiin talteen osaksi olion tietoja. Näitä tuloksia voisi "kysyä" oliolta helposti muuttujien kautta, eikä tietokoneen tarvitsisi laskea niitä kullekin Odds-oliolle kuin kertaalleen. Toisaalta tietokone laskisi jokaiselle luodulle Odds-oliolle kunkin näistä arvoista kerran (vaikka jokaista niistä ei ehkä joka oliolle tarvittaisi kertaakaan). Koneen täytyisi myös käyttää hieman lisää muistia näiden tietojen varastoimiseksi ilmentymämuuttujiin.

Huomaamme siis vaikutuksia muistinkäyttöön ja suoritusnopeuteen. Niiden painoarvo riippuu siitä, miten usein mitäkin näistä metodeista/muuttujista kyseisessä sovelluksessa tarvitaan. Pikkuesimerkissämme valinnalla ei ole mitään käytännön merkitystä, koska Odds-olioita luodaan muistiin vain muutama, niiden metodeita kutsutaan vain muutama kerta ja kukin olioista vie joka tapauksessa muistia hyvin vähän.

Valintaan metodin ja muuttujan välillä vaikuttavat myös muut seikat; joskus on aivan ratkaisevasti väliä sillä, tallennetaanko tietty tieto muistiin (val tai var) vai luodaanko tieto uudelleen kutsuttaessa (def). Muista esimerkiksi tämän luvun Hirvio-luokka, jonka saimme kuntoon vaihtamalla yhden muuttujan tilalle metodin, joka muodostaa hirviön kuvauksen senhetkisten tietojen perusteella. (Vinkki: vielä tällä kierroksella kohtaat tehtävän, jossa ongelma on päinvastainen eli tarvitaan nimenomaan muuttuja eikä metodia.)

Pannaanpa tähän vielä pieni mutta merkityksellinen pohdintatehtävä

Tallensit varmaankin äskeisessä tehtävässä Odds-olion kaksi luontiparametria val-muuttujiin? (Toivottavasti!)

Miten koko äskeinen "voiko defit muuttaa valeiksi?" -pohdinta muuttuisi, jos luontiparametrit menisivätkin talteen var-muuttujiin eikä valeihin?

Pohdi sitä. Paljastan vastauksen viikkokoosteessa 3.0, joka julkaistaan kierroksen sulkeuduttua.

Yksittäisolion räätälöinti luokasta

Pöhköillään lopuksi vähän yhdellä esimerkkiluokalla. Pöhköillessä opitut temput tulevat käyttöön seuraavissa luvuissa.

class Henkilo(val nimi: String):

  def lausu(lause: String) = this.nimi + ": " + lause

  def reagoiSrirachaan     = this.lausu("Onpa hyvä kastike.")

  def reagoiKryptoniittiin = this.lausu("Onpa kumma mineraali.")

end Henkilo

Kullakin henkilöllä on metodi, jolla hänet voi komentaa sanomaan jotakin. Metodi palauttaa merkkijonon, joka sisältää henkilön nimen ja pyydetyn lauseen.

Lisäksi henkilöä voi komentaa reagoimaan erilaisiin asioihin. Henkilö reagoi lausumalla asioita, ja siksipä...

... onkin näppärää toteuttaa nämä metodit kutsumalla henkilön itsensä lausu-metodia. Henkilöolio ikään kuin lähettää itselleen viestin: "Kun reagointimetodiani kutsutaan, komenna this-olio eli minut itseni suorittamaan lausu-metodi tietyllä parametriarvolla."

Käyttöesimerkkejä:

val eka = Henkilo("Jimmy")eka: Henkilo = Henkilo@5cf6635a
eka.lausu("Super-Duper!")res21: String = Jimmy: Super-Duper!
eka.reagoiSrirachaanres22: String = Jimmy: Onpa hyvä kastike.
eka.reagoiKryptoniittiinres23: String = Jimmy: Onpa kumma mineraali.
val toka = Henkilo("Lois")toka: Henkilo = Henkilo@645b797d
toka.reagoiKryptoniittiinres24: String = Lois: Onpa kumma mineraali.

Kaikki Henkilot siis pitävät srirachasta ja kummastelevat kryptoniittia, koska heidän luokkansa niin määrää. Juuri mitään muuta he eivät osaa tehdä, esimerkiksi lentää. Mutta samat lait eivät päde kaikkiin:

Metodin lisääminen ilmentymäkohtaisesti

object terasmies extends Henkilo("Clark"):
  def lenna = "WOOSH!"
end terasmies// defined object terasmies

Määritellään yksittäisolio, joka on erikoistapaus henkilöluokasta. Käytämme tähän extends-avainsanaa: tässä "laajennetaan" henkilöluokan määrittelyä yksittäisen terasmies-olion osalta.

Henkiloitä luodessa on annettava luontiparametri, joka määrää nimen. Tämän yksittäisen erikois-Henkilon määrittelyssä kirjaamme luontiparametrin osaksi extends-rimpsua.

Määritellään tälle henkilöoliolle uusi metodi. Tätä metodia ei siis ole muilla henkilöillä, mutta tällä oliollapa on.

Vapaaehtoisen loppumerkin voisi näin yksinkertaisesta oliosta hyvin jättää poiskin, mutta tähän se on esimerkin vuoksi kirjoitettu.

Teräsmies osaa lentää mutta myös kertoa nimensä ja reagoida asioihin kuten muutkin henkilöt. Tämäkin olio on henkilöolio.

terasmies.lennares25: String = WOOSH!
terasmies.nimires26: String = Clark
terasmies.reagoiKryptoniittiinres27: String = Clark: Onpa kumma mineraali.

Saimme räätälöityä tietylle oliolle uuden metodin. Entä voimmeko muokata luokalle yleisesti kirjoitettua metodimäärittelyä oliokohtaisesti?

Metodin korvaaminen

Vihreä kryptoniitti on Teräsmiehen heikkous. Jos haluamme kuvata hänet Henkilo-luokkaa käyttäen, voimmeko huomioida tämän seikan? Meidän pitäisi siis saada teräsmiestä kuvaava henkilöolio poikkeamaan tältä osin siitä, miten muut henkilöoliot käyttäytyvät.

object realistinenTerasmies extends Henkilo("Clark"):
  def lenna = "WOOSH!"
  override def reagoiKryptoniittiin = "GARRRRGH!"// defined object realistinenTerasmies
realistinenTerasmies.reagoiKryptoniittiinres28: String = GARRRRGH!

Määritellään Henkilo-luokan jo määrittelemä metodi uusiksi tälle yhdelle erikoisilmentymälle.

Tämä määritelmä korvaa (override) luokassa tehdyn yleisen määrittelyn. Jotta tällaista ei tule tehtyä vahingossa, on perusteltua, että Scala vaatii meidän merkitsevän korvaamisen override-avainsanalla. (Ilman tulisi virheilmoitus.)

Yhteenvetoa

  • Luokkien ohjelmakoodiin kirjataan ne ilmentymämuuttujat ja metodit, joita luokan ilmentymillä (olioilla) halutaan olevan.

  • Oliota luotaessa se saa omat kopionsa ilmentymämuuttujista, joille tallennetaan oliokohtaiset arvot.

  • Koodissa määritellään myös se, mitä luontiparametreja on annettava, kun uusi ilmentymä luodaan. Usein luontiparametrien arvoja tallennetaan sellaisenaan uuden olion ilmentymämuuttujiin.

  • On mahdollista ja silloin tällöin perusteltuakin laatia luokan tietylle ilmentymälle lisämetodeita. Voidaan myös korvata luokan määräämä toteutus ilmentymäkohtaiseksi räätälöidyllä toteutuksella.

  • Lukuun liittyviä termejä sanastosivulla: luokka, ilmentymä, ilmentymämuuttuja, instantioida, luontiparametri; this; korvata.

Tärkeää asiaa!

Ilmentymämuuttujat ja niiden suhde luontiparametreihin ovat monesti hankalanpuoleisia asioita olio-ohjelmoinnin oppimisessa. Aihe on kuitenkin luokkiin perustuvan olio-ohjelmoinnin ja tämän kurssin jatkon kannalta aivan keskeinen, ja siksi sitä on tässä luvussa selitetty eri tavoin. Jos suinkin mahdollista, jatka eteenpäin vasta sitten, kun sinusta tuntuu, että olet ymmärtänyt mainitut asiat. Kertaa esimerkkejä ja animaatioita tarpeen mukaan ja juttele assarien ja toisten opiskelijoiden kanssa.

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.

Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.

a drop of ink
Palautusta lähetetään...