Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 5.1: Oliot funktioina, luokat olioina
Tästä sivusta:
Pääkysymyksiä: Ovatko luokat olioita? Oliot luokkia? Funktiot olioita vai oliot funktioita? Pysyykö pääni kasassa?
Mitä käsitellään? apply
-metodit. Luokkien kumppanioliot.
Tehdasmetodit.
Mitä tehdään? Lähinnä luetaan.
Suuntaa antava työläysarvio:? Menisikö puoleen tuntiin?
Pistearvo: B5.
Oheisprojektit: Ei ole.
Johdanto
Tässä suhteellisen lyhyessä luvussa jatkamme sen setvimistä, miten laaja merkitys olioilla Scala-ohjelmissa onkaan.
Tässä aluksi pieni vektoriesimerkki:
val vektori = Vector("eka", "toka", "kolmas", "neljäs", "viides")res0: Vector[String] = Vector(eka, toka, kolmas, neljäs, viides) vektori.apply(3)res1: String = neljäs vektori(3)res2: String = neljäs
Nostetaan esille muutama kysymys.
- Vektorin yksittäisen alkion voi katsoa ilmaisulla
vektori(indeksi)
tai pidemmällä ilmaisullavektori.apply(indeksi)
. Miksi nämä kaksi erinäköistä mutta pohjimmiltaan samanlaista tapaa tehdä sama asia? - Olioitahan kuuluu komentaa metodikutsuilla, joten
vektori.apply(indeksi)
näyttää järkevältä. Mutta eikö ilmaisuvektori(indeksi)
ole vähän outo? Siinähän on viittausvektori
-muuttujan osoittamaan olioon, jolle sitten jotenkin annetaan parametriksiindeksi
-lausekkeen arvo. Käskyllä on funktiokutsun muoto mutta se on pikemminkin "oliokutsu". Ei kai oliota voi "suorittaa"? - Entä vektoriolion luova käsky? Luomme yleensä olioita
new
-sanalla; miksi se jätetään tässä pois?
Vastaukset liittyvät toisiinsa ja ovat monelta osin kytköksissä nimenomaan Scala-kieleen. Aloitetaan vyyhden purkaminen kahdesta ensimmäisestä kysymyksestä.
Erikoismetodi apply
"Olio funktiona"
Oliota ei varsinaisesti voi määrätä suoritettavaksi. Oliohan on kuvaus jostakin, johon liittyy metodeita, ja nimenomaan olion metodeita voi määrätä suoritettaviksi kutsumalla niitä.
Mistä sitten on kysymys käskyssä vektori(indeksi)
? Vastaus löytyy Scala-kielen
määrittelystä ja siitä, että apply
-nimi on kielessä erikoisasemassa.
Kun olioon viittaavan lausekkeen perään kirjoitetaan parametreja sulkuihin, niin ilmaisu
tulkitaan kyseisen olion apply
-nimisen metodin kutsuksi. vektori(indeksi)
on siis
toinen tapa antaa käsky vektori.apply(indeksi)
.
Sama toimii esimerkiksi merkkijonoille: "kissa".apply(0)
ja "kissa"(0)
tarkoittavat
samaa ja palauttavat molemmat merkin k
.
Kyseessä on yleinen sääntö, joka ei liity nimenomaisesti vektoreihin tai merkkijonoihin.
Voit ajatella vaikkapa niin, että apply
-metodi — jos oliolle sellainen on määritelty
— on eräänlainen "olion oletusmetodi", jonka olio suorittaa, kun ei muutakaan metodia
ole pisteen perässä mainittu.
Toinen tapa ajatella asiaa on, että apply
-metodi mahdollistaa "olion käyttämisen ikään
kuin se olisi funktio". Kuitenkin on tärkeää hahmottaa, että kutsu olio(parametrit)
määrää suoritettavaksi nimenomaan olion apply
-metodin eikä oliota itseään jossakin
yleisemmässä mielessä. Olio ei varsinaisesti ole funktio, vaan kyseessä on vain
lyhennysmerkintä.
Erilaisia apply
-metodeita
Eri luokkiin määritellyt apply
-metodit voivat tehdä erilaisia asioita. Esimerkki, jonka
jo näit, on vektorien, merkkijonojen ja muiden kokoelmien apply
, joka on määritelty
Scala-peruskirjastossa. On katsottu käteväksi, että ilmaisulla kokoelma(indeksi)
voi
katsoa tietyn alkion kokoelmasta. Niinpä kyseisille metodeille on annettu nimeksi juuri
apply
.
apply
-metodin voi laatia itsekin mihin tahansa luokkaan tai yksittäisoliolle. Ohjelmoija
voi määrätä sen tarvitsemat parametrit ja muun toiminnan. Esimerkiksi näin:
object testiolio {
def apply(sana: String, toinen: String) = {
println(sana + ", " + toinen + "!")
}
}
Nyt sekä käsky testiolio.apply("Ave", "Munde")
että käsky testiolio("Ave", "Munde")
kutsuvat apply
-metodia, joka tulostaa Ave, Munde!
Tällä kurssilla sinun ei useinkaan tarvitse itse laatia apply
-metodeita. Silti niiden
perusajatus on hyvä ymmärtää, jotta hahmotat paremmin esimerkiksi juuri vektoreita ja
merkkijonoja käsittelevät käskyt ja osaat tulkita virheilmoituksia sujuvammin.
Kumppanioliot eli "luokat olioina"
Siirrytään toiseen aiheeseen. Senkin yhteydessä tosin apply
nousee vielä esiin.
Johdanto: asiakasluokka
Tarkastellaan pientä esimerkkiä. Olemme laatimassa luokkaa — vaikkapa Asiakas
— ja
tavoitteenamme on numeroida kaikki tästä luokasta luodut ilmentymät eri positiivisilla
kokonaisluvuilla. Kokemus kertoo, että aloittelevan ohjelmoijan ensimmäinen luonnos
ratkaisusta voi näyttää suunnilleen tältä:
class Asiakas(val nimi: String) { Käytetään askeltajamuuttujaa montakoLuotu kirjaamaan, montako asiakasta on; alkuarvo 0. Kun asiakas luodaan, kasvatetaan askeltajan arvoa yhdellä. val numero = askeltajan arvo eli tähän mennessä luotujen asiakkaiden lukumäärä override def toString = "#" + this.numero + " " + this.nimi }
Pseudokoodiluonnos kääntyy suoraan Scalaksi näin:
class Asiakas(val nimi: String) {
private var montakoLuotu = 0
this.montakoLuotu += 1
val numero = this.montakoLuotu
override def toString = "#" + this.numero + " " + this.nimi
}
Kuitenkin kun tätä luokkaa käyttää, ei homma toimikaan niin kuin piti. Mieti koodia ja seuraavaa animaatiota sen suorituksesta ja selvitä, missä vika on.
Luokkakohtaiset ominaisuudet
Luvusta 2.3 asti on ollut esillä ajatus siitä, että luokka kuvaa tietynlaisten olioiden tietotyypin. Luokka määrittelee yleisiä piirteitä ilmentymistään; kullakin oliolla on omat kappaleensa luokan kuvaamista ilmentymämuuttujista. Juuri viimeksi mainittu seikka sai äskeisen asiakkaiden numerointiyrityksen epäonnistumaan.
On tilanteita, joissa halutaan liittää luokan kuvaamaan käsitteeseen yleisesti tiettyjä ominaisuuksia tai toimintoja. Siis halutaan, että nämä ominaisuudet tai toiminnot eivät liity kuhunkin kyseisentyyppiseen olioon erikseen vaan käsitteeseen itseensä kokonaisuutena. Esimerkkimme oliolaskuri on juuri tällainen ominaisuus: kullakin oliolla on numeronsa, mutta olioiden kokonaismäärä ei ole minkään yhden olion ominaisuus vaan koko asiakaskäsitteen.
Haluaisimme siis, että asiakasesimerkkimme toimisi suunnilleen näin:
Animaatio korostaa: haluaisimme tässä tapauksessa käsitellä luokkaa ikään kuin sekin olisi olio — ei eräs asiakasolio, vaan asiakaskäsitettä kuvaava olio, jolla on ominaisuutena laskurimuuttuja.
Scala-luokka ei itse ole olio, jota voisi käyttää aivan siihen tapaan kuin äskeisessä animaatiossa. Kuitenkin luokalle voidaan määritellä kaveri, joka on olio ja jonka avulla ylle piirretty algoritmi voidaan toteuttaa.
Luokan ja olion läheinen suhde (Katso kuvat!)
Tämä versio asiakasohjelmasta toimii halutulla tavalla:
object Asiakas {
private var montakoLuotu = 0
}
class Asiakas(val nimi: String) {
Asiakas.montakoLuotu += 1
val numero = Asiakas.montakoLuotu
override def toString = "#" + this.numero + " " + nimi
}
Kumppaniolio Asiakas
ei ole asiakasluokan ilmentymä eli sellainen olio, joka
luodaan käskyllä new Asiakas(nimi)
. Sen tyyppi ei ole Asiakas
! Kumppaniolio on
erillinen olio, johon on määritelty Asiakas
-käsitteeseen yleisesti liittyviä
piirteitä (tässä vain yksi).
Asiakas
-kumppanioliolla on muuttuja montakoLuotu
.
Tästä muuttujasta on muistissa vain yksi kopio, koska
kumppanioliotakin on vain yksi (ks. animaatio alla).
Vrt. asiakasolioiden nimet ja numerot, joita on yksi per
asiakasolio. Samoin alustus nollaksi tehdään vain kerran,
kun ohjelma ladataan käyttöön ja kumppaniolio tulee luoduksi.Asiakas
-tyyppistä
oliota luotaessa kasvatetaan Asiakas
-kumppaniolion
montakoLuotu
-muuttujan arvoa yhdellä ja sitten sen uusi
arvo kopioidaan asiakasolion ilmentymämuuttujaan numero
.Asiakas
-luokka ja sen kumppaniolio ovat "kavereita", jotka
pääsevät poikkeuksellisesti käsiksi myös toistensa yksityisiin
tietoihin.Kumppanioliot ovat siis yksittäisolioita. Scalan yksittäisoliot voidaan jakaa:
- kumppaniolioihin, joilla on sama nimi jonkin samassa tiedostossa määritellyn luokan kanssa, ja
- itsenäisiin yksittäisolioihin (standalone object), joilla ei ole luokkakumppania vaan jokin muu tarkoitus ohjelmassa.
Lähestulkoon kaikki aiemmissa luvuissa kohtaamasi yksittäisoliot olivat itsenäisiä, käynnistysoliot mukaan lukien.
Yksi lisäesimerkki kumppanioliosta löytyy Stars-projektin StarCoords
-luokan
yhteydestä: samassa tiedostossa on määritelty myös StarCoords
-niminen kumppaniolio.
Metodit kumppanioliossa
Kumppaniolioon voi myös määritellä metodeita aivan samoin kuin muihinkin yksittäisolioihin.
Lisätään kokeeksi yksi metodi Asiakas
-kumppaniolioon.
object Asiakas {
private var montakoLuotu = 0
def tulostaMaara() = {
println("On luotu " + this.montakoLuotu + " asiakasoliota.")
}
}
Nyt käskyllä Asiakas.tulostaMaara()
saa tulostettua raportin luotujen asiakasolioiden
lukumäärästä.
Kumppaniolion tietotyyppi
Kuten todettu, kumppaniolio ei ole ilmentymä siitä luokasta, jonka kumppani se on, vaan erillinen olio. Se ei siis ole tuon luokan määrittelemää tietotyyppiä.
Kullakin kumppanioliolla on oma tietotyyppinsä, johon ei kuulu
mikään muu olio. Samahan pätee yksittäisolioihin yleisemminkin
(luku 2.3). Tässä näkyy ero Asiakas
-luokan ilmentymien tyypin
ja Asiakas
-kumppaniolion tyypin välillä:
new Asiakas("Maija Mikälienen")res3: Asiakas = #1 Maija Mikälienen new Asiakas("Matti Mikälienen")res4: Asiakas = #2 Matti Mikälienen Asiakasres5: Asiakas.type = Asiakas$@185ea23
Asiakas
on tyyppi, jonka ilmentymiä ovat
kaikki asiakasluokasta luodut oliot.Asiakas.type
on asiakasluokan
kumppaniolion (eikä minkään muun olion)
tietotyyppi.static
?
Aiemmin muilla kielillä ohjelmoineille tiedoksi: Scalan
kumppaniolioilla (kuten joillakin muillakin yksittäisolioilla)
on Scala-ohjelmissa samansuuntainen tehtävä kuin esimerkiksi
Java-ohjelmissa on sellaisilla muuttujilla ja metodeilla, jotka
on määritelty sanaa static
käyttäen. Scalassa ei tarvita (eikä
ole) static
-määrettä, vaan kaikki hoidetaan olioilla.
Kumppaniolioiden käyttötarkoituksia
Luokan ilmentymälaskuri on klassinen johdantoesimerkki, joka selventää käsitteitä, mutta yleisemmin käyttötarkoituksia kumppaniolioille ovat:
- Vakiot: Kumppaniolio voi olla hyvä sijoituspaikka vakioille (luku 2.6).
Monet vakiot liittyvät tiettyyn luokkaan yleisesti eivätkä ole
ilmentymäkohtaisia. Esimerkiksi Stars-projektin
StarCoords
-luokan kumppanioliossa on määritelty vakiotMinValue
jaMaxValue
, joihin on tallennettu koordinaattien raja-arvot (-1.0 ja +1.0). - Apufunktiot: Jos on tarvetta kyseiseen luokkaan liittyville apufunktioille, jotka eivät ole ilmentymäkohtaisia, niin kumppanioliota voi käyttää pakkausolion tapaan sijoituspaikkana näille funktiolle. Tällaiset apufunktiot voivat olla yksityisiä tai julkisia.
- Tehdasmetodit:
Tehdasmetodeista
Laaditaan Asiakas
-kumppaniolioon vielä yksi metodi nimeltä luoUusi
:
object Asiakas {
private var montakoLuotu = 0
def tulostaMaara() = {
println("Tähän mennessä on luotu " + this.montakoLuotu + " asiakasoliota.")
}
def luoUusi(nimi: String) = new Asiakas(nimi)
}
Kutsulla Asiakas.luoUusi("Sasha")
saadaan nyt siis aikaan täsmälleen sama kuin suoralla
luontikäskyllä new Asiakas("Sasha")
.
Lisäesimerkki
StarCoords
-kumppanioliossa on fromPercentages
-niminen
tehdasmetodi, jota voit halutessasi tutkia.
Tällaista metodia sanotaan tehdasmetodiksi (factory method). Sen tehtävänä on ainoastaan tuottaa ja palauttaa uusi olio.
Huomaa, että laatimamme tehdasmetodi on nimenomaan Asiakas
-kumppaniolion metodi eikä
Asiakas
-luokan, koska uuden asiakkaan luominenhan ei ole olemassa olevaan asiakasluokan
ilmentymään liittyvä toiminto.
Muokataan tehdasmetodiamme vielä hieman: vaihdetaan sen nimeksi apply
.
object Asiakas {
private var montakoLuotu = 0
def tulostaMaara() = {
println("Tähän mennessä on luotu " + this.montakoLuotu + " asiakasoliota.")
}
def apply(nimi: String) = new Asiakas(nimi)
}
apply
-metodin erikoisluonteen tuntien voimmekin nyt luoda uusia asiakasolioita näin:
Asiakas("Sasha")
ilman new
-sanaa. Tämä käskyhän laventuu muotoon Asiakas.apply("Sasha")
.
Monet Scala-ohjelmien tehdasmetodit ovat nimenomaan apply
-nimisiä, jolloin olioita voi
luoda mahdollisimman yksinkertaisesti.
Tuttuja tehtaita
Scalalla ohjelmoidessa tehdasmetodien käyttämiseltä on vaikea välttyä, vaikka niitä ei itse laatisikaan. Olet itse asiassa jo itsekin käyttänyt niitä, ja on hyvä tiedostaa tämä.
Olet esimerkiksi luonut Vector
-olioita käskyillä kuten Vector(4, 10, 2)
ilman
new
-sanaa. Tuo käsky on lyhyempi versio käskystä Vector.apply(4, 10, 2)
, joka on
Vector
-luokan kumppaniolion apply
-metodin kutsu. Kyseinen apply
-metodi on
tehdasmetodi, joka luo ja palauttaa vektoriolion. Vektorin luominen käskyllä
new Vector(4, 10, 2)
ei edes toimi, koska kyseiselle luokalle ei ole tuollaista
luontitapaa määritelty.
Muitakin tehdasmetodeja olet jo tavannut:
- Olet käyttänyt ilmaisuja kuten
Some(newFave)
. Tuolloinkaan et käyttänytnew
-sanaa vaan tehdasmetodia.new
-sanan käyttö tässä tapauksessa olisi ollut sallittua mutta turhaa. - Olet luonut kuvia esimerkiksi
circle
- jarectangle
-funktioilla, jotka ovat pakkausoliono1
-metodeita. Niitäkin voi sanoaPic
-olioita luoviksi tehdasmetodeiksi. - Samoin esimerkiksi
Pic("face.png")
kutsuu tehdasmetodia: se on lyhennysmerkintäPic
-luokan kumppaniolionapply
-kutsulle.
Se, käytetäänkö tietyn luokan yhteydessä tehdasmetodia vai new
-sanaa, riippuu siitä,
miten kyseinen luokka on määritelty ja millaisia tehdasmetodeja mahdollisesti on
tarjolla. Tällä kurssilla sinun ei tarvitse osata itse suunnitella, millaisissa
tilanteissa kannattaisi määritellä tehdasmetodi; sikäli, kun tehdasmetodeita käytämme,
ne esitellään kurssimateriaalissa kuten tähänkin asti.
Miksi tehdasmetodeita?
Voi herätä ajatus, että ihan kiva, mutta mikä näissä tehdasmetodeissa
nyt on niin houkuttelevaa. Eikö tavallinen olioiden luominen
new
-sanalla riittäisi?
Asiakasluokkamme tapauksessa ehkä riittäisikin. Toki sitä, että
asiakasolion saa nyt luotua ilman new
-sanaa voi joku pitää
riittävänä valopuolena.
Miksi tehdasmetodit sitten ovat esimerkiksi Scala API -kirjastossa niin yleisiä?
Onnetonta kyllä, syitä tehdasmetodien käytölle on vaikeaa käsitellä syvällisesti kurssilla opitun asian pohjalta, mutta perustavoitteen voimme todeta. Se on joustavuus. Vertaa:
- Olion luominen
new
-sanalla, luokan nimellä ja konstruktoriparametreilla kietoo erottamattomasti yhteen kaksi asiaa:- mitä parametritietoja olion luomiseksi annetaan, ja
- minkä nimenomaisen luokan ilmentymä syntyy.
- Tehdasmetodilla nuo kaksi asiaa voi erottaa toisistaan.
Niinpä voidaan:
- Laatia useita eri metodeita, jotka luovat
ja palauttavat keskenään samantyyppisiä
arvoja erilaisilla perusteilla. Esimerkiksi
tehdasmetodit
Pic
,circle
jarectangle
kaikki tuottavatPic
-olioita erilaisista parametreista. - Laatia tehdasmetodi, joka ei aina palauta saman luokan ilmentymää vaan valitsee vaikkapa parametriensa perusteella, minkälainen olio luodaan ja palautetaan.
- Laatia useita eri metodeita, jotka luovat
ja palauttavat keskenään samantyyppisiä
arvoja erilaisilla perusteilla. Esimerkiksi
tehdasmetodit
Tehdasmetodien mielekkyydestä lisää Ohjelmointistudio 2 -kurssilla keväällä.
Yhteenvetoa
- Jos oliolla on
apply
-niminen metodi, se toimii ikään kuin "oletusmetodina": kutsuolio(parametrit)
laventuu automaattisesti muotoonolio.apply(parametrit)
.apply
-metodillista oliota voi siis käyttää "vähän kuin se olisi funktio".- Tätä tekniikkaa on käytetty mm. Scalan
merkkijono- ja vektoriluokissa. Niiden
apply
-metodit palauttavat parametrin määrämälle indeksille tallennetun tiedon.
- Joskus on tarpeen kuvata sellaisia ominaisuuksia tai toimintoja,
jotka eivät liity yksittäisiin luokan ilmentymiin (olioihin) vaan
luokkaan itseensä. Tähän tarkoitukseen voi Scalassa määritellä
luokalle kumppaniolion.
- Kumppaniolio on hyvä paikka esimerkiksi luokkaan liittyville vakioille.
- Tehdasmetodi on metodi, jonka tehtävä on luoda ja palauttaa uusi olio.
- Niitä käytetään useiden valmiiden
Scala-luokkien yhteydessä suoran
new
-käskyllä instantioinnin sijaan. - Scalassa tehdasmetodit ovat usein
kumppaniolioiden
apply
-metodeita, jolloin olioita voi luodaLuokanNimi(parametrit)
-muotoisilla käskyillä, esim.Vector(432, 32, 223)
.
- Niitä käytetään useiden valmiiden
Scala-luokkien yhteydessä suoran
- Lukuun liittyviä termejä sanastosivulla:
apply
; kumppaniolio; tehdasmetodi.
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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
Asiakas
lisäksi luokan kumppaniolio (companion object). Kumppaniolio on yksittäisolio, jolle annetaan prikulleen sama nimi kuin luokalle itselleen ja jonka määrittely kirjoitetaan samaan tiedostoon.