Luku 2.3: Luokat olioiden tyyppeinä

../_images/person06.png

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

Uusi olio luodaan käskyllä, jonka alkuun kirjoitetaan instantioitavan luokan nimi eli luotavan olion tyypin nimi. Luokkien nimet on tapana kirjoittaa Scalassa isolla alkukirjaimella.

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.

Arvioi seuraavien väittämien todenperäisyyttä tämän luvun tietojen valossa. Valitse paikkansa pitävät väittämät:

Oletetaan suoritetuiksi seuraavat käskyt:

import o1.luokkia.*
val a = Tyontekija("Eugenia Enkeli", 1965, 5000)
val b = a

Valitse paikkansa pitävät väitteet.

Luokkien merkityksestä... tai merkityksis

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 tai Experience),

  • kieleen kiinteästi kuuluvat tietotyypit (esim. Int, String), ja

  • muut 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 tai Pic 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 parametrittomalla kokonaishinta-metodilla. Puheluolio osaa laskea kokonaishintansa luontiparametrien perusteella ja samalla lisätä siihen paikallisverkkomaksun.

  • Vastaavasti Puhelu-olioilta voi pyytää sanallisen selityksen puhelutiedoista parametrittomalla kuvaus-metodilla.

Voit kokeilla luokan käyttöä REPLissä myös vapaasti, mutta tee ainakin seuraavat asiat:

  1. Luo uusi puheluolio ja määrittele muuttuja, johon tallennat viittauksen luomaasi olioon.

    1. Valitse muuttujan nimi itse. Se voi olla esimerkiksi soittoJennille.

    2. Kirjaa puhelun alkuhinnaksi 0,99 euroa, minuuttihinnaksi 0,47 euroa ja kestoksi 7,5 minuuttia. (Käytä desimaalierottimena kuitenkin pistettä, kuten Scalassa on tapana.)

  2. Kutsu puheluolion kokonaishinta-metodia. Huomaa, että olio on lisännyt paluuarvoon myös paikallisverkkomaksun, joka on 13 senttiä plus 1,3 senttiä per minuutti.

  3. 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:

  1. Luo yksi puhelinlaskuolio ja muuttuja, joka viittaa siihen. Valitse itse sekä asiakkaan nimi (esim. oma nimesi) että muuttujan nimi (esim. munLasku).

  2. Lisää aiemman ohjeen mukaan luomasi puheluolio juuri luomaasi laskuun. Käytä puheluolioon viittaavan muuttujan nimeä parametrilausekkeena.

  3. 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.

  4. Kutsu laskun kokonaishinta-metodia.

  5. Kutsu laskun erittely-metodia.

  6. Kirjoita kahdessa edellisessä kohdassa saamasi kokonaishintametodin ja erittelymetodin paluuarvot seuraavaan lomakkeeseen saadaksesi tehtäväpisteitä.

Laskun kokonaishinta, kun molemmat puhelut on lisätty:

Erittely (siis se koko merkkijono, joka sieltä tulee):

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.

  1. 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.

  2. 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

Seuraavassa koodissa määritellään pari var-muuttujaa ja pari val-muuttujaa ja käytetään samaa Tyontekija-luokkaa kuin ylempänäkin.

var eka     = Tyontekija("Eugenia Enkeli", 1963, 5500)
val toka    = Tyontekija("Teija Tonkeli", 1985, 3000)
val ekaVal  = eka
var tokaVar = toka

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

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ä.

Valitse kussakin kohdassa vaihtoehto, joka kuvaa annetun koodinpätkän toimintaa.

(Tässä ensimmäisessä koodissa on käytetty counterclockwise-metodia, joka on muuten sama kuin clockwise mutta kääntää vastapäivään.)

var hepanKuva = Pic("horse.png")
hepanKuva.counterclockwise(35)
show(hepanKuva)
var hepanKuva = Pic("horse.png")
hepanKuva = hepanKuva.clockwise(15)
hepanKuva.clockwise(60)
show(hepanKuva)

Ensin näin:

var hepanKuva = Pic("horse.png")
hepanKuva = hepanKuva.clockwise(15)

Ja sitten vielä näin:

hepanKuva = hepanKuva.clockwise(60)
show(hepanKuva)
val hepanKuva = Pic("horse.png")
val kaannettyVahan = hepanKuva.clockwise(15)
val kaannettyLisaa = kaannettyVahan.clockwise(60)
show(kaannettyLisaa)

Kuvilla on myös seuraavat metodit, vaikutuksettomia nekin:

  • scaleBy, joka ottaa ainoaksi parametrikseen Double-kertoimen, jolla kuvan kokoa "muutetaan" (ts. palautetaan uusi kuva, joka on skaalattu versio alkuperäisestä). Nollan ja ykkösen välinen kerroin pienentää kuvaa, ykköstä isompi suurentaa.

  • parametrittomat flipVertical ja flipHorizontal, jotka muodostavat vaaka- ja pystysuuntaisen peilikuvan alkuperäisestä.

Kokeile näitä metodeita vapaasti. Tee REPLissä ainakin seuraava:

  1. Ota käyttöön jokin Pic-tyyppinen kuva (kuvio tai netistä ladattu; miten haluat).

  2. Kutsu sille scaleBy-metodia ja suurenna kuvaa kertoimella 2.

  3. Käännä skaalattu kuva ylösalaisin flipVertical-metodilla.

  4. Kierrä ylösalaisin käännettyä kuvaa 15 astetta myötäpäivään clockwise-metodilla.

  5. Kutsu äskeisten vaiheiden lopputuloksena saamasi kuvan history-metodia ja kopioi alle REPLin kuvaus metodin palauttamasta arvosta.

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))

Tässä ensin yksi koodinpätkä:

// Esimerkki 1
val eka = rectangle(50, 100, Red)
val toka = rectangle(150, 100, Blue)
val yhdistelma = eka.leftOf(toka)
val kierretty = yhdistelma.clockwise(30)
show(kierretty)

Ja tässä viisi lisää:

// Esimerkki 2
val eka = rectangle(50, 100, Red)
val toka = rectangle(150, 100, Blue)
val yhdistelma = eka.leftOf(toka)
show(yhdistelma.clockwise(30))
// Esimerkki 3
val eka = rectangle(50, 100, Red)
val yhdistelma = eka.leftOf(rectangle(150, 100, Blue))
show(yhdistelma.clockwise(30))
// Esimerkki 4
val toka = rectangle(150, 100, Blue)
val yhdistelma = rectangle(50, 100, Red).leftOf(toka)
show(yhdistelma.clockwise(30))
// Esimerkki 5
val yhdistelma = rectangle(50, 100, Red).leftOf(rectangle(150, 100, Blue))
show(yhdistelma.clockwise(30))
// Esimerkki 6
show(rectangle(50, 100, Red).leftOf(rectangle(150, 100, Blue)).clockwise(30))

Kuinka moni esimerkeistä 2–6 tuottaa saman kuvan kuin esimerkki 1? Vastaa numerolla väliltä 0–5.

Kirjoita tähän Pic-tyyppinen lauseke, jossa käytät above- tai below-metodikutsua. Tuon metodikutsun tulee muodostaa ympyrän kuvasta ja suorakaiteen kuvasta yhdistelmäkuva, jossa suorakaide on ympyrän alapuolella.

Hoida koko homma yhdelle riville sopivalla lausekkeella samaan tyyliin kuin äskeisessä esimerkissä 6; älä käytä muuttujia. Voit olettaa, että import o1.* on annettu. Älä kirjaa mukaan show-komentoa, vaikka sitä itse REPLissä kokeilisitkin.

Voit valita ympyrän ja suorakaiteen koot ja värit itse. Piirrä vaikka puu.

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

../_images/flag_of_somalia.png

Laadi vaikutukseton funktio somalianLippu, joka

  • ottaa ainoaksi parametrikseen Doublen, 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 ja White.

Kirjoita funktio kierros2.scalaan 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

../_images/suomen_lippu.png

Tee samaan tiedostoon funktio suomenLippu, joka

  • ottaa Double-tyyppisen leveysparametrin kuten edellinenkin funktio, ja

  • palauttaa 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 uusi Pic; 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.

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