Luku 1.5: Kokoelmia ja viittauksia
Johdanto: kokoelmat ja alkiot
Valtaosa tietokoneohjelmista käsittelee lukuisia "tiedonjyväsiä", 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.
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)
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ää 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 ArrayBuffer
eita. 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]
. Kurssimateriaalin
tulevissa esimerkeissä on lukemisen helpottamiseksi
yksinkertaistettu REPL-näkymää tuollaisista kohdista;
esimerkiksi koko tuon rimpsun sijaan lukee vain Buffer[Double]
.
Puskurin indeksit ja niiden käyttö
Alla olevat esimerkit käyttävät seuraavaa esimerkkipuskuria ja siihen viittaavaa
lukuja
-nimistä muuttujaa.
val lukuja = Buffer(12, 2, 4, 7, 4, 4, 10, 3)lukuja: Buffer[Int] = ArrayBuffer(12, 2, 4, 7, 4, 4, 10, 3)
Muuttuja on tarpeen, jotta pääsemme kokoelmaan käsiksi sen luomiskäskyn jälkeen, kuten alla näet.
Puskurin alkion tutkiminen
Kukin puskurin alkio on tallennettu tietylle järjestysnumerolle eli indeksille (index). Moni puskureiden käyttötapa perustuu juuri indekseihin.
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
Puskuritehtäviä
Puskurin sisältöä voi vaihtaa
Puskuriin sijoittaminen
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 kun pyydämme raportin lukuja
-muuttujan arvosta, näemme, että
tietokoneen muistin sisältö on muuttunut neljännen alkion kohdalla:
lukujares4: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3)
Muutos vaikutti vain puskuriin. Se ei vaikuttanut 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 myöhemmin asettaa viittaamaan johonkin
toiseen puskuriin. Silti 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 voi tästä jättää poiskin. Alkiot
määräytyvät lausekkeista 2
, -1
ja 10
, ja Scala-työkalut
osaavat tästä päätellä, että halutaan luoda juuri Buffer[Int]
.
Lisää 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, mistä tässä luvussa vain raapaistiin pintaa. 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 animaatiotkaan eivät ole piirtäneet viittauksia
tiettyjen perustyyppisten arvojen yhteyteen. Alla on 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 itse merkkijono "kissa" on tallennettuna muuttujaan (kuten aiemmissa animaatioissa), eikä niin, että muuttuja pitää sisällään viittauksen toiseen paikkaan, josta löytyvät merkkijonon merkit (kuten tuossa ä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 11.2.
Lisähuomio muilla kielillä ohjelmoineille
Usein kysyttyä: onko näin muka Pythonissa, Javassa tms.?
Joillakin kurssilaisista on aiempaa kokemusta muista ohjelmointikielistä, kuten Pythonista. Ja osa heistä miettii tässä kohden kurssia seuraavaan tapaan:
Puskurien viittaus tuotti hieman hämmennystä, sillä vastaavaa ei ole minulle ainakaan Pythonissa tullut vastaan aikaisemmin.
Se, että monta muuttujaa voi osoittaa viittauksilla samaan puskuriin ei ole minulle ollenkaan intuitiivista. Jos ymmärsin oikein, kun tuollaista puskuria muokkaa, se vaikuttaa useaan muuttujaan. Se tuntuu hämmentävältä, varsinkin kun minulla on jo taustaa Pythonista, enkä ole siellä kohdannut vastaavaa.
Tämä aihe oli tosi kiinnostava. Tällainen ei ollut Javasta ollenkaan tuttua.
Tää viittausasia saa huomaamaan, miten Scala eroaa Pythonista.
Kuitenkin itse asiassa alkiokokoelmiin (ja muihinkin rakenteisiin) viittaaminen toimii Python-kielessä juuri samoin kuin Scalassakin. Tämän luvun esittelemä viittauksen käsite on merkityksellinen myös Python-ohjelmissa — ja Javassa ja monessa muussa kielessä.
Tässä esimerkki Pythonin REPListä:
>>> ekat_luvut = [123, 456, 789](Python-REPL ei kommentoi.) >>> tokat_luvut = ekat_luvut(Python-REPL ei kommentoi.) >>> ekat_luvut.append(1111)(Python-REPL ei kommentoi.) >>> tokat_luvut.append(2222)(Python-REPL ei kommentoi.) >>> ekat_luvut[123, 456, 789, 1111, 2222] >>> tokat_luvut[123, 456, 789, 1111, 2222]
Koodi luo kokoelman, johon viittaa kaksi muuttujaa ja jossa on alkioina aluksi kolme kokonaislukua.
append
-käsky lisää kokoelmaan alkion.
Tehdäänpä tuo kumman tahansa muuttujan
kautta...
... kokoelmia on tässä vain yksi, ja muutos näkyy kumman vain muuttujan arvoa tutkimalla.
Nuo Python-muuttujatkin siis säilyttävät viittauksia kokoelmiin samaan tapaan kuin Scala-muuttujat aiemmin tässä luvussa.
Maailmalla on valitettavasti paljonkin kursseja ja verkkotutoriaaleja, jotka yksinkertaistavat liiaksi, eivät selitä esimerkiksi tuota ja jättävät opiskelijoille virheellisiä vaikutelmia.
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, 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.
Puskuria luodeessa kirjataan sulkeiden sisään parametreiksi puskuriin tallennettavat alkiot. Tässä alkiot ovat mielivaltaisesti valittuja merkkijonoja. Parametrilausekkeet erotellaan toisistaan pilkuilla.