Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 1.5: Kokoelmia ja viittauksia
Tästä sivusta:
Pääkysymyksiä: Miten tallennan useamman toisiinsa liittyvän tiedon? Miten samaan tietokokoelmaan viitataan useasta ohjelman kohdasta?
Mitä käsitellään? Kokoelmien — erityisesti puskurien — käytön alkeita. Viittauksia.
Mitä tehdään? Ohjelmoidaan REPLissä ja luetaan.
Suuntaa antava työläysarvio:? Noin tunti.
Pistearvo: A20.
Oheisprojektit: Ei ole.
Johdanto: kokoelmat ja alkiot
Valtaosa tietokoneohjelmista käsittelee lukuisia "tiedonjyväsiä", jotka liittyvät yhteen tavalla tai toisella ja joista muodostuu luetteloja tai muita kokonaisuuksia. Ohjelmasta riippuen voi olla tarpeen pitää kirjaa vaikkapa useista mittaustuloksista, käyttäjän kirjaamista hotellikokemuksista, käyttäjän kavereista tai kurssille ilmoittautuneista opiskelijoista.
Tällaiseen tarkoitukseen käytetään alkiokokoelmia tai lyhyemmin sanoen vain kokoelmia (collection). Yksi kokoelma voi sisältää esimerkiksi kaikki käyttäjän kaverit tai sarjan mittaustuloksia. Yksittäistä kokoelman sisältämää tietoa sanotaan alkioksi (element); alkio voi siis olla esimerkiksi yksi mittaustulos, kokemus, kaveri tai opiskelija.
Kokoelmia on erilaisia. Koska tarve kokoelmille on yleinen, ohjelmointikieliin on määritelty valmiita kokoelmatyyppejä, joita ohjelmoija voi hyödyntää. Scala-kieli tarjoaa runsaan valikoiman kokoelmatyyppejä, joista käytämme nyt aluksi yhtä.
Puskurin luominen
Eräs Scala-kieleen määritelty kokoelmatyyppi on puskuri (buffer). Puskuri on kokoelma, jonka alkioilla on määrätty järjestys. Puskuriin voi lisätä uusia alkioita, ja vanhoja alkioita voi poistaa tai korvata uusilla. Voit ajatella, että puskuri on eräänlainen tietokoneen muistiin tallennettu luettelo.
Luodaan kokeeksi puskuri, jossa on alkioina neljä merkkijonoa. Eräs, joskin monisanainen, tapa luoda puskuri Scalalla on tässä:
scala.collection.mutable.Buffer("eka", "toka", "kolmas", "vielä neljäskin")res0: scala.collection.mutable.Buffer[String] = ArrayBuffer(eka, toka, kolmas, vielä neljäskin)
Buffer[String]
.
Hakasulkuihin merkitään Scalassa tyyppiparametri (type
parameter), joka on lisätarkennus siitä, mistä tietotyypistä on
kyse. Esimerkiksi tässä tyyppiparametri on String
: ei ole luotu
mitä tahansa puskuria vaan nimenomaan merkkijonoja sisältävä
puskuri. Puheessa tietotyypin Buffer[String]
voi lausua
vaikkapa "String
-puskuri", "merkkijonopuskuri" tai "buffer
of string".ArrayBuffer
eli eräällä tietyllä tekniikalla (taulukoilla; array)
toteutettu puskuri. Scalassa puskurit ovat oletusarvoisesti juuri
ArrayBuffer
eita. Tästä ei toistaiseksi tarvitse välittää sen
enempää.Puskurin luominen siis onnistui, mutta olipa kurjaa kirjoittaa tuo koko pakkauksen nimi luomiskäskyyn. Jos haluamme luoda useita puskureita, tuntuu turhalta toistaa pakkauksen nimeä. Vaan meillähän on jo lääke tähän monisanaisuuteen.
Lisää import
-käskystä ja pakkauksista
Edellä käytettiin puskurien täydellistä nimeä (fully qualified name)
scala.collection.mutable.Buffer
, joka muodostuu pakkauksen nimestä
scala.collection.mutable
ja varsinaisesta nimestä Buffer
pisteellä yhdistettynä.
Kuitenkin voimme etukäteen ilmoittaa käyttävämme kyseisen pakkauksen sisältöä luvusta
1.3 tutulla import
-käskyllä:
import scala.collection.mutable.Bufferimport scala.collection.mutable.Buffer
Nyt voimme luoda puskureita vain sanaa Buffer
käyttäen.
Buffer("eka", "toka", "kolmas", "vielä neljäskin")res1: scala.collection.mutable.Buffer[String] = ArrayBuffer(eka, toka, kolmas, vielä neljäskin) Buffer(2.40, 3.11, 4.56, 10.29, 8.11)res2: scala.collection.mutable.Buffer[Double] = ArrayBuffer(2.4, 3.11, 4.56, 10.29, 8.11)
Kaikissa seuraavissa tämän luvun esimerkeissä oletetaan, että edellä on annettu käsky
import scala.collection.mutable.Buffer
.
Erikoispakkaus scala
Aiemmin käyttämämme Scalan tietotyypit (Int
, Double
, String
) ovat "toimineet
suoraan". Miksei niille tarvittu import
ia mutta puskureille tarvittiin?
Myös Int
, Double
ja String
on määritelty eräässä Scalan valmiissa pakkauksessa.
Eikä missä tahansa pakkauksessa vaan kaikkein tärkeimmässä, jonka nimi on yksinkertaisesti
scala
. Esimerkiksi Int
-tietotyypin täydellinen nimi on scala.Int
. Tätä koko nimeä
ei kuitenkaan tarvitse käyttää koska erikoispakkauksen scala
sisällön katsotaan olevan
niin yleistarpeellista, että se on osa itse Scala-kielen määrittelyä. Tuon pakkauksen
sisältö on automaattisesti käytettävissä kaikissa Scala-ohjelmissa.
Tämän kurssimateriaalin REPL-esimerkeistä
Huom. Kuten yllä näkyy, REPL raportoi puskurin tyypin perinjuurisesti
käyttäen pakkauksen nimeä, esim. scala.collection.mutable.Buffer[Double]
.
Käytännössä kuitenkin tiedämme hyvin, että kyseessä on nimenomaan
kyseisessä pakkauksessa määritelty tyyppi, eikä pakkauksen nimellä
ole tässä meille lisäarvoa. Lukemisen helpottamiseksi tässä
kurssimateriaalissa on jatkossa hieman yksinkertaistettu REPLissä
näkyviä tyyppejä valikoiduista kohdista; esimerkiksi koko rimpsun
scala.collection.mutable.Buffer[Double]
sijaan alla lukee vain
Buffer[Double]
. Älä siis häkelly, jos kurssimateriaalin tulosteet
eivät näytä täsmälleen siltä, mitä itse saat vastaukseksi, kun
annat samat käskyt REPLissä.
Toinen huom. Kurssimateriaalin koodiesimerkeissä import
-käsky
on melko usein mukana esimerkin alussa, erityisesti kun halutaan
jonkin uuden työkalun kohdalla muistuttaa siitä pakkauksesta, jossa
työkalu sijaitsee. Toisaalta import
-käsky on rutiininomainen
"pakollinen paha", josta pitää huolehtia ennen monien Scala-työkalujen,
kuten tässä puskureiden, käyttöä. Siksi import
on jätetty monesta
tulevasta esimerkistä pois "itsestäänselvänä"; muista silti itse
antaa se.
Puskurit ja import o1._
Itse asiassa kurssimme oma työkalupakkaus o1
on määritelty siten,
että jos otat sen käyttöön, voit samalla käyttää myös puskureita
ilman toista import
-käskyä. Näin voit välttyä pidemmältä näppäilyltä
käyttäessäsi puskureita tällä kurssilla.
import o1._import o1._ Buffer(2.40, 3.11, 4.56, 10.29, 8.11)res3: Buffer[Double] = ArrayBuffer(2.4, 3.11, 4.56, 10.29, 8.11)
Puskurin käyttö
Kukin puskurin alkio on tallennettu tietylle järjestysnumerolle eli indeksille (index). Moni puskureiden käyttötapa perustuu juuri indekseihin. Kokeillaan puskurin alkioiden tutkimista, muuttamista ja lisäämistä.
Alla näet ensin, miten muuttujan avulla voi viitata alkiokokoelmaan kuten puskuriin. Muuttuja onkin tarpeen, jotta kokoelmaan pääsee käsiksi sen luomisen jälkeen.
val lukuja = Buffer(12, 2, 4, 7, 4, 4, 10, 3)lukuja: Buffer[Int] = ArrayBuffer(12, 2, 4, 7, 4, 4, 10, 3)
Puskurin sisällön tutkiminen
Tietylle indeksille tallennetun arvon voi katsoa tähän tapaan: ilmoitetaan, mistä puskurista katsotaan, ja perään suluissa haluttu indeksi:
lukuja(0)res4: Int = 12
Huomaa, että indeksit alkavat nollasta! Tässä siis katsottiin puskurin ensimmäinen alkio.
Vastaavaa ilmaisua voi käyttää lausekkeena vaikkapa muuttujaan sijoituksessa. Tässä
puskurin neljäs alkio (eli indeksillä 3 oleva alkio) sijoitetaan muuttujaan neljasLuku
:
val neljasLuku = lukuja(3)neljasLuku: Int = 7
Puskurin sisällön vaihtaminen
Puskurin sisältämän alkion voi vaihtaa toiseen kirjoittamalla indeksin sulkuihin ja perään yhtäsuuruusmerkin ja halutun uuden arvon. Tässä esimerkissä puskurin neljäs alkio vaihdetaan luvuksi 1.
lukuja(3) = 1
Tällä käskyllä itsellään ei ole kiinnostavaa arvoa. Käsky vain komentaa tietokonetta
muuttamaan puskurin sisältöä eikä varsinaisesti tuota mitään tulosta. Niinpä REPLkin
pysyy vaiti. Kuitenkin pyytämällä raportti lukuja
-muuttujan arvosta näemme, että
tietokoneen muistissa on tapahtunut muutos neljännen alkion kohdalla:
lukujares5: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3)
Muutos vaikutti vain puskuriin, ei muuttujaan, johon aiemmin tallensimme vanhan arvon:
neljasLukures6: Int = 7
Alkion lisääminen puskuriin
Operaattorilla +=
voi lisätä uuden alkion puskurin loppuun.
Puskurin koko siis kasvaa.
lukuja += 11res7: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11)
Puskurin käyttö muistuttaa kovasti muuttujien käyttöä, kuitenkin sillä lisäyksellä, että
tarvitaan indeksejä kohdistamaan käskyt tiettyyn kohtaan puskuria. Voitkin ajatella, että
puskuri on ikään kuin numeroitu luettelo var
-muuttujia. Siitä puheen ollen: huomaa, että
puskuria käytettiin äskeisessä esimerkissä val
-muuttujan avulla. Tämä ei estä muuttamasta
puskurin sisältöä; se estää vaihtamasta lukuja
-muuttujan arvoa eli sijoittamasta muuttujaan
viittausta, joka osoittaa johonkin toiseen puskuriin kuin muuttujan aiemmin varastoima viittaus.
Tyhjä puskuri
Monesti on hyödyllistä luoda puskuri, jossa ei vielä ole lainkaan alkioita. Esimerkiksi GoodStuff-ohjelmassa ei aluksi ole lainkaan kirjattuja kokemuksia, mutta niitä voi vähitellen lisätä sinne.
Tyhjän puskurin luomiseen liittyy yksi uusi asia. Se ei nimittäin kunnolla onnistu ihan vain näin:
val toimiikohan = Buffer()toimiikohan: Buffer[Nothing] = ArrayBuffer()
Tällä käskyllä saa kylläkin luotua tyhjän puskurin, mutta tyhjäksi se jääkin. Tietokone
ei tiedä, mitä aiot puskuriin tallentaa, ja Scala-työkalusto asettaa puskurin tietotyypiksi
Buffer[Nothing]
. Tällaisesta puskurista on harvoin iloa, koska sinne ei voi lisätä mitään.
Voit kuitenkin kertoa, minkä tyyppisen puskurin haluat luoda. Luodaan kokeeksi tyhjä
puskuri, johon voi myöhemmin lisätä merkkijonoja. Tyyppiparametri, joka on tässä tapauksessa
String
, kirjoitetaan hakasulkeisiin aiemmista REPL-tulosteista jo tuttuun tapaan:
val sanat = Buffer[String]()sanat: Buffer[String] = ArrayBuffer()
Näin luotuun tyhjään puskuriin voi lisätä merkkijonoja:
sanat += "kissa"res8: Buffer[String] = ArrayBuffer(kissa)
Itse asiassa tyyppiparametrin saa kirjata puskurinluontikäskyihin silloinkin, kun se ei välttämätöntä olekaan. Tämäkin toimii:
val lukuja = Buffer[Int](2, -1, 10)lukuja: Buffer[Int] = ArrayBuffer(2, -1, 10)
[Int]
-tyyppiparametrin voisi tästä jättää poiskin, koska Scala-työkalut osaavat päätellä
alkiot määräävistä lausekkeista 2
, -1
ja 10
, että halutaan luoda juuri Buffer[Int]
.Puskuritehtäviä
Viittaukset
Luvussa 1.4 sanottiin, että muuttujalla on vain yksi arvo. Eikö meillä nyt kuitenkin ole yhdessä puskurimuuttujassa useita arvoja?
Tavallaan, mutta ei oikeasti.
Samalla, kun kohta tarkastelemme tätä kysymystä, kohtaat viittauksen (reference) käsitteen. Tällä käsitteellä on myös laajempi merkitys ohjelmoinnin kannalta, kuten myöhemmin osoittautuu.
Huomasitko, että nimi kokeilu
on muuttujan nimi, ei puskurin? (Puskurilla ei ole nimeä.)
Tämä osoittautuu tärkeäksi aivan kohta.
Viittauksilla on konkreettinen merkitys ohjelmakoodin toiminnassa. Tarkastellaan toista animaatiota.
Viittaustehtävä
Tämän luvun merkityksestä ja viittauksista
Puskureilla ja muilla kokoelmatyypeillä voi tehdä valtavasti erilaisia asioita, ja tässä luvussa vain raapaistiin pintaa aiheesta. Näet runsaasti esimerkkejä kokoelmista tulevissa luvuissa; kuten jo edellä vihjattiin, esimerkiksi GoodStuff-ohjelmassa kokemuskategorian sisältämät päiväkirjamerkinnät kootaan puskuriin. Kokoelmien alkioina tulemme käyttämään muunkinlaisia arvoja kuin vain yksittäisiä lukuja ja merkkijonoja.
Tässä vaiheessa oleellisinta on tietää, miten puskureita luodaan ja miten niiden yksittäisiä arvoja tutkitaan ja vaihdetaan. Sekä se, että puskureita käsitellään viittausten kautta.
Viittauksen käsitettä tarvitaan jatkossa toistuvasti. Viittausten avulla käsitellään ohjelmissa myös monia muita asioita kuin puskureita.
Viittausasian oppimista voi sekoittaa se, että ohjelmoinnista puhuttaessa usein "oikaistaan"
hieman. Harvemmin sanotaan tai kirjoitetaan esimerkiksi, että "lukuja
-muuttujassa on
viittaus puskuriin" vaan epätarkemmin: "lukuja
-muuttujassa on puskuri". Saatetaan myös
puhua "lukuja
-puskurista", vaikka varsinaisestihan muuttujassa on vain viittaus, ja
lukuja
on muuttujan eikä puskurin nimi. (Kuten näit, yhteen puskuriin voi viitata useilla
erinimisillä muuttujilla.) Sanamuotojen yksinkertaistaminen on luonnollista ja kätevää,
eikä sitä tarvitse välttää, mutta se täytyy ymmärtää.
Psst! Itse asiassa...
Jos tarkkoja ollaan, niin myös monien Scala-kielen perustietotyyppien
arvot — esimerkiksi String
-tyyppiset arvot — ovat "viittausten
päässä" kuten puskuritkin. Tätä ei ole aiemmin mainittu, ja
kurssimateriaalin animaatioissakaan ei yleensä ole piirretty
viittauksia tiettyjen perustyyppisten arvojen yhteyteen. Tässä
kuitenkin esimerkkianimaatio, joka on "totuudenmukaisempi"
(yksityiskohtaisempi) kuin aiemmat merkkijonoja käsitelleet animaatiot,
mutta vähän epäselvempi:
Yleensä on kätevämpää ajatella yksinkertaistetusti niin, että esimerkiksi merkkijono "kissa" on itse tallennettuna muuttujaan, eikä niin, että muuttuja pitää sisällään viittauksen toiseen paikkaan, josta löytyvät merkkijonon merkit (kuten äskeisessä animaatiossa). Yksinkertaistettua tapaa käytetään paitsi aiemmissa myös tulevissa animaatioissa.
Tämä yksinkertaistus on esimerkiksi String
-arvojen yhteydessä
"turvallinen" eikä johda virheellisiin päättelyihin, koska —
toisin kuin puskuri — merkkijonoarvo ei koskaan sisäisesti muutu.
Merkkijonon voi kylläkin yhdistää toiseen merkkijonoon, jolloin syntyy
uusi merkkijono kuten äskeisessä esimerkissämme. Lisää aiheesta mm.
luvuissa 4.5 ja 10.2.
Yhteenvetoa
- Ohjelmoija voi varastoida useita "tiedonjyväsiä" — alkioita — yhteen kokoelmaan.
- Eräs kokoelmatyyppi on puskuri.
- Puskurin alkioilla on järjestysnumerot eli indeksit.
- Puskurin alkioita voi vaihtaa toisiin, ja puskuriin voi lisätä uusia alkioita.
- Puskuri on siis tavallaan joukko yhteen
liitettyjä
var
-muuttujia.
- Puskureita käsitellään viittausten kautta. Muuttujaan tallennetaan
viittaus, joka kertoo missä päin tietokoneen muistia itse puskuri on.
- Usea muuttuja voi viitata samaan puskuriin.
- Puskurin muuttaminen minkä tahansa siihen viittaavan muuttujan kautta "näkyy" kaikkien siihen viittaavien muuttujien kautta.
- Viittauksia käytetään samaan tapaan myös muunlaisten tietojen kuin alkiokokoelmien käsittelyyn, kuten jatkossa nähdään.
- Lukuun liittyviä termejä sanastosivulla: viittaus; (alkio)kokoelma, puskuri, alkio, indeksi; tyyppiparametri; pakkaus.
Päivitetty käsitekaavio:
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.
scala.collection.mutable
sisältöä.