Luku 2.3: Luokat olioiden tyyppeinä
Luokat
Kun ihminen näkee tuolin, hän kokee sen sekä kirjaimellisesti "juuri tuona tiettynä asiana" että abstraktisti "tuona tuolimaisena asiana". Abstraktio syntyy ihmismielen ihmeellisestä kyvystä sulauttaa samankaltaisia kokemuksia toisiinsa. Se ilmenee mielessä platonilaisena tuolin ideana eli tuoliutena.
—Dan Ingalls (käännetty englanninkielisestä lähteestä)
Kuvittele tilanne, jossa täytät paperille vaikkapa opintotukihakemusta. Et tietenkään aloita kirjoittamaan tyhjälle arkille, vaan käytät valmiiksi suunniteltua lomaketta, joka kertoo, mitä asioita hakemukseen kuuluu kirjata. Olisi aivan epäkäytännöllistä, että jokainen hakija kirjoittaisi lomakkeenkin itse, kun tarvittavat tiedot ovat kuitenkin kaikille samat. On parempi käyttää määrättyä lomaketyyppiä, josta voi luoda mielivaltaisen määrän kopioita täytettäviksi.
Palauta seuraavaksi mieleen luvussa 2.1 näkemäsi esitykset GoodStuff-ohjelman ja kuvitteellisen kurssi-ilmoittautumissovelluksen toiminnasta. Niissähän esiintyi lukuisia keskenään samankaltaisia olioita: monta kurssia, monta opiskelijaa ja monta kokemusoliota. Keskenään samankaltaisilla olioilla oli samantyyppiset tiedot ja samantyyppinen toimintatapa; esimerkiksi jokainen kurssiolio piti kirjaa kurssikoodista, opetuspaikasta ja ilmoittautuneista opiskelijoista.
Aivan kuin opintotukilomakkeita täyttäessä, on myös olioiden tapauksessa aivan tarpeetonta ja epäkäytännöllistä, että jokainen olio määriteltäisiin aina uutena yksittäistapauksena, kuten olemme tähän mennessä tehneet.
Pelkkien yksittäisolioiden käytössä on muitakin haasteita. Ohjelmoija kirjaa kunkin yksittäisolion tiedot ohjelmakoodiin erikseen, mutta tietääkö hän ohjelmaa kirjoittaessaan täsmälleen, kuinka paljon ja millaisia olioita ohjelman eri suorituskertojen aikana tullaan tarvitsemaan? Yleensä ei. Tarvittavien olioiden lukumäärä ja niiden muuttujien arvot määräytyvät usein vasta dynaamisesti, ohjelmaa ajettaessa, ja ne voivat riippua käyttäjän antamasta syötteestä. Esimerkiksi GoodStuff-sovelluksessa käyttäjä syöttää kirjattavien kokemusten tiedot.
Ohjelmoija voi sen sijaan kyllä määritellä etukäteen, minkälaisia olioita ohjelman ajon aikana tullaan tarvitsemaan eli minkä tyyppisistä tiedoista ohjelman on pidettävä kirjaa ja mitä toimintoja näihin tietoihin liittyy.
Tässä luvussa tutustumme olioiden tyyppeihin eli luokkiin (class) ja siihen, miten olioita luodaan luokkien kautta. Tällä tavoin määritellyt oliot ovat paljon yksittäisolioita yleisempiä.
Edetään tutun kaavan mukaan: Tässä luvussa koekäytät valmiiksi määriteltyjä luokkia. Sitten tulevissa luvuissa perehdyt siihen, miten luokkien ohjelmakoodi kirjoitetaan.
Oliot luokkien ilmentyminä
Seuraava esitys kuvaa luokkien ja olioiden välistä suhdetta lomakevertauskuvalla:
Perusajatus on siis se, että ohjelmoijina määrittelemme
luokkia ("lomaketyyppejä"),
sen, missä yhteyksissä luokkien kuvaaman kaltaisia olioita luodaan ("milloin otetaan uusi kopio lomakepohjasta täytettäväksi"; esim. kun käyttäjä syöttää uuden kokemuksen tiedot), ja
sen, mistä olioiden muuttujille tällöin saadaan arvot ("lomakkeen sisältö").
Jäljempänä käytämme tästä aiheesta usein seuraavia teknisempiä termejä. Paina ne mieleen.
Olion suhdetta luokkaan kuvataan sanomalla, että olio on luokan ilmentymä eli instanssi (instance) eli tietty yksittäistapaus luokasta.
Luokka on olion tietotyyppi (tai vain tyyppi; (data) type).
Uuden ilmentymän — siis olion — luomista luokkamäärittelyn perusteella sanotaan instantioinniksi (instantiation).
Luokan käyttö Scalalla
Tarkastellaan luokkaa Tyontekija
, joka kuvaa täsmälleen sen kaltaisia työntekijäolioita,
jollaisen loimme yksittäisoliona edellisessä luvussa. Ainoa mutta merkittävä ero
silloiseen on, että käytämme nyt yksittäisen olion sijaan tietotyyppiä, joka kuvaa
työntekijän käsitettä yleisesti. Sen avulla voimme luoda erinimisiä ja -palkkaisia
työntekijäolioita.
Kun käytössä on valmiiksi määritelty luokka, siitä voi luoda ilmentymän helposti. Alla on esimerkki työntekijäluokan käyttämisestä. Kokeile myös itse; käytettävä koodi on moduulissa Oliointro.
Luodaan ensin yksi uusi työntekijä:
Tyontekija("Eugenia Enkeli", 1963, 5500)res0: o1.luokkia.Tyontekija = o1.luokkia.Tyontekija@1145e21
Tässä välitetään luontiparametrit (eli konstruktoriparametrit;
constructor parameters) eli tiedot, joiden perusteella uusi
olio alustetaan. Tyontekija
-luokassa on määritelty, että
työntekijäoliota alustettaessa on annettava nimi, syntymävuosi
ja palkka. (Uusi työntekijä on oletusarvoisesti täyspäiväinen,
eli työaika on 1.0.) Se, millainen olio tulee luoduksi, riippuu
luokan ohjelmakoodista ja näistä parametriarvoista.
Instantiointikäsky on lauseke. Tietokone evaluoi sen varaamalla olion tiedoille muistista tilaa ja alustamalla olion luokassa määritellyllä tavalla.
Lausekkeen arvo on viittaus uuteen olioon. Se on tässä tyyppiä
Tyontekija
.
Kun arvona on viittaus olioon, eikä muunlaista tapaa tulostaa kyseinen olio ole määritelty, tulostuu REPLiin rimpsu, joka liittyy olioiden toteutukseen Scalassa. Voit nyt ajatella rimpsun tarkoittavan "viittaus erääseen työntekijäolioon".
On usein kätevää sijoittaa muuttujaan viittaus, joka osoittaa juuri luotuun olioon, kuten alla. Tässä luodaan saman luokan toinen ilmentymä ja sijoitetaan muuttujaan viittaus uuteen ilmentymään:
val juuriPalkattu = Tyontekija("Teija Tonkeli", 1985, 3000)juuriPalkattu: o1.luokkia.Tyontekija = o1.luokkia.Tyontekija@704234
Nyt muuttujan nimeä voi hyödyntää vaikkapa olion metodeita kutsuessa:
println(juuriPalkattu.kuvaus)Teija Tonkeli (s. 1985), palkka 1.0 * 3000.0 euroa
Huomasithan, että tässä luotiin kaksi kokonaan erillistä työntekijäoliota? Niillä on eri tiedot, mutta niille on kuitenkin yhteistä muun muassa se, että niillä on jotkin nimet, syntymävuodet ja kuukausipalkat.
Ilmentymistä, muuttujista ja viittauksista
Katsotaan pari lyhyttä animaatiota.
Jo edellinen animaatio näytti, että olioita käsitellään viittausten kautta kuten puskureitakin luvussa 1.5 käsiteltiin. Seuraava animoitu esimerkki korostaa viittausten merkitystä.
Tee animaation alussa pyydetty ennustus ja katso animaatio huolellisesti.
Viittaukset olioihin, viittaukset puskureihin
Ei ole sattumaa, että olioihin viitataan samalla tavalla kuin luvussa 1.5 käsiteltyihin puskureihin. Puskuritkin kun ovat olioita. Tästä lisää luvussa 4.2.
Pikkutehtäviä olioista, luokista ja viittauksista
Vaikka vähän toiston puolelle jo meneekin, niin kerrataan vielä pienillä kysymyksillä keskeisiä käsitteitä, joiden hahmottaminen on jatkon kannalta aivan ratkaisevaa.
Luokkien merkityksestä... tai merkityksistä
Luokat tietotyyppeinä
"Tietotyyppi"-termi on putkahtanut esiin paitsi tässä luvussa myös ennen kuin mainitsimme
luokat, ja maininnoilla on yhteys. Esimerkiksi Tyontekija
-luokka on työntekijäolioiden
tietotyyppi ja GoodStuff-ohjelman luokka Experience
kokemusolioiden tietotyyppi juuri
samassa mielessä kuin Int
-tietotyyppi edustaa kokonaislukuja.
Ohjelmissa käytetään monenlaisia tietotyyppejä:
ohjelmoijan itsensä tai toisten saman hankkeen parissa työskentelevien ohjelmoijien määrittelemät tietotyypit, jotka liittyvät kiinteästi sovelluksen aihepiiriin (esim.
Tyontekija
taiExperience
),kieleen kiinteästi kuuluvat tietotyypit (esim.
Int
,String
), jamuut enemmän tai vähemmän yleishyödylliset tietotyypit, jotka on otettu käyttöön jostakin ohjelmakirjastosta. Ne voivat olla jonkin kolmannen tahon laatimia. (Esim. puskurityyppi
Buffer
Scalan oheiskirjastossa taiPic
kurssimme kirjastossa.)
Yksittäisolioiden tietotyypit
Yksittäisolioillakin on tietotyyppinsä, kullakin ihan omansa. Kokeillaan vaikkapa luvun 2.1 papukaijalla:
papukaijares1: o1.yksittaisia.papukaija.type = o1.yksittaisia.package$papukaija$@48ba9542
Papukaija on tyyppiä, johon ei kuulu
mikään muu olio ja joka on merkitty
tässä papukaija.type
.
Kun määrittelet yksittäisolion, tulet samalla määritelleeksi sille oman tietotyypin. Yksittäisolioiden tietotyyppejä ei ole yleensä tarpeen nimenomaisesti koodissa käsitellä.
Itse asiassa
Int
-tietotyyppikin on (hieman erikoinen) luokka, mutta palataan
siihen asiaan luvussa 5.2.
Luokat käsitteellisen mallin osina
Luvussa 2.1 todettiin olio-ohjelmoinnin erääksi tavoitteeksi käsitteellisten mallien laatiminen. Luokat antavat hienoja uusia mahdollisuuksia käsitteelliseen mallintamiseen! Siinä missä oliot kuvaavat malliin kuuluvia yksittäisiä asioita (esim. tietyt työntekijät), luokat kuvaavat yleiskäsitteitä, joiden ilmentymiä nuo asiat ovat (esim. työntekijä käsitteenä). Olio-ohjelmoijan huomio usein kiinnittyykin luokkiin ja siihen miten luokat yhdessä kuvaavat yleisellä tasolla ne mahdolliset olioiden maailmat, joita syntyy ohjelma-ajojen aikana.
Yksittäisoliot vs. luokat
On itse asiassa melko harvinaista, että ohjelmointikielessä edes voi määritellä yksittäisolioita ohjelmakoodiin siihen tapaan kuin Scalassa. Monissa yleisesti käytetyissä olio-ohjelmointia tukevissa kielissä oliot luodaan aina luokista instantioimalla. Niin Scalassakin useimmiten.
Eivät luokat tosin olio-ohjelmoinnin ehdoton edellytys ole. Joissakin kielissä (joista suosituimpana JavaScriptissä) olioita kloonataan toisista olioista, ja yksittäinen olio voi toimia toisten samankaltaisten prototyyppinä. Siitä lisää esimerkiksi Wikipediassa.
Luokat abstraktioina
Useassa aiemmassa luvussa on puhuttu abstraktioista, ja olemme jälleen kohdanneet uudenlaisen abstrahoinnin muodon. Luokat ovat abstraktioita — yleistyksiä — toisaalta olioista ja toisaalta mallinnettavan aihepiirin käsitteistä.
Luokat ohjelmakoodin osina
Scalalla kirjoitetun olio-ohjelman koodi koostuu luokkien ja yksittäisolioiden määrittelyistä.
Usean luokan määrittelyt voi kirjoittaa samaan kooditiedostoon. Tämä on Scala-ohjelmissa tapana vain silloin, jos luokat liittyvät toisiinsa poikkeuksellisen kiinteästi. Tällä kurssilla yleensä määrittelet kunkin luokan omaan Scala-kooditiedostoonsa.
On yleinen käytäntö nimetä kooditiedosto ja sen sisältämä luokka tai yksittäisolio
samoin. Esimerkiksi luokka Tyontekija
määritellään tiedostossa nimeltä
Tyontekija.scala
, jonka sisältöön tutustumme seuraavassa luvussa.
Luokista ja metodeista
A class is where we teach objects how to behave.
—Richard Pattis
Huomasitko aiempaa työntekijäanimaatiota katsoessasi, että olioiden tiedot on tallennettu erilleen luokan tiedoista? Ja että metodit löytyvät nimenomaan luokan yhteydestä eivätkä olioiden?
Luokan tiedot ovat kaikille sentyyppisille olioille yhteiset, eikä niitä tarvitse kopioida jokaiseen olioon erikseen. Kussakin oliossa on tallessa tieto siitä, minkä tyyppinen se on. Näin päästään käsiksi olion metodeihin tarvittaessa.
Luokka määrää sen ilmentymillä olevat metodit ja siis kyseisentyyppisten olioiden
käyttäytymismallin. Esimerkiksi kaikilla Tyontekija
-luokan ilmentymillä on kuukausikulut
-
ja kuvaus
-metodit. Kukin olioista vastaa samaan tapaan, kun noita metodeita kutsutaan.
Jokainen olio kuitenkin nojaa omiin oliokohtaisiin tilatietoihinsa.
Olioiden rajoituksista
Luokan ilmentymää voi komentaa tekemään vain niitä tiettyjä asioita, joita se osaa, eli
joita sen edustamaan luokkaan on määritelty. Oikeannimisen metodin täytyy olla olemassa,
ja parametrien täytyy olla täsmälleen halutunlaiset. Kaikki seuraavat yritykset komentaa
Tyontekija
-tyyppistä oliota epäonnistuvat:
var esimerkki = Tyontekija("Matti Mikälienen", 1965, 5000)res2: o1.luokkia.Tyontekija = o1.luokkia.Tyontekija@177e207 esimerkki.syoKaalikeittoa("nam nam")-- Error: |esimerkki.syoKaalikeittoa("nam nam") |^^^^^^^^^^^^^^^^^^^^^^^ |value syoKaalikeittoa is not a member of o1.luokkia.Tyontekija esimerkki.ikaVuonna(2014, 2024)-- Error: |esimerkki.ikaVuonna(2014, 2024) | ^^^^^^^^^^ | Found: (Int, Int) | Required: Int esimerkki.ikaVuonna("2024")-- Error: |esimerkki.ikaVuonna("2024") | ^^^^^^ | Found: ("2024" : String) | Required: Int
Jos metodi löytyy mutta unohdat parametrit, ei välitöntä virhettä tule, mutta tulos ei ole haluttu ja näyttää kummalta:
esimerkki.ikaVuonnares3: Int => Int = Lambda$1354/0x0000000801109800@3a5ce4b8
Tuo sokellus tarkoittaa oleellisesti sitä, että Scala raportoi tulkinneensa lausekkeen
esimerkki.ikaVuonna
olevan eräs funktio. Kuitenkaan se ei kutsunut tuota funktiota eikä
selvittänyt työntekijän ikää, mikä olisikin ollut mahdotonta, koska parametriarvoa ei
annettu. (Oudossa käytöksessä on taustalla ihan ideaakin, johon pääsemme kiinni vasta
luvussa 6.1.)
Luokankäyttöharjoitus
Oliointro-moduulissa on Tyontekija
-luokan lisäksi muutakin. Luokka Puhelinlasku
kuvaa
(toki taas yksinkertaistetusti) telefirman asiakkaiden puhelinlaskuja; yksi sen ilmentymä
on tietyn asiakkaan lasku. Yhteen puhelinlaskuun liittyy jokin määrä puheluita (nolla tai
yli), joista kutakin kuvataan yhtenä Puhelu
-oliona eli Puhelu
-luokan ilmentymänä.
Harjoittele näiden luokkien käyttöä REPLissä alla olevien ohjeiden mukaisesti. Kumpikin luokka on annettu valmiina. Sinun ei tässä tehtävässä ole tarkoitus laatia niitä tai mitään muitakaan luokkia itse.
Jos tämä tehtävä tuntuu vaikealta, saattaa olla hyödyksi poiketa ensin hieman alempana olevassa Sudenkuoppia-osiossa ja palata sitten tehtävän pariin.
Puhelu
-luokka
Luokan Puhelu
piirteitä tarkemmin:
Puhelu
-oliota luotaessa on annettava luontiparametreiksi puhelun alkuhinta, puhelun minuuttihinta sekä kesto minuutteina. Kaikki nämä kolme ovat desimaalilukuja; hinnat ovat euroissa.Puhelu
-luokka määrittelee, että kultakin puheluoliolta voi tiedustella puhelun kokonaishintaa parametrittomallakokonaishinta
-metodilla. Puheluolio osaa laskea kokonaishintansa luontiparametrien perusteella ja samalla lisätä siihen paikallisverkkomaksun.Vastaavasti
Puhelu
-olioilta voi pyytää sanallisen selityksen puhelutiedoista parametrittomallakuvaus
-metodilla.
Voit kokeilla luokan käyttöä REPLissä myös vapaasti, mutta tee ainakin seuraavat asiat:
Luo uusi puheluolio ja määrittele muuttuja, johon tallennat viittauksen luomaasi olioon.
Valitse muuttujan nimi itse. Se voi olla esimerkiksi
soittoJennille
.Kirjaa puhelun alkuhinnaksi 0,99 euroa, minuuttihinnaksi 0,47 euroa ja kestoksi 7,5 minuuttia. (Käytä desimaalierottimena kuitenkin pistettä, kuten Scalassa on tapana.)
Kutsu puheluolion
kokonaishinta
-metodia. Huomaa, että olio on lisännyt paluuarvoon myös paikallisverkkomaksun, joka on 13 senttiä plus 1,3 senttiä per minuutti.Kutsu puheluolion
kuvaus
-metodia. (Hinnat näkyvät sen tuottamassa kuvauksessa pyöristettyinä; se ei ole tässä tärkeää.)
Puhelinlasku
-luokka
Eräitä luokan Puhelinlasku
piirteitä tarkemmin:
Puhelinlasku
-oliota luotaessa annetaan yksi luontiparametri: asiakkaan nimi merkkijonona.Laskuolioiden
lisaaPuhelu
-metodille annetaan parametriksi viittaus puheluolioon. Kun tätä vaikutuksellista metodia kutsutaan tietylle laskuoliolle, niin olio lisää kyseisen puhelun "itseensä" eli kyseiseen puhelinlaskuun.Laskuolioiden
kokonaishinta
-metodi on parametriton ja palauttaa kaikkien laskuun lisättyjen puheluiden hintojen summan.Myös
erittely
-metodi on parametriton. Se palauttaa monirivisen merkkijonon, jossa on kaikkien laskuun lisättyjen puheluiden hintatiedot.
Tee ensin seuraavat järjestyksessä ja kokeile sitten halutessasi muutakin:
Luo yksi puhelinlaskuolio ja muuttuja, joka viittaa siihen. Valitse itse sekä asiakkaan nimi (esim. oma nimesi) että muuttujan nimi (esim.
munLasku
).Lisää aiemman ohjeen mukaan luomasi puheluolio juuri luomaasi laskuun. Käytä puheluolioon viittaavan muuttujan nimeä parametrilausekkeena.
Lisää vielä toinenkin puheluolio samaan laskuun:
Kirjaa puhelun alkuhinnaksi 1,2 euroa, minuuttihinnaksi 0,4 euroa ja kestoksi 30 minuuttia.
Voit hoitaa puheluolion luomisen ja laskuun lisäämisen näpsäkästi yhdellä rivillä: kirjoita luokan nimellä alkava olionluomiskäsky
lisaaPuhelu
-metodin parametrilausekkeeksi. Näin viittaus juuri luotuun olioon välittyy saman tien metodin parametriksi.
Kutsu laskun
kokonaishinta
-metodia.Kutsu laskun
erittely
-metodia.Kirjoita kahdessa edellisessä kohdassa saamasi kokonaishintametodin ja erittelymetodin paluuarvot seuraavaan lomakkeeseen saadaksesi tehtäväpisteitä.
Päivitetty käsitekaavio
Sijoitetaan tässä välissä uudet käsitteemme kaavioon, joka on jatkoa luvun 2.2 vastaavalle.
Sudenkuoppia
Olio-ohjelmoinnin peruskäsitteisiin liittyy eräitä sudenkuoppia, joihin moni ohjelmoinnin vasta-alkaja on haksahtanut. Vältä sinä ne. Tässä luvussa asia on yritetty esittää niin, etteivät seuraavat virhekäsitykset pääsisi syntymään, mutta alleviivataan hieman vielä.
Luokat eivät ole oliosäiliöitä
On melko yleinen aloittelijan virhe ajatella, että luokat ovat jonkinlaisia olioiden ryhmiä tai varastoja, ja että oliot vastaavasti jollain tapaa sijoittuvat luokan sisään. Mutta: luokka on kuvaus tietynlaisten olioiden yhteispiirteistä eikä tällaisten olioiden säilömiseen tarkoitettu paikka. Tai aiempaa vertausta jatkaen: luokka on "lomaketyyppi" eikä "kansiollinen täytettyjä lomakkeita".
Käytännön merkitystä tällä on esimerkiksi sikäli, että luokan nimen kautta ei pääse käsiksi vaikkapa luetteloon sen "sisältämistä" olioista. Olioilla ei myöskään ole "luokan sisällä" (jossa ne eivät siis ole) mitään keskinäistä järjestystä, johon nojaten niitä voisi käsitellä. Olioihin viittaamiseen käytetään muuttujia.
On toki yleistä, että haluamme käsitellä useita toisiinsa liittyviä olioita ryhmänä. Silloin voimme panna oliot kokoelmaan, esimerkiksi puskuriin. Myöhemmissä luvuissa teemme tätä usein.
Luokan ilmentymillä ei ole "nimiä"
Koodiesimerkkejä lukiessa saattaa syntyä se virheellinen vaikutelma, että ainakin joidenkin luokkien ilmentymiksi luoduilla olioilla olisi sisäänrakennettu nimi tai vastaava tunniste, jolla noihin olioihin voisi ohjelmakoodissa viitata. Esimerkiksi työntekijäluokasta luomillamme ilmentymillä oli eräänä ominaisuutena nimi, pankkitililuokan ilmentymillä voisi olla kullakin oma tilinumero, kurssiolioilla kurssikoodi ja niin edelleen.
Kuitenkaan mitään mainituista ei voi käyttää ohjelmakoodissa olioon viittaamiseen. Se,
että kirjoittaa työntekijän nimen pisteen eteen — "Matti Mikälienen".ikaVuonna(2024)
— toimii täsmälleen yhtä kehnosti kuin se, että kirjoittaisit nimen sijaan olion palkan
tai työajan. Nimi, palkka ja työaika ovat kaikki olion sisältämiä tietoja, mutta mistään
niistä ei ole apua olioon viittaamisessa sen ulkopuolelta. Tähän käytetään muuttujia ja
niihin tallennettuja viittauksia kuten yllä olevissa esimerkeissä.
Tältä osin luokkien ilmentymät eroavat yksittäisolioista, joille on ohjelmakoodissa
määritelty nimi, joka toimii juuri kyseisen olion tunnisteena. Olemme esimerkiksi
viitanneet yksittäisolioihin nimeltä tili
ja papukaija
määrittelemättä muuttujia,
joihin tallentaisimme viittauksen näihin olioihin.
Toki voimme haluta laatia ohjelman, joka etsii olioiden joukosta sen, jolla on tietty piirre, esimerkiksi tietty nimi tai kurssikoodi. Tämä on erillinen ongelma, johon löytyy erilaisia ratkaisuja. Niistä lisää muun muassa luvuissa 5.5 ja 9.2.
Olioon viittaava muuttuja ei ole olion nimi
var esimerkki = Tyontekija("Matti Mikälienen", 1965, 5000)
Jos tätä Scala-koodiriviä ajattelee yhtenä kokonaisuutena, voi syntyä ajatus, että
rivillä luodaan Tyontekija
-tyyppinen olio, jonka nimeksi asetetaan esimerkki
.
Tulkinta ei pidä paikkaansa, vaan rivillä tehdään useita asioita: määritellään muuttuja,
luodaan olio luokan ilmentymäksi ja lopuksi sijoitetaan muuttujaan viittaus, joka
osoittaa luotuun olioon.
Aiemmista esimerkeistä olet jo nähnyt, että usea eri muuttuja voi samanaikaisesti
sisältää viittauksen samaan paikkaan muistissa (samaan olioon) ja että yksikin
var
-muuttuja voi viitata suorituksen eri vaiheissa eri olioihin. Niinpä on tärkeää
muistaa, ettei esimerkki
ole olion vaan muuttujan nimi. Asia käy konkreettisesti ilmi
kurssimateriaalin animaatioista.
Miksi ilmentymillä ei ole nimiä?
Mikään ilmentymän ominaisuuksista ei siis ole ilmentymään viittaamiseen kelpaava tunniste, eikä ilmentymään viittaavan muuttujan nimikään ole ilmentymän itsensä ominaisuus. Miksi muuttujat nimetään eikä luokkien ilmentymät?
Syitä ovat muun muassa seuraavat kaksi. Nämä syyt konkretisoituvat vähitellen, kun olio-ohjelmointikokemuksesi karttuu.
Yhteen olioon voi viitata ohjelmassa useasta eri kohdasta, ja oliolla voi olla erilaisia "rooleja" eri kohdissa. Siksi on perusteltua viitata siihen eri nimillä eri kohdista. Vertaa reaalimaailman roolit: "presidentti", "kokouksen puheenjohtaja", "aviovaimoni", "äitini", "joku vastaantulija", "seuraava asiakas", "nettitilauksen tekijä" ja "lentomatkustaja" voivat kaikki viitata yhteen ja samaan kohteeseen, kun eri tahot tarkastelevat henkilöä eri näkökulmista.
Usein halutaan, että tietyssä "roolissa" oleva olio voi vaihtua toiseen ohjelman suorituksen aikana. Silti halutaan yhdellä tietyllä nimellä päästä käsiksi siihen olioon, joka sillä hetkellä toimii kyseisessä roolissa. Yksi esimerkki on GoodStuff-ohjelman suosikkikokemus, joka voi vaihtua ohjelma-ajon aikana. (Ja vertaa taas reaalimaailmaan: presidenttinä, seuraavana asiakkaana tai aviopuolisona oleva henkilö voi vaihtua.)
Pikkutehtävä: muuttujat ja olion tilaan vaikuttaminen
Lisää harjoitusta: kuvat olioina
Edellisellä kierroksella loimme kuvia Pic
-tietotyypin avulla. Pic
on itse asiassa
luokan nimi, ja kukin Pic
-tyyppinen arvo on olio, joka pitää sisällään tiedon siitä,
mitä kuvassa on.
Pic
-tyyppi sopii monenlaisten kuvien esittämiseen. Jotta erisorttisten kuvien luominen
olisi kätevää, o1
-pakkaukseen on määritelty useita eri funktioita, jotka luovat Pic
-olioita. Esimerkkejä näistä ovat circle
- ja rectangle
-funktiot: ne luovat uuden
kuvaolion ja palauttavat viittauksen luomaansa olioon. Emme ole luoneet kuvioita
kirjoittamalla Pic(...)
, vaan olemme käyttäneet noita näppäriä apufunktioita.
Toistaiseksi olemme käyttäneet Pic
-tyyppisiä arvoja vain antamalla niitä parametriksi
show
-funktiolle, jolla kuvan saa näkyviin. Mutta kuvaolioilla on myös runsas valikoima
metodeita. Alla on kuvattu niistä muutama.
Kuvan perusominaisuuksia
Kuvaoliolta voi kysyä sen leveyden ja korkeuden.
val pikkuympyra = circle(100, Red)pikkuympyra: Pic = circle-shape pikkuympyra.widthres4: Double = 100.0 pikkuympyra.heightres5: Double = 100.0 val isompi = circle(pikkuympyra.width * 3, Red)isompi: Pic = circle-shape isompi.widthres6: Double = 300.0
Samat toiminnot ovat käytettävissä myös toisin muodostetuille kuvaolioille:
val ladattu = Pic("ladybug.png")ladattu: Pic = ladybug.png println("Kuvassa on yhteensä " + ladattu.width * ladattu.height + " pikseliä.")Kuvassa on yhteensä 900.0 pikseliä.
Kuvien "muokkausta"
Kuvaa voi pyörittää. Kokeile niin näet!
val suorakaide = rectangle(100, 200, Orange)suorakaide: Pic = rectangle-shape val kierretty = suorakaide.clockwise(45)kierretty: Pic = rectangle-shape (transformed) show(kierretty)show(suorakaide)
clockwise
-metodille annetaan parametriksi kierron suuruus
asteina.
Metodi palauttaa uuden kuvan, joka on käännetty versio alkuperäisestä. REPL kuvailee asian näin.
clockwise
-metodi siis kuitenkaan ei millään tavoin vaikuta
jo olemassa olevaan kuvaolioon (kuten ei mikään muukaan
Pic
-luokan metodeista). Alkuperäinen kuva on edelleen se,
mikä se olikin.
history
-metodi palauttaa tiedon siitä, miten kuva on luotu:
kierretty.historyres7: List[String] = List(clockwise, rectangle)
Tämä teksti kertoo, että kuva on luotu tekemällä ensin suorakaiteen kuva ja sitten kiertämällä sitä.
Kuvia yhteen
Muodostetaan kuva, jossa on kaksi suorakaidetta vierekkäin:
val eka = rectangle(50, 100, Red)eka: Pic = rectangle-shape val toka = rectangle(150, 100, Blue)toka: Pic = rectangle-shape val yhdistelma = eka.leftOf(toka)yhdistelma: Pic = combined pic show(yhdistelma)
leftOf
-metodia vastaavasti voit käyttää myös metodeita rightOf
, above
ja below
.
Kokeile! Esimerkiksi Pong-pelin (luku 1.2) pelikenttä on muodostettu tähän tyyliin
kuvioita yhdistelemällä. Alla olevassa esimerkissä puolestaan rakennetaan yhdistelmä,
jossa sama kuva toistuu kaksi-kertaa-kaksi muodostelmassa:
val testikuva = Pic("https://en.wikipedia.org/static/images/project-logos/enwiki.png")testikuva: Pic = https://en.wikipedia.org/static/images/project-logos/enwiki.png val vierekkain = testikuva.leftOf(testikuva)val vierekkain: Pic = combined pic show(vierekkain.above(vierekkain))
Viime kierroksella laadit mm. funktion pystypalkki
, joka palautti Pic
-tyyppisen arvon.
Viittauksia kuvaolioihin palauttavat myös seuraavissa tehtävissä laadittavat funktiot,
joista ensimmäinen, helpoin, on vapaaehtoinen.
Pikkutreeni
Kirjoita vaikutukseton funktio nelikko
, joka
ottaa parametrikseen kuvan, ja
palauttaa(!) kuvan, jossa annettu kuva toistuu kaksi kertaa kaksi -muodostelmassa.
Funktio siis tekee saman kuin hieman ylempänä annettu koodi mutta mielivaltaiselle parametrikuvalle.
Kirjoita funktio ensimmäiseltä kierrokselta tuttuun Aliohjelmia-moduuliin,
mutta nyt kierros2.scala
-tiedostoon.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lipputehtävä 1: Somalia
Laadi vaikutukseton funktio somalianLippu
, joka
ottaa ainoaksi parametrikseen
Double
n, joka on lipun leveys,palauttaa tuonlevyisen kuvan Somalian lipusta, jonka
korkeus on kaksi kolmasosaa leveydestä,
tähden leveys on neljä kolmastoistaosaa koko lipun leveydestä, ja
värit ovat
RoyalBlue
jaWhite
.
Kirjoita funktio kierros2.scala
an Aliohjelmia-moduulissa. (Huomaa tiedoston nimi.)
Viisisakaraisen tähden voi luoda o1
-pakkauksen metodilla star
, jolle annetaan
parametreiksi leveys ja väri. Tähden saa paikoilleen onto
-metodilla, josta on tässä
toisenlainen käyttöesimerkki:
val tummaYmpyra = circle(300, Black)tummaYmpyra: Pic = circle-shape show(testikuva.onto(tummaYmpyra))
Kun lasket, älä unohda miten kokonaislukujen jakolasku toimii (luku 1.3):
> 2 / 3 res8: Int = 0 2 * 150.0 / 3res9: Double = 100.0
Funktiosi tulee vain palauttaa lipun kuva, ei laittaa sitä näkyviin. Sitä siis tulee voida käyttää esimerkiksi näin:
show(somalianLippu(400))
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lipputehtävä 2: Suomi
Tee samaan tiedostoon funktio suomenLippu
, joka
ottaa
Double
-tyyppisen leveysparametrin kuten edellinenkin funktio, japalauttaa tuonlevyisen kuvan Suomen lipusta, jossa on oheisen kuvan (ja siis virallisten säädösten) mukaiset mittasuhteet.
Käytä värejä White
ja Blue
.
Ohjeita ja vinkkejä:
Laske ensin "perusyksiköksi" tuo kuvassa x:ksi merkitty mitta. Muodosta sitä käyttäen oikean kokoiset lipun palaset suorakaiteina. Asemoi ne vierekkäin tai allekkain sopivasti. Älä käytä skaalausmetodia.
Huomaa, että funktion parametri kertoo lipun koko leveyden, ei tuota kuvassa x:llä merkittyä lyhyempää osaa.
Käytä paikallisia muuttujia.
Muista:
Pic
-oliot ovat muuttumattomia. Jos kohdistat kuvaan muokkaustoiminnon, syntyy uusiPic
; alkuperäinen ei muutu.Ks. esim. hepanpyöritysesimerkki ja muut tämän luvun esimerkit.
Muista, ettei tämänkään funktiosi kuulu laittaa lippua näkyviin
show
’lla.Voit kokeilla, osaatko ratkaista tehtävän sekä ilman
onto
-metodia että sen avulla.
Jos lippuusi jää "ohuita valkoisia rakoja", katso tämä
Valitsemastasi ratkaisutavasta riippuen saattaa käydä niin, että sinisten osien väliin jää pieni valkoinen "rako", vaikket sellaista ohjelmoinut. Jos näin käy, älä välitä. Kyseessä on pieni vajavaisuus käyttämässämme grafiikkakirjastossa; se on korjauslistalla. Voit silti palauttaa ratkaisusi.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Sen kun piirtelet muutakin.
Yhteenvetoa
Yksittäisolioiden lisäksi voidaan määritellä myös luokkia, jotka kuvaavat toisiaan muistuttavien olioiden yhteisiä piirteitä (muuttujia ja metodeita).
Luokkien käyttäminen kätevöittää olio-ohjelmointia. Esimerkiksi Scala-kielisissä olio-ohjelmissa luokkamäärittelyillä on yleensä huomattavasti yksittäisolioita suurempi merkitys.
Kun käytössä on luokan määrittely, voi luokan instantioida eli siitä voi luoda ilmentymän, olion. Ilmentymällä on luokan yleiset piirteet (muuttujat ja metodit) mutta tietyt ilmentymäkohtaiset muuttujien arvot.
Scalassa instantiointi käy lausekkeella, joka on muotoa
LuokanNimi(luontiparametrit)
.Luontiparametreja käytetään uuden olion alustamiseen.
Näkökulmasta riippuen luokat ovat: 1) ohjelmakoodin osia, 2) tietotyyppejä, 3) yleiskäsitteiden kuvauksia käsitteellisessä mallissa sekä 4) ohjelmoinnissa käytettyjä abstraktioita.
Olioita käsitellään viittausten kautta.
Viittauksia voi esimerkiksi sijoittaa muuttujiin.
Samaan olioon voi osoittaa viittaus useasta paikasta.
Toisaalta samassa (
var
-)muuttujassa voi olla ensin viittaus yhteen olioon ja myöhemmin toiseen.
Lukuun liittyviä termejä sanastosivulla: luokka, tietotyyppi; ilmentymä eli instanssi, instantioida, luontiparametri; viittaus; abstraktio.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen, Juha Sorva ja Jaakko Nakaza. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki; sitä ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
Uusi olio luodaan käskyllä, jonka alkuun kirjoitetaan instantioitavan luokan nimi eli luotavan olion tyypin nimi. Luokkien nimet on tapana kirjoittaa Scalassa isolla alkukirjaimella.