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

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.

Oheismoduulit: Ei ole.

../_images/person01.png

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:

Buffer("eka", "toka", "kolmas", "vielä neljäskin")res0: scala.collection.mutable.Buffer[String] = ArrayBuffer(eka, toka, kolmas, vielä neljäskin)
Puskuria luotaessa laitetaan sulkeiden sisään parametreiksi puskuriin tallennettavat alkiot. Tässä alkiot ovat mielivaltaisesti valittuja merkkijonoja. Parametrilausekkeet erotellaan toisistaan pilkuilla.
REPL raportoi, että tuloksen tyyppi on Buffer[String]. Hakasulkeisiin 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".
REPLiin tulostuu kuvaus luodusta puskurista alkioineen. Kuten näkyy, puskuri sisältää sinne laitetut merkkijonot juuri siinä järjestyksessä, missä ne annettin luomiskäskylle parametreiksi.
REPL vielä mainitsee, että tarkemmin sanoen tuli luotua ArrayBuffer eli eräällä tietyllä tekniikalla (taulukoilla; array) toteutettu puskuri. Scalassa puskurit ovat oletusarvoisesti juuri ArrayBuffereita. Tästä ei toistaiseksi tarvitse välittää sen enempää.

Alla on kaksi lisäesimerkkiä: kaksialkioinen merkkijonopuskuri sekä viisialkioinen Double-lukuja sisältävä puskuri.

Buffer("ABC", "XYZ")res1: scala.collection.mutable.Buffer[String] = ArrayBuffer(ABC, XYZ)
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)
Huomaa puskurien erilaiset tyyppiparametrit, jotka määrittyvät automaattisesti alkioiden mukaan.
Sivuhuomio oppimateriaalista: REPL raportoi puskurin tyypin perinjuurisesti kirjaten mukaan pakkauksen nimen, esim. scala.collection.mutable.Buffer[Double]. Tämän kurssimateriaalin tulevissa esimerkeissä on lukemisen helpottamiseksi hieman yksinkertaistettu REPLissä näkyviä tyyppejä; esimerkiksi koko tuon rimpsun sijaan lukee vain Buffer[Double]. Älä siis häkelly, jos kurssimateriaalin REPL-tulosteet eivät näytä täsmälleen siltä, mitä itse saat vastaukseksi, kun annat samat käskyt REPLissä.

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 luomiskäskyn 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 sulkeissa haluttu indeksi.

lukuja(0)res3: Int = 12

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 indeksin 3 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 sulkeisiin 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:

lukujares4: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3)

Muutos vaikutti vain puskuriin, ei muuttujaan, johon aiemmin tallensimme vanhan arvon:

neljasLukures5: Int = 7

Alkion lisääminen puskuriin

Operaattorilla += voi lisätä uuden alkion puskurin loppuun. Puskurin koko siis kasvaa.

lukuja += 11res6: 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. val ei estä muuttamasta puskurin sisältöä; se ei ole puskurin ominaisuus lainkaan. Kun lukuja on val, niin tuota muuttujaa ei voi laittaa viittaamaan myöhemmin johonkin toiseen puskuriin. Kuitenkin sen puskurin sisältö, johon lukuja viittaa, voi muuttua.

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"res7: 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ä

Seuraavien pikkukysymysten avulla voit harjoitella puskureihin liittyvien peruskäskyjen ymmärtämistä. Päättele annettujen koodinpätkien toiminta. Kokeile myös itse REPLissä ohjelmoiden.

Tutki seuraavia käskyjä. Mitä ne tekevät puskurille?

val testi = Buffer(4, 10, 3, 10, 15, -2)
val indeksi = 4
val poimittu = testi(2)
println(poimittu + testi(indeksi) + testi(4))

Kirjoita tähän tulostuskäskyn tulostama luku:

Minkälaiseksi seuraavat alkionlisäyskäskyt muokkaavat puskurin?

val sanoja = Buffer[String]()
val sana = "alkioita"
sanoja += "me"
sanoja += "olem" + sanoja(0)
sanoja += sana
println(sanoja(0) + " " + sanoja(1) + " " + sanoja(2))

Kirjoita tähän tulostuskäskyn tulostama teksti:

Tutki seuraavia käskyjä. Mitä ne tekevät puskurille?

val testi = Buffer(4, 10, 3, 10, 15, -2)
var indeksi = 0
testi(indeksi) = 0
indeksi = indeksi + 1
testi(indeksi) = 0
indeksi = indeksi + 1
testi(indeksi) = 0
indeksi = indeksi + 1
testi(indeksi) = 0

Mikä on näiden käskyjen suorittamisen jälkeen puskurin alkioiden summa?

Oletetaan, että seuraavat koodirivit on juuri suoritettu.

val lukuja = Buffer(10.5, 10.3, 9.8, 7.9, 10.2, 9.7)
val lukumaara = 6
val tyhja = Buffer[Double]()

Alla on muutama yritys poimia yksittäinen alkio. Mitkä kaikki niistä aiheuttavat virheilmoituksen (ja miksi)?

Oletetaan, että on luotu puskuri ja siihen viittaava muuttuja nimeltä lukuja täsmälleen kuten edellisessä kohdassa. Kirjoita tähän Scala-kielinen käsky, jolla voidaan lisätä puskurin viimeiseksi (eli seitsemänneksi) alkioksi luku 9.9.
Oletetaan taas, että on olemassa puskuri ja lukuja-muuttuja samaan tapaan. Kirjoita tähän Scala-kielinen käsky, joka poimii puskurista viidennen alkion (esimerkissämme 10.2) ja tulostaa sen. (Siis vain sen luvun omalle rivilleen eikä muuta.)

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ä

Käy seuraava koodi läpi ajatuksella ja vastaa kysymyksiin. Jos tehtävä tuntuu haastavalta, se voi helpottua, jos piirrät kuvan puskureista, muuttujista ja viittauksista. Voit myös syöttää koodin REPLiin. Jos jää epäselvää, kertaa puskuri- ja viittausasiaa yltä tai kysy neuvoa.

var henkilo = Buffer("Sauli")
var toinen = Buffer("Sauli")
var presidentti = toinen

Montako puskuria tämä koodi luo?

Montako johonkin puskuriin osoittavaa muuttujaa tämä koodi luo?

Suoritetaan lisäksi nämä rivit:

presidentti += "Väinämö"
toinen += "Niinistö"

Montako puskuria nyt on olemassa?

Montako alkiota on nyt siinä puskurissa, johon muuttuja presidentti viittaa?
Montako alkiota on nyt siinä puskurissa, johon muuttuja toinen viittaa?

Sitten suoritetaan tämäkin rivi:

toinen = Buffer("Jenni", "Haukio")

Montako alkiota on nyt siinä puskurissa, johon muuttuja presidentti viittaa?

Montako alkiota on nyt siinä puskurissa, johon muuttuja toinen viittaa?

Ja lopuksi vielä:

toinen = henkilo
henkilo = Buffer("Lennu")

Moniko muuttujista viittaa nyt sellaiseen puskuriin, jossa on nimet "Jenni" ja "Haukio"?

Moniko muuttujista viittaa nyt sellaiseen puskuriin, jossa on vain nimi "Sauli" eikä muuta?

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 5.2 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!

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, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, 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 suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee- ja Kelmu.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Tapa, jolla käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin, on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on tällä hetkellä Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.

A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen ovat luoneet Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi ja Paweł Stróżański yhteistyössä Juha Sorvan, Otto Seppälän, Arto Hellaksen ja muiden 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...