Kurssin viimeisimmän version löydät täältä: O1: 2024
- CS-A1110
- Kierros 11
- Luku 11.1: Taulukoita ja rikkinäinen juna
Luku 11.1: Taulukoita ja rikkinäinen juna
Tästä sivusta:
Pääkysymyksiä: Saisinko lisäharjoitusta silmukoista, koodin testaamisesta ja debuggerin käytöstä? Sana array on esiintynyt materiaalissa — mitä se tarkoittaa?
Mitä käsitellään? Em. aiheita. Huolellisuutta. Taulukot kokoelmatyyppinä.
Mitä tehdään? Ensin vähän luettavaa. Suurin osa ajasta etsitään virheitä annetusta ohjelmakoodista.
Suuntaa antava työläysarvio:? Kolme, neljä tuntia? Jos jumitat työlääseen virheenetsintätehtävään, pyydä apua.
Pistearvo: B80.
Oheismoduulit: Train (uusi).
Johdanto: taulukot mainittu
Taulukkoa tarkoittava array-sana on jo vilahtanut kurssilla:
val lukupuskuri = Buffer(1, 2, 3)lukupuskuri: Buffer[Int] = ArrayBuffer(1, 2, 3) val sanataulukko = "yksi,kaksi,kolme".split(",")sanataulukko: Array[String] = Array(yksi, kaksi, kolme)
split
-metodi (luku 5.2) jakaa merkkijonon osiin ja
palauttaa ne — ei vektorissa eikä puskurissa vaan
taulukossa.Taulukot (array) ovat alkiokokoelmia ja muistuttavat puskureita ja vektoreita:
- Kuten puskurit ja vektorit, taulukot sisältävät alkioita tallennettuina eri indekseille.
- Taulukon alkion voi vaihtaa toiseksi. Tässä suhteessa taulukko muistuttaa puskuria ja eroaa vektorista.
- Taulukon koko on kuitenkin muuttumaton kuten vektorinkin. Koko määrätään taulukkoa luodessa, eikä taulukossa voi myöhemmin olla eri määrää indeksejä kuin aluksi.
Kovin tutunsorttinen rakenne on siis kyseessä. Katsotaan ensin, miten taulukkoja käytetään, ja palataan sitten siihen, millaisissa tilanteissa niitä voi haluta käyttää.
Taulukot Scalalla
Array
-kokoelmatyyppi
Taulukot, samoin kuin vektorit ja toisin kuin puskurit, ovat käytettävissä kaikissa
Scala-ohjelmissa ilman import
-käskyä. Taulukon voi luoda muista kokoelmista tuttuun
tyyliin:
val taulukko = Array("eka", "toka", "kolmas", "neljäs")taulukko: Array[String] = Array(eka, toka, kolmas, neljäs) val toinenTaulukko = Array.tabulate(5)( indeksi => 2 * indeksi )toinenTaulukko: Array[Int] = Array(0, 2, 4, 6, 8)
Yhtä tuttuun tapaan toimivat myös taulukon indeksit ja metodit:
taulukko(2)res0: String = kolmas taulukko(3) = "vika"taulukkotaulukko: Array[String] = Array("eka", "toka", "kolmas", "vika") taulukko.sizeres1: Int = 4 taulukko.indexOf("kolmas")res2: Int = 2 taulukko.mkString(":")res3: String = eka:toka:kolmas:vika taulukko.map( _.length )res4: Array[Int] = Array(3, 4, 6, 4)
Kokonaan uuden alkion lisääminen (ja taulukon koon kasvattaminen) ei onnistu:
taulukko += "vielä yksi?"<console>:13: error: type mismatch;
Alustamattoman taulukon luominen: ofDim
Joskus on kätevää luoda tietynkokoinen kokoelma, jonka sisältö asetetaan vasta myöhemmin
erillisillä käskyillä. Scala API tarjoaa tähän tarkoitukseen Array.ofDim
-metodin (jota ei
löydy vektoreille tai puskureille).
val taulukko = Array.ofDim[Int](5)taulukko: Array[Int] = Array(0, 0, 0, 0, 0)
ofDim
-metodille tulee antaa tyyppiparametri hakasulkeissa.
Tässä taulukon alkioina on Int
-arvoja.Luomalla taulukon tähän tapaan saamme varattua halutun määrän paikkoja (muistitilaa) alkioille, jotka eivät ole tiedossa vielä taulukonluontikäskyä suoritettaessa, mutta joiden (enimmäis)lukumäärä on asetettu.
Kutsun Array.ofDim
voi lukea array of dimensions eli vapaasti kääntäen "taulukko,
jolla on eri ulottuvuuksissa mitat". Nimensä mukaisesti metodi sopii myös "moniulotteisten"
eli sisäkkäisten taulukkojen luomiseen (vrt. "moniulotteiset" vektorit luvusta 6.1):
val kaksiulotteinen = Array.ofDim[Int](2, 3)kaksiulotteinen: Array[Array[Int]] = Array(Array(0, 0, 0), Array(0, 0, 0)) val kolmiulotteinen = Array.ofDim[Double](2, 2, 2)kolmiulotteinen: Array[Array[Array[Double]]] = Array(Array(Array(0.0, 0.0), Array(0.0, 0.0)), Array(Array(0.0, 0.0), Array(0.0, 0.0)))
Huomaa: ofDim
vain luo taulukon, se ei luo sille mitään merkityksellistä sisältöä.
On luodussa taulukossa kuitenkin aina jotain. Alkioiden oletusarvo riippuu niiden
tyypistä:
- lukutyypeillä (
Char
mukaan lukien) nolla, - totuusarvoilla
false
, ja - kaikilla muilla tyypeillä, myös esim.
String
-tyypillä, "olematon arvo"null
(joka on virheiden kutualusta; luku 4.3).
Oletusarvot voi ja yleensä onkin tarpeen korvata muilla arvoilla joko ennemmin tai myöhemmin.
Tarkkana oletusarvojen kanssa!
Olkoon käytössä luokka nimeltä Footballer
. Luodaan taulukko näin:
val finnishTeam = Array.ofDim[Footballer](11)
On yleinen aloittelijan ajatusvirhe unohtaa, ettei tuollainen käsky luo ensimmäistäkään ilmentymää
taulukon alkiotyypiksi mainitusta luokasta Footballer
, vaikka luokin Array[Footballer]
-tyyppisen
arvon. Luoduksi tulee ainoastaan yksitoista null
-arvoa sisältävä taulukko. Taulukkoon voi
sijoittaa erikseen luotuja Footballer
-olioita näin:
finnishTeam(0) = new Footballer("Tinja-Riikka Korpela")
finnishTeam(1) = // etc.
Vain jos taulukkoon todella on sijoitettu pelaajaolioita, voi niiden metodeita kutsua
kuten alla synnyttämättä NullPointerException
-virhetilannetta.
finnishTeam(9).score()
val keeper = finnishTeam(0)
println(keeper.name)
Eikö kuulosta hyödylliseltä?
Taulukko on ominaisuuksiltaan rajoittuneempi kokoelmatyyppi kuin puskuri, koska sen kokoa
ei voi muuttaa. Minkä vain toiminnon, jonka saa toteutettua taulukkoja käyttäen, saa
toimimaan myös puskureilla. (Ja toisinkin päin kyllä.) Silloin, kun halutaan kuvata
täysin muuttumatonta indeksoitua alkiokokoelmaa, voi käyttää vektoria. Alustamattomissa
taulukon alkioissa vaanii null
.
Miksi siis vaivautuisit opettelemaan vielä yhden kokoelmatyypin? Tässä eräitä syitä:
- Yleisyys: Taulukot kuuluvat ohjelmoijan yleissivistykseen. Taulukko on yleinen kokoelmatyyppi, perusrakenne, jota käytetään muiden kokoelmatyyppien toteutuksessa apuna. Se esiintyy hyvin monissa ohjelmointikielissä; useassa kielessä se on yleisimmin käytetty kokoelmatyyppi.
- Tehokkuus: Taulukon käytöllä saavutetaan joissakin tilanteissa tehokkuusparannuksia. Tämä ei ole Scala-taulukoiden käyttösyistä vähäisimpiä, mutta jätämme jälleen tehokkuusnäkökulman jatkokurssien asiaksi. Tiivis vertailu Scalan kokoelmatyyppien tehokkuudesta löytyy Scalan kotisivuilta.
- Luontevat käyttötilanteet: Taulukko on luonnollinen valinta,
jos halutaan kuvata kokoelmaa, jonka sisältö voi muuttua mutta
jonka koko on muuttumaton tai jonka koolla on jokin ennalta määrätty
yläraja. Esimerkiksi ruudukkoa kuvaava
Grid
-luokka (luku 7.4) on toteuttu "kaksiulotteista" taulukkoa apuna käyttäen. - Valmiit ohjelmakirjastot:
Array
-tyyppiä on käytetty monissa ohjelmakirjastoissa, muun muassa Scalan perus-API:ssa. Yksi esimerkki on yllä mainittusplit
-metodi.
Taulukot puskurien toteutuskeinona
Puskurin yhteydessä näkynyt ilmaisu ArrayBuffer
eli "taulukkopuskuri"
johtuu tavasta, jolla Scalassa käytetty puskuriluokka on toteutettu:
kullakin puskurioliolla on sisäisesti apunaan jonkin kokoinen
taulukko-olio, johon puskurin alkiot tallennetaan. Kun aputaulukosta
loppuu lisättäessä tila kesken, vaihtaa puskuriolio sen suurempaan
taulukkoon, johon se kopioi vanhat alkiot ja lisää uudet. Nimi
ArrayBuffer
viittaa tässä siis taulukoilla toteutettuun puskuriluokkaan.
Tällaisen puskurinkin käyttö on siis eräänlaista taulukon käyttöä, joskin epäsuorasti ja puskurin käyttäjän kannalta lähes huomaamattomasti.
Myös Scalan luokan Vector
toteutuksessa on käytetty apuna taulukkoa.
Junan debuggausta
Seuraavassa harjoituksessa on monelle ammattiohjelmoijalle tuttu tilanne: pitää setviä jonkun toisen tekemä koodisotku.
Tehtävänanto
Moduuli Train sisältää luokkia, joilla kuvataan junavaunuja ja niissä olevia
matkustajapaikkoja kuvitteellisessa ja yksinkertaistetussa paikanvarausjärjestelmässä.
Koodi on kirjoitettu vahvasti imperatiivisella tyylillä ja
rakentuu taulukoiden, tilanmuutosten ja do
- ja while
-silmukoiden varaan.
Annettu koodi ei ole esimerkillistä. Pahin puute on, että se ei toimi oikein.
Tehtävänäsi on laatia käynnistysolio, jolla testaat annettuja luokkia ja paikannat virheet. Löydetyt virheet pitää myös korjata, mutta se on tämän tehtävän selvästi helpompi osio.
Luokkien toivottu toiminta on kuvattu moduulin Scaladoc-dokumentaatiossa.
Ohjeita ja vinkkejä
Älä jumita tähän tehtävään tuntikausiksi. Pyydä neuvoa ajoissa.
Testaa koodi aluksi kunnolla. Siis kutsu metodeita kattavasti erilaisilla parametriarvoilla ja erilaisissa järjestyksissä. Tämä auttaa selvittämään, mikä toimii ja mikä ei. Tämän jälkeen debuggaa koodi eli paikanna virheiden syyt.
Luo käynnistysolio, jolla testaat ohjelmaa. Voit hyödyntää myös IntelliJ’n debuggerityökalua. Debuggeritta tehtävä voi olla huomattavastikin vaikeampi.
Virheitä on kaikkiaan kahdeksan kappaletta. Kukin niistä on selvästi eri osassa koodia.
Useat annetun koodin metodeista saavat luokat toimimaan oudosti tai ohjelman kaatumaan, jos niille antaa "selvästi hölmöjä" parametriarvoja (esim. asetetaan negatiivinen hyttien määrä makuuvaunuun tai lisätään junaan
null
-arvo vaunuolion sijaan). Tällaisia tapauksia ei tässä tehtävässä lueta virheiksi. Keskity löytämään sellaisia virheitä, jotka selvästi saavat koodin toimimaan toisin kuin spesifikaatio määrää, vaikka parametriarvot ovatkin järkeviä.SittingCar
on luokista monimutkaisin. Tutki muut luokat ensin. OtaSittingCar
käsittelyyn, kun ymmärrys tästä ohjelmakokonaisuudesta ja oma testausprosessi ovat vähän hioutuneet.Eräillä luokista on myös kumppanioliot määriteltyinä samaan tiedostoon. Nämä yksinkertaiset oliot ovat olemassa vain toimiakseen vakioiden sijoituspaikkoina.
Koodiin ei tarvitse tehdä tehokkuus- tai tyyliparannuksia, vaikka aihetta sellaisille löytäisitkin.
Myös ns. pöytätestaus (desk checking) voi auttaa, kuten tässä aiempivuotisessa palautteessa:
Tämä oli kyllä omalla tavallaan vaikea tehtävä, mutta aika helposti se ratkesi, kun tulostin ohjelman paperilla, otin lyijykynän käteen ja otin nivaskan saunalukemiseksi saunaan. :)
Saunonta ei ole menetelmän kannalta välttämätön.
Lisätehtävä: yksikkötestaus ja testausstrategiat
Tämä osin haastava bonustehtävä on tarkoitettu lähinnä sellaisille opiskelijoille, joilla on aiempaa ohjelmointikokemusta. Motivoituneet aloittelijat voivat kyllä myös tehdä tämän tehtävän. Testauksesta varsinaisemmin mm. kurssilla Ohjelmointistudio 2.
Tutustu internetin avustuksella järjestelmällisempiin tapoihin testata luokkia. Tee yksi tai useampia seuraavista.
- Etsi tietoa yksikkötestauksesta (unit testing). Yksikkötestaamalla pyritään varmistamaan, että kukin yksittäinen ohjelman osa toimii niin kuin sen pitää.
- Tee hieman kirjallisuustutkimusta testausstrategioista.
Selvitä, millaisiin strategioihin voi tukeutua, jotta
testeistä saa kattavat mutta välttyy tekemästä
järjettömän monta erillistä testiä. Valitse jokin
tunnettu testausstrategia (esim. all-pairs testing)
ja sovella sitä
o1.train
-pakkauksen luokkiin. - Etsi tietoa ScalaTest-ohjelmakirjastosta, jolla voi laatia yksikkötestejä Scala-ohjelmille. Selvitä, miten IntelliJ tukee ScalaTest-kirjaston käyttöä. Voit myös itse kokeilla käyttää tätä kirjastoa, jos haluat ja sinulla on aikaa perehtyä aiheeseen.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Yhteenvetoa
- Taulukko on muuttumattoman kokoinen alkiokokoelma, jonka sisältö
voi kuitenkin muuttua.
- Taulukot ovat yleisiä monissa ohjelmointikielissä ja ohjelmissa.
- Taulukoita käytetään, koska siitä voi tilanteesta riippuen olla esimerkiksi tehokkuusetua.
- Lukuun liittyviä termejä sanastosivulla: taulukko; testaus.
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.
ArrayBuffer
, kun olemme käyttäneet puskureita.