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

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

../_images/person07.png

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)
REPLin tulosteissa on läpi kurssin näkynyt ArrayBuffer, kun olemme käyttäneet puskureita.
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.
Tavalliseksi parametriksi annetaan luku, joka määrää taulukon koon. Tässä luodaan viisipaikkainen taulukko.
Metodi luo ja palauttaa halutunkokoisen taulukon, jonka sisältönä on oletusarvoja, tässä tapauksessa nollia.

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ä mainittu split-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. Ota SittingCar 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.

  1. Etsi tietoa yksikkötestauksesta (unit testing). Yksikkötestaamalla pyritään varmistamaan, että kukin yksittäinen ohjelman osa toimii niin kuin sen pitää.
  2. 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.
  3. 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.

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