Jyväskylän yliopisto Luentomoniste 11 |
|
University of Jyväskylä |
OHJELMOINTI 2
uudistettu painos
Vesa Lappalainen ja Santtu Viitanen
Jyväskylä 2012
Raino A. E. Mäkinen
Jyväskylän yliopisto
Tietotekniikan laitos
PL 35 (Agora)
40014 Jyväskylän yliopisto
fax (014) 260 4980
http://www.mit.jyu.fi
Copyright © 2012 Vesa Lappalainen, Santtu Viitanen ja
Jyväskylän yliopisto
ISBN 978-951-39-4624-1 (uudistettu painos)
ISSN 1456-9787
Jyväskylän yliopistopaino
Jyväskylä 2012
Alkuun annan sulle vinkin, joutavia on juorut muiden:
Luppo loppui, alkoi arki, kutsuu koulu - niinkö luulet? Uskoppas: YLIOPISTO vaatii työtavat totiset!
Ostolla oppi ei tulene eikä kauhan kaadannalla, myös ei vastuun välttämällä, työnsä muilla teettämällä
Jos sun mielesi tekevi, aivosi odottelevi, vilauttaa vinkin voinen, opastukset ongelmille. |
Pakko tehdä on demoja, harjoitukset harjoitella, itse illoin ihmetellä, kovin koodailla kotona.
Harjoitustyö haastavasta, syntyy aiheesta omasta, kokonaisuuden kuvaksi, metsän puilta mieltäväksi.
Luontune ei kurssi yksin: moni meitä auttamassa, ohjaajat opastamassa, valistaen vaadittaissa.
Toki saarnailen salissa, kerron joukolle jotakin, esimerkkejä esitän ynnä vaiheita valotan. |
Tämä moniste on tarkoitettu oheislukemistoksi Ohjelmointi 2-kurssille. Vaikka monisteen yksi teema onkin Java-kieli, ei kieli ole monisteen päätarkoitus. Päätarkoituksena on esitellä ohjelmointia. Esitystavaksi on valittu yhden ohjelman suunnitteleminen ja toteuttaminen alusta lähes loppuun saakka. Tämä Top-Down -metodi tuottaa varsin suuren kokonaisuuden, jonka hahmottaminen saattaa aluksi tuntua vaikealta.
Kunhan oppii kuitenkin katsomaan kokonaisuuksiin yksityiskohtien sijasta, asia helpottuu. Yksityiskohtia harjoitellaan monisteen esimerkeissä (Bottom-Up), joista suuri osa liittyy monisteen malliohjelmaan, mutta jotka silti voidaan käsittää mallista riippumattomina palasina.
Monisteen ohjelmat on saatavissa myös elektronisesti, jotta niiden toimintaa voidaan kokeilla kunkin vaiheen jälkeen.
Java-kieltä ja sen ominaisuuksia on monisteessa sen verran, että lukijalla on juuri ja juuri erittäin pienet mahdollisuudet selvitä ilman muuta kirjallisuutta.
Lukijan kannattaakin ilman muuta hankkia ja seurata tämän monisteen rinnalla jotakin varsinaista Java-ohjelmointikirjaa. Hyvä kotimainen vaihtoehto on esimerkiksi: Jorma Kyppö, Mika Vesterholm: Java-ohjelmointi, 2008, Talentum Oyj. Myös ohjelmointiympäristön mukana olevasta OnLine-avustuksesta (Help) saa tarvittavaa lisätietoa.
Monisteen esimerkkiohjelmat löytyvät elektronisessa muodossa:
Mikroluokka: hakemisto: n:\kurssit\ohj2\moniste\esim
WWW: URL: https://svn.cc.jyu.fi/srv/svn/ohj2/moniste/esimerkit/src/
Edellä mainittuun polkuun lisätään vielä ohjelman yhteydessä mainittu polku.
Monisteessa on lukuisia esimerkkitehtäviä, joiden tekeminen on oppimisen kannalta lähes välttämätöntä. Vaikka lukija saattaa muuta kuvitellakin, ovat monisteen vaikeimmat tehtävät monisteen alussa. Mikäli loppupuolen tehtävät tuntuvat vaikeilta, ei monisteen alkuosa olekaan hallinnassa. Siksi kehotankin lukijaa aina vaikeuksia kohdatessaan palaamaan monisteen alkuosaan; siitä ei monikaan voi sanoa, ettei asioita ymmärtäisi.
Lopuksi kiitokset kaikille työtovereilleni monisteen kriittisestä lukemisesta. Erityisesti Tapani Tarvainen on auttanut suunnattomasti C-kieleen tutustumistani ja Jonne Itkonen vastaavasti tutustumista Java ja C++-kieleen ja olio-ohjelmointiin.
Alkuperäinen versio allekirjoitettu Palokassa 28.12.1991
Monisteen 3. korjattuun painokseen on korjattu edellisissä monisteissa olleita painovirheitä sekä lisätty lyhyt C-kielen "referenssi". Lisäksi kunkin esimerkkiohjelman alkuun on laitettu kommentti siitä, mistä tiedostosta lukija löytää esimerkin. Myös hakemistoa on parannettu vahventamalla määrittelysivun sivunumero.
Monisteen 4. korjattuun painokseen on jopa vaihdettu monisteen nimi: Ohjelmointi++, kuvaamaan paremmin olio-ohjelmoinnin ja C++:n saamaa asemaa. Tätä kirjoittaessani moniste ei ole vielä kokonaan valmis ja kaikkia siihen tulevia muutoksia en vielä tässä pysty luettelemaan.
Joka tapauksessa olen monisteeseen lisännyt tekstiä - valitettavasti nimenomaan ohjelmointikieleen liittyvää - jota vuosien varrella olen huomannut opiskelijoiden jäävän kaipaamaan. Lisäksi kunkin luvun alkuun on lisätty suppea luettelo luvun pääteemoista ja luvussa esiintyvistä kielen piirteistä sekä niiden syntaksista. Tämä syntaksilista on helppolukuinen "lasten" syntaksi, varsinainen tarkka ja virallinen syntaksi pitää katsoa kielen määrityksistä.
Ohjelmalistauksiin on lisätty syntax-highlight, eli kielen sanat on korostettu ja näin lukijan toivottavasti on helpompi löytää mitkä termit on itse valittavissa ja mitkä täytyy kirjoittaa juuri kuvatulla tavalla. Myös joitakin vinkkejä on lisätty. Pedagogisesti on vaikea päättää saako esittää virheellisiä tai huonoja ohjelmia lainkaan, mutta vanha viidakon sananlasku sanoo että "Viisas oppii virheistä, tavallinen kansa omista virheistä ja tyhmä ei niistäkään". Siis mahdollisuus "viisaillekin" ja nämä virheelliset ohjelmat on merkitty surullisella naamalla: ☹. Näin aivan jokaisen ei tarvitse rämpiä jokaista sudenkuoppaa pohjia myöten ominpäin.
Olio-ohjelmointi on kuvattu esimerkkien avulla ja varsinainen oliosuunnittelu - joka on erittäin tärkeää - on jätetty erittäin vähälle. Suosittelenkin lukijalle jonkin oliosuunnitteluun liittyvän kurssin käymistä tai kirjan lukemista.
Myös tätä kurssia edeltävä kurssi Ohjelmoinnin alkeet on kokenut muutoksia ja vaikka kurssi meneekin nykyisin entistä pitemmälle, ei tästä monisteesta ole kuitenkaan poistettu kaikkea päällekkäisyyttä Ohjelmoinnin alkeet -monisteen kanssa. Toimikoon nämä päällekkäisyyden kertauksena ja kohtina, joissa luennoilla voidaan asia sivuttaa nopeammin. Joka tapauksessa lukijan kannattanee pitää myös Ohjelmoinnin alkeet -moniste tämän monisteen rinnalla.
Monisteen uusi painos on kirjoitettu C++:n sijasta Java-ohjelmointia silmällä pitäen.
Monisteen vuoden 2012 versioon on lisätty graafisen käyttöliittymän tekeminen Swing-kehyksellä sekä korjattua lukuisia pieniä yksityiskohtia. Kiitoksia Santtu Viitaselle tehdystä työstä.
Palokassa 25.12.2011
Vesa Lappalainen
Alkoi kurssi, alkoi uusi
tuska tuli, moni jo huusi:
Javaa jankuttaa tuo ukko
syntaksia sammaltaapi.
Tokko tavalla tuollasella
ohjelmoimaan oppimahan
Java kieltä pänttämähän
Ceetä kalloon taikomahan.
Arvelee, ajattelevi,
pitkin päätänsä pitävi:
Ei oo ulkoo oppimista,
kieli väkisin vääntämistä.
Pohtimaan pitää heretä
ongelmia oikomahan
sulamahan suunittelu
pohja vankaksi valaman.
Tämän monisteen tarkoituksena on toimia tukimateriaalina opeteltaessa sekä algoritmisen että olio-ohjelmoinnin alkeita. Aluksi meidän tulee ymmärtää mitä kaikkea ohjelmointi pitää sisällään. Aivan liian usein ohjelmointi yhdistetään päätteen äärellä tapahtuvaan jonkin tietyn ohjelmointikielen koodin naputtamiseen. Tämä on ehkä ohjelmoinnin näkyvin, mutta myös toisaalta mekaanisin ja helpoin osa.
Ohjelmointi voidaan jakaa esimerkiksi seuraaviin vaiheisiin:
• tehtävän saaminen
• tehtävän tarkentaminen ja tarvittavien toimintojen hahmottaminen
• ohjelman toimintojen ja tietorakenteiden suunnittelu, oliosuunnittelu
• yksityiskohtaisten olioiden ja algoritmien suunnittelu
• OHJELMOINTITYÖKALUN VALINTA
• algoritmien/luokkien metodien tarkentaminen valitulle työkalulle
• ohjelmakoodin kirjoittaminen
• ohjelman testaus
• ohjelman käyttöönotto
• ohjelman ylläpito
Kannattaa huomata että listalla varsinaisesti tietokoneella tehtävä työ on listan viimeisissä kohdissa. Pitkään ohjelmistojen suunnittelu ja toteutus seurasivatkin orjallisesti vaihe vaiheelta yllä olevan kaltaista listaa. Tämä vesiputousmalliksi kutsuttu toteutustapa oli ennen ohjelmistokehityksen kulmakivi, jolla toteutettiin käytännössä kaikki ohjelmointiprojektit. Maailmalla ilmestyi kuitenkin tutkimuksia joiden mukaan suurin osa ohjelmistoprojekteista itse asiassa epäonnistui, mikä tietysti on hälyttävää millä tahansa alalla.
Nykyään perinteisen vesiputousmallin rinnalle on noussut niin sanottu ketterä ohjelmistokehitys (Agile Software Development). Se on joukko menetelmiä joiden leimaavin piirre on ohjelmien kehittäminen pienissä pätkissä, joissa jokaisessa toteutetaan kaikki ohjelmistokehityksen vaiheet. On jopa mahdollista että yksi ihminen työstää useaa vaihetta kerralla! Ketterillä menetelmillä on oma julistus, Agile Manifesto, jonka arvoja ne pyrkivät noudattamaan.
• Yksilöt ja vuorovaikutus yli prosessien ja työkalujen
• Toimiva ohjelma yli kokonaisvaltaisen dokumentaation
• Asiakasyhteistyö yli sopimusneuvottelujen
• Muutoksiin vastaaminenyli suunnitelman seuraamisen
Tavoitteita tulkittaessa täytyy kuitenkin muistaa, että vaikka menetelmät pitävätkin lihavoituja asioita arvokkaampana, niin ne eivät tee silti muista merkityksettömiä.
Vaikka tavoitteet ovatkin yhteneväiset, niin menetelmien väliset erot ovat usein suuria. Jotkut saattavat painottuvat projektinhallintaan, kun taas joku tarjoaa käytännön ohjeita ohjelmoijan työskentelytapoihin.
Tämän kurssilla opetuksessa ja varsinkin harjoitustyön toteuttamisessa pyritään mahdollisuuksien mukaan soveltamaan ja lainaamaan paljon niin sanotulta Extreme Programming (XP) menetelmältä. Tietenkin menetelmä on kehitetty työelämän tarpeisiin, eikä sen soveltaminen sellaisenaan opetuskäyttöön ole mahdollista.
• Iteraatiot
• Aiemmista kokemuksista oppiminen
• Testilähtöinen ohjelmointi
• Pariohjelmointi
• Uudelleenrakentaminen
• Yhteisomistajuus
• Jatkuva integrointi
• Et tule tarvitsemaan sitä (yksinkertainen rakenne)
• Hallinnon (opettajat ja ohjaajat) taustatuki
• Tasainen työtahti
• Julkaisujen suunnittelu
• Hyväksyntätestaus
• Lyhyin väliajoin tuotettavat julkaisut (pienet julkaisut)
Aluksi kurssi keskittyy ohjelmoinnin perusteiden, kuten algoritmien ja oman ohjelman suunnitteluun. Nykyisin suunnittelun alkuvaiheessakin tarvittava dokumentointi ja ideoiden sekä vaihtoehtojen kirjaaminen tehdään käyttäen tekstinkäsittelyohjelmia ja/tai kaavioiden piirtoa piirto-ohjelmilla. Varsinaisesta koodauksesta ei kuitenkaan alkuvaiheessa ole kysymys.
Ohjelman kehityksen eri vaiheissa saatetaan tarvittaessa palata takaisin alkumäärityksiin. Kuitenkin ohjelman valittujen toimintojen muuttaminen oman laiskuuden tai osaamattomuuden takia ei ole suotavaa. Ei saa lähteä ompelemaan kissalle takkia ja huomata, että kangas riittikin lopulta vain rahapussiin.
Usein ohjelmointikursseilla unohdetaan itse ohjelmointi ja keskitytään valitun työkalun - ohjelmointikielen - esittelyyn. Ajanpuutteen takia tämä onkin osin ymmärrettävää. Kuulijat kuitenkin hämääntyvät, eivätkä ymmärrä luennoitsijan tekevän edellä kuvatun listan kaltaista suunnittelutyötä myös kunkin pienen malliesimerkin kohdalla. Kokenut ohjelmoija saattaa pystyä hahmottamaan ongelman ratkaisun ja tarvittavat erikoistapaukset päässään silloin, kun on kyse erittäin lyhyistä malliesimerkeistä. Jossain vaiheessa ohjelmoinnin oppimista suunnittelu ja koodin kirjoittaminen tuntuvat sulautuvan yhteen.
Opiskelun alkuvaiheessa on kuitenkin syytä keskittyä nimenomaan ongelman analysointiin ja ohjelman suunnitteluun. Tässä paras apu on usein terve maalaisjärki. Mitä vähemmän ymmärtää itse ohjelmointikielistä, sitä vähemmän kielet rajoittavat luovaa ajattelua.
Usein ohjelman suunnittelu voidaan aloittaa jopa käyttöohjeen kirjoittamisella! Tällöin tulee tutkituksi ohjelmalta vaaditut ominaisuudet ja toimintojen loogisuus sekä helppokäyttöisyys! Nykytyökaluilla voidaan myös rakentaa suhteellisen helposti ensin ohjelman käyttöliittymä ilman oikeita toimintoja. Tätä "protoa" voidaan sitten tutkia yhdessä asiakkaan kanssa ja päättää toimintojen loogisuudesta ja riittävyydestä.
Kun ohjelmaan on suunniteltu halutut toimenpiteet ja päätetty mitä tietorakenteita tarvitaan, on edessä työkalun valinta. Nykypäivänä ei ole itsestään selvää, että valitaan työkaluksi jokin perinteinen ohjelmointikieli. Vastakkain pitää asettaa erilaiset sovelluskehittimet, valmisohjelmat kuten tietokannat ja taulukkolaskennat, ehkä jopa tavallinen tekstinkäsittely sekä ohjelmointikielet. Matemaattisissa ongelmissa jokin symbolisen tai numeerisen laskennan paketti saattaa olla soveltuva.
Ratkaisu voi koostua myös useiden eri ohjelmien toimintojen yhdistelemisestä: CAD -ohjelmalla piirretään/digitoidaan kartan pohjakuva, tietokantaohjelmalla pidetään kirjaa paikoista ja pienellä C/C++ tai Java-kielisellä ohjelmalla suoritetaan ne osat, joita CAD-ohjelmalla tai tietokantaohjelmalla ei voida suorittaa.
Joskus työkaluksi valitaan prototyyppiä varten jokin sovelluskehitin tai tietokantaohjelmisto. Kun halutut toiminnot on perusteellisesti testattu ja tuotetta tarvitsee edelleen kehittää, voidaan ohjelmointi toteuttaa uudelleen vaikkapa Java-kielellä. Prototyyppi on rinnalla toimivana ja uudessa ohjelmassa käytetään samoja tietoja ja toimintoja.
Mikäli työkalun valinnassa päädytään olio/lausekieleen (esim. C++ tai Java), ei pyörää kannata keksiä uudelleen. Nelikulmioon nähden kolmikulmiossa on yksi poksaus vähemmän kierroksella, mutta kyllä silti ympyrä on paras. Siis käytetään toisten kirjoittamia valmiita olioita ja/tai aliohjelmapaketteja "likaisessa" työssä.
Aina tietenkin puuttuu joitakin alemman tason palasia. Nämä tietysti koodataan JA TESTATAAN ERILLISINÄ ennen varsinaiseen ohjelmaan liittämistä.
Siis itse koodaus on pienten aputyökalujen etsimistä, tekemistä, testaamista ja dokumentointia. Lopullinen koodaus on näiden aputyökaluista muodostuvan palapelin yhteen liittäminen.
Jo koodausvaiheessa kannattaa miettiä ongelman yleisiä ominaisuuksia. Jos ollaan kirjoittamassa telinevoimistelun pistelaskua naisten sarjaan, niin koodissa ei mitenkään tulisi estää ohjelman käyttöä myös miesten sarjassa. Siis telineiden nimet ja määrät pitäisi olla helposti muutettavissa.
Koodausta voidaan tehdä joko BOTTOM-UP periaatteella, jolloin ensin rakennetaan työkalut (=olioluokat/aliohjelmat) jotka sitten kasataan yhteen. Toinen mahdollisuus on koodaus TOP-DOWN periaatteella, jolloin päätoiminnat kirjoitetaan ensin ja alatoiminnoista tehdään aluksi tyhjiä laatikoita. Myöhemmin valmiita ja testattuja alitoimintoja liitetään tähän runkoon. Valitulla menetelmällä ei ole vaikutusta lopputulokseen ja joskus voikin olla hyvää vaihtelua siirtyä näpertelemään pikkuasioiden kimpussa isojen kokonaisuuksien sijasta tai päinvastoin.
Missään tapauksessa ohjelma ei synny siten kuin se kirjallisuudessa näyttää olevan: alkumäärittelyt, aliohjelmat ja päämoduuli.
Koodaajan on osattava hyvin käytettävä työkalu, esim. ohjelmointikieli. Kuitenkin jonkin ohjelmointikielen hyvän osaamisen avulla on suhteellisen helppo kirjoittaa myös muunkielisiä ohjelmia.
Koodaus on pääosin tekstinkäsittelyä ja 10-sormijärjestelmä nopeuttaa koodin syntymistä oleellisesti. Myös hyvä tekstinkäsittelytaito valmiiden palasten siirtelemisineen ja kopioimisineen helpottaa tehtävää.
Ohjelman testaus alkaa jo suunnitteluvaiheessa. Valitut algoritmit ja toiminnot pitää pöytätestata teoriassa ennen niiden koodaamista. Suunnitteluvaiheessa täytyy miettiä kaikki mahdolliset erikoistapaukset ja todeta algoritmin selviävän niistäkin tai ainakin määritellä miten erikoistapauksissa menetellään. Testitapaukset kirjataan ylös myöhempää käyttöä varten.
Koodausvaiheessa kukin yksittäinen aliohjelma/luokka testataan kaikkine mahdollisine syötteineen pienellä testiohjelmalla. Aliohjelman kommentteihin voidaan kirjata suunnitteluvaiheessa todettu testiaineisto ja testausvaiheessa ruksataan testatut toiminnot ja erikoistapaukset. Tavan heikkous piilee kuitenkin siinä, että mikäli haluamme muuttaa nyt alkuperäistä koodia, meidän on mahdoton tietää vaikuttaako muutos johonkin toiseen ohjelmiston osa-alueeseen joka käyttää koodia hyväkseen.
Nykyisin ratkaisuksi on kehitetty testausta automatisoivia työkaluja, kuten Javan käyttämä JUnit. Yksikkötestauksen idea on kirjoittaa jokaisen ohjelmiston osaan testikoodi, mikä voidaan ajaa keskitetysti vaikka koko ohjelmistolle kerralla.
Eräs yksikkötestausta hyödyntävä tekniikka on testivetoinen kehitys (TDD, test-driven development). Sen tarkoituksena on kirjoittaa koodi testattavaksi ja testit ennen varsinaisen ohjelmakoodin kirjoittamista. Tämän ehkä aluksi nurinkuriselta tuntuvalla ajatuksella on kuitenkin useita hyötyjä. Kyse ei ole niinkään testaustyökalusta, vaan ohjelman suunnittelusta, josta syntyykin sivutuotteena valmiit testitapaukset.
Tällä kurssilla testaamiseen voi käyttää myös Jyväskylän yliopistossa kehitettyä ComTest työkalua. Työkalu helpottaa JUnit testien tekemistä ja sen avulla pystyy samalla luomaan myös kattavan JavaDoc dokumentaation.
Lopullisen ohjelman toimivuus riippuu hyvin paljon siitä, miten hyvistä palasista se on kasattu.
Ennen virheiden löytämiseksi testiohjelmiin lisättiin tulostuslauseita. Nykyisin tehokkaat debuggerit helpottavat testausta huomattavasti: ohjelman toimintaa voidaan seurata askel kerrallaan ja epäilyttävien muuttujien arvoja voidaan tarkistaa kesken suorituksen. On myös mahdollista laittaa ohjelma pysähtymään jonkin muuttujan saadessa virheellisen arvon.
Testaus on vaihe, missä hyvä koneenkäyttörutiini ja epäluulo ovat suureksi avuksi.
Ketterien menetelmien tärkeimpiä osa-alueita on jatkuva vuorovaikutus asiakkaan kanssa. Aluksi tällä kurssilla asiakkaana toimii oppilas itse suunnitellessaan harjoitustyöohjelman toiminnot ja käyttötarkoituksen. Harjoitustyötä tehdään pienissä vaiheissa, eli iteraatioissa, joiden tarkoitus on pitää ohjelma jatkuvasti toimivana kokonaisuutena, mihin on helppo lisätä uusia ominaisuuksia yksi kerrallaan. Vaiheen päätettyä tehdään hyväksymistestaus, jossa työ esitellään asiakkaalle (ohjaajalle), jolta saa vinkkejä ja palautetta ohjelman toiminnan parantamiseksi.
Jokaisessa vaiheessa toteutetaan jokin ohjelman osa-alue tai parannellaan vanhaa. Tämä työ sisältää suunnittelun, testauksen, koodauksen ja dokumentoinnin.
Ketteriä menetelmiä käyttämällä ohjelman käyttöönottovaiheessa sen pitäisi olla testattu ja valmis. Tietysti julkaisuversioonkin pääsee lähes aina livahtamaan joitakin bugeja, mutta niiltä ei taitavinkaan ohjelmoija voi välttyä. Asiakas on lisäksi pidetty mukana koko prosessin ajan, eikä ikäviä yllätyksiä - joissa ohjelma ei olekaan toiminnallisuudeltaan sitä mitä on odotettu - pääse syntymään.
Jos kuitenkin ohjelmasta paljastuu virheitä tai puuttuvia toimintoja. Virheet pitää korjata ja puuttuvat toiminnot mahdollisesti lisätä, jolloin ollaan jälleen ohjelmansuunnittelun alkuvaiheessa. Hyvin suunniteltuun ohjelmaan saattaa olla helppo lisätä uusia toimintoja ja vastaavasti huonosti suunnitellussa saattavat jopa tietorakenteet mennä uusiksi. Tosin tätäkään ei pidä pelätä, sillä yksinkertaisesti aina ei ole mahdollista ottaa etukäteen kaikkea huomioon.
Myös ohjelman alkuperäiset kirjoittajat ovat saattaneet häipyä ja mikäli kehitysprosessiin ei ole kiinnitetty tarpeellista huomiota, niin joku onneton kesätyöntekijä joutuu ensitöikseen paikkaamaan toisten huonosti dokumentoimaa sotkua.
Ohjelmointi ei yleensä ole yhden henkilön työtä. Eri henkilöt voivat tehdä eri vaiheita ohjelmoinnissa. Lähes aina tulee tilanne, missä jonkin toisen kirjoittamaa koodia joudutaan korjailemaan.
Oli ohjelmaa tekemässä kuinka monta henkilöä tahansa (vaikka vain yksi), pitää ohjelmointi jakaa vaiheisiin. Oikeaa ohjelmaa on mahdoton "nähdä" valmiina Java-kielisinä lauseina heti tehtävän määrityksen antamisen jälkeen. Aloitteleva ohjelmoija kuitenkin haluaisi pystyä tähän (koska hän "näkee" määrityksestä: Kirjoita ohjelma joka tulostaa "Hello world", heti myös Java-kielisen toteutuksen). Tämän takia ohjelmoinnin helpoin osa, eli koodaus koetaan ohjelmoinnin vaikeimmaksi osaksi - suunnittelu on unohtunut!
Valitulla ohjelmointikielellä ei ole suurtakaan merkitystä ohjelmoinnin toteuttamiseen. Jokin kieli saattaa soveltua paremmin johonkin tehtävään, mutta pääosin BASIC, Fortran, Pascal, C, Modula-2, ADA jne. ovat samantyylisiä lausekieliä. Samoin oliokielistä esimerkiksi C++, Java, C#, Delphi (Pascal) ja Python ovat hyvin lähellä toisiaan. Kun yhden osaa, on toiseen siirtyminen jo helpompaa.
Jos joku kuvittelee, ettei hänen tarvitse koskaan ohjelmoida C/C++ taiJava-kielellä, voi hän olla aivan oikeassakin. Nykyisin kuitenkin jokaisessa tietokantaohjelmassa, taulukkolaskentaohjelmassa ja jopa tekstinkäsittelyohjelmissakin (vrt. esim. TEX, joka on tosin ladontaohjelma) on omat ohjelmointikielensä. Osaamalla jonkin ohjelmointikielen perusteet, voi saada paljon enemmän hyötyä käyttämästään valmisohjelmasta. Ja joka väittää selviävänsä nykymaailmassa (ja sattuu lukemaan tätä monistetta) esimerkiksi ilman tekstinkäsittelyohjelmaa on suuri valehtelija!
Nyt tavuja taikomahan,
koodia kokoamahan?
Tuosta tokkopa tulisi
ohjelmaapa oivallista.
Ongelma jo täytyy olla
suunnitelma siivitellä
aikeet aina aatostella
toki tarpeet tarkastella.
Saatatko tuon jo sanoa
tieto kusta tarvitahan
ohjelman ositeltavan
jo bitteiksi pilkottavan.
Nyt liimaile liittymätä
sitä silmälle suotavaksi
käyttäjälle nähtäväksi
muille mutristeltavaksi.
Mitä tässä luvussa käsitellään?
• tehtävän "analysointi"
• ohjelman vaatimien aputiedostojen sisällön suunnittelu
• ohjelman suunnittelu ohjelman tulosteiden avulla
• suunnitelman korjaus
• tarvittavien algoritmien hahmottaminen
• relaatiotietomalli
Ohjelman suunnittelu aloitetaan aina tehtävän tarkastelulla. Annettua tehtävää joudutaan usein huomattavasti tarkentamaan.
Olkoon tehtävänä suunnitella kerhon jäsenrekisteri. Onko kerho iso vai pieni? Mitä tietoja jäsenistä tallennetaan? Mitä ominaisuuksia rekisteriltä halutaan?
Mikäli sovitaan, että kerho on kohtuullisen pieni (esim. alle 500 jäsentä), ei meidän heti alkuun tarvitse miettiä parhaita mahdollisia hakualgoritmeja eikä tiedon tiivistämistä.
Mitä tietoja jäsenistä tarvitaan?
- nimi
- hetu
- katuosoite
- postinumero
- postiosoite
- kotipuhelin
- työpuhelin
- autopuhelin
- liittymisvuosi
- tämän vuoden maksetun jäsenmaksun suuruus
- lisätietoja
jne...
Mitä ominaisuuksia rekisteriltä halutaan?
- kerholaisten lisääminen
- kerholaisten poistaminen
- tietyn kerholaisen tietojen hakeminen
- tietyn kerholaisen tietojen muuttaminen
- postitustarrat postinumerojärjestyksessä
- nimilista nimen mukaisessa järjestyksessä
- lista jäsenmaksua maksamattomista jäsenistä
jne...
On varsin selvää, ettei tätä nimenomaista tehtävää kannattaisi nykypäivänä lähteä itse ohjelmoimaan, vaan turvauduttaisiin tietokantaohjelmaan. Joissakin erikoistapauksissa saatetaan vaatia ominaisuuksia, joita tietokantaohjelmasta ei saada. Tällöin työkaluksi valittaisiin lausekieli ja tietokantaohjelmiston aliohjelmakirjasto, joka hoitelee varsinaiset tietokannan ylläpitoon yms. liittyvät toimenpiteet.
Edellinen analyysi on kuitenkin tehtävä työkalusta riippumatta! Esimerkin vuoksi jatkamme tehtävän tutkimista hieman pidemmälle tavoitteena ohjelmoida jäsenrekisteri jollakin lausekielellä.
Mikäli työkalun valinnassa on päädytty johonkin lausekieleen, on jossain vaiheessa päätettävä käytettävistä tietorakenteista. Esimerkin tapauksessa meillä on selvästikin joukko yhden henkilön tietoja. Mikäli yhden henkilön tietoa pidetään yhtenä yksikkönä (tietueena), on koko tietorakenne taulukko henkilöiden tiedoista. Taulukko voidaan tarvittaessa toteuttaa myös lineaarisena listana tai jopa puurakenteena. Mikäli kyseessä on pieni rekisteri, mahtuu koko tietorakenne ohjelman ajon aikana muistiin.
Missä tiedot tallennetaan kun ohjelma ei ole käynnissä? Tietenkin levyllä tiedostona. Minkä tyyppisenä tiedostona? Tiedoston tyyppinä voisi olla binäärinen tiedosto alkioina henkilötietueet. Tällaisen tiedoston käsittely hätätapauksessa on kuitenkin vaikeata. Varmempi tapa on tallentaa tiedot tekstitiedostoksi, jota tarvittaessa voidaan käsitellä millä tahansa tekstinkäsittelyohjelmalla. Tällöin on lisäksi usein mahdollista käsitellä tiedostoa taulukkolaskentaohjelmalla tai tietokantaohjelmalla ja näin joitakin harvinaisia toimintoja voidaan suorittaa rekisterille vaikkei niitä olisi alunperin edes älytty laittaa ohjelmaan mukaan.
Minkälainen tekstitiedosto? Ehkäpä yhden henkilön tiedot yhdellä rivillä? Miten yhden henkilön eri tiedot erotetaan toisistaan? Mahdollisuuksia on lähinnä kaksi: erotinmerkki tai tietty sarake. Valitaan erotinmerkki. Usein on mukavaa lisäksi laittaa joitakin huomautuksia eli kommentteja tiedostoon. Siis tallennustiedoston muoto voisi olla vaikkapa seuraava:
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
;sukunimi etunimi|hetu|katuosoite|postinumero|postiosoite|kotipuhelin|työpuhelin|
Ankka Aku|010245-123U|Ankkakuja 6|12345|ANKKALINNA|12-12324||
Susi Sepe|020347-123T||12555|Takametsä|||
Ponteva Veli|030455-3333||12555|Takametsä|||
Tällaisenaan tiedosto on varsin suttuinen luettavaksi. Vaikka valitsimmekin erotinmerkin erottamaan tietoja toisistaan, voimme silti kirjoittaa vastaavat tiedot allekkain sopimalla, ettei loppuvälilyönneillä ole merkitystä.
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
;sukunimi etunimi |hetu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
Susi Sepe |020347-123T| |12555 |Takametsä | | |
Ponteva Veli |030455-3333| |12555 |Takametsä | | |
Nyt tiedostoa on helpompi lukea ja tyhjien kenttien jättäminen ei ole vaikeaa. Tiedosto vie kuitenkin levyltä enemmän tilaa kuin ensimmäinen versio, mutta sillä ei tietenkään nykyään ole mitään merkitystä. Lisäksi yhden henkilön tiedot eivät mahdu kerralla näyttöön. Onneksi kuitenkin lähes kaikki tekstieditorit suostuvat rullaamaan näyttöä myös sivusuunnassa. Mikäli saman henkilön tietoja jaettaisiin eri riveille, tarvitsisi meidän valita vielä tietueen loppumerkki (nytkin se on valittu: rivinvaihto).
Jatkosuunnittelu on ehkä helpointa tehdä suunnittelemalla ohjelman toimintaa käyttöohjeen tai käyttöliittymän tavoin.
Vaikka nykyaikaisilla ohjelmointiympäristöillä käyttöliittymän piirtäminen on tottuneelle käyttäjälle todella nopeaa, niin ensikertalaisen on kuitenkin helpompaa nopeampaa toteuttaa alustava suunnittelu perinteisesti esimerkiksi kynällä ja paperilla.
Suunnittelussa toimitaan käyttäjän ja helppokäyttöisyyden (= myös nopea käyttö, ei aina välttämättä hiiri) ehdoilla. On myös huomioitava ohjelmoitava alusta ja siinä vakiintuneet tavat toteuttaa toimintoja.
Ohjelma käynnistetään klikkaamalla kerho.jar-ikonia tai antamalla komentoriviltä komento
java -jar kerho.jar
Kun ohjelma käynnistyy, tulostuu näyttöön
Kerhon tiedot on tallennettu vaikkapa tiedostoon nimet.dat (hakemistoonkelmit). Näin voimme ylläpitää samalla ohjelmalla useiden eri kerhojen tietoja. Mitäpä jos tiedostoa ei ole? Tällöin voi syynä olla kirjoitusvirhe tai se, ettei rekisteriä ole vielä edes aloitettu. Miten ohjelman tulee tällöin menetellä? Mikäli käyttäjä antaa tiedoston nimen, jollaista ei tunneta, tulostuu näyttöön:
Tällöin käyttäjä voi aloittaa syöttämään uusia jäseniä tai jos kirjoitti nimen väärin, hän voi ottaa menusta Avaa-valinnan ja antaa uuden nimen
Edellä on edetty siihen saakka, kunnes ohjelmassa on päädytty pääikkunaan.
Pääikkunassa on seuraava menurakenne.
Tiedosto Muokkaa Apua
======== ======= =====
Talleta Lisää uusi jäsen Apua
Avaa... Poista jäsen... Tietoja...
Tulosta...
Lopeta
Seuraavaksi voimme lähteä tarkastelemaan eri alakohtien toimintaa.
Pääikkunan vasemmassa reunassa näkyy Hakuehto. Tästä voi valita minkä kentän mukaan etsitään. Tämän jälkeen tekstikenttään voi syöttää hakuehdon ja listaan tulee vain ne jäsenet joille haku toteutuu. Hakutermi saa löytyä valitusta kentästä mistä kohti vaan. Esimerkiksi jos kirjoitetaan hakuehtoon s, niin haetaan kaikki jäsenet joiden nimessä on s jossakin kohti.
Löytyneet jäsenet lajitellaan valitun hakukentän perusteella.
Valittua jäsentä voidaan muokata menemällä tietoihin oikeaan kohtaan ja kirjoittamalla uusi arvo. Jos tietoon syötetään jotakin mikä ei kelpaa toimitaan seuraavasti.
Hetussa syötetty muodossa: 010243G1234
Tulee ilmoitus:
Väärä erotinmerkki
Samalla virheellinen syöttökenttä menee punaiseksi.
Luo uuden tyhjän jäsenen.
Poistaa listasta valitun jäsenen. Varmistaa ennen poistoa.
Poistetaanko jäsen Ankka Iines?
Ok Cancel
Tulostaa hakuehdon täyttävät jäsenet erilliseen ikkunaan halutussa muodossa. Tässä "esikatselussa" voi vielä muuttaa tietoja ja sitten tulostaa paperille.
Ohjelman lopetuksessa tulee huolehtia siitä, että ohjelman aikana mahdollisesti rekisteriin tehdyt muutokset tulevat tallennetuiksi. Tämä voidaan tehdä automaattisesti tai tallennus voidaan varmistaa käyttäjältä. Automaattisen tallennuksen tapauksessa alkuperäinen tiedosto on ehkä syytä tallentaa eri nimelle.
Näyttää selaimessa ohjelman käyttöohjeen
Näyttää ohjelmasta tietoja vähän samaan tapaan kuin aloitusikkunassakin.
Kun vaihe on valmis ja ohjelma täyttää sille asetetut vaatimukset se käydään läpi yhdessä asiakkaan ja tiimin kanssa, eli tässä tapauksessa jollakin kurssin ohjaajista. Usein tässä vaiheessa keksiikin työhön jotain parannuksia, mitä ei itse ole tullut ajatelleeksi. Viat yleensä korjataan seuraavaan vaiheeseen mennessä, mutta mikäli työ on jäänyt huomattavan keskeneräiseksi, niin kannattaa näyttää koko vaihe uudestaan.
Ensimmäisessä vaiheessa tutkitaan siis kirjoitettua käyttöohjetta, tietorakennetta ja piirrettyjä kuvia, jotka muodostavat alustavan suunnitelman ohjelman toiminnallisuudesta.
Alussa tyypillisimmät viat liittyvät liian minimalistiseen dokumentaatioon. Dokumentaatiossa on hyvä pyrkiä täsmällisyyteen, sillä luotua tietoa hyödynnetään jatkuvasti projektin edetessä. Kaikki yleisimmät virheet johtuvat siitä, että yritetään oikaista asioissa, jotka vievät muutenkin vain murto-osan vaiheeseen käytetystä ajasta.
Tyypillistä on että mallitiedostot näyttävät jokseenkin tältä:
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
;sukunimi etunimi |hetu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
Joku1 |00000-5555 |Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
Joku2 |…
…
Tiedoston sisällöstä saa nyt jonkinlaisen idean, mutta selkeyden vuoksi mallidataa tarvitsee useita rivejä ja sen tulisi koostua "oikeista" arvoista. Dokumentaatiota kirjoittaessa pieni ajan säästäminen kostautuu useasti projektin edetessä. Kun sisällön tekee nyt kunnolla, niin samaa dataa voi käyttää hyödyksi viidennessä vaiheessa tietorakennekuvaa piirrettäessä, sekä mallitiedostona ohjelmaa luotaessa.
Lisäksi kannattaa miettiä ohjelman käyttötarkoitusta. Olisi rasittavaa jos Kerho aina varmistaisi saako käyttäjän lisätä, koska virheellisesti luodun henkilön tietoja voi kuitenkin jälkikäteen muokata. Käyttäjälle näytettävät varmistusdialogit sopivat peruuttamattomien muutoksien yhteyteen, mutta väärässä paikassa käytettynä ne hidastavat käyttöä täysin turhaan.
Ennen harjoitustyön näyttämistä kannattaa aina käydä tarkistamassa kurssin wikistä malliharjoitustyö ja tyypilliset harjoitustyön viat, jolloin selviää turhalta korjaamiselta.
Nyt olemme selvillä ohjelman toiminnasta. Edellisestä käyttöohjeesta voimme etsiä mitä työkaluja (aliohjelmia) tarvitsemme ohjelman toteutuksessa. Ainakin seuraavat tulevat helposti mieleen:
1. tiedoston lukeminen
2. tiedoston tallentaminen
3. henkilön tietojen kysyminen päätteeltä
4. tiedoston lajittelu haluttuun järjestykseen
5. tiedon etsiminen tiedostosta tietyllä hakuehdolla
6. uuden henkilön lisääminen tiedostoon
7. henkilön poistaminen tiedostosta
Mikäli tutkimme yo. palasia tarkemmin, tarvitsemme ehkä seuraavia pienempiä ohjelman palasia (apualiohjelmia):
1. pitkän merkkijonon pilkkominen osamerkkijonoihin annetun merkin kohdalta
2. loppuvälilyöntien poistaminen merkkijonosta
3. isojen ja pienien kirjainten muuttaminen merkkijonossa esimerkiksi:
AKU ANKKA -> Aku Ankka
aku ankka -> Aku Ankka
aKU ANkKa -> AKU ANKKA
4. hetun oikeellisuuden tarkistus
5. ovatko merkkijonot "*aku*" ja "AKU ANKKA" samoja?
Seuraava vaihe olisi suunnitelman koodaaminen valitulle ohjelmointikielelle. Voisimme kirjoittaa aluksi löytämiämme alimman tason aliohjelmia (BOTTOM-UP-suunnittelu) ja testata ne toimiviksi. Voisimme myös kirjoittaa pääohjelman ja tyhjiä aliohjelmia testataksemme ohjelman rungon (TOP-DOWN). Puuttuvien toimintojen kohdalla ohjelma voidaan laittaa sanomaan.
TOIMINTAA EI OLE VIELÄ TOTEUTETTU!
Emme kuitenkaan osaa vielä riittävästi ohjelmointikieltä, jotta voisimme aloittaa koodauksen. Huomattakoon, ettei yllä olevassa suunnitelmassa ole missään kohti vedottu käytettävään ohjelmointikieleen. Palaamme myöhemmin takaisin ohjelman osien koodaamiseen.
Vaikka sihteerimme ei juuri nyt huomannutkaan, saattaa hän tulevaisuudessa esimerkiksi kysyä miten rekisterillä pidettäisiin yllä tietoja jäsenten harrastuksista. Mietitäänpä?
Ensin miten harrastukset muuttaisivat tiedostomuotoamme?
Eräs mahdollisuus olisi lisätä kunkin rivin loppuun jollakin erotinmerkillä harrastukset:
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
; sukunimi etunimi |hetu |…|harrastukset
Ankka Aku |010245-123U|…|kalastus,laiskottelu,työn pakoilu
Susi Sepe |020347-123T|…|possujen jahtaaminen,kelmien kerho
Ponteva Veli |030455-3333|…|susiansojen rakentaminen
Ratkaisu toimisi tietyissä erityistapauksissa. Ongelmia tulisi esimerkiksi jos pitäisi kuhunkin harrastukseen liittää esimerkiksi harrastuksen aloitusvuosi, viikoittain harrastukseen käytetty tuntimäärä jne.
Edellinen ongelma ratkeaisi esimerkiksi laittamalla henkilön tietojen rivin perään jollakin tavalla eroavia rivejä, joilla harrastuksen on lueteltu:
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
; sukunimi etunimi |hetu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
- kalastus | 1955 | 20
- laiskottelu | 1950 | 20
- työn pakoilu | 1952 | 40
Susi Sepe |020347-123T| |12555 |Takametsä | | |
- possujen jahtaaminen | 1954 | 20
- kelmien kerho | 1962 | 2
Ponteva Veli |030455-3333| |12555 |Takametsä | | |
- susiansojen rakentaminen | 1956 | 15
Ratkaisu olisi aivan hyvä ja tämän ratkaisun valitsemiseksi meidän ei tarvitsisi tehdä mitään muutoksia tiedostomuotoomme vielä tässä vaiheessa.
Huono puoli on kuitenkin se, että tämän muotoisen tiedoston siirrettävyys muihin järjestelmiin on varsin huono.
Suurin osa tämän hetken valmiista järjestelmistä käyttää relaatiotietokantamallia. Tämä tarkoittaa sitä, että koko tietokanta koostuu pienistä tauluista, jossa kukin rivi (=tietue) on samaa muotoa. Eri taulujen välillä tiedot yhdistetään yksikäsitteisten avainkenttien avulla. Meidän esimerkissämme nimet.dat olisi yksi tällaisen taulu ja sosiaaliturvatunnus kelpaisi yhdistäväksi avaimeksi (relaatioksi).
Kuitenkin sosiaaliturvatunnus on varsin pitkä kirjoittaa ja välttämättä sitä ei saada kaikilta. Jos tällainen pelko on olemassa, täytyy avain luoda itse. Itse ohjelman käyttäjän ei tarvitse tietää mitään tästä uudesta muutoksesta, vaan ohjelma voi itse generoida avaimet ja käyttää niitä sisäisesti.
Valitaan vaikkapa juoksevasti generoituva numero. Jos jäseniä poistetaan jää ko. jäsenen numero vapaaksi eikä sitä yritetäkään enää käyttää. Uuden jäsenen numero olisi sitten aina suurin jäsenen numero +1.
Kelmien kerho ry
; Kenttien järjestys tiedostossa on seuraava:
;id|sukunimi etunimi |hetu |katuosoite |postinumero|postiosoite|kotipuhelin|työpuhelin|
1 |Ankka Aku |010245-123U|Ankkakuja 6 |12345 |ANKKALINNA |12-12324 | |
2 |Susi Sepe |020347-123T| |12555 |Takametsä | | |
4 |Ponteva Veli |030455-3333| |12555 |Takametsä | | |
Harrastukset kirjoitetaan toiseen tiedostoon (hakemistoon kelmit), jossa tunnusnumerolla ilmaistaan kuka harrastaa mitäkin harrastusta.
id|harrastus |aloit |viikossa
1 |kalastus | 1955 | 20
1 |laiskottelu | 1950 | 20
1 |työn pakoilu | 1952 | 40
2 |possujen jahtaaminen | 1954 | 20
2 |kelmien kerho | 1962 | 2
4 |susiansojen rakentaminen | 1956 | 15
Nyt esimerkiksi kysymykseen "Mitä Sepe Susi harrastaa" saataisiin vastus etsimällä ensin Sepe Suden tunnus (2) tiedostosta nimet.dat. Sitten etsittäisiin ja tulostettaisiin kaikki rivit joissa tunnus on 2 tiedostosta harrastukset.dat.
Myös vastaus kysymykseen "Ketkä harrastavat laiskottelua" löytyisi suhteellisen helposti.
Tämä ratkaisu vaatii muutoksen tiedostomuotoomme jo suunnitelman tässä vaiheessa, mutta toisaalta mikäli ratkaisu valitaan, voidaan sen ansiosta lisätä jatkossa vastaavia "monimutkaisia" kenttiä rajattomasti tekemällä kullekin oma "taulu".
Valitsemmekin siis tämän ratkaisun, eli annamme kullekin jäsenelle tunnusnumeron heti alusta pitäen. Itse ohjelman käyttösuunnitelmaan ei tässä vaiheessa tarvita muutoksia.
Kirjoita algoritmi joka relaatiomallin tapauksessa vastaa kysymykseen "Ketkä harrastavat harrastusta X".
Nykyisin on kovasti muotia, että jokainen ohjelma osaa lukea ja kirjoittaa XML-muotoista tiedostoa (Extensible Markup Language). Meidän ohjelmamme tiedosto voisi olla vaikka seuraavan näköinen XML-muotoisena:
<?xml version="1.0"?>
<kerho>
<kerhonnimi>Kelmien kerho ry</kerhonnimi>
<maxjasenia>13</maxjasenia>
<jasen>
<id>1</id>
<nimi>Ankka Aku</nimi>
<hetu>010245-123U</hetu>
<katuosoite>Ankkakuja 6</katuosoite>
<postinumero>12345</postinumero>
<postiosoite>ANKKALINNA</postiosoite>
<kotipuhelin>12-12324</kotipuhelin>
<harrastukset>
<harrastus>kalastus</harrastus>
<aloit>1955</aloit>
<viikossa>20</viikossa>
</harrastukset>
<harrastukset>
<harrastus>laiskottelu</harrastus>
<aloit>1950</aloit>
<viikossa>20</viikossa>
</harrastukset>
<harrastukset>
<harrastus>tyon pakoilu</harrastus>
<aloit>1952</aloit>
<viikossa>40</viikossa>
</harrastukset>
</jasen>
<jasen>
<id>2</id>
<nimi>Susi Sepe</nimi>
<hetu>020347-123T</hetu>
<postinumero>12555</postinumero>
<postiosoite>Takametsa</postiosoite>
<harrastukset>
<harrastus>possujen jahtaaminen</harrastus>
<aloit>1954</aloit>
<viikossa>20</viikossa>
</harrastukset>
<harrastukset>
<harrastus>kelmien kerho</harrastus>
<aloit>1962</aloit>
<viikossa>2</viikossa>
</harrastukset>
</jasen>
<jasen>
<id>4</id>
<nimi>Ponteva Veli</nimi>
<hetu>030455-3333</hetu>
<postinumero>12555</postinumero>
<postiosoite>Takametsa</postiosoite>
<harrastukset>
<harrastus>susiansojen rakentaminen</harrastus>
<aloit>1956</aloit>
<viikossa>15</viikossa>
</harrastukset>
</jasen>
</kerho>
Kuten edeltä nähdään, on XML varsin tuhlaileva tallennusmuoto. Sen käyttöä puoltaa lähinnä sen standardinmukaisuus. Tuon tiedoston voi lukea tulevaisuudessa vaikka millä ohjelmalla. Haittapuolena on työläämpi lukeminen omassa ohjelmassa. Tosin jos on tarkoitus selvitä vain ylläkuvatun mukaisesta tiedostosta, ei koodaus ole kovin paljon monimutkaisempaa kuin muidenkaan tiedostomuotojen kanssa. Lisäksi esim. Java-kieleen löytyy useita XML-jäsentimiä valmiiksi käytettävinä luokkina.
Laske mikä edellä esitetyistä vaihtoehtoisista tiedostomuodoista on tilaa säästävin kun rivinvaihtomerkin lasketaan vievän yhden merkin verran tilaa ja välilyönnit "unohdetaan". Laske karkeasti "merkkejä/jäsen".
Luultavammin nopein tapa käyttöliittymäsuunnittelijalle tai ohjelmoijalle on suunnitella graafinen käyttöliittymä kynän sijaan suoraan jollakin nykyaikaisella graafisella ohjelmointiympäristöllä. Tähän osaan sitten lisätään heti tai jälkeenpäin itse toiminnallisuus. Tällaisia työkaluja on esimerkiksi Eclipse (vaatii WindowBuilder pluginin), NetBeans, Visual Studio, C++Builder, Delphi ja myös muiden ohjelmointikielten resurssityökalut.
Ohjelma suunnitellaan nimenomaan "piirtämällä" käyttäjälle näkyvä käyttöliittymän osa. Tässä tilanteessa on mahdollista pitää jopa asiakas mukana, jolloin ohjelman vaatimukset ja suunnitelmat selkeytyvät molempiin suuntiin. Ohjelmasta saattaa puuttua jotain tärkeitä ominaisuuksia, se saattaa olla liian vaikea käyttää, eikä ole myöskään tavatonta että alustavasti ohjelmaan on suunniteltu jopa tarpeettomia osia. Tilanteessa kiteytyy pitkälti se miksi johdanto-osuudessa esitellyt ketterien menetelmien arvot ovat käytännössä niin toimivia.
Tyypillisesti käyttöliittymät koostuvat ns. komponenteista. Ohjelmointikielelle tehdyt valmiit käyttöliittymäkirjastot sisältävät joukon valmiita komponentteja, kuten ikkunoita, paneeleita, tai vaikkapa nappeja. Yleensä komponentit rakentuvat muutamasta erilaisesta tyypistä, joskin käytettyjen kirjastojen välillä saattaa olla jonkinlaisia eroja.
Ikkunat (Windows) ovat korkean tason komponentteja, jotka sisältävät muita komponentteja. Ohjelmassa on yleensä yksi pääikkuna, jolla tosin voi olla lapsia, eli toisia ikkunoita. Tyypillisesti tyhjä ikkuna sisältää yläpalkin, jossa on ikoni, tekstiä, ikkunan kokoon vaikuttavat pikanäppäimet ja rasti sulkemista varten. Ikkunan "tyhjä" osio koostuu säiliöstä.
Säiliöt (Containers) ovat ikkunoita matalamman tason komponentteja, joiden tehtävä on helpottaa muiden komponenttien ryhmittelyä erilaisten sijoitteluiden (layout) avulla. Monimutkaiset rakenteet saattavat vaatia useiden sisäkkäisten ja rinnakkaisten säiliöiden käyttöä.
Valikot (Menus) ovat tapa jäsennellä kontrolleja. Sijoitellaan usein ohjelman yläreunaan tai esimerkiksi hiiren oikean painikkeen taakse.
Kontrollit (Controls), kuten esimerkiksi napit, tekstikentät, muokattavat tekstikentät, edistymispalkit ja valintalaatikot, ovat tyypillisesti matalimman tason komponentteja, jotka toteuttavat jotakin täsmällistä toiminnallisuutta.
Kuva 3.1 Kerhon käyttöliittymässä käytettyjä Swing-komponentteja
Komponenttien sijoittelussa ja niiden toiminnassa kannattaa matkia paljon muita ohjelmia. Toki valikkopalkki on mahdollista laittaa vaikka ikkunan alareunaan, mutta samalla varmasti kasvattaa käyttäjän kynnystä oppia ohjelman sujuva käyttö. Kannattaa myös miettiä ohjelman käyttötarkoitusta ja alustaa. Hiiri ole välttämättä ainoa tapa käyttää graafistakaan ohjelmaa, joten usein tarvittavaan ohjelmaan on hyvä olla jonkinlaisia käyttöä nopeuttavia pikanäppäimiä. Toisaalta helppokäyttöisimmänkään tietokoneohjelman käytettävyys tuskin siirtyy sellaisenaan kännykälle.
On kuitenkin selvää, ettei valmis kirjasto voi tarjota suoraan kaikkea tarpeellista. Tähän on käytännössä kolme erilaista lähestymistapaa. Ensimmäinen - yleensä tarpeettoman työläs - ratkaisu on luoda tarvittavan toiminnallisuuden tarjoavat osa itse. Tällaisen komponentin pitää täyttää tietty määrä sille asetettuja vaatimuksia, jonka jälkeen se on käytettävissä käyttöliittymässä. Käytännöllisempää kuin tyhjästä aloittaminen usein onkin ylikirjoittaa ja laajentaa haluttu toiminnallisuus jo valmiista komponentista.
Ohjelmoinnissa hyvä nyrkkisääntö on, että samaa koodia ei kannata kirjoittaa kahdesti, vaan silloin se tulee refaktoroida esimerkiksi uuteen funktioon. Sama periaate toimii käyttöliittymien kohdalla. Kerho-ohjelmasta huomaamme, että jäsenten tiedot syötetään kenttiin, joissa vasemmalla puolella on tekstiä ja oikealla syötekenttä. Tällöinhän olisi kätevää, jos voisimme yhdistää nämä kaksi komponenttia yhdeksi kokonaisuudeksi. Tätä ratkaisua kutsutaan koostamiseksi. Käyttäessämme Javan Swing kirjastoa, voimme luoda esimerkiksi EditPanel -komponentin, jolla on vaihdettava tekstikenttä (JLabel) ja kirjoituskenttä (JTextField).
Kuva 3.2 Koostettu komponentti EditPanel
Kuva 3.3 EditPanel -komponentteja allekkain
Käytettävästä ympäristöstä ja ohjelmointikielestä riippumatta käyttöliittymien suunnittelutyökalut muistuttavat hyvin paljon toisiaan. Esimerkkinä on käytetty Eclipseen asennettua WindowBuilder Pro -laajennusta Javan Swing-ympäristössä. Toiminnallisuuteen ei kuitenkaan syvennytä kuin pinnallisesti, koska työkalujen kehittyminen on nopeaa ja on mahdollista, että seuraavan version kohdalla tämäkin moniste on jo vanhentunut.
Kuva 3.4 Eclipse ja WindowBuilder Pro -plugin
1. Komponentit ja niiden sisäkkäinen rakenne (Structure). Kannattaa myös huomata että kaikki esikatselussa näkyvä, pääikkunaa myöten, on oma komponenttinsa.
2. Ominaisuudet (Properties). Komponentin voi aktivoidaa joko rakenne- tai esikatseluikkunasta, jolloin sen ominaisuuksia voi muuttaa. Erilaisilla komponenteilla on toisistaan eroavat ominaisuudet. Tällaisia ominaisuuksia ovat esimerkiksi napissa lukeva teksti tai sen koko.
3. Käytettävissä olevat työkalut ja komponentit (Palette). Kokoelma valmiita komponentteja, joita pystyy ottamaan käyttöön vetämällä halutun joko rakenne- tai esikatseluikkunaan
4. Esikatselu (Preview).
Ohjelmointikieli ja siinä käytetyt kirjastot kyllä tuovat itse ohjelmakoodiin suuriakin eroja. Nykyään monet kirjastot käyttävät hyväkseen XML-tiedostoja, joihin tallennetaan ulkoasun ja komponenttien ominaisuudet samaan tapaan kuin html-tiedostoihin. Kurssilla käytetty Javan Swing-kirjasto ei kuitenkaan tätä mahdollisuutta ainakaan vielä tarjoa. XML:n käytöllä saavutetaan ainakin teoriassa parempi siirrettävyys eri järjestelmien ja laitteiden välillä. Eri tekniikoiden välillä ei kuitenkaan ole mitään yhtenäistä standardia, mutta XML-pohjaiset toteutukset on kuitenkin helpompi tulkita.
Käyttöliittymien toiminnallisuus toteutetaan tapahtumien (event) avulla. Hiiren oikea näppäin halutun komponentin päällä avaa valikon josta voi tutkia erilaisista käyttäjän toimista aktivoituvia tapahtumia. Oikean toiminnallisuuden kanssa pitää hieman miettiä. Miten painikkeen (Button) painaminen tapahtuu? Nopeasti voisi ajatella että kun hiiren vie painikkeen päälle ja painaa (MousePressed), niin on intuitiivista että seuraa tapahtuma. Näinhän ei kuitenkaan ole, vaan yleensä tapahtuma seuraa vasta sitten kun hiiri on painettu alas ja päästetty ylös (MouseClicked). Oikea tapahtuma painikkeelle on kuitenkin ActionPerformed, joka huomio myös näppäimistön avulla tehdyt valinnat.
Kuva 3.5 WindowBuilder Pro ja valikko tapahtumien hallintaan
Tapahtuman luominen lisää seuraavat rivit koodiin. Ohjelma lisää painikkeelle uuden tapahtumakuuntelijan. Ohjelmakoodin toiminnan ymmärtäminen vaatii kuitenkin suhteellisen edistynyttä olio-ohjelmoinnin tietämystä, joten ei kannata säikähtää vaikka tekninen toteutus ei täysin aukeaisikaan.
buttonTallenna.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent arg0) {
//tähän voi kirjoittaa omaa ohjelmakoodia
tallenna();
}
});
Lohkoon jossa kutsumme tallenna()-metodia olisimme tietysti voineet kirjoittaa halutun toiminnallisuuden suoraankin. Metodikutsu on kuitenkin parempi tapa, koska koodista tulee näin paremmin jäsenneltyä ja tulkittavaa. On myös mahdollista että tarvitsemme samaa toiminnallisuutta useampaan kertaan. Tällöin voisimme laittaa esimerkiksi CTRL+S pikanäppäinyhdistelmän osoittamaan samaan metodiin.
Tässä vaiheessa ohjelmamme ei osaa kuitenkaan vielä tallentaa mitään, joten metodin tehtävä on vain kertoa se erillisessä dialog-ikkunassa.
private void tallenna() {
JOptionPane.showMessageDialog(null, "ei osata vielä tallentaa");
}
Kirjettä jos kirjoittelet,
ulkomaille viestittelet,
tokko Ruohtia viskomassa,
turhaan sanoja kiskomassa?
Aloittanet aatoksilla,
kotokielellä pohtimalla,
viestin vääntöö valmistellen,
siistimiseksi sisällön.
Sama kaava koodatessa
kääntäjätä käskiessä
kotokieltä alkuun käytä
vasta sitten ruutuun täytä.
Algoritmit alkuun teeppä
koneen kimppuun vasta meeppä
kun selvillä on tarkka kaava
jopa kääntyy Cee ja Jaava.
Mitä tässä luvussa käsitellään?
• mikä on algoritmi
• vertailu ja lajittelu
• algoritmin kompleksisuus
• alkion etsiminen joukosta
Kun ohjelman suunnittelu on edennyt siihen pisteeseen, että tarvitaan yksityiskohtaisia algoritmeja, meneekin jo monella sormi suuhun.
Vaikeudet johtuvat taas liian hankalasta ajattelutavasta ja siitä, että algoritmi yritetään nähdä osana koko ohjelmaa. Tästä ajattelutavasta on luovuttava ja osattava määritellä tarvittava algoritmi omana kokonaisuutenaan, jota suunniteltaessa sitten unohdetaan kaikki muu.
Algoritmi on se joukko toimenpiteitä, joilla annettu tehtävä saadaan suoritettua. Mieti esimerkiksi miten selostat kaverillesi ohjeet juna-asemalta opiskeluboxiisi.
Voit tietysti antaa ohjeet myös muodossa "Tule osoitteeseen Ohjelmoijankuja 17 B 5". Tämäkin on varsin hyvä algoritmi. Kaverin vain oletetaan nyt osaavan enemmän. Kaverin oletetaan osaavan etsiä katuluettelosta kadun paikka ja keksivän itse menetelmän tulla asemalta sinne.
Toisaalta kaverisi saattaa hypätä taksiin ja sanoa kuskille osoitteen. Tämä on hyvä ja helppo algoritmi, mutta ehkä liian kallis opintovelkaiselle opiskelijalle. Mikäli algoritmia tarvitaan useasti, voidaan sitä myöhemmin parantaa tyyliin:
- kävele asemalta sinne ja sinne
- hyppää bussiin se ja se
- jne
Tarkennettu algoritmi voisi olla myös seuraavanlainen:
Valitse seuraavista:
1. Kello 7-20:
- kävele kirkkopuistoon
- nouse bussiin no 3 joka lähtee 15 yli ja 15 vaille
2. Sinulla on rahaa tai saat kimpan:
- ota taksi
3. Ei rahaa tai haluat ulkoilla:
- kävele
Edellä eri kohdat eivät ole toisiaan poissulkevia. Kello voi olla 9 ja rahaakin voi olla, mutta siitä huolimatta halutaan kävellä. Hyvässä algoritmissa ei saa olla tällaisia epätäsmällisyyksiä, vaan ohjelmoijan tulee etukäteen jo päättää mitä missäkin tapauksessa tehdään. Esimerkiksi:
1. Jos haluat ulkoilla, niin
- kävele.
2. Muuten jos kello 7-20:
- kävele kirkkopuistoon
- nouse bussiin no 3 joka lähtee 15 yli ja 15 vaille
3. Muuten jos sinulla on rahaa tai saat kimpan:
- ota taksi
4. Muuten
- kävele
Tässäkin algoritmissa jää vielä kaverillekin tehtävää: Miten kävellään? Miten astutaan bussiin jne..
No tätä ei kaverille ehkä enää selostetakaan. Lapsille nämä asiat on aikanaan opetettu ja myöhemmin ne kuitataan yhdellä tai kahdella sanalla. Sama pätee ohjelmoinnissakin. Kerran tehtyä ei joka kerran pureksita uudelleen (vrt. aliohjelma)!
Yritä kirjoittaa ohjeet siitä miten kävellään.
Kirjoita kaverillesi kävelyohjeet (missä käännytään, ei miten kävellään) rautatieasemalta asunnollesi.
Kerhon jäsenrekisteriä suunniteltaessa tulee jossakin kohtaa vastaan tilanne, jossa nimet tai osoitteet pitää pystyä lajittelemaan jollakin tavalla.
Jos osaamme lajitella numeroita, niin osaammeko lajitella nimiä? Vastaus on KYLLÄ. Mikä numeroiden lajittelussa on oleellista? Oleellista on tietää onko numero A pienempi kuin numero B. Miten tämä sitten soveltuu nimille? Jos osaamme päättää onko nimi A aakkosissa ennen kuin nimi B, on ongelma ratkaistu.
Verrataanpa erilaisia nimiä:
A: Kassinen Katto
B: Ankka Aku
B on ensin aakkosissa. Miksi? Koska B:n ensimmäinen kirjain (A) on ennen nimen A ensimmäistä kirjainta (K).
A: Kassinen Katto
B: Karhukopla 701107
Nytkin B on ensin. Siis miten vertaamme kahta nimeä?
Vertaamme nimiä merkki kerrallaan kunnes vastaan tulee eri-
suuret kirjaimet. Kumpi erisuurista kirjaimista on aakko-
sissa ennen, määrää sen kumpi nimistä on aakkosissa ennen.
Siinä meillä on algoritmi joka on varsin selvä. Jos algoritmi haluttaisiin vielä kirjoittaa "lausekieliseen" muotoon, niin se olisi suurin piirtein seuraavanlainen:
1. siirry kummankin nimen ensimmäiseen kirjaimeen
2. jos kummankin nimen viimeinen merkki on ohitettu, niin nimet ovat samat
3. jos toisessa nimessä viimeinen merkki on ohitettu, niin se on ennen
aakkosissa
4. verrataan vuorossa olevia kirjaimia kummastakin nimestä
- jos samat, niin siirrytään seuraaviin kirjaimiin ja jatketaan kohdasta
2.
- jos erisuuret, niin se ensin aakkosissa, jonka kirjain on ensin
Tähän vielä pieni "viilaus enemmän strukturoidummaksi", niin meillä olisikin valmis (ali)ohjelma nimien vertaamiseksi.
Vaikka esitimmekin algoritmin "lausekielisenä" kohdittain numeroituna, ei koskaan pidä unohtaa sitä ennen ollutta sanallista versiota, joka on selkeämpi kuvaus siitä ideasta, mitä tehdään!
Siis kirjoita aina ensin sanallinen kuvaava kuvaus algoritmista ja vasta sitten sen yksityiskohtainen "lausekielinen" versio!
Näin ollen on aivan yksi lysti opettelemmeko järjestämään nimiä vai numeroita. Siksi paneudummekin seuraavassa numeroiden järjestämiseen. Kuulostaako vaikealta?
Otapa käteesi korttipakka ja ota sieltä esiin vaikkapa vain kaikki padat. Nyt sinulla on joukko "numeroita" (A=14, K=13, Q=12, J=11), yhteensä 13 kappaletta. Sekoita kortit! Yritä järjestää kortit suuruusjärjestykseen siten, ettet tarvitse pöytätilaa kuin yhden kortin verran, loput kortit pidät kädessäsi.
Millaisen algoritmin saat? Ehkäpä seuraavan (insertion sort):
Pöydällä on lajiteltujen kasa. Aluksi tietysti tyhjä. Ota
kädestäsi seuraava kortti ja laita pöydällä olevaan kasaan
omalle paikalleen. Jatka kunnes kädessä ei enää kortteja.
"Lausekielisenä":
1. ota kädessä olevan kasan päällimmäinen kortti
2. sijoita se pöydällä olevaan kasaan paikalleen
3. mikäli kortteja vielä jäljellä, niin jatka kohdasta 1.
Algoritmisi voi olla myös seuraava (selection sort):
Etsitään aina pienin kortti ja laitetaan se pöydälle olevan
kasan päällimmäiseksi. Jatketaan kunnes kädessä olevat
kortit on loppu.
Eli "lausekielisenä":
1. etsi kädessäsi olevista korteista pienin
2. laita se pöydällä olevan pinon päällimmäiseksi
3. mikäli vielä kortteja jäljellä, niin jatka kohdasta 1.
Mitä muita mahdollisia "lajittelumenetelmiä" keksit?
Siinä eräitä ratkaisuja tähän "hirveän vaikeaan" ongelmaan. Ratkaisuissa on tiettyjä huonoja puolia, mutta ratkaisut ovat todella yksinkertaisia ja jokaisen itse keksittävissä.
Mikäli kahden kortin vertaaminen lasketaan yhdeksi "operaatioksi", niin kuinka monta "operaatiota" joudumme tekemään, jotta pakka on lajiteltu Selection Sortilla?
Edellisen tehtävän vastausta sanotaan algoritmin kompleksisuudeksi.
Edellinen algoritmi (selection sort) toimi siten, että kortit jäivät pöydälle suurin päällimmäiseksi. Miten algoritmia pitää muuttaa, jotta pienin saataisiin päällimmäiseksi?
Ei siis ole suurtakaan väliä pitääkö lajitella nouseva vai laskeva järjestys!
Kokeillaanpa vielä erästä algoritmia: Sotke kortit kädessäsi uudelleen.
Bubble sort:
Vertaa aina kahta peräkkäistä korttia keskenään. Mikäli ne
ovat väärässä järjestyksessä, vaihda ne keskenään. Kun koko
pakka on käyty lävitse, aloita alusta ja jatka kunnes yhtään
kertaa ei tarvitse vaihtaa peräkkäisiä kortteja.
Tuleeko pakka järjestykseen tällä algoritmilla? Voidaanko algoritmia nopeuttaa mitenkään? Kirjoita algoritmista "lausekielinen" versio.
Kirjoita nyt joukko pahvilappuja, joissa kussakin on henkilön nimi, osoite ja puhelinnumero.
Sekoita laput ja kokeile toimiiko edelliset algoritmit mikäli laput järjestetään nimien mukaan. Ai tyhmä ehdotus! Tässä se onkin ohjelmoinnin vaikeus. Asiat ovat yksinkertaisia! Eiväthän ne osoitteet siellä lajittelua sotke.
Mikäli laput järjestetään nimen mukaan, sanotaan nimen olevan lajitteluavaimena. Lajitteluavaimeksi voitaisiin valita myös osoite tai puhelinnumero. Mikäli kahdella henkilöllä olisi sama nimi, voitaisiin nämä kaksi järjestää osoitteen perusteella. Tällöin lajitteluavain muodostuisi merkkijonosta johon olisi yhdistettynä nimi ja osoite.
Kaikki edelliset algoritmit ovat kompleksisuudeltaan normaalitapauksessa samanlaisia.
Mikä edellisistä algoritmeista loppuu nopeasti, mikäli kortit jo olivat järjestyksessä?
Ohjelman toimintaan saattamisen kannalta olisi riittävää löytää jokin toimiva algoritmi. Myöhemmin, mikäli ohjelman toiminta todetaan hitaaksi ko. algoritmin kohdalta, voidaan algoritmia yrittää tehostaa. Lajittelussa tehostus saattaisi olla vaikkapa QuickSort (mukana mm. C-kielen standardikirjastossa).
Jos algoritmin kompleksisuus on esimerkiksi 2n2+n, sanotaan että kompleksisuus on O(n2), eli usein kiinnostaa vain kompleksisuuden suurin "potenssi". QuickSortin keskimääräinen kompleksisuus on O(n log2n). On olemassa myös erikoistapauksissa toimivia lajitteluja, joissa kompleksisuus on O(n). Piirrä kuva jossa on Selection Sortin, QuickSortin ja lineaarisen lajittelun käyttämä "aika" piirrettynä lajiteltavien alkioiden (n=10,100,1000,10000,1000000) funktiona.
Tutki kumpiko on työmäärältään edullisempaa jos järjestettyyn taulukkoon tulee lisättäväksi suuri määrä uusia alkiota
1) lisätä alkio aina taulukkoon oikealle paikalleen
2) lisätä alkio aina taulukon loppuun ja kun kaikki alkiot on lisätty, niin lajitella taulukko
Edellisissä lajittelualgoritmeissa oli vielä muutamia aukkopaikkoja! Etsi pienin? Laita oikealle paikalleen?
Miten kädessä olevista korteista voidaan etsiä pienin. Yksi mahdollisuus on kuljettaa "pienin ehdokasta" läpi koko pakan. Mikäli matkan varrelta löytyy parempi ehdokas, otetaan tämä tilalle. Edellä mainittu kuplalajittelu korjattuna perustuu nimenomaan tähän ideaan.
Entä jos kädessä olevien korttien järjestystä ei haluta muuttaa? Voisimme menetellä esimerkiksi seuraavasti (alkuarvaus ja arvauksen korjaaminen):
0. vedä kädessä olevan pakan ylin kortti hieman esille
ota ensimmäinen kortti tutkittavaksi
1. vertaa tutkittavaa korttia ja esiinvedettyä korttia
2. mikäli tutkittava on pienempi, vedä se esiin ja työnnä
edellinen takaisin
3. siirry tutkimaan seuraavaa korttia ja jatka kohdasta 1.
kunnes olet tutkinut koko pakan
Miten kortti sijoitetaan paikalleen jo lajiteltuun kasaan? Esimerkiksi seuraavasti:
0. laita uusi kortti päällimmäiseksi lajiteltuun kasaan
1. vertaa uutta ja seuraavaa
2. mikäli väärässä järjestyksessä, niin vaihda ne keskenään
ja jatka kohdasta 1.
Usein tulee vastaan myös tilanne, jossa tietyn henkilön tiedot pitäisi hakea esimerkiksi nimen mukaan. Mikäli valittu tietorakenne on järjestetty nimen mukaan, voidaan hakemisessa käyttää vaikkapa puolitushakua.
Nimen hakeminen ei taas poikenne kortin etsimisestä järjestetystä korttipakasta vai mitä?
Kun kortit ovat järjestämättä, niin miten löydät haluamasi kortin?
Ota seuraava kortti. Mikäli etsittävä niin lopeta, muuten ota taas seuraava.
Algoritmi on OK 13 kortille, mutta kokeilepa Äystön etsimistä puhelinluettelosta tällä algoritmilla (muista lukea jokainen nimi ennen Äystöä)!
Mikäli 13 korttiasi on järjestyksessä ja sinun pitäisi mahdollisimman vähällä pläräämisellä löytää vaikkapa pata 4, niin miten voisit menetellä?
1. laita pakka pöydälle kuvapuolet ylöspäin
2. laita pakka puoliksi
3. laita molemmat pakat pöydälle kuvapuolet ylöspäin
4. kummassako kasassa etsittävä on?
5. heitä se pakka pois jossa etsittävä ei ole
6. jos etsittävä ei päällimmäinen, niin jatka kohdasta 1.
Vaikuttaa tyhmältä 13 kortille, mutta kokeilepa 1000 kortilla! Tai kokeile nyt etsiä ÄYSTÖÄ puhelinluettelosta tällä algoritmilla.
Kirjoita puolitushausta kunnon "lausekielinen versio" kun meillä on sivunumeroitu kirja, jonka kullakin sivulla on täsmälleen yhden henkilön tiedot. Sivunumeroita kirjassa on N-kappaletta. Aloitat sivuista S1=0 ja S2=N+1. Miten jatkat mikäli pitää etsiä nimi NIMI?
Mikä on puolitushaun kompleksisuus?
Tätä on ohjelmointi! Kykyä (ja rohkeutta) sanoa selvät asiat täsmällisesti. Jossain vaiheessa vaihdamme vain täsmällisyyden astetta ja "lausekielen" sijasta siirrymme käyttämään oikeata lausekieltä, esim. Java-kieltä. Nämä omatekoiset algoritmit kannattaa kuitenkin säilyttää ja kirjata näkyviin todellisen ohjelman kommentteihin. Arviot algoritmin nopeudesta kannattaa myös laittaa kommentteihin, jotta jälkeenpäin on helpompi etsiä jo tekovaiheessa hitaaksi epäiltyjä kohtia. Miksi jättää seuraavalle lukijalle sama tehtävä ihmeteltäväksi, jos olemme sen toteutuksen jo jonnekin kirjanneet.
Algoritmit kannattaa testata huolellisesti jossain tutussa ympäristössä. Hyvin moni ohjelmointiongelma vektoreiden (=taulukko, =kasa kortteja, =ruutupaperi, =sivunumeroitu kirja jne.) kanssa samaistuu johonkin jokapäiväiseen ilmiöön. Kuten etsiminen puhelinluettelosta, korttipakan järjestäminen jne. Yritä etsiä näitä yhteyksiä ja kokeile ensin ratkaista ongelma tällä tavoin. Siirrä ratkaisu sitten "lausekielelle" ja lopulta ohjelmointikielelle.
Äläkä yritä liikaa, vaan jaa aina ongelma pienempiin osiin, kunnes tulee vastaan sen kokoisia osaongelmia, jotka osataan ratkaista! Tällaista osaongelman ratkaisijaa sanotaan ohjelmointikielessä aliohjelmaksi.
Kun osaongelma on ratkaistu, unohda se miten sen ratkaisija toimii ja käsittele ratkaisijaa vain yhtenä yksinkertaisena toimenpiteenä (vrt. aikaisempi kävelyesimerkki). Tämä on myös eräs ohjelmoinnin "vaikeus". Kirjoittaja haluaa nähdä kaikkien osien toiminnan yhtäaikaisesti. Tämä on kuitenkin mahdotonta. Siis kun jokin osa tekee hommansa, niin tehköön se sen miten tahansa.
Huono on johtaja joka kyttää koko ajan alaisiaan, eikä luota siihen, että nämä tekevät heille annetun tehtävän. Tässä mielessä ohjelmointia voisi verrata yrityksen johtamiseen: Johtaja jakaa koko yrityksen pyörittämisessä tarvittavia tehtäviä alaisilleen (aliohjelmille). Nämä saattavat edelleen jakaa joitakin osatehtäviä omille alaisilleen (aliohjelma kutsuu toista aliohjelmaa) jne. Johtaja (=ohjelmoija ja pääohjelma) kokoaa alaisten tekemän työn toimivaksi kokonaisuudeksi ja firma tuottaa.
Kirjoita algoritmi polkupyörän kumin paikkaamiseksi.
Kirjoita algoritmi sunnuntai-illan viettoa varten (muista että ohjelmoinnin demot on maanantaina).
Kirjoita algoritmi 10 ei-alimittaisen kalan onkimiseksi mato-ongella.
Kirjoita algoritmi pöydälle levitetyn 13 kortin kääntämiseksi päinvastaiseen järjestykseen.
Tarvitaan nyt silmukoita,
kaiken maailman taulukoita,
eri ehtoja kummastella,
aliohjelmia aavistella.
Mitä tässä luvussa käsitellään?
• silmukat ja valintalauseet
• totuustaulut
• pöytätesti
• muuttujat
• taulukot
• osoittimet
Vaikka jatkossa keskitymmekin oliopohjaiseen ohjelmointiin, tarvitaan yksittäisen olion metodin toteutuksessa algoritmeja. Riippumatta käytettävästä ohjelmointikielestä, tarvitaan algoritmeissa aina tiettyjä samantyyppisiä rakenteita.
Käsittelemme seuraavassa tyypilliset rakenteet nopeasti lävitse. Tarvitsisimme asioille enemmänkin aikaa, mutta otamme asiat tarkemmin esille käyttämämme ohjelmointikielen opiskelun yhteydessä. Lukijan on kuitenkin asioita tarkennettaessa syytä muistaa, ettei rakenteet ole mitenkään sidottu ohjelmointikieleen. Vaikka ne näyttäisivät kielestä täysin puuttuvankin (esim. assembler), voidaan ne kuitenkin lähes aina toteuttaa.
Triviaaleja algoritmeja lukuun ottamatta algoritminen suoritus tarvitsee ehdollisia toteutuksia:
Jos kello yli puolenyön ota taksi
muuten mene linja-autolla
Ehtolauseita voi ehtoon tulla useampiakin ja tällöin on syytä olla tarkkana sen kanssa, mihin ehtoon mahdollinen muuten-osa liittyy:
Jos kello 00.00-07.00
Jos sinulla on rahaa niin ota taksi
muuten kävele
muuten mene linja-autolla
Jos sinulla on muuttujassa t tunnit ja muuttujassa m minuutit, niin kirjoita algoritmi miten lisäät n minuuttia kellonaikaan t:m.
Kirjoita algoritmi g-painoisen kirjeen postimaksun määräämiseksi (saat keksiä hinnaston itse).
Usein ehtoja kasaantuu niin paljon, että peräkkäiset ja sisäkkäiset ehtolauseet muodostavat varsin sekavan kokonaisuuden. Tällöin voi olla helpompi käyttää valintalausetta:
Yritys myy verkkokaupasta erilaisia tietoteknisiä tuotteita. Jokaisella tuoteryhmällä on vastaava henkilö, jonka sähköpostiin halutaan ohjata oikeat tukipyynnöt. Käyttäjä valitsee tuotekategorian alasvetovalikosta, jonka perusteella pyyntö lähtetään oikealle henkilölle.
Tietokoneet -> Matti
Puhelimet -> Marko
Kamerat -> Terttu
Audio,Muut -> Niko
Mieti kuinka valintalauseen logiikka korvattaisiin ehtolauseiden avulla.
Hyvin usein algoritmi tarvitsee toistoa: Esimerkiksi ohjeet (vuokaavio) hiekkarannalla toimimiseksi jos nenä näyttää merelle päin:
Kuva 4.3 do-silmukka ja do-while-silmukka
Ehtolause voi olla silmukan alussa, tällöin on mahdollista, ettei silmukan runkoa tehdä yhtään kertaa. Ehto voi olla myös silmukan jälkeen, jolloin silmukan runko tehdään vähintään yhden kerran. Joissakin kielissä on lisäksi mahdollisuus silmukan rungon keskeltä poistuminen.
Silmukoihin liittyy aina ohjelmoinnin eräs klassisimmista vaaroista: päättymätön silmukka! Tämän takia silmukoita tulee käsitellä todella huolella. Eräs oleellinen asia on aina muistaa suorittaa silmukan rungossa jokin silmukan lopetusehtoon vaikuttava toimenpide. Mitä tapahtuu muuten?
Myös silmukan suorituskertojen lukumäärän kanssa tulee olla tarkkana. Silmukka tulee helposti suoritettua yhden kerran liikaa tai yhden kerran liian vähän.
Mitä eroa on kahdella edellä esitetyllä "uimaan-meno" -algoritmilla? Mitä ehtoja algoritmiin voisi vielä lisätä?
Kirjoita algoritmi lukujen 1-100 yhteenlaskemiseksi sekä do-while- että while -silmukan avulla.
Algoritmeissa tarvitaan usein muuttujia.
kellonaika
rahan määrä
Yksinkertaisessa tapauksessa muuttuja voi olla yksinkertaista tyyppiä kuten kellonaika (jos ilmaistu minuutteina), rahasumma jne.
Yksinkertainen luvun jaollisuuden testausalgoritmi voisi olla vaikkapa seuraavanlainen:
Jaetaan tutkittavaa lukua jakajilla 2,3,5,7...luku/2.
Jos jokin jako menee tasan, niin ei alkuluku:
0. Laita jakaja:=2, kasvatus:=1,
Jos luku=2 lopeta, alkuluku
1. Jaa luku jakajalla. Meneekö jako tasan?
- jos menee, on luku jaollinen jakajalla, lopeta
2. Kasvata jakajaa kasvatus arvolla (jakaja:=jakaja+kasvatus)
3. Kasvatus:=2; (koska parillisilla ei kannata enää jakaa)
4. Onko jakaja<luku/2?
- jos on, niin jatka kohdasta 1
- muuten lopeta, luku on alkuluku
Piirrä jaollisuuden testausalgoritmista vuokaavio.
Hyvin usein algoritmi kannattaa pöytätestata. Pöytätesti alkaa kirjoittamalla sarakkeiksi kaikki algoritmissa esiintyvät muuttujat. Muuttujiksi voidaan kirjoittaa myös algoritmissa esiintyviä ehtoja. Tällainen muuttuja voi saada arvon kyllä tai ei. Pöytätestin riveiksi kirjoitetaan algoritmin eteneminen vaiheittain. Sarakkeisiin muuttujille kirjoitetaan uusia arvoja vain niiden muuttuessa.
Testataan esimerkiksi edellisen esimerkin algoritmi:
askel |
Luku |
Jakaja |
Kasvatus |
Luku/Jakaja |
Jako tasan? |
Jakaja<Luku/2? |
Tulostus |
0 |
25 |
2 |
1 |
|
|
|
|
1 |
|
|
|
12.500 |
ei |
|
|
2 |
|
3 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
3<12.5 |
|
1 |
|
|
|
8.333 |
ei |
|
|
2 |
|
5 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
5<12.5 |
|
1 |
|
|
|
5.000 |
kyllä |
|
Jaollinen 5:llä |
askel |
Luku |
Jakaja |
Kasvatus |
Luku/Jakaja |
Jako tasan? |
Jakaja<Luku/2? |
Tulostus |
0 |
23 |
2 |
1 |
|
|
|
|
1 |
|
|
|
11.500 |
ei |
|
|
2 |
|
3 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
3<11.5 |
|
1 |
|
|
|
7.667 |
ei |
|
|
2 |
|
5 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
5<11.5 |
|
1 |
|
|
|
4.600 |
ei |
|
|
2 |
|
7 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
7<11.5 |
|
1 |
|
|
|
3.286 |
ei |
|
|
2 |
|
9 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
9<11.5 |
|
1 |
|
|
|
2.556 |
ei |
|
|
2 |
|
11 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
11<11.5 |
|
1 |
|
|
|
2.091 |
ei |
|
|
2 |
|
13 |
|
|
|
|
|
3 |
|
|
2 |
|
|
|
|
4 |
|
|
|
|
|
13>11.5 |
Alkuluku |
Usein pöytätesti antaa hyviä vinkkejä myös algoritmin jatkokehittelylle. Käytännön työssä osa pöytätestistä voidaan suorittaa debuggereiden avulla. Joskus kuitenkin voi olla niin paljon esitietoa algoritmille, että tarvittavan testiohjelman rakentaminen voi olla työlästä. Pöytätestihän voidaan aloittaa minkälaisesta alkutilasta tahansa. Samoin yksi pöytätestin etuja on siitä jäävä historia. Usein debuggerit näyttävät vain yhden ajanhetken tilanteen, siis yhden pöytätestin rivin kerrallaan.
Tarvitsisimmeko algoritmin kohtaa 4 lainkaan? Voitaisiinko algoritmin lopetus hoitaa muuten?
Pöytätestaa edellinen algoritmi kun syöttönä on luku 121.
Pöytätestaa molemmat Ynnää luvut 1-100 -algoritmisi versiona Ynnää luvut 1-6.
Tutkikaamme aikaisempia korttipakkaesimerkkejämme! Nyt tietorakenteeksi ei enää riitäkään pelkkä yksi muuttuja. Mikäli pakasta on otettu esiin pelkät padat, tarvitsisimme 13 muuttujaa. Näiden kunkin nimeäminen erikseen olisi varsin työlästä.
Tarvitsemme siis jonkin muun tietorakenteen. Mahdollisuuksia on useita: listat, jonot, pinot ja taulukot. Ohjelmoinnin alkuvaiheessa taulukot ovat tietorakenteista helpoimpia, joten keskitymme niihin aluksi.
Varataan pöydältä tilaa leveyssuunnassa 13 kortille. Varattua tilaa voimme nimittää taulukoksi tai vektoriksi. Taulukon yksi alkio on yhdelle kortille varattu paikka. Taulukon yhden alkion sisältö on se kortti, joka on siinä paikassa.
Mikäli numeroimme varatut paikat vaikkapa 0:sta alkaen vasemmalta oikealle, on meidän korteillamme osoitteet 0-12:
Nyt voimme käsitellä yksittäisiä kortteja aivan kuin ne olisivat yksittäisiä muuttujia. Viittaamme tiettyyn korttipaikkaan (taulukon alkioon) sen indeksillä (olkoon taulukon nimi kortit):
paikassa kortit[5] meillä on pata 9
paikassa kortit[8] meillä on pata akka
Minkälaisia algoritmeja tulee vastaan taulukoita käsiteltäessä? Esim. ♠9:n siirtäminen taulukon viimeiseksi vaatisi ♠4:en siirtämistä paikkaan 5. ♠6:en siirtämistä paikkaan 6, ♠Q:n siirtämistä paikkaan 7 jne. Näin loppuun saataisiin raivatuksi paikka ♠9:lle.
Lajittelun ilman valtaisaa korttien siirtelyä voisimme hoitaa seuraavasti:
80. laita alku paikkaan 0
1. etsi alku paikasta lähtien pienin kortti
2. vaihda pienin ja paikassa alku oleva kortti
3. alku:=alku+1
4. mikäli alku<suurin indeksi, niin jatka 1
Sovitaan, että ässä=1. Nyt pienimmän kortin etsimisalgoritmi voisi olla seuraava:
0. Alkuarvaus: pien.paikka:=alku, tutki:=alku
1. Jos kortit[tutki] < kortit[pien.paikka]
niin pien.paikka:=tutki
2. tutki:=tutki+1
3. Jos tutki<=suurin indeksi, niin jatka 1.
Voisimme vielä pöytätestata algoritmin:
|
pien. |
|
Kortit |
[tutki]< |
tutki< |
||||||||||||
askel |
paikka |
tutki |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
[pp] |
suur.ind |
0 |
0 |
0 |
♠7 |
♠3 |
♠K |
♠2 |
♠5 |
♠9 |
♠4 |
♠6 |
♠Q |
♠10 |
♠J |
♠A |
♠8 |
|
|
1 |
|
|
⇧t |
|
|
|
|
|
|
|
|
|
|
|
|
7<7 ei |
|
2&3 |
|
1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
1 |
|
⇧ |
t |
|
|
|
|
|
|
|
|
|
|
|
3<7 juu |
|
2&3 |
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
⇧ |
t |
|
|
|
|
|
|
|
|
|
|
K<3 ei |
|
2&3 |
|
3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
3 |
|
|
⇧ |
|
t |
|
|
|
|
|
|
|
|
|
2<3 juu |
|
2&3 |
|
4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
⇧ |
t |
|
|
|
|
|
|
|
|
5<2 ei |
|
2&3 |
|
5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
⇧ |
|
t |
|
|
|
|
|
|
|
9<2 ei |
|
2&3 |
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
⇧ |
|
|
t |
|
|
|
|
|
|
4<2 ei |
|
2&3 |
|
7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
⇧ |
|
|
|
t |
|
|
|
|
|
6<2 ei |
|
2&3 |
|
8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
⇧ |
|
|
|
|
t |
|
|
|
|
Q<2 ei |
|
2&3 |
|
9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
⇧ |
|
|
|
|
|
t |
|
|
|
10<2 ei |
|
2&3 |
|
10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
⇧ |
|
|
|
|
|
|
t |
|
|
J<2 ei |
|
2&3 |
|
11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
11 |
|
|
|
|
⇧ |
|
|
|
|
|
|
|
t |
|
A<2 juu |
|
2&3 |
|
12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
juu |
1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
⇧ |
t |
8<A ei |
|
2&3 |
|
13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ei |
Oletetaan, että pienimmän etsimisalgoritmi toimii. Pöytätestaa edellä esitelty lajittelualgoritmi edellisen pöytätestin mukaisella korttien järjestyksellä.
Onko tämä insertion sort? Missä on lajiteltujen kasa ja missä lajittelemattomien?
Kirjoita algoritmi kuvakorttien poistamiseksi taulukosta käyttäen indeksejä.
Edellisessä pöytätestissä merkitsimme pienen merkin niiden korttien kohdalle, joita kunakin hetkenä tutkimme. Kun tutkimme esimerkiksi paikoissa 3 ja 10 olevia kortteja (P2 ja PJ) voisimme sanoa, että muuttuja pien.paikka osoitti korttiin pata 2 ja muuttuja tutki korttiin pata jätkä. Näin ollen voisimme oikeastaan sanoa, että (indeksi)muuttujat pien.paikka ja tutki ovat osoittimia korttipakkaan. Niiden osoittamassa paikassa (indeksit 3 ja 10) on tietyt kortit (P2 ja PJ).
Lajittelualgoritmi voitaisiin lausua esimerkiksi:
0. levitä kortit rinnakkain pöydälle
osoita vasemman käden etusormella ensimmäiseen korttiin
1. etsi vasemman käden osoittamasta kohdasta alkaen oikealle
pienin kortti ja osoita sitä oikean käden etusormella
2. vaihda etusormien kohdalla olevat kortit keskenään
3. siirrä vasemman käden etusormea yksi kortti oikealle päin
4. mikäli vasen sormi vielä kortin päällä, jatka kohdasta 1.
Osoittimen ja indeksin ero on siinä, että osoittimen tapauksessa emme yleensä koskaan ole kiinnostuneita itse osoittimen arvosta (osoitteesta, siitä indeksistä missä kohdin olemme menossa), vaan osoittimen osoittaman paikan sisällöstä (sormen kohdalla olevan kortin arvo tai ei korttia). Indeksejä käsitellessämme tutkimme monesti myös itse indeksin arvoa (tutki=3->kortit[tutki]=P2).
Osoitin voi tarvittaessa osoittaa myös itse taulukon ulkopuolelle. Mikäli kirjoittaisimme pöydälle numeroita, voisimme osoittaa sormella yhtä hyvin pöydälle kirjoitettuja numeroita (älkää hyvät ihmiset nyt töhrätkö pöytää!) kuin pöydälle levitettyjä kortteja (taulukon alkioita).
Siis indeksit liittyvät kiinteästi taulukoihin ja osoittimet voivat liittyä mihin tahansa tietorakenteisiin alkaen yksinkertaisesta muuttujasta päätyen monimutkaisen lista- tai puurakenteen alkioon.
Javassa tällä tavalla käyttäytyviä osoittimia vastaavat iteraattorit. Itse asiassa Javan kaikki "oliomuuttujat" ovat osoittimia, niitä sanotaan vaan viitteiksi. Erona esimerkiksi C++:n osoittimiin on se, että Javan viitteitä ei voi muuttaa muuta kuin osoittamaan toista alkiota. Eli Javan viitteillä käsky "siirry yksi alkio eteenpäin" on mahdotonta. Javan iteraattoreilla tämä sen sijaan onnistuu. C++:ssa on aidot osoittimet - joiden kanssa voi helposti myös möhliä laittamalla osoittimen osoittamaan paikkaan johon se ei saisi osoittaa). C++:ssa on myös viitteet (reference), joita tosin ei voi siirtää mihinkään luomisen jälkeen. C++:n iteraattorit muistuttavat jopa syntaksiltaan C++:n osoittimia ja itse asiassa C++:n osoitin käy algoritmissa paikkaan, johon tarvitaan iteraattori.
Kirjoita algoritmi kuvakorttien poistamiseksi taulukosta käyttäen osoittimia.
Pöytätestaa algoritmi.
Yksiulotteista taulukkoa voidaan verrata rivitaloon tai ruutupaperin yhteen riviin. Kaksiulotteinen taulukko on vastaavasti kuten kapea kerrostalo tai koko ruutupaperin yksi sivu. Tarvitsemme vastaavasti useampia osoitteita (indeksejä) osoittamaan millä rivillä ja missä sarakkeessa liikumme.
Alla on esimerkki 5x7 taulukosta (♠=pata, ♣=Risti, ♦=ruutu, ♥=hertta):
|
0 |
1 |
2 |
3 |
4 |
5 |
6 |
0 |
♠ |
|
|
|
♠ |
|
|
|
7 |
|
|
|
A |
|
|
1 |
|
♣ |
|
♥ |
|
|
|
|
|
K |
|
5 |
|
|
|
2 |
|
|
♦ |
|
|
|
|
|
|
|
A |
|
|
|
|
3 |
♥ |
♠ |
♦ |
♠ |
♥ |
♥ |
♦ |
|
7 |
2 |
2 |
9 |
6 |
3 |
7 |
4 |
|
|
♥ |
|
|
|
♥ |
|
|
|
2 |
|
|
|
J |
Jos taulukon nimi on peli, niin paikassa 3,1 on kortti pata 2:
peli[3][1] = ♠2
Kirjoita kaikkien esimerkissä olevien korttien osoitteet em. muodossa.
Kaksiulotteista taulukkoa nimitetään usein matriisiksi.
Usein taulukoiden indeksit ilmoitetaan eri järjestyksessä kuin koordinaatiston (x,y)-koordinaatit. Tämä johtuu siitä ajattelutavasta, että taulukon rivi sinänsä voidaan kuvitella yhdeksi alkioksi (rivityypiksi) ja tällöin ilmaisu
peli[3]
tarkoittaa koko riviä (♥7, ♠2, ♦2, ♠9, ♥6, ♥3, ♦7), jonka indeksi on kolme. Mikäli tämän perään laitetaan vielä [1], niin tarkoitetaan ko. tietorakenteen alkiota jonka indeksi on yksi (♠2).
Tarvittaessa moniulotteiset taulukot voidaan muodostaa yksiulotteisenkin taulukon avulla. Esimerkin taulukko voitaisiin muodostaa yksiulotteisesta taulukosta siten, että yksiulotteisen taulukon 7 ensimmäistä alkiota kuvaisivat matriisin 0:ta riviä, 7 seuraavaa matriisin ensimmäistä riviä jne.
Siis mikäli yksiulotteisen taulukon nimi olisi pakka, niin voisimme käyttää samaistuksia:
peli[3][1] = pakka[7*3+1]
peli[j][i] = pakka[7*j+i]
Olemme siis numeroineet kaksiulotteisen taulukon alkiot juoksevasti. Voimmehan tehdä näin myös kerrostalon huoneistoille tai teatterin istumapaikoille.
Taulukot voivat olla myös useampiulotteisia, esimerkiksi 3x4x5 taulukko:
0 1 2 3 4
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐2
0 │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐1
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐0
└─┤ │ └─┤PJ│ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
1 │ ┌┴─┐ │ ┌┴─┐ │R5┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤HA┴─┐ └─┤ ┌┴─┐
└─┤ │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
2 │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐
└─┤ │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐
3 │P7┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐
└─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐
└─┤ │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
isopeli[0][0][1]=PJ
isopeli[2][1][2]=R5
isopeli[1][1][3]=HA
isopeli[2][3][0]=P7
Esitä 5 muuta sijoitusta taulukkoon.
Esitä kaava miten edellä oleva 3-ulotteinen taulukko voitaisiin esittää yksiulotteisella taulukolla.
Aikaisempi satunnaisen matkaajan sanastomme on oikeastaan myös kolmiulotteinen taulukko:
0 1 2
0 minä jag i
1 sinä du you
2 hän han he
Se on kaksiulotteinen taulukko sanoista. Mitä sitten yksi sana on? Se on yksiulotteinen taulukko kirjaimista!
0 1 2 3 4
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐2
0 │h┌┴─┐ │ä┌┴─┐ │n┌┴─┐ │ ┌┴─┐ │ ┌┴─┐1
└─┤s┌┴─┐ └─┤i┌┴─┐ └─┤n┌┴─┐ └─┤ä┌┴─┐└─┤ ┌┴─┐0
└─┤m │ └─┤i │ └─┤n │ └─┤ä │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐2
1 │h┌┴─┐ │a┌┴─┐ │n┌┴─┐ │ ┌┴─┐ │ ┌┴─┐1
└─┤d┌┴─┐ └─┤u┌┴─┐ └─┤ ┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐0
└─┤j │ └─┤a │ └─┤g │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐2
2 │h┌┴─┐ │e┌┴─┐ │ ┌┴─┐ │ ┌┴─┐ │ ┌┴─┐1
└─┤y┌┴─┐ └─┤o┌┴─┐ └─┤u┌┴─┐ └─┤ ┌┴─┐└─┤ ┌┴─┐0
└─┤i │ └─┤ │ └─┤ │ └─┤ │ └─┤ │
└──┘ └──┘ └──┘ └──┘ └──┘
Siis "you" sanan indeksi on [1][2] ja sen kirjaimen "y" indeksi on [0]. Siis kaiken kaikkiaan "you"-sanan "y"-kirjaimen indeksi olisi [1][2][0].
Taulukko voitaisiin järjestää 3-ulotteiseksi myös toisinkin. Esimerkiksi yhdessä "tasossa" olisi yksi kieli jne.
Esitä edellisessä esimerkissä kaikkien kirjainten indeksit.
Millaisella yhden kirjaimen sijoituksella muuttaisit sanan "han" sanaksi "hon"?
Mitenkä tavallinen kirja voitaisiin kuvitella 3-ulotteiseksi taulukoksi?
Miten kirja voitaisiin kuvitella 4-ulotteiseksi taulukoksi?
Piirrä edellisiin perustuen esimerkki 4-ulotteisesta taulukosta ja anna muutama esimerkkisijoitus.
Osoitinmuuttuja osoittaisi myös moniulotteisessa taulukossa yhteen alkioon kerrallaan. Esimerkiksi osoittamalla "you"-sanan "y"-kirjaimeen.
Moniulotteisen ja yksiulotteisen taulukon väliset muunnokset ovat tärkeitä, koska tietokoneen muisti (1997) on lähes aina yksiulotteinen. Siis loogisesti moniulotteiset taulukot joudutaan lopulta aina toteuttamaan yksiulotteisina. Onneksi useat kielet sisältävät moniulotteiset taulukot tietotyyppinä ja kääntäjät tekevät sitten muunnoksen. Tästä huolimatta esimerkiksi C-kielessä joudutaan usein muuttamaan moniulotteisia taulukoita yksiulotteisiksi.
Taulukko voi olla myös taulukko osoittimista. Esimerkiksi sanastomme tapauksessa kaikki sanat voisivat olla yhdessä "möykyssä":
0 1 2 3
0123456789012345678901234567890123
minä jag i sinä du you hän han he
Itse sanasto voisi sitten olla taulukko osoittimia sanojen alkupaikkoihin:
|
0 |
1 |
2 |
0 |
00 |
05 |
09 |
1 |
11 |
16 |
19 |
2 |
23 |
27 |
31 |
Siis taulukon paikasta sanasto[1][0] löytyy osoitin. Tämän osoittimen arvo on tässä esimerkissä 11. Siis osoitin viittaa sanan "sinä" alkuun. Tässä 2-ulotteinen taulukko osoittimista 1-ulotteiseen merkkitaulukkoon
// C++:lla
char *sanasto[3][3];
Mitä ongelmia edellä olisi, mikäli yhdenkin sanan pituutta kasvatettaisiin?
Voitaisiinko edellä käyttää samoja sanoja uudestaan ja jos niin miten?
Osoitinmuuttujaa voitaisiin kuvitella myös seuraavasti: Olkoon meillä osoitekirja (osoitteet) jossa on sivuja:
sivu 0: |
|
sivu 1: |
|
sivu 2: |
Kassinen Katto Katto 3452 |
|
Susi Sepe - |
|
Ankka Aku Ankkalinna 1234 |
Meidän osoitekirjamme on tavallaan taulukko osoittimista (tässä tapauksessa osoitteita, älä sotke termejä!). Taulukon osoitteet paikassa 1 (sivu 1) on osoite "Sepe Sudelle". Mitä tapahtuu mikäli kirjoitamme kokonaan uuden henkilön osoitteen sivulla 1 olevan osoitteen päälle (sijoitetaan uusi arvo osoitinmuuttujalle sivu[1])?
sivu 1: |
|
C++:lla: sivu[1] = &Batman; Javalla: Sivu[1] = Batman; |
Batman Gotham City 999 |
|
Mitä tapahtuu "Sepe Sudelle"? Tuskinpa sijoitus osoitekirjassamme siirtää "Sepe Sutta" yhtään mihinkään "Perämetsästä", tai tekee edes häntä murheelliseksi! Tämä on eräs tyypillinen virhekäsitys osoitinmuuttujia käytettäessä. Osoitinmuuttujaan sijoittaminen ei muuta tietenkään itse tiedon sisältöä. Mutta sijoittaminen siihen paikkaan johon osoitinmuuttuja osoittaa (esimerkissämme "Sepe Suden" asuntoon) muuttaa tietenkin myös itse tiedon sisältöä.
// C++:lla
sivu[1] = uusi_osoite; // ei vaikuta Sepe Suteen
*sivu[1] = uusi_henkilo; // laittaa uuden henkilön Sepen osoitteeseen
// = "tähdätään osoitetta pitkin"
// Javalla
sivu[1] = uusi_osoite; // ei vaikuta Sepe Suteen
sivu[1].setNimi("Batman") // tämän lähemmäksi Javalla ei pääse
Vastaavasti jos meillä on indeksimuuttuja nimeltä snro, niin sijoitus muuttujalle
snro=2
ei muuta mitenkään itse sivun sisältöä. Vasta sijoitus
sivu[snro]=
muuttaisi sivun 2 sisältöä.
Aliohjelma on tarkempi kuvaus tietylle asialle. Tämä kuvaus esitetään jossakin toisessa kohdassa ja sitä ei suinkaan tarvitse joka kerta lukea uudelleen.
Keittokirjassa lukee:
pyöritä lihapullataikinasta pyöreitä pullia
paista pullat kauniin ruskeiksi
Miten pullat paistetaan kauniin ruskeiksi. Tämä olisi edellisen algoritmin aliohjelma. Kokenut kokki ei välitä aina (eikä edes suostuisi) joka reseptin kanssa lukea itse paistamisohjetta. Aloittelija tätä saattaisi tarvita, jottei naapuri hälyttäisi palokuntaa paikalle liian savunmuodostuksen takia. Siis toivottavasti keittokirjasta jostakin kohti löytyisi myös itse paistamisohje.
Aliohjelmille on tyypillistä, että ne saattavat suoriutua vastaavista tehtävistä eri muuttujillekin. Näitä kutsukerroista riippuvia muuttujia sanotaan parametreiksi.
Esim. lihapullan paisto-ohje saattaa semmoisenaan kelvata myös tavalliselle pullalle:
paista("paistettava","c")
Korvaa seuraavassa sana "paistettava" sillä mitä olet
paistamassa ja "c" sillä lämpötilalla, joka keittokirjan
ohjeessa ko. paistettavan kohdalla on:
0. laita "paistettavat" pellille
1. lämmitä uuni "c"-asteeseen
2. laita pelti uuniin
...
9. mikäli "paistettavat" ovat mustia mene ostamaan
kaupasta valmiita
...
Täydennä edellinen paistamisalgoritmi. Onko parametreja tarpeeksi?
Usein helpostakin tehtävästä seuraa monia eri vaihtoehtoja, joiden täydellinen hallitseminen pelkästään päässä saattaa olla ylivoimaista. Tällöin avuksi tulee totuus- ja päätöstaulut. Päätöstaulu on totuustaulun pidemmälle viety versio.
Tutkikaamme aluksi muutamaa esimerkkiä jotka eivät suoranaisesti liity ohjelmointiin, mutta joissa esiintyy täsmälleen vastaava tilanne:
Uusien opiskelijoiden lähtötasotesti 1991 (n. 20% oli osannut vastata oikein tähän tehtävään):
Tehtävä 3.4: Seisot tienhaarassa tuntemattomassa maassa. Toinen teistä vie pääkaupunkiin. Maan asukkaat joko valehtelevat aina tai puhuvat aina totta. Toisen tien alussa seisoskelee yksi heistä. Esität hänelle kyllä vai ei -kysymyksen ja saat vastauksen. Mitä kahta seuraavista neljästä kysymyksestä olisit voinut käyttää ollaksesi varma siitä, kumpi tie vie pääkaupunkiin - riippumatta siitä valehteleeko asukas vai ei?
1. Viekö tämä tie pääkaupunkiin?
2. Olisitko sanonut, että tämä tie vie pääkaupunkiin, jos olisin kysynyt sinulta sitä?
3. Onko niin, että tämä joko on tie pääkaupunkiin, tai sitten sinä puhut totta (mutta ei molemmin tavoin)?
4. Onko totta, että tämä tie vie pääkaupunkiin ja sen lisäksi sinä puhut totta?
Erotelkaamme eri kysymykset:
1 = |
A |
Viekö pääkaupunkiin? |
|
B |
Puhutko totta? (mielenkiinnon vuoksi) |
2 = |
C |
Olisitko sanonut että vie jos A? |
3 <- |
D |
Tie pääk. XOR puhut totta? |
4 <- |
E |
Tie pääk. AND puhut totta? |
Nyt voimme kirjoittaa eri vaihtoehtojen mukaan seuraavan totuustaulun (V=vastaus kun valehteleminen otetaan huomioon):
Tie vie pää- |
Puhuu |
1 |
|
2 |
|
3 |
|
|
4 |
|
kaupunkiin |
totta |
A |
B |
C |
D |
|
V |
E |
|
V |
E |
E |
K |
K |
E |
E |
|
K |
E |
|
K |
E |
K |
E |
K |
E |
K |
|
K |
E |
|
E |
K |
E |
E |
K |
K |
K |
|
E |
E |
|
K |
K |
K |
K |
K |
K |
E |
|
E |
K |
|
K |
Siis 2 ja 3 vastauksissa on järkevä korrelaatio siihen viekö tie pääkaupunkiin vai ei.
Mistä tiedämme milloin kaikki vaihtoehdot on kirjoitettu? Mikäli systeemiin vaikuttavia asioita on n kappaletta ja kukin on kyllä/ei tyyppinen (0/1), niin vaihtoehdot on helpointa saada aikaan kirjoittamalla kaikki n-bittiset binääriluvut järjestyksessä (esimerkissämme n=2) ja suorittamalla sitten tarvittavat samaistukset (esim. E=0 ja K=1). Vaihtoehtoja on tällöin 2n.
00 -> E E
01 -> E K
10 -> K E
11 -> K K
Olkoon meillä tehtävä, jossa yksi muuttuja voi saada arvot K,E,tyhjä ja toinen muuttuja arvot 5 ja 10. Kirjoita kaikki ko. muuttujien kombinaatiot.
Mikäli meillä on vaihtoehtoja n kappaletta ja kukin voi saada ki eri arvoa, niin montako eri kombinaatiota saamme aikaiseksi?
Ottakaamme toinen vastaava tehtävä:
Tehtävä 4.3: Pekka valehtelee maanantaisin, tiistaisin ja keskiviikkoisin; muina viikonpäivinä hän puhuu totta. Paavo valehtelee torstaisin, perjantaisin ja lauantaisin; muina viikonpäivinä hän puhuu totta. Eräänä päivänä Pekka sanoi: "Eilen valehtelin!" Paavo vastasi: "Niin minäkin!" Mikä viikonpäivä oli?
Minä päivinä kaverukset saattaisivat sanoa ko. lausuman? Näitä päiviä on tietysti ne, jolloin joko eilen valehdeltiin ja tänään puhutaan totta TAI eilen puhuttiin totta ja tänään valehdellaan. (XOR)
|
Pekka |
valehteli eilen |
Paavo |
valehteli |
|
sunnuntai |
|
|
|
k sanoo |
|
maanantai |
V |
sanoo |
|
|
|
tiistai |
V |
k |
|
|
|
keskiviikko |
V |
k |
|
|
|
torstai |
|
k sanoo |
V |
sanoo |
<== |
perjantai |
|
|
V |
k |
|
lauantai |
|
|
V |
k |
|
sunnuntai |
|
|
|
k sanoo |
|
Totuustaulun tavoitteena on siis kerätä kaikki mahdolliset vaihtoehdot ohjelmoijan silmien eteen, ja näin kaikki mahdollisuudet voidaan analysoida ja käsitellä.
Eräällä saarella asuu luonnonkansa. Puolet kansan asukkaista aina valehtelevat ja toinen puoli puhuu aina totta. Lisäksi heidän kielensä on tuntematon. On saatu selville, että "BAL" ja "DA" tarkoittavat "kyllä" ja "ei", muttei sitä kumpiko tarkoittaa kumpaa. He ymmärtävät suomea mutta vastaavat aina omalla kielellään. Vastaasi tulee yksi saaren asukas.
a) Mitä saat selville kysymyksellä "Tarkoittaako BAL KYLLÄ"?
b) Millä kysymyksellä saat selville mikä sana on kyllä?
Jälleen maahan jossa asukkaat joko valehtelevat tai puhuvat aina totta. Tapaat kolme asukasta A:n, B:n ja C:n. He sanovat sinulla
A: Me kaikki kolme valehtelemme! B: Tasan yksi meistä puhuu totta!
Mitä ovat A, B ja C?
Entä jos asukkaat sanovat:
A: Me kaikki kolme valehtelemme! B: Tasan yksi meistä valehtelee!
Entä jos:
A: Minä valehtelen mutta B ei!
Entä jos:
A: B valehtelee! B: A ja C ovat samaa tyyppiä!
Vielä yksi:
A sanoo: B ja C ovat samaa tyyppiä. C:ltä kysytään: Ovatko A ja B samaa tyyppiä?
Mitä C vastaa?
Ehtoja usein yhdistellään loogisten operaatioiden avulla:
Mikäli kello 7-20 ja et halua ulkoilla
- mene bussilla
...
Mikäli sinulla on rahaa tai saat kimpan
- ota taksi
Yksittäinen ehto antaa tulokseksi tosi (T=true) tai epätosi (F=false). Ehtojen tulosta voidaan usein myös kuvata 1 tai 0. Ehtojen yhdistämistä loogisilla operaatioilla kuvaa seuraava totuustaulu (myös C++:n loogiset operaattorit merkitty):
|
|
|
|
|
|
|
ja |
|
|
tai |
|
ehd. tai |
|
ei |
|
|
|
||
|
|
|
|
|
|
|
AND |
|
|
OR |
|
|
XOR |
|
|
NOT |
|
|
|
|
p |
|
|
q |
|
p |
&& |
q |
p |
|| |
q |
p |
^ |
q |
|
! |
p |
^ toimii jos p ja q boolean |
|
F |
|
0 |
F |
|
0 |
F |
|
0 |
F |
|
0 |
F |
|
0 |
T |
|
1 |
|
|
F |
|
0 |
T |
|
1 |
F |
|
0 |
T |
|
1 |
T |
|
1 |
T |
|
1 |
|
|
T |
|
1 |
F |
|
0 |
F |
|
0 |
T |
|
1 |
T |
|
1 |
F |
|
0 |
|
|
T |
|
1 |
T |
|
1 |
T |
|
1 |
T |
|
1 |
F |
|
0 |
F |
|
0 |
|
Huomattakoon edellä, että AND operaatio toimii kuten kertolasku ja OR operaatio kuten yhteenlasku (mikäli määritellään 1+1=1). Siis loogisia operaattoreita voidaan käyttää kuten normaaleja algebrallisia operaattoreita ja niillä operoiminen vastaa tavallista algebraa. Loogisten operaatioiden algebraa nimitetään Boolen -algebraksi.
Ehtojen sieventämisessä käytettäviä kaavoja voidaan todistaa oikeaksi totuustaulujen avulla. Todistetaan esimerkiksi de Morganin kaava (vrt. joukko-oppi, 1=true, 0=false):
NOT (p AND q) = (NOT p) OR (NOT q)
Jaetaan ensin väittämä pienempiin osiin:
NOT e1 = e2 OR e3
|
|
e1 |
e2 |
e3 |
|
|
|
p |
q |
p AND q |
NOT p |
NOT q |
NOT e1 |
e2 OR e3 |
|
0 |
0 |
0 |
1 |
1 |
1 |
1 |
|
0 |
1 |
0 |
1 |
0 |
1 |
1 |
|
1 |
0 |
0 |
0 |
1 |
1 |
1 |
|
1 |
1 |
1 |
0 |
0 |
0 |
0 |
|
Koska kaksi viimeistä saraketta ovat samat ja kaikki muuttujien p ja q arvot on käsitelty, on laki todistettu!
Todista oikeaksi myös toinen de Morganin kaava:
NOT (p OR q) = (NOT p) AND (NOT q)
Yhteenlaskun ja kertolaskun välillähän pätee osittelulaki:
p * (q + r) = (p * q) + (p * r)
Samaistamalla * <=> AND ja + <=> OR todetaan loogisille operaatioillekin osittelulaki:
p AND (q OR r) = (p AND q) OR (p AND r)
Todista oikeaksi toinen osittelulaki (toimiiko vast. yhteenlaskulla ja kertolaskulla):
p OR (q AND r) = (p OR q) AND (p OR r)
|
|
|
e1 |
e2 |
e3 |
|
|
|
p |
Q |
r |
q AND r |
p OR q |
p OR r |
p OR e1 |
e2 AND e3 |
|
0 |
0 |
0 |
|
|
|
|
|
|
0 |
0 |
1 |
|
|
|
|
|
|
0 |
1 |
0 |
|
|
|
|
|
|
0 |
1 |
1 |
|
|
|
|
|
|
1 |
0 |
0 |
|
|
|
|
|
|
1 |
0 |
1 |
|
|
|
|
|
|
1 |
1 |
0 |
|
|
|
|
|
|
1 |
1 |
1 |
|
|
|
|
|
|
Huomaa, että totuustauluun tulee nyt 8 riviä (koska kolme muuttujaa)!
Käytä de Morganin kaavoja tai osittelulakia seuraavien ehtojen sieventämiseen:
a) ei ole totta että hinta alle 5 mk ja paino yli 10 kg
b) NOT (kello<=7 OR rahaa>50 mk)
c) ((hinta < 5) tai (rahaa>10)) ja ((hinta < 5) tai (kello>9))
Mikäli edellä esitetyt asiat tuntuvat ymmärrettäviltä, niin ohjelmoinnissa ei tule olemaan mitään vaikeuksia. Jos vastaavat asiat tuntuvat vaikeilta ohjelmoinnin kohdalla, kannattaa palata takaisin tähän lukuun ja yrittää samaistaa asioita ohjelmointikieleen.
Taulukoiden samaistaminen ruutupaperiin, korttipakkaan tai muuhun tuttuun asiaan auttaa asian käsittelyä. Osoitinmuuttuja on yksinkertaisesti jokin (vaikkapa sormi) joka osoittaa johonkin (vaikkapa yhteen kirjaimeen).
Silmukat ja ehtolauseet ovat hyvin luonnollisia asioita.
Aliohjelmat ovat vain tietyn asian tarkempi kuvaus. Tarvittaessa tiettyä asiaa ei ongelmaa tarvitse heti ratkaista, vaan voidaan määritellä aliohjelma, joka hoitaa homman ja kirjoitetaan itse aliohjelman määrittely joskus myöhemmin.
C-kielessä merkkijonot tullaan esittämään taulukoina kirjaimista. Merkkijonon loppu ilmaistaan kirjaimella NUL. Siis esimerkiksi Kissa olisi seuraavan näköinen
0 |
1 |
2 |
3 |
4 |
5 |
|
K |
i |
s |
s |
a |
NUL |
|
Kirjoita seuraavat algoritmit. Erityisesti kirjoita ensin algoritmin sanallinen versio
1. Välilyöntien poistaminen jonon alusta.
2. Välilyöntien poistaminen jonon lopusta.
3. Ylimääräisten (2 tai useampia) välilyöntien poistaminen jonosta.
4. Kaikkien ylimääräisten (alku-, loppu- ja monikertaiset) välilyöntien poistaminen.
5. Jonon muuttaminen siten, että kunkin sanan 1. kirjain on iso kirjain.
6. Tietyn merkin esiintymien laskeminen jonosta.
7. Esiintyykö merkkijono toisessa merkkijonossa (kissatarha, sata-> esiintyy; kissatarha, satu-> ei esiinny).
Kirjoita seuraavat algoritmit:
1. Onko vuosi karkausvuosi vai ei. (Huom! 1900 ei, 2000 on)
2. Montako karkausvuotta on kahden vuosiluvun välillä.
3. Jos 1.1 vuonna 1 oli maanantai, niin mikä viikonpäivä on 1.1 vuonna x? (Oletetaan että kalenteri olisi ollut aina samanlainen kuin nytkin. Vihje! Tutki almanakkaa peräkkäisiltä vuosilta.)
4. Onko päivämäärä pp.kk.vvvv oikeata muotoa?
Ompi Jaavaa ompi Ceetä,
Adaa ompi Pascalia.
Kieltä vanhaa, kieltä uutta
ne kaukaa sekä läheltä.
Monta kieltä monta mieltä,
kummajaisilla ku noilla,
voi koodia väänneskellä,
kaikellailla keikistellä.
Mitä tässä luvussa käsitellään?
• katsotaan mitä syntaktista eroa on eri ohjelmointikielillä yksinkertaisessa esimerkissä
Jossakin vaiheessa ohjelmoinnin opiskelua tullaan siihen, että ohjelma pitäisi toteuttaa jollakin olemassa olevalla ohjelmointikielellä (on tosin ohjelmointikursseja, joilla käytetään keksittyä ohjelmointikieltä).
Seuraavassa esitämme ohjelman jonka ainoa tehtävä on tulostaa teksti:
Terve! Olen ??-kielellä kirjoitettu ohjelma.
Itse ohjelman suunnittelu on tällä kertaa varsin triviaali ja tehtävä ei tarvitse varsinaista tarkennustakaan, kaikki on sanottu tehtävän määrityksessä. Siis valitaan vain käytetyn kielen tulostuslause.
Jotta lukija ymmärtäisi, ettei eri kielten välillä ole kuin pieni ero, esitämme ohjelman useilla eri kielillä. Ohjelman lopun jälkeen olevan poikkiviivan alapuolella on mahdollisesti esitetty miten ohjelmaa voitaisiin kokeilla MS-DOS-koneessa:
/* C-kieli */
#include <stdio.h>
int main(void)
{
printf("Terve! Olen C-kielellä kirjoitettu ohjelma.\n");
return 0;
}
-------------------------------------------------------------------
- käynnistä vaikkapa Turbo C
TC OLEN.C
- kirjoita ohjelma
- paina [Ctrl-F9]
// C++ -kieli
#include <iostream.h>
int main(void)
{
cout << "Terve! Olen C++ -kielellä kirjoitettu ohjelma.\n";
return 0;
}
-------------------------------------------------------------------
- käynnistä vaikkapa Turbo C++
TC OLEN.CPP
- kirjoita ohjelma
- paina [Ctrl-F9]
- huomattakoon, että myös OLEN.C kelpaisi sellaisenaan C++ ohjelmaksi
// Java -kieli
public class Olen {
public static void main(String[] args) {
System.out.println("Terve! Olen Java-kielella kirjoitettu ohjelma.");
}
}
-------------------------------------------------------------------
- Kirjoita Olen.java jollakin editorilla
- käännä: javac Olen.java
- aja: java Olen
// C# -kieli
public class Olen
{
public static void Main(string[] args)
{
System.Console.WriteLine("Terve! Olen C#-kielella kirjoitettu ohjelma.");
}
}
-------------------------------------------------------------------
- Kirjoita HelloWorld.cs jollakin editorilla
- käännä: csc HelloWorld.cs
- aja: HelloWord
{ Pascal-kieli }
PROGRAM olen(OUTPUT);
BEGIN
WRITELN('Terve! Olen Pascal-kielellä kirjoitettu ohjelma.');
END.
-------------------------------------------------------------------
- käynnistä vaikkapa Turbo Pascal
TP OLEN.PAS
- kirjoita ohjelma
- paina [Ctrl-F9]
C Fortran-kieli
PRINT*,'Terve! Olen Fortran-kielellä kirjoitettu ohjelma.'
STOP
END
-- ADA-kieli
with TEXT_IO; use TEXT_IO;
procedure ADA_MALLI is
pragma MAIN;
begin
PUT("Terve! Olen ADA-kielellä kirjoitettu ohjelma."); NEW_LINE;
end ADA_MALLI;
REM BASIC-kieli
PRINT "Terve! Olen BASIC-kielellä kirjoitettu ohjelma."
END
-------------------------------------------------------------------
- käynnistä vaikkapa Quick Basic
QBASIC OLEN.BAS
- kirjoita ohjelma
- paina [Alt-R][Return]
Ç APL-kieli
oç'Terve! Olen APL-kielellä kirjoitettu ohjelma.'
(* Modula-2 -kieli *)
MODULE olen;
FROM InOut IMPORT WriteString, WriteLn;
BEGIN
WriteString("Terve! Olen Modula-2 -kielellä kirjoitettu ohjelma.");
WriteLn;
END olen.
; Common Lisp
(print "Terve! Olen Common Lisp-kielellä kirjoitettu ohjelma.")
-------------------------------------------------------------------
- Kirjoita rivi jossakin Common Lisp-tulkissa, kuten CLISP
( FORTH-kieli )
: Olen
" Terve! Olen FORTH-kielellä kirjoitettu ohjelma." TYPE CR
;
; 8086 assembler
DOSSEG
.MODEL TINY
.STACK
.DATA
viesti DB 'Terve! Olen 8086-assemblerilla kirjoitettu ohjelma.',0DH,0AH,'$'
.code
olen PROC NEAR
MOV AX,@@DATA
MOV DS,AX ; Viestin segmentti DS:ään
MOV DX,OFFSET viesti ; Viestin offset osoite DX:ään
MOV AH,09H ; Funktiokutsu 9 = tulosta merkkijono DS:DX
INT 21H ; Käyttöjärjestelmän kutsu
MOV AX,4C00H ; Funktiokutsu 4C = ohjelman lopetus
INT 21H ; Käyttöjärjestelmän kutsu
olen ENDP
END olen
-------------------------------------------------------------------
- kirjoita ohjelma jollakin ASCII-editorilla nimelle OLEN.ASM
- anna käyttöjärjestelmässä komennot (oletetaan että TASM on polussa):
TASM OLEN
TLINK OLEN
OLEN
Siis erot eri kielten välillä ovat hyvin kosmeettisia (Pascalin BEGIN on C:ssä { jne.). Jollakin kielellä asia pystyttiin esittämään hyvin lyhyesti ja jossakin tarvitaan enemmän määrittelyjä. Ainoastaan assembler-versio on sellaisenaan epäselvä, suoritettavia lauseita on täytynyt kommentoida enemmän.
Kullakin ohjelmointikielellä on omat etunsa. Pascal on hyvin tyypitetty kieli ja sillä ei ole aloittelijankaan niin helppo tehdä eräitä tyypillisiä ohjelmointivirheitä kuin muilla kielillä. Standardi Pascal on kuitenkin hyvin suppea ja siitä puuttuu esim. merkkijonojen käsittely.
Turbo Pascalin laajennukset tekevät kielestä erinomaisen ja nopean kääntäjän ja UNIT-kirjastojen ansiosta se on todella miellyttävä käyttää. Nykyisin lisäksi Delphi-sovelluskehittimen kielenä on juuri Turbo Pascalista laajennettu Object Pascal. Delphi on eräs merkittävimmistä ja helppokäyttöisimmistä työkaluista Windows-ohjelmointiin.
BASIC-kieli on yleensä suppea kieli ja siksi helppo oppia. Lukuisten eri murteiden takia ohjelmien siirtäminen ympäristöstä toiseen on lähes mahdotonta. Microsoftin Visual Basic on kuitenkin Windows-ympäristössä nostanut Basicin jopa ohjelmankehittäjien työkaluksi. Alkuperäiset Basic-murteet olivat huonoja opiskelukieliä rajoittuneiden rakenteiden ja automaattisen muuttujanluomisen vuoksi. Visual Basicin uusimmissa versioissa on jo mukana yleisimmin tarvittavat rakenteet.
Fortran on luonnontieteellisissä sovelluksissa eniten käytetty kieli ja siihen on saatavissa laajat aliohjelmakirjastot useisiin numeriikan ongelmiin. Mikroissa kääntäjät ovat kuitenkin hitaita. Fortran-77 standardi on eräs parhaista standardeista, jota seuraamalla ohjelma toimii lähes koneessa kuin koneessa. Fortranin uusin standardi -90 tarjoaa ennen kielestä puuttuneet rakenteet.
ADA on Pascalin kaltainen vielä tarkemmin tyypitetty kieli, jossa on joitakin olio-ominaisuuksia. Se on Yhdysvaltain puolustusministeriön tukema kieli, joten se lienee tulevaisuuden kieliä. Sopii erityisesti raskaiden reaaliaikatoteutusten kirjoittamiseen (esim. ohjusjärjestelmät). GNU ADA tuo kääntäjän käyttämisen mahdolliseksi jokaiselle. Lisäksi ADA-95 tuo kieleen olio-ominaisuudet.
C-kieli on välimuoto Pascalista ja konekielestä. Ohjelmoijalle sallitaan hyvin suuria vapauksia, mutta toisaalta käytössä on hyvät tietotyypit ja rakenteet. Hyvä kieli osaavan ohjelmoijan käsissä, mutta aloittelija saattaa saada aikaan katastrofin. ANSI-C on suhteellisen hyvin standardoitu ja sitä seuraamalla on mahdollista saada ohjelma toimimaan pienin muutoksin myös toisessakin laiteympäristössä. Lisäksi ANSI-C:n tuoma funktioiden prototyypitys ja muutkin tyyppitarkistukset poistavat suuren osan ohjelmointivirheiden mahdollisuuksista, eivät kuitenkaan kaikkia. UNIX -käyttöjärjestelmän leviämisen myötä C on kohonnut erääksi kaikkein käytetyimmistä kielistä.
C++ on C-kielen päälle kehitetty oliopohjainen ohjelmointikieli. Aikaisemmin C-kielellä oli niin suuri merkitys, että se kannatti ehkä opetella aluksi. Nykyisin jokainen merkittävä C-kääntäjä on myös C++-kääntäjä. Oliopohjaisen ohjelmoinnin kannalta on parempi mitä aikaisemmin olio-ohjelmointi opetellaan. Valitettavasti C++ ei ole hybridikielenä (multi paradigm) paras mahdollinen ohjelmoinnin opetteluun. Kuitenkin paremmin opetteluun soveltuvat kielet ovat usein "leikkikieliä", kuten alkuperäinen Pascalkin oli. Delphi olisi mahtavan graafisen kirjastonsa ja kehitysympäristönsä ansiosta loistava opettelutyökalu, valitettavasti vaan lehti-ilmoituksissa harvoin haetaan Delphi-osaajia! Kohtuullisena kompromissina C++:kin voidaan valita opettelukieleksi, kunhan ei heti yritetä opetella kaikkia kielen kommervenkkeja.
Java on verkkoympäristössä tapahtuvaan ohjelmointiin kehitetty oliokieli. Javan erikoisuus on se, että se käännetään siirrettävään Java-tavukoodimuotoon. Tätä tavukoodia voidaan sitten suorittaa lähes missä tahansa ympäristössä Java-virtuaalikoneen avulla. Javaa on sanottu C++:aa yksinkertaisemmaksi, mutta kuitenkin Java kirjat ovat yhtä paksuja kuin C++ kirjatkin. Nykyisin onkin monesti niin, ettei itse kieli ole ongelma, vaan sille kehitettyjen aliohjelmakirjastojen opettelu ja käyttö. Javan suurimpana etuna on sen lähes kaikissa koneissa toimiva graafinen kirjasto. Java-työkalut kehittyvät kovaa vauhtia, valitettavasti käyttöliittymän graafiseen suunniteluun tarkoitetut työkalut eivät ole keskenään yhteensopivia.
C# on Microsoftin vuonna 2000 julkaisema oliopohjainen ohjelmointikieli. Syntaksiltaan se on muistuttaa paljon C:tä, mutta sen kehityksessä on otettu paljon inspiraatiota myös Javasta, Delphistä ja Visual Basicista. Javan ja C#:n samankaltaisuus on ollut jonkinlainen kiistelyn aihe jopa kielten kehittäjien kesken. Kielten viimeisimmät päivitykset ovat kuitenkin tuoneet kehittyneimpiin ominaisuuksiin huomattavia eroja. Hyvänä esimerkkinä C#:n 3.0:ssa esitelty Language Integrated Query (LINQ) laajennus toi kieleen monia funktionaalisen ohjelmoinnin ominaisuuksia. Javan tavoin myös C# ohjelmat ajetaan virtuaalikoneen päällä. Käytettävä virtuaalikone perustuu Microsoftin Common Language Infastructure spesifikaatioon. Suosituin CLI toteutus on Microsoftin oma virtuaalikone, mutta myös muutama vapaan lähdekoodin vaihtoehto kuten Mono on saatavilla.
Tämän kurssin eri versioissa on käytetty esimerkkikielenä Pascalia, C:tä, C++:aa ja nyt tässä versiossa Javaa. Kielen valinta ei suuria merkitse, ohjelmointi on kuitenkin perusteiltaan samanlaista. Joitakin vivahde-eroja kuitenkin tulee valitun kielen mukana.
Kommenttit jo käyttämäksi
muistiksipa merkit muille
selvennykseksi sepille
omaksikin ovat iloksi.
Vakioksi alkuun tiedot
kevenee koodin korjaaminen
mukavampi muutos aina
sulavampi säätäminen.
Koodi ensin käännettävä
syntaksikin syynättävä
tuo tulkilla tulkattava
siitä sitten suorittava.
Mitä tässä luvussa käsitellään?
• Java-kielisen ohjelman peruskäsitteet
• kääntämisen ja linkittämisen merkitys
• paketin käyttöönotto
• vakioarvot
Syntaksi:
kommentti: /* vapaata tekstiä, vaikka monta riviäkin*/
kommentti: // loppurivi vapaata tesktiä
luokan ottaminen: importpaketin_nimi.Luokka; import paketin_nimi.*;
vakio: static final tyyppi nimi =arvo;
tulostus: System.out.println(merkijono);
merkkijono: "merkkejä"
Luvun esimerkkikoodit:
https://svn.cc.jyu.fi/srv/svn/ohj2/moniste/esimerkit/src/alkeet/
Ohjelman toteuttamista varten täytyy valita jokin todellinen ohjelmointikieli. Lopullisesta ohjelmasta ei valintaa toivottavasti huomaa. Valitsemme käyttökielen tällä kurssilla puhtaasti "markkinaperustein": paljon käytetyn ja työelämässä kysytyn - Java.
Mikäli olet käynyt jo Ohjelmointi 1 kurssin C#:lla, niin ei turhaan kannata säikähtää kielen vaihtumista. Java ja C# ovat ominaisuuksiltaan hyvin samankaltaisia kieliä. Molemmat ovat staattisesti tyypitettyjä olio-ohjelmointikieliä, molempien suoritus tapahtuu virtuaalikoneessa ja lisäksi ne ovat hyvin samankaltaisia jopa syntaksiltaan.
Erot käsitellään tässä monisteessa sitä mukaan kun niitä tulee vastaan. Merkittävimmät niistä koskevat kuitenkin vasta kielten uudempia ja kehittyneempiä ominaisuuksia, eikä niitä ole edes Ohjelmointi 1 kurssin puitteissa tullut vastaan. Internetissä on monia eroista kielten eroa käsitteleviä artikkeleita, mutta yksi hyvä paikka on Ville Salosen kandinaatintutkielma:
https://svn.cc.jyu.fi/srv/svn/ohj2/ville/Ville_Salonen_-_Javasta_Csharppiin_-_Kandidaatintutkielma.pdf
Ville on myös pohtinut esseessään hieman edistyneempää asiaa ja syitä eroavaisuuksiin. Näkökulma on enemmänkin "konepellin alta", joten seuraava linkki voi olla hyvää luettavaa tämän kurssin jälkeen.
https://svn.cc.jyu.fi/srv/svn/ohj2/ville/Javasta_Csharpiin_pintaa_syvemmalta.pdf
Tarpeen vaatiessa monisteen muutamaan ensimmäiseen lukuun Java-kielisten esimerkkien rinnalle on kirjoitettu myös C# versiot. Kurssin kannalta C#:n osaaminen ei kuitenkaan ole olennaista.
// Ohjelma tulostaa tekstin Hello world!
class Hello {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
// C# -kieli
class Hello
{
public static void Main(string[] args)
{
System.Console.WriteLine("Hello world!");
}
}
Ohjelmat ovat selvästikin hyvin samankaltaiset. Ensimmäiseksi kannattaa kiinnittää huomio syntaksiin. Javan (yleisesti käytetty) tapa aloittaa lohko { -merkillä ilman rivinvaihtoa eroaa hieman C#:n vastaavasta. Molemmat ohjelmat kääntyisivät kummallakin tavalla, mutta koodin luettavuus helpottuu, kun käyttää virallista tyyliä. Lisäksi kehitysympäristöjen tarjoamien koodin formatointityökalujen käyttö on helpompaa jos oletusasetuksia ei tarvitse erikseen muuttaa. Vähän tärkeämpi koodin muotoiluun liittyvä asia on, että funktiot on tapana kirjoittaa Javassa pienellä, kun C#:n käytäntö on nimetä ne isolla. Jälleen kuitenkin kumpikin tyyli menee molempien kääntäjistä läpi.
On myös helppo huomata että käytettyjen kirjastojen ja funktioiden nimet ovat muutenkin erilaiset. Silti usein tulee vastaan tilanteita, joissa käytettyjen kirjastojen nimet ovat, jolleivät peräti samoja, niin ainakin hyvin samankaltaisia. Tähän liittyy kuitenkin se ansa, että samallakin tavalla nimettyjen funktioiden toiminta saattaa erota ratkaisevasti toisistaan! Esimerkiksi C#:issa usein viedään parametrina alku ja osavälin pituus kun Javassa viedään parametrina alku ja loppu-indeksi, joka ei enää tule mukaan. Jos aloitetaan esimerkiksi jonon alusta, niin silloin samat parametrin arvot toimivat molemmilla, mutta muussa tapauksessa ei:
// 012345
String jono = "abcde";
C#: jono.Substring(0,3) => abc
jono.Substring(1,3) => bcd
Java: jono.substring(0,3) => abc
jono.substring(1,3) => bc
Javassa jälkimmäinen lause tuottaa bc, mutta C#:ssa bcd. Syy selviää metodien dokumentaatiota tutkimalla. Ensimmäinen esimerkki tulostaa merkkijonon joka alkaa paikasta (kirjaimesta) kaksi ja loppuu kolmanteen paikkaan, kun taas toinen ottaa toisesta paikasta lähtien kolme merkkiä.
Kannattaa siis aluksi varmistaa dokumentaatiosta mitä onkaan tekemässä. Ohjelmointikielien samankaltaisuuksista huolimatta jokaisella kielellä on omat tapansa toteuttaa asioita. Se minkä vuoksi Javan substring metodin viimeinen parametri 3 ei sisällytäkään vastaavaa merkkiään palautettavaan merkkijonoon, johtuu Javan käytännöstä jättää merkki pois (exclude), joka on tyypillistä toiminnallisuutta useille Java-funktioille. Valittu tapa säästää käytännössä usein turhia -1 laskuja.
Kirjoita Java-ohjelma joka tulostaa:
Terve!
Olen Matti Meikäläinen 25 vuotta.
Asun Kortepohjassa.
Puhelinnumeroni on 603333.
Ohjelmakoodi kirjoitetaan millä tahansa tekstieditorilla tekstitiedostoon vaikkapa nimelle Hello.java. Yleensä tiedoston tarkentimella annetaan sille tyyppi, jonka avulla käyttöjärjestelmä ja ohjelmat saavat lisätietoa minkälainen tiedosto on kyseessä.
Valmis tekstitiedosto käännetään ko. kielen kääntäjällä. Käännöksestä muodostuu usein objektitiedosto, joka on jo lähellä lopullisen ohjelman konekielistä versiota. Objektitiedostosta puuttuu kuitenkin mm. kirjastorutiinit. Kirjastorutiinien kutsujen kohdalla on "tyhjät" kutsut.
Java-kielen tapauksessa käännöksen tuloksena syntyy Java-virtuaalikoneen (JVM) ymmärtämää tavukoodia. Esimerkin tiedosto kääntyy esimerkiksi komennolla:
javac Hello.java
Käännöksen tuloksena syntyvässä Hello.class-tiedossa on siis Java-tulkin ymmärtämää tavukoodia. Kuitenkin siitäkin puuttuu itse kirjastorutiinit. Erona muihin kieliin on se, että käännetty tiedosto toimii niissä ympäristöissä, joissa on JVM ja nuo puuttuvat rutiinit.
Varsinaisissa käännettävissä kielissä käännös pitää suorittaa uudelleen jos ohjelma halutaan siirtää toiseen ympäristöön.
Linkittäjällä (kielestä riippumaton ohjelma) liitetään kirjastorutiinit käännettyyn objektitiedostoon. Linkittäjä korvaa tyhjät kutsut varsinaisilla kirjastorutiinien osoitteilla, kunhan se saa selville mihin kohti muistia kirjastorutiinit sijoittuvat. Näin saadaan valmis ajokelpoinen konekielinen versio alkuperäisestä ohjelmasta.
Javan tapauksessa varsinaista linkittämistä ei tarvita, vaan ohjelman suorituksen aikana etsitään tarpeellisia luokkia. Luokkien etsiminen voi tapahtua heti kun ensimmäistä luokkaa ladataan muistiin ("static" resolution) tai vasta kun luokkaan viitataan ("laziest" resolution).
Käännetty ohjelma ajetaan käyttöjärjestelmästä riippuen yleensä kirjoittamalla ohjelman alkuperäinen nimi. Tällöin käyttöjärjestelmän lataaja-ohjelma lataa ohjelman konekielisen version muistiin ja siirtää prosessorin ohjelmalaskurin ohjelman ensimmäisenä suoritettavaksi tarkoitettuun käskyyn. Vielä tässäkin vaiheessa osa aliohjelmakutsujen osoitteista voidaan muuttaa vastaamaan sitä todellista osoitetta, johon aliohjelma muistiin ladattaessa sijoittui. Tämän jälkeen vastuu koneen käyttäytymisestä on ohjelmalla. Onnistunut ohjelma päättyy aina ennemmin tai myöhemmin käyttöjärjestelmän kutsuun, jossa ohjelma pyydetään poistamaan muistista.
Kuva 6.1 Ohjelman kääntäminen ja linkittäminen
Javan tapauksessa ajaminen suoritetaan antamalla .class tai .jar tiedosto Java-virtuaalikoneelle (Java Virtual Machine, JVM). Esimerkkimme tapauksessa komennolla
java Hello
Jos luokasta Hello löytyy julkinen luokkametodi (staattinen metodi) nimeltä main, niin ohjelman suoritus aloitetaan siitä. Mikäli metodia ei löydy, tulee virheilmoitus:
Exception in thread "main" java.lang.NoSuchMethodError: main
Kuva 6.2 Java-ohjelman kääntäminen ja linkittäminen
Alkuperäisellä editorilla kirjoitetulla ohjelmakoodilla ei ole tavallista kirjettä kummempaa virkaa ennen kuin teksti annetaan kääntäjäohjelman tutkittavaksi. Käännöksen jälkeen alkuperäinen teksti voitaisiin periaatteessa vaikka hävittää - käytännössä se tietysti säilytetään mm. ylläpidon takia. Siis me kirjoitamme tekstiä, joka ehkä (toivottavasti) muistuttaa Java-kielen syntaksin mukaista ohjelmaa. Vasta käännös ja linkkaus tekevät todella toimivan ohjelman.
On olemassa ohjelmankehitysympäristöjä, joissa editori, kääntäjä ja linkkeri (sekä mahdollisesti debuggeri, virheenjäljitin) on yhdistetty käyttäjän kannalta yhdeksi toimivaksi kokonaisuudeksi. Esimerkkeinä Eclipse, NetBeans, Microsoftin Visual Studio ja Borland-C++ Builder . Kaikissa listassa mainituissa kehittimissä on myös tuki käyttöliittymän suunnittelulle.
Esimerkiksi Borlandin ympäristöissä ohjelma kirjoitetaan tekstinä ja kun ohjelmakoodi on valmis, saadaan koodi käännettyä, linkitettyä ja ladattua ajoa varten vain painamalla [F9](tai [Ctrl-F9] versiosta riippuen).
Mahdollisia muita integroitujen ympäristöjen ominaisuuksia ovat mm: UML-kaavioiden ja muiden dokumenttien automaattinen tuottaminen (esim. Delphin ModelMaker, JavaDoc-yhteistoiminta Java-kehittimissä), jotka perinteisesti ovat olleet CASE-suunnitteluohjelmien aluetta. Lisäksi myös koodin generointi kaavioista onnistuu rajoitetusti.
Seuraavaksi tutkimme ohjelmaa lause kerrallaan:
import java.lang.System;
/**
*
* Ohjelma tulostaa tekstin Hello world!
* @author Vesa Lappalainen
* @version 1.0, 03.01.2003
*/
class Hello2 {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
// Ohjelma tulostaa tekstin Hello world!
tai
/* Ohjelma tulostaa tekstin Hello world! */
Ohjelman alussa on kommentoitu mitä ohjelman tekee. Yleensä ohjelmakoodit on hyvä varustaa kuvauksella siitä, mitä ohjelma tekee, kuka ohjelman on tehnyt, milloin ja miksi. Milloin ohjelmaa on viimeksi muutettu, kuka ja miten.
Lisäksi jokainen vähänkin ei-triviaali lause tai lauseryhmä kommentoidaan. Kommenttien tarkoituksena on kuvata ohjelmakoodia lukevalle lukijalle se mistä on kyse.
Lohkokommentti alkaa /* -merkkiyhdistelmällä ja päättyy */ -merkkiyhdistelmään. Lohkokommentteja voidaan sijoittaa Java-koodissa mihin tahansa mihin voitaisiin pistää myös välilyönti. Rivin loppuminen ei sinänsä lopeta lohkokommenttia. Kommentin sisällä SAA esiintyä / ja *-merkkejä yhdessä tai erikseen, muttei lopettavaa yhdistelmää */.
Yleinen virhe on unohtaa lohkokommentin loppusulku pois. Mikäli esimerkissämme puuttuisi kommentin loppusulku, olisi koko loppuohjelma kommenttia ja mitään ohjelmaa ei siis olisikaan. Mikäli kääntäjä antaa vyöryn ihmeellisiä virheilmoituksia, kannattaa aina ensin tarkistaa kommenttisulkujen täsmäävyys. Tosin tähän auttaa nykyisten ohjelmointiympäristöjen värikoodien käyttö eri ohjelman osille, eli esimerkiksi kommentit näkyvät eri värisinä ja puuttuva komenttisulku paljastuu välittömästi.
Javassa yhden rivin kommentti voidaan ilmaista myös // -merkkiyhdistelmällä, jolloin rivinloppu lopettaa kommentin.
Itse ohjelmakoodi kommentoidaan seuraavasti:
• selviä kielen rakenteita ei saa kommentoida. Ei siis
i=5; // sijoitetaan i on 5 /* TURHA! */
• kuitenkin mikäli lauseella on selvä merkitys algoritmin kannalta, kommentoidaan tämä
i=5; // aloitetaan puolivälistä
• ryhmitellään lauseet tyhjien rivien avulla loogisiksi kokonaisuuksiksi. Tällaisen kokonaisuuden alkuun voidaan laittaa kommenttirivi, joka kuvaa kaikkien seuraavien lauseiden merkitystä.
• mikäli tekee mieli kommentoida lauseryhmä, kannattaa miettiä voitaisiinko koko ryhmä kirjoittaa aliohjelmaksi. Aliohjelman nimi sitten kuvaisi toimintaa niin hyvin, ettei kommenttia enää tarvittaisikaan. Kuitenkin jos näin suunnitellulle aliohjelmalle tulee iso kasa (liki 10) parametreja, täytyy asiaa ajatella uudestaan.
• muuttujien nimet valitaan kuvaaviksi. Kuitenkin mitä lokaalimpi muuttujan käyttö, sitä lyhyemmäksi nimi voidaan jättää. i ja j sopivat aivan hyvin silmukkamuuttujien nimiksi ja p yms. osoittimen nimeksi (lokaalisti).
• globaaleja muuttujia vältetään 'kaikin keinoin'
• olioiden ansiosta globaalit muuttujat voidaan yleensä välttää kokonaan!
• vakiotyyliset (alustetaan esittelyn yhteydessä eikä ole tarkoitus ikinä muuttaa) globaalit muuttujat on sallittu sellaisenaan ja niiden nimet kannattaa ehkä kirjoittaa isolla.
• funktioiden paluuarvolle valitaan tietty tyyli, joka pyritään säilyttämään koko ohjelman ajan. Esimerkiksi true = onnistui ja false epäonnistui.
/**
* Ohjelma tulostaa tekstin Hello world!
* @author Vesa Lappalainen
* @version 1.0, 03.01.2003
*/
Jos tiedot annetaan Javan dokumentoinnin standardimuodossa, niin tiedostoista saadaan sitten koostettua helposti HTML-muotoinen dokumentti. JavaDocin mukainen kommentti alkaa "sululla" /** ja päättyy normaaliin kommentin loppumerkkiin.
Dokumentaatiokommenttien käyttö helpottaa dokumentaation hallitsemista. Muuttaessaan funktion toiminnallisuutta ohjelmoijan on helppo muuttaa myös dokumentaatiota, koska se on saatavilla samasta paikasta. Lisäksi kehittyneet ohjelmointiympäristöt osaavat lukea ja näyttää oikein muodostetun dokumentaation automaattisesti koodia kirjoittaessa.
Kommentointi kannattaa käytännössä tehdä yksittäisten metodien tarkkuudella. Myös tämän monisteen esimerkeistä on pyritty tekemään JavaDocin mukaisia
Lisäinformaatiota dokumentaatioon annetaan tagien muodossa @-merkillä ja sen jälkeen tulevalla avainsanalla. Esimerkistä löytyvät koodin tekijän ja version ilmoittavat @author ja @version -tagien käyttäminen on hyödyllistä vaikkapa tiimeissä tapahtuvissa ohjelmointiprojekteissa. Kaksi muuta tärkeää merkintää ovat aliohjelmatasolla käytettävät @param ja @return, joilla kuvaillaan halutut parametrit ja palautettava arvo.
Katso lisää ohjeita JavaDocin ja tagien käytöstä osoitteessa
Mikäli käytetty editori ei tue automaattisesti kommentointia, kannattaa kirjoittaa aina tarvittavat kommentit vaikka tiedostoihin a.t (alku) jam.t (metodi) ja opetella käyttämään editorin "lisää tiedosto" toimintoa.
Harvoin voi tehdä ohjelman joka tulee täysin toimeen ilman muiden apua. Javan tapauksessa ilman muiden luokkien apua. Jotta kääntäjä tietäisi mistä puhutaan, pitää kertoa mistä paketista luokka löytyy. Paketista java.lang löytyy System-niminen luokka, josta löytyy tarvitsemamme out-olio. Eli pitäisi oikeastaan kirjoittaa:
java.lang.System.out.println("Hello world!");
Olioihin ja luokkiin paneudumme tarkemmin luvussa 9.
Jos kuitenkin samaan luokkaa tarvitaan useasti ja halutaan lyhentää kirjoittamista, voidaan import-lauseella kertoa ennen varsinaista koodin aloittamista apuna tarvittavat luokat.
import java.lang.System;
Jos haluttaisiin ottaa kaikki tietyn paketin luokat käyttöön, tämä voitaisiin tehdä rivillä:
import java.lang.*;
Poikkeuksen muodostaa paketti java.lang jota ei tarvitse välttämättä erikseen esitellä lainkaan. Näinhän oli tehty ensimmäisessä esimerkissämme.
C# osaajille tiedoksi, että using sanalla on vastaava toiminnallisuus on C#:ssa kuin importilla. C# ei kuitenkaan tue jokerimerkin käyttöä, joten kaikki kirjastot määritellään siinä täsmällisesti.
class Hello2 {
Jokainen Java-ohjelma sisältää vähintään yhden julkisen luokan. Kunkin tiedoston nimi on oltava sama kuin tiedostossa olevan julkisen luokan nimi + '.java'. Palaamme luokkiin ja olioihin tarkemmin hieman myöhemmin. Usein olio-ohjelmoinnissa on tapana että luokkien nimet aloitetaan isolla kirjaimella.
Luokan esittely ja toteutus alkaa aaltosululla { ja päättyy toiseen lopettavaan aaltosulkuun }.
Kun Java-tavukoodi ladataan muistiin, etsitään ensin ladatusta luokasta (tai muuten erikseen ilmoitetusta luokasta) pääohjelmaa, josta koodin suoritus aloitetaan. Pääohjelman nimi on aina oltava main. Oikeassa ohjelmassa on pääohjelman lisäksi useita luokkia ja metodeita (luokkien sisällä olevia aliohjelmia).
main-metodi voi olla myös useammassa luokassa, jolloin kullakin main-metodilla voidaan testata kyseisen luokan toiminta. Näin helpotetaan yksikkötestausta (modulitestausta). Tästä lisää kun pääsemme tarkemmin olioiden ja luokkien kimppuun. Huom! Javassa vain luokkien nimet aloitetaan isolla kirjaimella.
Seuraavaksi esitellään ohjelman pääohjelma ("oikea" ohjelma koostuu isosta kasasta aliohjelmia ja yhdestä pääohjelmasta, jonka nimi on main).
public tarkoittaa, että metodi on julkisesti näkyvä. Muuten metodi ei näkyisi luokan ulkopuolelle eikä sitä voitaisi suorittaa.
static tarkoittaa että metodi on ns. luokkametodi, eli se voidaan suorittaa, vaikkei luokasta olisi olemassa yhtään esiintymää eli oliota. Luokkametodi ei voi käyttää luokan olioiden attribuutteja suoraan (koska oliota ei välttämättä ole).
void ilmoittaa, että metodi jota kirjoitamme ei palauta mitään arvoa (eng. void = mitätön).
main tarkoittaa pääohjelman nimeä. Tämä TÄYTYY aina olla main. Muut metodit voidaan nimetä vapaasti.
( Metodin parametrilistan (argumenttilistan) alkusulku.
String[] ilmoittaa että metodi saa parametrinaan taulukollisen (hakasulut tarkoittavat taulukkoa) merkkijonoja. Nämä ovat merkkijonot tulevat ohjelmaan käynnistyksen yhteydessä olevina parametreina. Käynnistys parametreja voi olla nolla tai useita.
args itse keksitty nimi jolla merkkijonotaulukkoon viitataan. Tämä nimi voi olla mikä tahansa.
) Metodin parametrilistan (argumenttilistan) loppusulku.
Ohjelma
/**
* Ohjelma tulostaa kutsun mukana tulleet parametrit
* @author Vesa Lappalainen
* @version 1.0, 03.01.2003
*/
classHello3 {
public static void main(String[] args) {
for(int i=0; i<args.length; i++)
System.out.println("Parametri " + i + ": " + args[i]);
}
}
classHello3
{
public static void Main(string[] args)
{
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine("Parametri " + i + ": " + args[i]);
}
Console.ReadKey();
}
}
Tulostaisi seuraavalla tavalla:
E:\kurssit\ohj2\moniste\esim\java-alk>java Hello3 eka toka kolmas
Parametri 0: eka
Parametri 1: toka
Parametri 2: kolmas
{ } Javassa isompi joukko lauseita kootaan yhdeksi lauseeksi sulkemalla lauseet aaltosulkuihin. Metodin täytyy aina sisältää aaltosulkupari, vaikka siinä olisi vain 0 tai 1 suoritettavaa lausetta.
System.out.println("Hello world!")
System on paketista java.lang löytyvä luokka, jossa on joukko hyödyllisiä oliota ja metodeja.
out on Systemluokan olio, joka sisältää mm. tulostukseen tarvittavia metodeja.
println("?") tulostaa ajonaikana sen tekstin, joka on lainausmerkkien välissä. Tulostuksen jälkeen vaihdetaan uudelle riville. Jos tarvitsee tulostaa useita eri tekstejä tai muuttujia välissä, voidaan niistä muodostaa +-operaatiolla uusi merkkijono, esimerkiksi:
System.out.println("Parametri " + i + ": " + args[i]);
; puolipiste lopettaa lauseen. Puolipiste voidaan sijoittaa mihin tahansa lopetettavaan lauseeseen nähden. Sen eteen voidaan jättää välilyöntejä tai jopa tyhjiä rivejä. Sen pitää kuitenkin esiintyä ennen uuden lauseen alkua. Näin Java-kieli ei ole rivisidonnainen, vaan Java-kielinen lause voi jakaantua usealle eri riville tai samalla rivillä voi olla useita Java-kielisiä lauseita.
Puolipisteen unohtaminen on tyypillinen syntaksivirhe. Ylimääräiset puolipisteet aiheuttavat tyhjiä lauseita, joista tosin ei ole mitään haittaa: "Tyhjän tekemiseen ei kauan mene" - sanoo tyhjän toimittaja.
Isoilla ja pienillä kirjaimilla on Java-kielessä eri merkitys. Siis EI VOIDA KIRJOITTAA:
SysTem.Out.printLn("Hello!") // VÄÄRIN! ☹
Välilyöntejä, tabulointimerkkejä, rivinvaihtoja ja sivunvaihtoja nimitetään yleisesti yhteisellä nimellä "white space". Käännettäessä kommentit muutetaan yhdeksi välilyönniksi, joten myös kommenteista voitaisiin käyttää nimitystä "white space". Jatkossa käytämme nimitystä tyhjä tai tyhjä merkki, kun tarkoitamme "white space".
Java-koodi voi sisältää tyhjiä merkkejä missä tahansa, kunhan niitä ei kirjoiteta keskelle sanaa tai tekstiä määrittelevän ""-parin ollessa auki. ""-parin sisällä tyhjätkin merkit ovat merkityksellisiä.
Siis kääntäjän kannalta malliohjelmamme voitaisiin kirjoittaa myös seuraavillakin tavoilla:
class ☹
Hello4
{
public
static
void
main
(
String[]
args)
{
System
.
out
.
println
(
"Hello world!"
)
;
}
}
class Hello5{public static void main ☹
(String[] args){
System.out.println("Hello world!" );}}
☹
class Hello6{public static void main(String[]args){System.out.println("Hello world!");}}
Yleinen tyyli on kuitenkin jakaa koodia riveihin ja sisentää lohkoja muutamalla pykälällä. Kunnes lukija on varma omasta tyylistään, kannattaa matkia tässä monisteessa (ei kuitenkaan edellisiä esimerkkejä) esitettyä kirjoitustapaa ohjelmille.
Voimme määritellä ohjelmaamme vakioita; eli arvoja jotka esiintyvät ohjelmassa täsmälleen yhden kerran. Näin ohjelmastamme saadaan helpommin muuteltava. Esimerkiksi seuraava ohjelma tulostaisi myös tekstin "Hello world!": . Vakioiden nimet on tapana kirjoittaa isoilla kirjaimilla.
/**
* Ohjelma tulostaa Hello World! Tulostettava teksti on vakiona
* @author Vesa Lappalainen
* @version 1.0, 03.01.2003
*/
class Hello7 {
static final String TERVE = "Hello";
static final String MAAILMA = "world!";
public static void main(String[] args) {
System.out.println(TERVE + " " + MAAILMA);
}
}
Kirjoita edellisestä ohjelmasta suomenkielellä tulostava versio (= suomenna ohjelma).
Kirjoita aikaisemmasta "Matti Meikäläinen asuu Kortepohjassa" -ohjelmasta versio, jossa nimi, osoite ja puhelin on esitelty vakioina.
Vakiomäärittelyä voitaisiin käyttää esimerkiksi kokonaislukuvakioiden määrittelemiseen:
/**
* Ohjelma tulostaa tietoja kuutiosta
* @author Vesa Lappalainen
* @version 1.0, 04.01.2003
*/
class Kuutio {
public static final String TAHOKAS = "Kuutiossa";
public static final int KARKIA = 8;
public static final int SIVUTASOJA = 6;
public static final int SARMIA = 12;
public static void main(String[] args) {
System.out.print (TAHOKAS + " on " + KARKIA + " kärkeä,");
System.out.print (" " + SIVUTASOJA + " sivutasoa ja");
System.out.println(" " + SARMIA + " särmää.");
}
}
/**
* <summary> Ohjelma tulostaa tietoja kuutiosta </summary>
*/
class Kuutio
{
public const string TAHOKAS = "Kuutiossa";
public const int KARKIA = 8;
public const int SIVUTASOJA = 6;
public const int SARMIA = 12;
public static void Main(string[] args)
{
System.Console.Write(TAHOKAS + " on " + KARKIA + " kärkeä,");
System.Console.Write(" " + SIVUTASOJA + " sivutasoa ja");
System.Console.WriteLine(" " + SARMIA + " särmää.");
}
}
Muuta edellistä ohjelmaa siten, että tulostetaan samat asiat tetraedristä.
Turha koodi muuttujitta,
ompi onneton ohjelmaksi.
Parametri kutsuun pistä
aliohjelmalle argumentti.
Tarjoappa käyttöön tuota
metodia mielekästä
rutiinia riittävätä
itse tarkoin testattua.
Mitä tässä luvussa käsitellään?
• muuttujat
• malliohjelma jossa tarvitaan välttämättä muuttujia
• oliomuuttujat eli viitemuuttujat
• aliohjelmat, eli funktiot (metodit)
• aliohjelman testaaminen
• erilaiset aliohjelmien kutsumekanismit
• parametrin välitys
• lokaalit muuttujat
• pöytätesti
• yksikkötestit
Syntaksi:
Seuraavassa muut = muuttujan nimi, koostuu kirjaimista,0-9,_, ei ala 0-9
muut.esittely: tyyppi muut =alkuarvo; // 0-1 x =alkuarvo
sijoitus: muut = lauseke;
merkkijonon lukeminen, ks. Syotto-luokka
aliohj.esittely: tyyppi aliohj_nimi(tyypi muut, tyyppi muut); // 0-n x muut
aliohj.kutsu muut = aliohj_nimi(arvo, arvo); // 0-1 x muut=, 0-n x arvo
olion luonti Tyyppi olion_nimi = newTyyppi(parametrit);
Luvun esimerkkikoodit:
https://svn.cc.jyu.fi/srv/svn/ohj2/moniste/esimerkit/src/muuttujat/
Satunnainen matkaaja ajelee tällä kertaa kotimaassa. Autoillessa hänellä on käytössä Suomen tiekartan GT -karttalehtiä, joiden mittakaava on 1:200000. Viivoittimella hän mittaa kartalta milleinä matkan, jonka hän aikoo ajaa. Ilman matikkapäätä laskut eivät kuitenkaan suju. Siis hän tarvitsee ohjelman, jolla matkat saadaan muutettua kilometreiksi.
Millainen ohjelman toiminta voisi olla? Vaikkapa seuraavanlainen:
C:\OMA\MATKAAJA>matka[RET]
Lasken 1:200000 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>35[RET]
Matka on luonnossa 7.0 km.
C:\OMA\MATKAAJA>matka[RET]
Lasken 1:200000 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>352[RET]
Matka on luonnossa 70.4 km.
C:\OMA\MATKAAJA>
Edellisessä toteutuksessa on vielä runsaasti huonoja puolia. Mikäli samalla haluttaisiin laskea useita matkoja, niin olisi kätevämpää kysellä matkoja kunnes kyllästytään laskemaan. Lisäksi olisi ehkä kiva käyttää muitakin mittakaavoja kuin 1:200000. Muutettava matka voitaisiin tarvittaessa antaa jopa ohjelman kutsussa. Voimme lisätä nämä asiat ohjelmaan myöhemmin, kunhan kykymme siihen riittävät. Toteutamme nyt kuitenkin ensin mainitun ohjelman.
Ohjelmamme poikkeaa aikaisemmista esimerkeistä siinä, että nyt ohjelman sisällä tarvitaan muuttuvaa tietoa: matka millimetreinä. Tällaiset muuttuvat tiedot talletetaan ohjelmointikielissä muuttujiin. Muuttuja on koneen muistialueesta varattu tarvittavan kokoinen "muistimöhkäle", johon viitataan käytännössä muuttujan nimellä.
Kone viittaa muistipaikkaan muistipaikan osoitteella. Kääntäjäohjelman tehtävä on muuttaa muuttujien nimiä muistipaikkojen osoitteiksi. Kääntäjälle täytyy kuitenkin kertoa aluksi, minkä kokoisia 'möhkäleitä' halutaan käyttää. Esimerkiksi kokonaisluku voidaan tallettaa pienempään tilaan kuin reaaliluku. Mikäli haluaisimme varata vaikkapa muuttujan, jonka nimi olisi matka_mm kokonaisluvuksi, kirjoittaisimme seuraavan Java-kielisen lauseen (muuttujan esittely):
int matka_mm; // yksinkertaisen tarkkuuden kokonaisluku
Pascal -kielen osaajille huomautettakoon, että Pascalissahan esittely oli päinvastoin:
var matka_mm: integer;
Tulos, eli matka kilometreinä voitaisiin laskea muuttujaan matka_km. Tämän muuttujan on kuitenkin oltava reaalilukutyyppinen (ks. esimerkkiajo), koska tulos voi sisältää myös desimaaliosan:
double matka_km; // kaksinkertaisen tarkkuuden reaaliluku
On olemassa myös yksinkertaisen tarkkuuden reaaliluku float, mutta emme tarvitse sitä tällä kurssilla. Samoin kokonaisluvusta voidaan tehdä "tosi lyhyt", "lyhyt" tai "kaksi kertaa isompi":
int matka_km;
short sormia; // max 32767
byte varpaita; // max 127
long valtion_velka_Mmk; // Tarvitaan ISO arvoalue
Muuttujan määritys voisi olla myös
volatile static long sadasosia;
Tulemme kuitenkin aluksi varsin pitkään toimeen pelkästään seuraavilla perustyypeillä:
short - kokonaisluvut -32 768 - 32 767, 16-bit
int - kokonaisluvut -2 147 483 648 - 2 147 483 647, 32-bit
double - reaaliluvut n. 15 desim. -> 1.7e308
char - kirjaimet 16 bit Unicode
boolean - true tai false
Primitiivimuuttujiin liittyvät myös C#:n ja Javan suurimmat erot. Ensimmäiseksi ainoastaan positiivisille luvuille tarkoitettua unsigned tietotyyppiä ei Javasta löydy, vaan primitiivityypit voivat olla aina sekä positiivisia, että negatiivisia. Lisäksi C# on eräässä mielessä Javaa puhtaampi oliokieli, koska ohjelmoijalle päin kaikki sen tietotyypit käyttäytyvät kuin oliot (periytyvät object-kantaluokasta). Käytännössä C#:ssa kuitenkin primitiivit muunnetaan olioiksi vasta kun se on tarpeellista (boxing), tai vastaavasti arvo voidaan muuttaa takaisin primitiivityypiksi (unboxing). Tämä siksi että malli mahdollistaa myös tehokkaan tiedonkäsittelyn ja laskennan. Olioiden käsittely on huomattavasti raskaampaa kuin primitiivimuuttujien.
Katso lisää Javan tietotyypeistä linkistä:
Ohjelman käyttämä mittakaava kannattaa sijoittaa ehkä vakioksi, tällöin ainakin ohjelman muuttaminen on helpompaa. Samoin vakioksi kannattaa sijoittaa tieto siitä, paljonko yksi km on millimetreinä (1 km = 1000 m, 1 m = 1000 mm). Ohjelmastamme tulee tällöin esimerkiksi seuraavan näköinen:
package muuttujat.matka;
import java.util.*;
/**
* Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
* @author Vesa Lappalainen
* @version 1.1 / 25.01.2007
*/
class MatkaScan {
static final double MITTAKAAVA = 200000.0;
static final double MM_KM = 1000.0*1000.0;
public static void main(String[] args) {
int matka_mm;
double matka_km;
// Ohjeet
System.out.println("Lasken 1:" + MITTAKAAVA +
" kartalta millimetreinä mitatun matkan");
System.out.println("kilometreinä luonnossa.");
// Syöttöpyyntö ja vastauksen lukeminen
System.out.print("Anna matka millimetreinä>");
Scanner in = new Scanner(System.in);
String s = in.nextLine();
if ( s.equals("") ) { System.out.println("Kiitti"); return; }
matka_mm = Integer.parseInt(s);
// Datan käsittely
matka_km = matka_mm*MITTAKAAVA/MM_KM;
// Tulostus
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
}
Lukija huomatkoon, että muuttujien ja vakioiden nimet on pyritty valitsemaan siten, ettei niitä tarvitse paljoa selitellä. Tästä huolimatta isommissa ohjelmissa on tapana kommentoida muuttujan esittelyn viereen muuttujan käyttötarkoitus. Mekin pyrimme tähän myöhemmin.
Syötteen lukeminen onnistui aika kivuttomasti java.util-paketista löytyvän Scanner-luokan avulla (luokista lisää myöhemmissä luvuissa). Kuten ohjelmoinnissa yleensäkin, niin saman asian voi toteuttaa kuitenkin monella tavalla. Java oli esimerkiksi vuosia ilman helppokäyttöistä tähän työhön soveltuvaa työkalua, jolloin lukemisen toteuttaminen oli huomattavasti monimutkaisempaa.
…
// Syöttöpyyntö ja vastauksen lukeminen
System.out.print("Anna matka millimetreinä>");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
}
if ( s == null ) return;
if ( s.equals("") ) return;
matka_mm = Integer.parseInt(s);
…
}
Kokeile ottaa vakioiden edestä pois sana static. Mitä tällöin tapahtuu ja miksi? Onko final-sanan poistamisella sama vaikutus (palauta ensin static)?
Muuttujien nimissä on sallittuja kaikki kirjaimet (myös skandit, itse asiassa kaikki Unicode-kirjaimet) sekä numerot (0-9) sekä alleviivausviiva (_). Muuttujan nimi ei kuitenkaan saa alkaa numerolla. Muuttujia saa esitellä (declare) useita samalla kertaa, kunhan muuttujien nimet erotetaan toisistaan pilkulla. Yleinen Java-tapa on että muuttujan nimi alkaa pienellä kirjaimella ja sen jälkeen jokainen muuttujan nimessä oleva alkava sana alkaa isolla kirjaimella (parasTulos).
Muuttujan nimi ei myöskään saa olla mikään vakioista (literal):
true false null
eikä mikään seuraavista avainsanoista (keyword):
abstract assert boolean break byte case catch char class const * continue default do |
double else extends final finally float for goto * if implements import instanceof |
int interface long native new package private protected public return short static |
strictfp ** super switch synchronized this throw throws transient try while void volatile |
Tähdellä (*) merkityt sanat on varattu myöhempään käyttöön.
Vaikka muuttujan nimi saakin sisältää skandeja, kannattaa niiden käytöstä pidättäytyä toistaiseksi ainakin luokkien nimissä, koska luokan nimi on samalla tiedoston nimi ja skandit tiedostojen nimissä aiheuttavat edelleen ongelmia.
Javan nimeämiskäytännöistä katso lisää linkistä:
http://www.oracle.com/technetwork/java/codeconv-138413.html
Merkitse edelliseen taulukkoon kunkin avainsanan viereen se, missä kohti monistetta ko. sana on selitetty.
Mitkä seuraavista ovat oikeita muuttujan esittelyjä ja mitkä niistä ovat hyviä:
int o;
int 9_kissaa;
int _9_kissaa;
doublepitkä_matka, pitkaMatka;
int i, j, kissojen_maara, kissojenMäärä;
int auto, pyora, juna;
Muuttujalle voidaan antaa ohjelman aikana uusia arvoja käyttäen sijoitusoperaattoria = tai ++,--,+=,-=,*= jne. -operaattoreilla.
Sijoitusmerkin = vasemmalle puolelle tulee muuttujan nimi ja oikealle puolelle mikä tahansa lauseke, joka tuottaa halutun tyyppisen tuloksen (arvon). Lausekkeessa voidaan käyttää mm. operaattoreita +,-,*,/ ja funktiokutsuja. Lausekkeen suoritusjärjestykseen voidaan vaikuttaa suluilla ( ja):
kenganKoko = 42;
pi = 3.14159265358979323846;
// usein käytetään Math-luokan PI vakiota
pi = Math.PI;
pinta_ala = leveys * pituus;
ympyranAla = pi*r*r;
hypotenuusa = vastainen_kateetti/sin(kulma);
matka_km = matka_mm*MITTAKAAVA/MM_KM;
Seuraava sijoitus on tietenkin mieletön:
r*r = 5.0; /* MIELETÖN USEIMMISSA OHJELMOINTIKIELISSA! */ ☹
Eli sijoituksessa tulee vasemmalla olla sen muistipaikan nimi, johon sijoitetaan ja oikealla arvo joka sijoitetaan.
Huom! Java-kielessä = merkki EI ole yhtäsuuruusmerkki, vaan nimenomaan sijoitusoperaattori. Yhtäsuuruusmerkki on = =.
Esittele edellisissä sijoitus -esimerkeissä tarvittavat muuttujat.
Muuttujan esittelyn (declaration) yhteydessä muuttujalle voidaan antaa myös alkuarvo (alustus, definition). Muuttujien alustaminen tietyllä arvolla on tärkeää, koska alustamattoman muuttujan arvo saattaa olla hyvinkin satunnainen. Alustamattoman muuttujan käyttö onkin jälleen eräs tyypillinen ohjelmointivirhe. Java-kääntäjä tosin ilmoittaa virheenä jos muuttujaa yritetään käyttää ennen kuin sille on annettu alkuarvo.
int kengan_koko = 32, takin_koko = 52;
double pi = Math.PI, r = 5.0;
Javassa tosiaan on tehty melkoisen vaikeaksi tietojen lukeminen päätteeltä. Monissa muissa kielissä esimerkiksi kokonaisluvun lukemista varten on huomattavasti yksinkertaisemmat rakenteet tarjolla:
scanf("%d",&matka_mm); /* C-kieli */
cin >> matka_mm; // C++ -kieli
readln(matka_mm); // Pascal-kieli
Rehellisyyden nimissä on kyllä sanottava, ettei oikeassa elämässä mikään noistakaan ole hyvä käytännön ratkaisu. Jos käyttäjä syöttää muuta kuin kokonaisluvun, on virheestä toipuminen kaikissa esitetyissä kielissä varsin työlästä.
Usein helpoin ratkaisu onkin lukea tieto ensin merkkijonoon ja sitten "kaivaa" merkkijonosta tarvittava informaatio. Tästä saadaan lisäetuna samalla se, että voidaan käsitellä myös muita kuin numeerisia arvoja eikä ohjelmasta tarvitse tehdä sellaista että jokin tietty luku tarkoittaa ohjelman lopettamista:
Javan IO-systeemi on varsin monimutkainen. Sitä ei olekaan suunniteltu aloittelevaa käyttäjää silmällä pitäen, vaan mahdollisimman laajennettavaksi. Sellaiseksi että samoilla luokilla voitaisiin hoitaa tiedon lukeminen tiedostosta ja verkosta.
// Syöttöpyyntö ja vastauksen lukeminen
System.out.print("Anna matka millimetreinä>");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
}
Alkuun tarvitsemme olion, joka pystyy lukemaan kokonaisen rivin ja tunnistaa meidän puolestamme rivin lopun. Tämä saadaan aikaiseksi yhdistämällä System-luokan olio in lukijaan (InputStreamReader) ja yhdistämällä se puskuroituun lukijaan (BufferedReader):
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
Sama voitaisiin tehdä useammallakin lauseella:
InputStreamReader instream = new InputStreamReader(System.in);
BufferedReader in = new BufferedReader(instream);
Tässä tapauksessa emme kuitenkaan tarvitse itse käyttää apuluokkaa instream, joten tyydymme yhden rivin versioon.
Saatu uusi olio in pystyy lukemaan päätteeltä tietoa. Esimerkiksi metodi readLine lukee kokonaisen rivin. Eli käyttäjä syöttää merkkejä päätteelle ja painaa Enter. Jos tulee jokin ongelma syöttövirran kanssa olio heittää poikkeuksen IOException. Tässä tapauksessa emme välitä poikkeuksista muuta kuin, että se on otettava vastaan (catch).
Nyt lohkon
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
}
jälkeen merkkijono-oliossa s on joko päätteeltä luettu arvo tai mikäli jokin meni vikaan, niin tyhjä merkkijono. Vielä on mahdollista että syöttövirta katkaistiin kesken kaiken. Windows-konsolilla tämä tapahtuu jos painetaan Ctrl-Z ja Unix/Linux-konsolilla Ctrl-C. Tällöin olioviite s ei viittaa mihinkään (sen arvo on null).
Siksipä tutkimmekin seuraavaksi mistä on kyse ja lopetamme ohjelman ilman sen suurempia mukinoita:
if ( s == null ) return;
if ( s.equals("") ) return;
Tuon voi kirjoittaa myös yhdelle riville, koska Javan ||-operaattori (tai) suorittaa totuusarvoista lauseketta vain siihen saakka kunnes totuusarvo selviää:
if ( ( s == null ) || ( s.equals("") ) ) return;
Huomattakoon että myös muoto
if ( ( s == null ) | ( s.equals("") ) ) return;
on syntaktisesti oikein, mutta tarkoittaa hieman eri asiaa. Looginen lopputulos molemmissa on ehdon lausekkeelle sama. Mutta | -operaattorilla molemmat lausekkeet suoritetaan aina. Ja tässä tapauksessa tämä olisi virhe jos s olisi null.
Kaiken edellä mainitun jälkeen meillä on käytössä oliossa s käyttäjän syöttämä merkkijono. Seuraava ongelma on saada tämä merkkijono muutettua numeroksi, jolla voidaan jopa jotakin laskeakin. Kokonaisluvun tapauksessa tämä onnistuu käyttämällä luokkaa Integer ja pyytämällä tätä selvittämään luvun arvon:
matka_mm = Integer.parseInt(s);
Mikäli käyttäjä on kiltisti syöttänyt kokonaisluvun, niin kaikki menee hienosti. Mutta jos käyttäjä antaa merkkijonon, joka on jotakin muuta kuin kokonaisluku, niin silloin parseInt heittää poikkeuksen:
bash-2.05a$ java Matka
Lasken 1:200000.0 kartalta millimetreinä mitatun matkan
kilometreinä luonnossa.
Anna matka millimetreinä>kolme
Exception in thread "main" java.lang.NumberFormatException: kolme
at java.lang.Integer.parseInt(Integer.java:414)
at java.lang.Integer.parseInt(Integer.java:463)
at Matka.main(Matka.java:32)
bash-2.05a$
Jos haluamme tästäkin siististi selvitä ja vielä ystävällisesti huomauttaa käyttäjälle, tarvitsee muunnoksen ympärille laittaa myös poikkeuskäsittely ja vielä koko lukeminen silmukkaan. Kaikkien näiden muutosten jälkeen pelkkä yhden kokonaisluvun lukeminen viekin jo likemmäksi 20 riviä ja "sotkee" muuten yksinkertaisen ohjelmamme rakenteen lähes täysin.
Tämän takia onkin ilman muuta järkevää eristää lukemiskoodi omaksi metodikseen:
/**
* Kysytään kokonaisluku. Jos annetaan ei-luku, kysytään uudelleen.
* @param kysymys näytölle tulostettava kysymys
* @param oletus arvo jota käytetään jos painetaanpelkkä Ret
* @return käyttäjän kirjoittama kokonaisluku
*/
public static int kysyInt(String kysymys, int oletus)
{
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while ( true ) {
System.out.print(kysymys+" >");
String s = "";
try {
s = in.readLine();
} catch (IOException ex) {
continue; // jatkaa silmukkaa
}
if ( ( s == null ) || ( s.equals("") ) ) return oletus;
try {
return Integer.parseInt(s);
} catch (NumberFormatException ex) {
System.out.println("Ei numero: " + s);
}
}
}
Nyt omassa ohjelmassamme voidaan korvata "koko hirveä sotku" vain yhdellä rivillä:
matka_mm = kysyInt("Anna matka millimetreinä",0);
Lisäsimme aliohjelmaamme vielä kutsuun yhden parametrin: oletus. Näin voidaan käyttäjälle antaa mahdollisuus painaa pelkästään Enter ja silti saadaan järkevä vastaus.
Lisää apumetodiin kysyInt vielä oletusarvon tulostaminen sulkuihin ennen väkäsen tulostamista. Eli tulostus olisi:
Anna matka millimeterinä (0) >
Seuraava kysymys sitten onkin että mihin tuo apumetodi kysyInt kirjoitetaan? Yksinkertainen vaihtoehto on kirjoittaa se joko ennen tai jälkeen main-metodia. Tässä ratkaisussa olisi se huono puoli, että tuo metodi voisi olla käyttökelpoinen vaikka missä ohjelmassa. Siksipä se kannattaa kirjoittaa omaan luokkaansa. Mutta mihin tämä luokka kirjoitetaan? Yleiskäyttöisyyden nimissä tuo luokka kannattaa kirjoittaa omaan tiedostoonsa.
Kirjoitammekin koodin vaikkapa tiedostoon Syotto.java:
import java.io.*;
/**
* Aliohjelmia tietojen lukemiseen päätteeltä
* @author Vesa Lappalainen
* @version 1.0/08.01.2003
*/
public class Syotto {
/**
* Kysytään kokonaisluku. Jos annetaan ei-luku, kysytään uudelleen.
* @param kysymys näytölle tulostettava kysymys
* @param oletus arvo jota käytetään jos painetaanpelkkä Ret
* @return käyttäjän kirjoittama kokonaisluku
*/
public static int kysyInt(String kysymys, int oletus)
{
...
}
public static void main(String[] args) {
int i;
i = kysyInt("Anna kokonaisluku",12);
System.out.println("Luku oli: " + i);
}
}
Olio-ohjelmoinnin - samoin kun minkä tahansa muun ohjelmoinnin - yksi tavoite on modulaarinen testaus. Eli jokainen palanen testataan - jos suinkin vain mahdollista - omana kokonaisuutenaan. Näin lopullinen ohjelma voidaan koostaa toimiviksi todetuista palikoista.
Syotto-luokkaan on myös kirjoitettu pääohjelma ja nyt testaus voidaan tehdä ensin pelkälle Syotto-luokalle ennen sen liittämistä muuhun ohjelmaan. Komentoriviltä tämä tapahtuisi nyt vaikkapa:
bash-2.05a$ javac Syotto.java
bash-2.05a$ java Syotto
Anna kokonaisluku >
Luku oli: 12
bash-2.05a$ java Syotto
Anna kokonaisluku >392
Luku oli: 392
bash-2.05a$ java Syotto
Anna kokonaisluku >kolme
Ei numero: kolme
Anna kokonaisluku >0
Luku oli: 0
bash-2.05a$
Vaikka tässä tapauksessa luokka testattiinkin lukemalla tieto päätteeltä, ei missään tapauksessa pidä tätä yleistää. Yleensä paras testiohjelma on sellainen, joka automaattisesti kokeilee testattavaa yksikköä (oliota, metodia) niillä arvoilla joilla sitä tulee kuormittaa. Hyvä testiohjelma sitten kertoo millä arvoilla yksikkö toimi kuten pitikin ja millä ei toiminut. Ihminen testaajana on kaikista testaajista huonoin, koska ihminen väsyy ja muutoksen jälkeen helposti laiskuuksissaan jättää testaamatta niillä arvoilla, jotka jo ennen muutosta oli testattu. Kuitenkin muutos saattaa tuottaa virheitä jo testattuun osaan ja siksi testi pitää aina aloittaa aivan alusta jokaisen muutoksen jälkeen.
Nyt kun uusi luokka, tai oikeastaan tässä tapauksessa uusi apumetodi, on huolellisesti testattu, se voidaan ottaa käyttöön. Nyt kun yksinkertaisuuden vuoksi emme vielä käyttäneet paketteja, niin luokka löytyy jos se on samassa hakemistossa kuin sitä käyttävä luokka. Eli ainoa muutos ohjelmakoodiin on kertoa mistä luokasta metodi kysyInt löytyy.
matka_mm = Syotto.kysyInt("Anna matka millimetreinä",0);
Tee vastaavasti luokkaan Syotto metodit kysy_double ja kysyString. Tuleeko paljon samanlaista koodia? Kannattaisiko käyttää jotakin hyväksi? Lisää luokan testiohjelmaan testi uusillekin metodeille.
Muuta matka-ohjelmaa siten, että myös mittakaava kysytään käyttäjältä. Mikäli mittakaavaan vastataan pelkkä [RET] pitää mittakaavaksi tulla 1:200000.
C-kielessä osoittimet piti opetella heti ohjelmoinnin alussa, jos halusi tehdä minkäänlaisia järkeviä aliohjelmia. C++:ssa ongelmaa voidaan kiertää viitemuuttujien (references) avulla. Javassa on myös vastaava käsite, eli kaikki Javan olio-muuttujat ovat tosiasiassa viitemuuttujia. Ne ovat kuitenkin tietyssä mielessä perinteisen C:n osoittimen ja C++:n viitteen välimuoto. Javan viitemuuttujan voi laittaa osoittamaan toistakin oliota kesken koodin. C++:n viitemuuttuja osoittaa aina samaan olioon, mihin se luotiin osoittamaan.
Tutkimme seuraavaksi Javan viitemuuttujien käyttäytymistä. Tehdään ohjelma, jossa päällisin puolin näyttäisi olevan kaksi samanlaista merkkijonoa ja kaksi samanlaista kokonaislukuoliota. Merkkijonot ovat Javassa olioita ja merkkijonomuuttujat viitteitä noihin olioihin.
/**
* Tutkitaan olioviitteiden käyttäytymistä
* @author Vesa Lappalainen
* @version 1.0, 08.01.20003
*/
class Jonotesti {
private static void tulosta(boolean b) {
if ( b ) System.out.println("Samat ovat");
else System.out.println("Erilaiset ovat");
}
public static void main(String[] args) {
String s1 = "eka";
String s2 = new String("eka");
tulosta(s1 == s2); // Erilaiset ovat
tulosta(s1.equals(s2)); // Samat ovat
int i1 = 11;
int i2 = 10 + 1;
tulosta(i1 == i2); // Samat ovat
Integer io1 = new Integer(3);
Integer io2 = new Integer(3);
tulosta(io1 == io2); // Erilaiset ovat
tulosta(io1.equals(io2)); // Samat ovat
tulosta(io1.intValue()== io2.intValue()); // Samat ovat
s2 = s1;
tulosta(s1 == s2); // Samat ovat
}
}
Koodiin on rivien viereen kommentoitu mitä mikäkin rivi tulostaisi.
Javassa on kahden tyyppisiä muuttujia, aikaisemmin lueteltuja perustyyppisiä (boolean, char, byte, short, int, long, float, double) muuttujia ja sitten oliomuuttujia. Oliomuuttujat Javassa ovat aina vain viitteitä todellisiin olioihin. Edellisessä esimerkissä muuttujat s1,s2,io1,io2 ovat olioviitteitä. Silti olioviitteistä puhekielessä käytetään helposti nimitystä olio.
Ohjelman kaikki muuttujat ovat lokaaleja muuttujia. Eli ne on esitelty lokaalisti main-metodin sisällä eivätkä "näy" näin ollen main-metodin ulkopuolelle. Tällaisille muuttujille varataan tilaa yleensä kutsupinosta. Kun kaikki muuttujat on esitelty ja alustettu, pino voisi hieman yksinkertaistettuna olla näiden lokaalien muuttujien kohdalta suurin piirtein seuraavan näköinen:
Javassa itse olioiden tila varataan muualta dynaamisen muistinhallinnan hoitamalta alueelta. Usein tätä muistia nimitetään keko- tai kasamuistiksi (heap). Kun ohjelmoija pyytää new-operaattorilla uuden olion, muistinhallinta etsii sopivan vapaan muistipaikan ja palauttaa viitteen tähän muistipaikkaan. Todellisuudessa olioviitteet ovat hieman monimutkaisempia. Asiasta voi lukea lisää sivuilta:
Asian ymmärtämiseksi meille kuitenkin riittää yllä piirretty yksinkertaistettu malli.
Vaikka molemmat viitteet s1 ja s2 osoittavat sisällöltään samanlaiseen olioon, palauttaa vertailu
( s1 == s2 ) // onko s1 sama kuin s2, => true tai false
epätoden arvon. Miksikö? Koska vertailussa verrataan muuttujien arvoja, ei niitä arvoja, joihin muuttujat viittaavat. Esimerkissä on kuviteltu että ensimmäinen "eka"-merkkijono olisi sijoittunut muistissa osoitteeseen 8010 ja toinen osoitteeseen 8040. Siis itse asiassa kysytäänkin:
( 8010 == 8040 )
mikä ei ole totta. Javan primitiivityypit sen sijaan sijoittuvat suoraan arvoina pinomuistiin (tai myöhemmin olioiden attribuuttien tapauksessa oliolle varattuun muistialueeseen). Siksi vertailu
( i1 == i2 )
on totta. Merkkijonoja vastaavasti myös kokonaislukuoliot io1 ja io2 käyttäytyvät samalla tavalla. Javassa on kokonaislukuoliot sitä varten, että primitiivityyppejä ei voi tallentaa Javan tietorakenneluokkiin. Piilottamalla primitiivityyppejä "kääreeseen", voidaan näitä "kääreitä" sitten tallentaa tietorakenteisiin.
Jos sijoitetaan "olio" toiseen "olioon", niin tosiasiassa sijoitetaan viitemuuttujien arvoja, eli sijoituksen s2 = s1 jälkeen molemmat merkkijono-olioviitteet "osoittavat" samaan olioon.
Kuva 7.2 Kaksi viitettä samaan olioon
Sijoituksen jälkeen kuvassa muistipaikkaan 8040 ei osoita (viittaa) enää kukaan ja tuo muistipaikka muuttuu "roskaksi". Kun Javan roskienkeruu (garbage-collection, gc) seuraavan kerran käynnistyy, "vapautetaan" tällaiset käyttämättömät muistialueet. Tätä automaattista roskienkeruuta on pidetty yhtenä syynä Javan menestykseen. Samalla täytyy kuitenkin varoittaa että muisti on vain yksi resurssi ja Javassa on automatiikka vain muistin hoitamiseksi. Muut resurssit kuten esimerkiksi tiedostot ja tietokannat pitää edelleen hoitaa samalla huolellisuudella kuin muissakin kielissä. Jopa C++:aa huolellisemmin, koska Javassa ei ole C++:n tapaan automaattisia olioita.
Javan viitemuuttuja voidaan siis laittaa "osoittamaan" milloin tahansa toista oliota. Tämä tapahtuu sijoittamalla viitemuuttujaan joko olemassa olevan olion viite
s2 = s1; // laitetaan s2 viittaamaan samaan paikkaan kuin s1
tai luomalla uusi olio,
String s2 = new String("eka"); // laitetaan s2 viittaamaan uuteen olioon
jolloin new-operaattorin palauttama viite sijoitetaan. Käytännössä Javan viitteet ovat siis oikeastaan osoittimia. Javan viitteillä ei kuitenkaan voi "edetä" C++:n osoittimien tapaan (esim. s1++). Tämä osoitinaritmetiikan puute on toinen Javan hyväksi puoleksi usein mainostettu ominaisuus (tosin ääneen tämä sanotaan "Javassa ei ole osoittimia", lisäksi on tosin totta että Javassa ei todellakaan ole viitteitä tai osoittimia primitiivityyppeihin).
Viitemuuttujan arvo voi olla myös null. Tämä tarkoittaa sitä, ettei oliomuuttuja viittaa mihinkään todelliseen olioon ja tällaista viitemuuttujaa ei saa käyttää ennen kuin siihen on sijoitettu jonkin todellisen olion viite. Yksi Java-ohjelmien yleisimmistä virheistä onkin "null pointer reference" kun ohjelmoija ei ole huolellinen viitteiden kanssa.
Hyvin usein pitää siis testata
if ( s1 != null ) { // nyt voi käyttää s1 viitettä huoletta
Eräs ohjelmoinnin tärkeimmistä rakenteista on aliohjelma. C-kielessä kaikkia erityyppisiä aliohjelmia nimitetään funktioiksi; joissakin muissa kielissä eri tyyppejä erotetaan eri nimille. Javassa oikeastaan aliohjelmia nimitetään metodeiksi. Kuitenkin kaikkia tähän asti käytettyjä metodeja voidaan suhteellisen hyvällä omallatunnolla nimittää aliohjelmiksi tai C:n tapaan funktioiksi. Aikaisempien esimerkkien metodit nimittäin kaikki ovat olleet static-määreellä varustettuja metodeja ja tällaisten metodien virallinen nimi on luokkametodi. Lisäksi kun esimerkkiemme luokkametodit eivät ole koskeneet mihinkään luokan ominaisuuteen, ei metodeilla ole oikeastaan ollut luokan kanssa muuta tekemistä kuin se, että ne ovat olleet luokan sisällä. Tällöin niitä voi aivan hyvin kutsua aliohjelmiksi. Luokan merkitys on toistaiseksi ollut vain pitää joukkoa metodeja omassa "nimiavaruudessaan". C++:ssa vastaava rakenne hoidetaankin yleensä käyttäen nimiavaruuksia.
Aliohjelmaa käytetään seuraavissa tapauksissa:
1. Haluttu tehtävä on valmiiksi jonkun toisen kirjoittamana aliohjelmana esimerkiksi standardikirjastossa (y=Math.sin(x))
2. Haluttua tehtävää suoritetaan usein liki samanlaisena joko samassa ohjelmassa tai jossain toisessa ohjelmassa.
3. Haluttu tehtävä muodostaa selvän kokonaisuuden, jonka toiminta on ilmaistavissa muutamalla sanalla riittävän selkeästi (= aliohjelman nimi).
4. Haluttua tehtävää ei juuri sillä hetkellä osata tai viitsitä ohjelmoida. Tällöin määritellään millainen aliohjelma tarvitaan ja kirjoitetaan tarvittavaan kohtaan pelkkä aliohjelman kutsu. Itse aliohjelma voidaan aluksi toteuttaa varsin yksinkertaisena ja korjata myöhemmin tekemään sen varsinainen tehtävä.
5. Rakenne saadaan selkeämmän näköiseksi.
Aliohjelma esitellään vastaavasti kuin "pääohjelmakin", eli Javan main-metodi. Esimerkiksi satunnaisen matkaajan mittakaavaohjelmassa voisimme kirjoittaa käyttöohjeet omaksi aliohjelmakseen:
import java.io.*;
/**
* Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
class Matka_a1 {
static final double MITTAKAAVA = 200000.0;
static final double MM_KM = 1000.0*1000.0;
/**
* Tulostaa ohjelman käyttöohjeet
*/
private static void ohjeet() {
System.out.println("Lasken 1:" + MITTAKAAVA +
" kartalta millimetreinä mitatun matkan");
System.out.println("kilometreinä luonnossa.");
}
public static void main(String[] args) {
int matka_mm;
double matka_km;
ohjeet();
matka_mm = Syotto.kysyInt("Anna matka millimetreinä",0);
// Datan käsittely
matka_km = matka_mm*MITTAKAAVA/MM_KM;
// Tulostus
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
}
Tämän etu on siinä, että saimme pääohjelman selkeämmän näköiseksi.
Voisimme jatkaa pääohjelman selkeyttämistä. Tavoite voisi olla aluksi vaikkapa kirjoittaa pääohjelma muotoon:
ohjeet();
matka_mm = Syotto.kysyInt("Anna matka millimetreinä",0);
matka_km = mittakaava_muunnos(matka_mm);
tulosta_matka(matka_km);
Tällainen pääohjelma tuskin tarvitsisi paljoakaan kommentteja.
Edellä on käytetty kolmen eri tyypin aliohjelmia (funktioita)
1.ohjeet();- parametriton aliohjelma
2. mittakaava_muunnos(matka_mm); - funktio, joka palauttaa tuloksen nimessään
3. tulosta_matka(matka_km); - aliohjelma, jolle vain viedään arvo, mutta mitään arvoa ei palauteta
Valmis ohjelma, jossa myös aliohjelmat on esitelty, näyttäisi seuraavalta (rivien numerointi on myöhemmin esitettävää pöytätestiä varten):
* Ohjelmalla lasketaan mittakaavamuunnoksia 1:200000 kartalta
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
public class Matka_a3 {
static final double MITTAKAAVA = 200000.0;
static final double MM_KM = 1000.0*1000.0;
/**
* Tulostaa ohjelman käyttöohjeet
*/
private static void ohjeet() {
System.out.println("Lasken 1:" + MITTAKAAVA +
" kartalta millimetreinä mitatun matkan");
System.out.println("kilometreinä luonnossa.");
}
/**
* Muuttaa mm mittakaavan mukaisesti kilometreiksi
* @param matka_mm muutettavat millit
* @return mittakavan mukaiset kilometrit
*/
private static double mittakaava_muunnos(int matka_mm)
{
return matka_mm*MITTAKAAVA/MM_KM;
}
/**
* Tulostaa matkan kilometreinä
* @param matka_km tulostettava kilometrimäärä
*/
private static void tulosta_matka(double matka_km)
{
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
/**
* Varsinainen pääohjelma matka kysymiseksi ja laskemiseksi
* @param args ei käyttöä
*/
public static void main(String[] args) {
int matka_mm;
double matka_km;
ohjeet();
matka_mm = Syotto.kysyInt("Anna matka millimetreinä",0);
matka_km = mittakaava_muunnos(matka_mm);
tulosta_matka(matka_km);
}
}
Edellä olevasta huomataan, että aliohjelmat jotka eivät palauta mitään arvoa nimessään, esitellään void-tyyppisiksi.
mittakaava_muunnos on reaaliluvun palauttava funktio, joten se esitellään double -tyyppiseksi.
Seuraavaksi pöytätestaamme ohjelmamme toiminnan:
|
main |
mi..muunnos |
tulosta |
|
||
lause |
matka_mm |
matka_km |
matka_mm |
tulos |
matka_km |
tulostus |
46 ohjeet() |
?? |
?? |
|
|
|
|
13-17 System |
|
|
|
|
|
Lasken 1:200000 |
47 matka_mm= |
352 |
|
|
|
|
Anna matka ... |
48 matka_km |
|
|
352 |
|
|
|
26 return |
|
|
|
70.4 |
|
|
48 matka_km |
|
70.4 |
|
|
|
|
49 tulosta |
|
|
|
|
70.4 |
|
33-36 System |
|
|
|
|
|
Matka on luo.. |
50 } |
|
|
|
|
|
|
Emme enää käyneet läpi sitä, mitä Syotto.kysyInt tekee, koska se oli testattu erikseen ja sen jälkeen aliohjelma voidaan käsittää "valmiina kieleen kuuluvana käskynä".
Mikäli kukin "omatekoinen" aliohjelmakin olisi testattu erikseen, riittäisi meille pelkkä pääohjelman testi:
|
main |
|
|
lause |
matka_mm |
matka_km |
tulostus |
46 ohjeet() |
?? |
?? |
Lasken 1:200000 |
47 matka_mm= |
352 |
|
Anna matka .. |
48 matka_km |
|
70.4 |
|
49 tulosta |
|
|
Matka on luo.. |
50 } |
|
|
|
Tämä on testaustapa, johon tulisi pyrkiä. Isossa ohjelmassa ei ole enää mitään järkeä testata sitä jokainen aliohjelma kerrallaan. Koodiin liitettyjen aliohjelmien tulee olla testattuja kukin erillisinä ja lopullinen testi on vain viimeisimmän mallin mukainen!
Huomattakoon, ettei parametrien nimillä aliohjelmien esittelyissä ja kutsuissa ole mitään tekemistä keskenään. Nimi voi olla joko sama tai eri nimi. Parametrien idea on nimenomaan siinä, että samaa aliohjelmaa voidaan kutsua eri muuttujien tai mahdollisesti vakioiden tai lausekkeiden arvoilla. Esimerkiksi nyt kirjoitettua tulosta_matka aliohjelmaa voitaisiin kutsua myös seuraavasti:
/**
* Esimerkkejä kutsua aliohjelmaa eri tavoin
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
public class Matka_a4 {
/**
* Tulostaa matkan kilometreinä
* @param matka_km tulostettava kilometrimäärä
*/
private static void tulosta_matka(double matka_km)
{
System.out.println("Matka on luonnossa " + matka_km + " km.");
}
/**
* Varsinainen pääohjelma matka kysymiseksi ja laskemiseksi
* @param args ei käyttöä
*/
public static void main(String[] args) {
double d = 50.2;
tulosta_matka(d); // eri niminen muuttuja
tulosta_matka(30.7); // vakio
tulosta_matka(d+20.8); // lauseke
tulosta_matka(2*d-30.0); // lauseke
}
}
Edellä aliohjelman kutsut voidaan tulkita seuraaviksi sijoituksiksi aliohjelman tulosta_matka lokaaliin parametrimuuttujaan matka_km:
matka_km = d;
matka_km = 30.7;
matka_km = d+20.8;
matka_km = 2*d-30.0
Aliohjelma jouduttiin edellä vielä kirjoittamaan uudestaan (käytännössä kopioimaan edellisestä ohjelmasta), mutta myöhemmin opimme miten aliohjelmia voidaan kirjastoida standardikirjastojen tapaan (ks. moduuleihin jako), jolloin kerran kirjoitettua aliohjelmaa ei enää koskaan tarvitse kirjoittaa uudestaan (eikä kopioida).
Funktion arvo palautetaan return -lauseessa. Jokaisessa ei-void -tyyppiseksi esitellyssä funktiossa tulee olla vähintään yksi return -lause. void-tyyppisessäkin voi olla return-lause. Tarvittaessa return-lauseita voi olla useampiakin:
public static int suurempi(int a, int b)
{
if ( a >= b ) return a;
return b;
}
Kun return -lause tulee vastaan, lopetetaan HETI funktion suoritus. Tällöin myöhemmin olevilla lauseilla ei ole mitään merkitystä. Näin ollen useat return-lauseet ovat mielekkäitä vain ehdollisissa rakenteissa. Siis seuraavassa ei olisi mitään mieltä:
public static int hopo(int a) ☹
{
int i;
return 5; /* Palauttaa aina 5!!! */
i = 3 + a;
return i+2;
}
return-lausetta ei saa sotkea siihen, että parametrina vietyjä olioita voidaan pyytää muuttamaan sisältöään funktion aikana:
/**
* Esimerkki funktiosta joka muuttaa myös parametriään
* @author Vesa Lappalainen
* @version 1.0 / 05.01.2003
*/
public class FunJaOlio {
private static int pituus_ja_muuta(StringBuffer s)
{
int pit = s.length();
s.delete(0,pit).append("toka"); // pääohjelman jono muuttuu nyt
return pit;
}
public static void main(String[] args) {
int i; StringBuffer jono = new StringBuffer("eka");
i = pituus_ja_muuta(jono);
System.out.println("i=" + i + ", jono="+jono); // tulostaa: i=3, jono=toka
}
}
Edellä ei kutsusta näe millään tavalla, että kutsun jälkeen jono on muuttunut. Yhtenä Java-kielen miinuksena voidaankin pitää sitä, että siitä puuttuu C++-kielessä oleva mekanismi suojata oliot muutoksilta aliohjelman suorituksen aikana (const).
Näin paljon jääkin ohjelmoijan vastuulle, eli ohjelmoijan pitää nimetä aliohjelmat siten, että niiden nimi jo paljastaa jos jotakin parametria muutetaan ohjelman suorituksen aikana. Ja sitten aliohjelmat on tehtävä huolellisesti, etteivät ne todellakaan muuta kutsuparametrejaan jollei se ole aliohjelmien tarkoitus.
Mitä pääohjelma FunJaOlio tulostaisi jos aliohjelma olisikin ollut:
private static int pituus_ja_muuta(StringBuffer s)
{
s.append("toka");
return s.length();
}
Kirjoita edellisestä tehtävästä versio jossa muutat kaikki StringBuffer => String ja korvaat append-metodin concat-metodilla. Mitä tulostuu?
Koska funktio-aliohjelma palauttaa valmiiksi arvon, voitaisiin Matka_a3.java:n pääohjelma kirjoittaa myös muodossa:
public static void main(String[] args) {
double matka_mm;
ohjeet();
matka_mm = Syotto.kysyInt("Anna matka millimetreinä",0);
tulosta_matka(mittakaava_muunnos(matka_mm));
}
Funktioita käytetään silloin, kun aliohjelman tehtävänä on palauttaa vain yksi täsmällinen arvo. Math-luokan funktioita ovat:
abs, acos, asin, atan, atan2, ceil, cos, exp, floor, IEEEremainder, log, max, min, pow, random, rint, round, sin, sqrt, tan, toDegrees, toRadians
Funktioita käytetään, kuten matematiikassa on totuttu:
double alpha = 1.32, a = 4, b=3;
double c = Math.sqrt(a*a+b*b) + Math.asin((Math.sin(alpha)+0.2)/2.0);
kysy_matka ja kysy_mittakaava voitaisiin kirjoittaa myös funktioiksi, ja tällöin niitä voitaisiin kutsua esim. seuraavasti:
matka_km = kysy_matka()*kysy_mittakaava()/MM_KM;
Vaarana olisi kuitenkin se, ettei voida olla aivan varmoja kumpi funktiosta kysy_matka vai kysy_mittakaava suoritettaisiin ensin ja tämä saattaisi aiheuttaa joissakin tilanteissa yllätyksiä.
Tämän vuoksi pyrimmekin kirjoittamaan funktioiksi vain sellaiset aliohjelmat, jotka palauttavat täsmälleen yhden arvon ja jotka eivät ota muuta informaatiota ympäristöstä kuin sen mitä niille parametrina välitetään. Eli tavoitteena on se, että funktioiden kutsuminen lausekkeen osana olisi turvallista. Tämä ei valitettavasti ole aina Javassa mahdollista, koska Javan aliohjelmakutsuista puuttuu muissa kielissä oleva muuttujaparametrin välitys (Pascal: var, C: osoitin *, C++ referenssi &).
Muissa kielissä aliohjelmat kirjoitamme siten, että arvot palautetaan osoitteen avulla. Hyvin yleinen C-tapa on kuitenkin palauttaa tällaisenkin aliohjelman onnistumista kuvaava arvo funktion nimessä (vrt. esim. scanf C-kielessä).
Katso SDK:n dokumenteista kunkin Math-luokan funktion parametrien määrä ja tyyppi sekä se mitä kukin todella tekee.
Kirjoita edellä mainitut kysy_matka ja kysy_mittakaava nimessään arvon palauttavina funktioina.
Kirjoita funktiot, jotka palauttavat r-säteisen ympyrän pinta-alan ja r-säteisen pallon tilavuuden.
Kirjoita pääohjelma, jossa pinta-ala ja tilavuus -funktiot testataan.
Jatka edellä mainittua ketjuttamista siten, että koko pääohjelma on vain yksi lauseke (ohjeet-kutsu saa olla oma rivinsä jos haluat). Tosin tämä on C-hakkerismia eikä mikään tavoite helposti luettavalta ohjelmalta. Itse asiassa hyvä kääntäjä tekee automaattisesti tämän kaltaista optimointia (mitä muka voitiin säästää?).
Valmiin aliohjelman kutsuminen on helppoa: etsitään aliohjelman esittely ja kirjoitetaan kutsu, jossa on vastaavan tyyppiset parametrit vastaavissa paikoissa.
Esimerkiksi funktion Math.sin esittely saattaa olla muotoa:
sin
public static double sin(double a)
Returns the trigonometric sine of an angle. Special cases:
- If the argument is NaN or an infinity, then the result is NaN.
- If the argument is zero, then the result is a zero with the same
sign as the argument.
A result must be within 1 ulp of the correctly rounded result.
Results must be semi-monotonic.
Parameters:
a
- an angle, in radians.
Returns:
the sine of the argument.
Funktion tyyppi on double ja sille viedään double tyyppinen parametri. Funktio ei muuta mitään parametrilistassa esiteltyä parametriaan (mistä tietää?). Siis funktiota ei ole mitään mieltä kutsua muuten kuin sijoittamalla sen palauttama arvo johonkin muuttujaan tai käyttämällä funktiota osana jotakin lauseketta. x:ää vastaava parametri voi olla mikä tahansa double tyyppisen arvon palauttava lauseke (tietysti mielellään sellainen joka tarkoittaa kulmaa radiaaneissa):
doublekulman_sini,a,b,x,y;
...
kulman_sini = Math.sin(x);
...
y = Math.sin(x/2) + Math.cos(a/3);
...
Funktiota voitaisiin tietysti kutsua myös muodossa:
double x = 3.1;
Math.sin(x); ☹
mutta kutsussa olisi yhtä vähän järkeä kuin kutsussa
double x=3.1;
x + 3.0; ☹
tai jopa
3.0; ☹
Mihin lausekkeiden arvot menisivät? Eivät minnekään! Tosin Javassa kääntäjäkään ei päästä lävitse kahta viimeksi mainittua vaihtoehtoa, eli pelkää vakioita tai muuttujia sisältävää lauseketta, jota ei sijoiteta mihinkään.
Usein aloittelijan näkee yrittävän kutsua muodoissa
y = double Math.sin(double a); ☹
y = Math.sin(double a)
mutta näissäkään ei ole järkeä, koska parametrin tyypin esittely kuuluu vain aliohjelman otsikon puoleiseen päähän, ei kutsupäähän.
Yksi yleinen aloittelijan virhe on tehdä paljon aliohjelmia, jotka tulostavat. Pikemminkin pitää toimia päinvastoin, eli aliohjelmien on tehtävä oma työnsä ja annettava sitten tulokset muille tulostettavaksi.
Jos halutaan että aliohjelma kuitenkin tulostaa, niin useimmiten sille kannattaa siinä tapauksessa viedä parametrina tietovirta johon tulostetaan. Samoin tulostavien aliohjelmien nimessä kannattaa tavalla tai toisella ilmaista että aliohjelma aikoo tulostaa. Palaamme tähän esimerkin kanssa seuraavissa luvuissa. Alla kuitenkin pikainen esimerkki:
import java.io.*;
/**
* Testataan tietovirran viemistä parametrina
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Tulostustesti {
private static void tulosta(OutputStream os,int h, int m) {
PrintStream out = new PrintStream(os);
out.println("" + h + ":" + m);
}
public static void main(String[] args) throws FileNotFoundException,
IOException {
int h=12, m=15;
// Tulostaminen näyttöön
tulosta(System.out,h,m);
// Tulostaminen tiedostoon
FileOutputStream f = new FileOutputStream("Tulostustesti.txt");
try {
tulosta(f,h,m);
} finally {
f.close();
}
// Tulostaminen tavutietovirtaan, joka voidaan muuttaa sitten merkkijonoksi
ByteArrayOutputStream bs = new ByteArrayOutputStream();
tulosta(bs,h,m);
String s = bs.toString();
System.out.println(s); // Lisätty, jotta nähdään tulos.
}
}
Tietovirtojen ansiosta samoja aliohjelmia voidaan käyttää myös järjestelmässä, jossa varsinaista konsolitulostusta ei voi tehdä. Tällaisia ovat mm. graafiset käyttöliittymät.
Myös kerho-ohjelman on tarkoitus tulostaa jäsenien tietoja. Kun ongelmaa miettii tarkemmin, niin ei ole juuri mitään mieltä tehdä tulostusikkunaa pelkästään yhteen tarkoitukseen. Kannattaa siis luoda tietovirtojen avulla yleiskäyttöinen ikkuna.
/**
* Tulostaa kerhon tiedot TulostusDialogiin
*/
protected void tulosta() {
TulostusDialog dialog = new TulostusDialog();
kerhoswing.tulostaValitut(dialog.getTextArea());
dialog.setVisible(true);
}
Ensimmäisellä rivillä luodaan uusi ikkuna, mutta vasta viimeisellä asetetaan se näkyväksi. Keskimmäinen rivi kutsutaan tulostaValitut()-metodia, jolle viedään parametrina juuri luodun ikkunan tekstikenttä.
Seuraavassa luodaan oma tietovirta TextAreaOutputStream.java, jolle viedään edelleen parametrina tulostusikkunan tekstikenttä. Nyt viimeisellä rivillä tulostaa kenttään, samalla tavoin kun tulostaisimme esimerkiksi System.out tietovirtaan.
/**
* Tulostaa listassa olevat jäsenet tekstialueeseen
* @param text alue johon tulostetaan
*/
public void tulostaValitut(JTextArea text) {
PrintStream os = TextAreaOutputStream.getTextPrintStream(text);
os.println("Tähän tulostuisi jäsenten tiedot");
}
Toisaalta viimeisin vaihe on ehkä hieman turha. Nykyaikaisessa käyttöliittymäohjelmoinnissa tietovirrat eivät useinkaan ole käytännöllisin tapa siirtää tekstiä. Nyt kuitenkin Kerhon vanha versio on toiminut komentorivin päällä, joten siellä löytyy valmiit metodit tietovirtojen käyttöön. Olisi myös ollut mahdollista käyttää tekstikenttää suoraan.
dialog.getTextArea.setText("Tähän tulostuisi jäsenten tiedot");
Itse tulostusikkunaan riittää yksinkertainen rakenne yhdellä tekstikentällä ja muutamalla napilla. Tekstikenttä kannattaa jättää käyttäjän editoitavaksi.
Kuva 7.3 Tulostusikkuna
Kuva 7.4 Tulostusikkunan asemointi Swing-kirjastolla
Nyt rakennettuna on yleiskäyttöinen tulostusikkuna. Jäljellä on vielä painikkeiden toiminnallisuuden ohjelmointi. Ok-napin painaminen sulkee ikkunan.
okButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
dispose();
}
});
Itse tulostamiseen Swing-kirjasto tarjoaa helposti käytettävät työkalut.
buttonTulosta.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
try {
getTextArea().print();
} catch (PrinterException e) {
// e.printStackTrace();
}
}
});
Suuressa osassa edellisissä esimerkeissämme meillä on ollut vain 0 tai yksi parametria välitettävänä aliohjelmaan. Käytännössä usein tarvitsemme useampia parametreja. Esimerkiksi edellisessä paamenu-aliohjelmassa pitäisi oikeastaan tulostaa myös kerhon nimi.
Ottakaamme esimerkiksi mittakaava_muunnos -funktio. Mikäli ohjelma haluttaisiin muuttaa siten, että myös mittakaavaa olisi mahdollista muuttaa, pitäisi myös mittakaava voida välittää muunnos-aliohjelmalle parametrina. Kutsussa tämä voisi näyttää esim. tältä:
matka_km = mittakaava_muunnos(32,10000.0);
Vastaavasti funktio-esittelyssä täytyisi olla kaksi parametria:
private static doublemittakaava_muunnos(int matka_mm, double mittakaava)
{
return matka_mm*mittakaava/MM_KM;
}
Kun kutsu suoritetaan, välitetään aliohjelmalle parametrit siinä järjestyksessä, missä ne on esitelty. Voitaisiin siis kuvitella aliohjelmakutsun aiheuttavan sijoitukset aliohjelman parametrimuuttujiin (tosin sijoitusjärjestystä ei taata, eli ei tiedetä kumpi sijoitus suoritetaan ensin):
mittakaava = 10000.0;
matka_mm = 32;
Jos kutsu on muotoa
matka_km = mittakaava_muunnos(matka_mm, MITTAKAAVA);
kuvitellaan sijoitukset:
matka_mm = matka_mm; // Pääohjelman muuttuja matka_mm sijoitetaan aliohjelman
// vastinpaikassa olevaan muuttujaan
mittakaava = MITTAKAAVA; // Ohjelman vakio toiseeen aliohjelman muuttujaan
Siis vaikka kutsussa ja esittelyssä esiintyykin sama nimi, ei nimien samuudella ole muuta tekemistä kuin mahdollisesti se, että turha on väkisinkään keksiä lyhennettyjä huonoja nimiä, jos kerran on hyvä nimi keksitty kuvaamaan jotakin asiaa.
Parametreista osa, ei yhtään tai kaikki voivat olla myös oliota.
Huom! Vaikka kaikilla aliohjelman parametreille olisikin sama tyyppi, täytyy jokaisen parametrin tyyppi mainita silti erikseen:
public static double nelion_ala(double korkeus, double leveys)
Lisää Paamenu.java:n aliohjelmaan paamenu parametriksi myös kerhon nimi.
Kirjoita funktio root_1(a,b,c), joka palauttaa jomman kumman toisen asteen yhtälön ax2+bx+c=0 juurista (oletetaan tällä kertaa, että a<>0 ja D = b2-4ac >= 0. Miksi oletetaan?).
Kirjoita funktio root_1 joka palauttaa toisen asteen polynomin P(x) = ax2+bx+c arvon (muista viedä parametrina myös a,b ja c).
Kirjoita pääohjelma, jolla voidaan testata root_1 - aliohjelma (jotenkin myös se, että tulos toteuttaa yhtälön).
Aloitteleva ohjelmoija sotkee yleensä aliohjelmakutsua tehdessään kutsuvan ja kutsuttavan parametrien nimiä keskenään. Parametrien nimillä ei ole Java-kielessä mitään merkitystä. Aliohjelmakutsussa ratkaisee vain parametrien paikka. Kunkin kutsussa oleva arvo "sijoitetaan" vastinparametrilleen kun aliohjelmaan mennään. Seuraava esimerkki havainnollistaa tätä:
/**
* Esimerkki miten parametrin paikka ratkaisee, ei nimi
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Parampaikka {
private static void ali(int a, int b, int c) {
System.out.println("a=" + a + " b=" + b + " c=" + c);
}
public static void main(String[] args) {
int a=1,b=2,c=3;
ali(a,b,c); // Tulostaa: a=1 b=2 c=3
ali(b,a,c); // Tulostaa: a=2 b=1 c=3
ali(c,a,b); // Tulostaa: a=3 b=1 c=2
ali(10,c,c); // Tulostaa: a=10 b=3 c=3
}
}
On olemassa myös kieliä, joissa parametrit ovat nimettyjä. Tällainen on tarpeen jos parametreja on niin paljon, ettei niitä kaikkia välitetä joka kutsussa. Esimerkki tällaisesta kielestä on vaikkapa Microsoft Visual Basic for Application (VBA).
Javassa - samoin kuin monessa muussakin nykykielessä - on mahdollista kuormittaa (overload) aliohjelman nimeä. Eli samassa näkyvyysalueessa saa esiintyä samannimisiä aliohjelmia kunhan niiden parametrit eroavat toisistaan määrältään ja/tai tyypiltään.
private static doublemittakaava_muunnos(int matka_mm, double mittakaava)
{
return matka_mm*mittakaava/MM_KM;
}
private static double mittakaava_muunnos(int matka_mm)
{
return matka_mm*MITTAKAAVA/MM_KM;
}
...
matka_km = mittakaava_muunnos(20);
...
matka_km = mittakaava_muunnos(32,20000.0);
Kääntäjä pystyy kutsussa päättelemään oikean aliohjelman parametrien määrän ja tyypin mukaan.
Kirjoita yhden parametrin mittakaava_muunnos siten, että se kutsuu kahden parametrin mittakaava_muunnosta.
Kukin aliohjelma muodostaa oman kokonaisuutensa. Edellä olleissa esimerkeissä aliohjelmat eivät tiedä ulkomaailmasta mitään muuta, kuin sen, mitä niille tuodaan parametreina kutsun yhteydessä.
Vastaavasti ulkomaailma ei tiedä mitään aliohjelman omista muuttujista. Näitä aliohjelman lokaaleja muuttujia on esim. seuraavassa:
private static int pituus_ja_muuta(StringBuffer s)
{
int pit = s.length();
s.delete(0,pit).append("toka"); // pääohjelman jono muuttuu nyt
return pit;
}
s - aliohjelman parametrimuuttuja (tässä tapauksessa viite merkkijonoon).
pit - aliohjelman lokaali apumuuttuja pituuden säilyttämiseksi
Yleensäkin Java-kielessä lausesulut { ja } muodostavat lohkon, jonka ulkopuolelle mikään lohkon sisällä määritelty muuttuja tai tyyppimääritys ei näy. Näkyvyysalueesta käytetään englanninkielisessä kirjallisuudessa nimitystä scope. Lokaaleilla muuttujilla voi olla vastaava nimi, joka on jo aiemmin esiintynyt jossakin toisessa yhteydessä. Lohkon sisällä käytetään sitä määrittelyä, joka esiintyy lohkossa:
/** ☹
* Testataan Javan muuttujien lokaalisuutta
* @author Vesa Lappalainen
* @version 1.0, 13.01.2003
*/
public class Lokaali {
static private char ch='A';
static private void ali() {
double ch = 4.5;
System.out.println("Reaaliluku " + ch);
}
public static void main(String[] args) {
System.out.println("Kirjain " + ch);
{
int ch = 5;
System.out.println("Kokonaisluku " + ch);
ali();
}
System.out.println("Kirjain " + ch);
}
}
Ohjelma tulostaa:
Kirjain A
Kokonaisluku 5
Reaaliluku 4.5
Kirjain A
Saman tunnuksen käyttäminen eri tarkoituksissa on kuitenkin kaikkea muuta kuin hyvää ohjelmointia.
Korjaa edellinen ohjelma siten, että kullakin erityyppisellä muuttujalla on eri nimi.
Ainoa Java-kielen tuntema parametrinvälitysmekanismi on parametrien välittäminen arvoina. Tämä tarkoittaa sitä, että aliohjelma saa käyttöönsä vain (luku)arvoja, ei muuta. Olkoon meillä esimerkiksi ongelmana tehdä aliohjelma, jolle viedään parametreina tunnit ja minuutit sekä niihin lisättävä minuuttimäärä. Jos ensimmäinen yritys olisi seuraava:
/** ☹
* Yritetään lisätä metodissa parametrien arvoja
* @author Vesa Lappalainen
* @version 1.0, 18.01.2003
*/
public class Aikalisa {
private static void lisaa(int h, int m, int lisa_min) {
int yht_min = h*60 + m + lisa_min;
h = yht_min / 60;
m = yht_min % 60;
}
private static void tulosta(int h, int m) {
System.out.println("" + h + ":" + m);
}
public static void main(String[] args) {
int h=12,m=15;
tulosta(h,m);
lisaa(h,m,55);
tulosta(h,m);
}
}
Tämä ei tietenkään toimisi! Hyvä (C-) - kääntäjä jopa varoittaisi että:
Warn : aikalisa.cpp(8,2):'m' is assigned a value that is never used
Warn : aikalisa.cpp(7,2):'h' is assigned a value that is never used
Mutta miksi ohjelma ei toimisi? Seuraavan selityksen voi ehkä ohittaa ensimmäisellä lukukerralla. Tutkitaanpa tarkemmin mitä aliohjelmakutsussa oikein tapahtuu. Oikaisemme seuraavassa hieman joissakin kohdissa liian tekniikan kiertämiseksi, mutta emme kovin paljoa. Esimerkki on kirjoitettu vastaavasta C++-ohjelmasta. Javassa periaatteessa tapahtuu samalla tavalla. Katsotaanpa ensin miten kääntäjä kääntäisi aliohjelmakutsun (Borland C++ 5.1, 32-bittinen käännös, rekisterimuuttujat kielletty jottei optimointi tekisi konekielisestä ohjelmasta liian monimutkaista):
lisaa(h,m,55);
muistiosoite assembler selitys
-------------------------------------------------------------------------
004010F9 push 0x37 pinoon 55
004010FB push [ebp-0x08] pinoon m:n arvo
004010FE push [ebp-0x04] pinoon h:n arvo
00401101 call lisaa mennään aliohjelmaan lisää
00401106 add esp,0x0c poistetaan pinosta 12 tavua (3 x int)
Kun saavutaan aliohjelmaan lisaa, on pino siis seuraavan näköinen:
muistiosoite sisältö selitys
------------------------------------------------------------------------
064FDEC 00401106 <-ESP paluuosoite kun aliohjelma on suoritettu
064FDF0 0000000C h:n arvo, eli 12
064FDF4 0000000F m:n arvo, eli 15
064FDF8 00000037 lisa_min, eli 55
Eli aliohjelmaan saavuttaessa aliohjelmalla on käytössään vain arvot 12,15 ja 55. Näitä se käyttää tässä järjestyksessä omien parametriensa arvoina, eli m,h,lisa_min.
Esimerkiksi Pascal ja C/C++ -kielissä olisi tarjota tähän sellainen ratkaisu, että aliohjelman parametrit olisivatkin viitteitä (tai osoittimia) kutsuvan ohjelman muuttujiin ja niihin tehty muutos muuttaisi suoraan kutsuvan ohjelman muuttujia. Javassa tämä on mahdollista vain olioille, koska oliot välitettiin viitteinä.
C++: void lisaa(int &h, int &m, int lisamin); kutsu: lisaa(h,m,55);
Pascal: procedure lisaa(var h,m:integer; lisamin:integer); kutsu: lisaa(h,m,55);
C: void lisaa(int *h, int *m, int lisamin); kutsu: lisaa(&h,&m,55)
Kokeilepa laittaa ajaksi esim. 12:05. Mitä tulostuu? Miten vian voisi korjata?
Kirjoita aliohjelma lue_kello, joka kysyy ja lukee arvon kellonajalle, syöttö muodossa 12:15.
Uuden aliohjelmien kirjoittaminen kannattaa aina aloittaa aliohjelmakutsun kirjoittamisesta vähintään testiohjelmaan. Näin voidaan suunnitella mitä parametreja ja missä järjestyksessä aliohjelmalle viedään. Näinhän teimme mittakaava-ohjelmassakin.
Muuttujat voidaan esitellä myös luokan kaikissa metodeissa näkyväksi. Mikäli muuttujat esitellään kaikkien ohjelman aliohjelmalausesulkujen ulkopuolella, näkyvät muuttujat koko luokan alueella. Jos muuttujat vielä varustetaan vaikkapa public määreellä, niin luokan ulkopuolisetkin luokat voivat niitä käyttää. Tällaista on syytä välttää. Seuraava ohjelma on kaikkea muuta kuin hyvän ohjelmointitavan mukainen, mutta pöytätestaamme sen siitä huolimatta:
/**
* Mitä ohjelma tulostaa??
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Alisotku {
/**
* Palauttaa merkkijonon kokonaislukuna
* @param s muutettava merkkijono
* @return merkkijonosta saatu kokonaisluku
*/
private static int i(StringBuffer s) {
return Integer.parseInt(s.toString());
}
/**
* Sijoittaa kokonaisluvun arvon merkkijonoon
* @param s merkkijono johon tulos sijoitetaan
* @param i kokonaisluku joka sijoitetaan
*/
private static void set(StringBuffer s,int i) {
s.delete(0, s.length()).append(""+i);
}
/* 01 */ static int a; static StringBuffer b; static int c;
/* 02 */
/* 03 */ private static void ali_1(StringBuffer a, int b)
/* 04 */ {
/* 05 */ int d;
/* 06 */ d = i(a);
/* 07 */ c = b + 3;
/* 08 */ b = d - 1;
/* 09 */ a.append(""+(c - 5));
/* 10 */ }
/* 11 */
/* 12 */ static private void ali_2(StringBuffer a, StringBuffer b)
/* 13 */ {
/* 14 */ int c;
/* 15 */ c = i(a) + i(b);
/* 16 */ set(a,9 - c);
/* 17 */ set(b,32);
/* 18 */ }
/* 19 */
/* 20 */ public static void main(String[] args) {
/* 21 */ StringBuffer d = new StringBuffer(); b = new StringBuffer();
/* 22 */ a=1; set(b,2); c=3; set(d,4);
/* 23 */ ali_1(d,c);
/* 24 */ ali_2(b,d);
/* 25 */ ali_1(d,3+i(d));
/* 26 */ System.out.println("" + a + " " + b + " " + c + " " + d);
/* 27 */ }
}
Käsittelemme (huonosti nimettyjä) metodeja i ja set "operaattoreina", eli oletamme niiden toiminnan tunnetuksi, eikä pöytätestissä askelleta niihin sisälle.
Pöytätestin tekeminen aloitetaan piirtämällä sarakkeet kutakin isompaa ohjelmassa olevaa kokonaisuutta varten. Esimerkissä näitä ovat
• suoritettava lause
• luokkamuuttujat
• main-metodi
• metodit ali_1 ja ali_2
• keko
• lisäksi kannattaa laskea välitulokset jonnekin auki
Sitten kukin sarake jaetaan vielä osiin siinä olevien muuttujien määrän mukaan. Kekoa varten tarvitaan karkeasti yhtä monta saraketta kuin ohjelmassa on suoritettavia new-operaattoreita (tai String a = "kissa"; tyyppisiä lauseita) .
Lyhyyden vuoksi olemme seuraavassa merkinneet N1 = ensimmäinen new:llä luotu olio ja N2 on toinen. Lisäksi on otettu c-mäinen merkintä &N1, eli viite olioon N1. Merkintä L.c tarkoittaa seuraavassa luokan muuttuja c (jos on vaara sekaantua muuhun). Merkintää := on käytetty välilaskutoimituksissa erottamaan sijoitusta = -merkistä. Merkintä * muuttujien yläpuolella on muistutuksena sitä, että kyseessä on viitemuuttujat ja niiden käsittely muuttaa aina jotakin muuta muistipaikkaa. Pöytätestissä siis sarakkeet ovat muistipaikkoja ja rivit muistipaikkojen arvo tiettynä ajanhetkenä. Muistipaikka on merkitty harmaalla jos se ei ole voimassa tiettynä ajanhetkenä.
|
luokan |
main |
ali_1 |
ali_2 |
keko |
apulaskut |
|||||||
|
|
* |
|
|
* |
|
|
* |
* |
|
SB |
SB |
|
lause |
a |
b |
c |
d |
a |
b |
d |
a |
b |
c |
N1 |
N2 |
|
01 int a; |
0 |
null |
0 |
|
|
|
|
|
|
|
|
|
|
21 d = new |
|
&N2 |
|
&N1 |
|
|
|
|
|
|
"" |
"" |
syntyy tyhjät merkkijonot |
22 a=1; b=2 |
1 |
o-> |
3 |
o-> |
|
|
|
|
|
|
"4" |
"2" |
|
23 ali_1(d,c |
|
|
|
|
&N1 |
3 |
|
|
|
|
|
|
ali_1(&N1,c) |
05 int d |
|
|
|
|
|
|
? |
|
|
|
|
|
|
06 d = i(a); |
|
|
|
|
|
|
4 |
|
|
|
|
|
d:=i(N1)=4 |
07 c = b+3; |
|
|
6 |
|
|
|
|
|
|
|
|
|
L.c:= 3+3 = 6 |
08 b = d-1; |
|
|
|
|
|
3 |
|
|
|
|
|
|
b:= 4-1 = 3 |
09 a.ap(c-5) |
|
|
|
|
o-> |
|
|
|
|
|
"41" |
|
L.c-5=1; N1:="4"+"1"="41" |
24 ali_2(b,d |
|
|
|
|
|
|
|
&N2 |
&N1 |
|
|
|
ali_2(&N2,&N1) |
14 int c; |
|
|
|
|
|
|
|
|
|
? |
|
|
|
15 c=i(a)+i( |
|
|
|
|
|
|
|
|
|
43 |
|
|
c:=41+2 = 43 |
16 set(a,9-c |
|
|
|
|
|
|
|
o-> |
|
|
|
"-34" |
N2:=9-c=-34; |
17 set(b,32) |
|
|
|
|
|
|
|
|
o-> |
|
"32" |
|
|
25 ali_1(d,3 |
|
|
|
|
&N1 |
35 |
|
|
|
|
|
|
ali_1(&N1,3+32) |
06 d = i(a) |
|
|
|
|
|
|
32 |
|
|
|
|
|
d:=i(N1)=32 |
07 c = b+3; |
|
|
38 |
|
|
|
|
|
|
|
|
|
L.c:= 35+3 = 38 |
08 b = d-1; |
|
|
|
|
|
31 |
|
|
|
|
|
|
b:= 32-1 = 31 |
09 a.ap(c-5 |
|
|
|
|
o-> |
|
|
|
|
|
|
|
L.c-5=33; N1:="32"+"33"="3233" |
26 printl |
|
|
|
|
|
|
|
|
|
|
"3233" |
|
Tulostus: 1 -34 38 3233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
============= |
Luokkamuuttujat ovat rinnastettavissa globaaleihin muuttujiin. Samoin kun seuraavassa luvussa päästään käsiksi varsinaiseen olio-ohjelmointiin, niin myös julkiset attribuutit ovat rinnastettavissa globaaleihin muuttujiin. Globaaleiden muuttujien käyttöä tulee ohjelmoinnissa välttää. Tuskin mistään on tullut yhtä paljon ohjelmointivirheitä, kuin vahingossa muutetuista globaaleista muuttujista!
Käytännössä pöytätestiä voidaan monesti korvata hyvällä debuggerilla. Debuggerista valitettavasti ei useinkaan näe suorituksen historiaa. Ennen kun debuggerit eivät olleet niin yleisiä, korvattiin niitä sijoittamalla ohjelmakoodin sekaan muuttujien arvoja tulostavia lauseita. Joissakin tapauksissa tähänkin vielä joudutaan turvautumaan.
Pöytätestaa seuraava ohjelma:
/**
* Mitä ohjelma tulostaa??
* @author Vesa Lappalainen
* @version 1.0, 19.01.2003
*/
public class Alisotk2 {
private static int i(StringBuffer s) {
return Integer.parseInt(s.toString());
}
private static void set(StringBuffer s,int i) {
s.delete(0, s.length()).append(""+i);
}
/* 01 */ private static StringBuffer b; private static int c;
/* 02 */
/* 03 */ private static void s_1(StringBuffer a, int b)
/* 04 */ {
/* 05 */ int d;
/* 06 */ d = i(a);
/* 07 */ c = b + 3;
/* 08 */ b = d - 1;
/* 09 */ set(a,c - 5);
/* 10 */ }
/* 11 */
/* 12 */ private static void a_2(int a, StringBuffer b)
/* 13 */ {
/* 14 */ c = a + i(b);
/* 15 */ { int c; c = i(b);
/* 16 */ a = 8 * c; }
/* 17 */ set(b,175);
/* 18 */ }
/* 19 */
/* 20 */ public static void main(String[] args) {
/* 21 */ StringBuffer a = new StringBuffer("4"); int d=9;
/* 22 */ System.out.println("" + a + " " + b + " " + c + " " + d);
/* 23 */ b=new StringBuffer("3"); c=2; d=1;
/* 24 */ s_1(b,c);
/* 25 */ a_2(d,a);
/* 26 */ s_1(a,3+d);
/* 27 */ System.out.println("" + a + " " + b + " " + c + " " + d);
/* 28 */ }
}
Tähän mennessä esimerkit on testattu vain testipääohjelmilla. Tapa on toimiva kun toteutettavan ohjelmiston koko on pieni ja kaikki sen moduulit voi käydä suhteellisen pienellä työllä läpi.
Monisteessa aiemmin esiteltiin Alkulukuohjelma.
/**
* Aliohjelmalla tutkitaan onko parametrina tuotu
* luku alkuluku vai ei<br>
* Algoritmi: Jaetaan tutkittavaa lukua jakajilla 2,3,5,7...luku/2.
* Jos jokin jako menee tasan, niin ei alkuluku:
*
* @param luku tutkittava luku
* @return luvun jolla jaollinen tai 1 jos alkuluku
*/
public static int onkoAlkuluku(int luku) {
int jakaja = 2;
int kasvatus = 1;
if ( luku == 2 ) return 1; // 0
do {
intjakojaannos = luku % jakaja;
if (jakojaannos == 0) return jakaja; // 1
jakaja += kasvatus; // 2
kasvatus = 2; // 3
} while (jakaja < luku / 2);
return 1;
}
Testipääohjelmalla testaaminen metodilla, joka kertoo jos tulos ei ollutkaan odotettu.
/**
* Testipääohjelma onkoAlkuluku aliohjelman testaukseen
* @param luku
* @param odotettuVastaus true = alkuluku, false != alkuluku
*/
public static int testiOnkoAlkuluku(int luku, int pieninJakaja) {
int tulos = onkoAlkuluku(luku);
if (pieninJakaja != tulos)
{
System.out.println("Luvun "+luku+" pienin jakaja on "+pieninJakaja+
" mutta oli "+tulos);
return 1;
}
return 0;
}
public static void main(String[] args) {
testiOnkoAlkuluku(25, 5);
testiOnkoAlkuluku(2, 1);
testiOnkoAlkuluku(4, 2);
testiOnkoAlkuluku(123, 3);
testiOnkoAlkuluku(7, 1);
}
Ohjelmiston koon kasvaminen asettaa kuitenkin uusia haasteita testaukselle.
• Mitä tehdä tilanteessa, jossa tehdään muutos funktioon, jota jo useat ohjelman osat käyttävät? Testipääohjelmilla joudutaan käymään käsin läpi jokainen moduuli, jotta voimme varmistua siitä ettei mikään mennyt rikki.
• Ohjelmakoodin sekaan kirjoitetut testit jäävät rasittamaan ohjelman tuotantoversiota.
• Koodin luettavuus heikkenee, kun testausrutiinit paisuttavat yksinkertaistakin varsin suureksi.
Ratkaisuksi edellä esiteltyihin ongelmiin on kehitetty menetelmä nimeltä yksikkötestaus (Unit Testing). Testipääohjelmien käyttö on jo periaatteessa eräänlaista yksikkötestausta, mutta nykyään termillä viitataan lähinnä ympäristöihin ja työkaluihin joilla testausta voidaan helpottaa ja automatisoida mahdollisimman pitkälle. Varsinaisten testitapausten (test case) lisäksi voidaan rakentaa myös koko projektin laajuisia testisarjoja (test suite), jotka voi ajaa vaikkapa automaattisesti yön aikana.
Nykyisin suosittu ohjelmointitekniikka on testivetoinen kehitys (Test Driven Development, TDD). Sen idea on kehittää koodi valmiiksi testattavaksi tehdä testit etukäteen, jonka jälkeen vasta aloitetaan itse ohjelmointi. Suunnitteluvaiheessa tapahtuva testaus selkeyttää ohjelman rakennetta ja toimintaa, jolloin se on helppo toteuttaa suoriutumaan halutuista selkeistä vaatimuksista. Kyse ei siis ole niinkään testauksesta, vaan ohjelman suunnittelusta, jonka sivutuotteena syntyvät kaikki testitapaukset.
Alkuun testien tekeminen saattaa vaikuttaa ylimääräiseltä työltä. Käytännössä kokeneinkin ohjelmoija joutuu kuitenkin joka tapauksessa kokeilemaan ohjelmansa toiminnallisuuden tavalla tai toisella. Kattavien testitapauksen kirjoittaminen etukäteen minimoi tällaiset turhat häiriöt ja mahdollistaa keskittymisen itse asiaan. Yksikkötestaukseen käytettävät kirjastot sisältävät myös valmiita funktioita tavallisimpiin testausrutiineihin, kuten esimerkin merkkijonojen vertailuun.
Javassa yksikkötestaus tapahtuu yleensä JUnit -kirjastolla, jota kehitysympäristöt kuten Eclipse ja NetBeans tukevat suoraan. Testit on mahdollista ohjelmoida hieman testipääohjelmien tapaan suoraan testattavan asian yhteyteen, mutta tavallisesti testit kuitenkin erotetaan erillisiin paketteihin. JUnit-testit erotetaan muusta koodista @test -tagilla. Edellisen ohjelman testaus lyhentyy muotoon.
@Test
public void testOnkoAlkuluku19() {
assertEquals("mahdollista virheenjäljitystä helpoittava viesti", 5,
onkoAlkuluku(25));
assertEquals(1, onkoAlkuluku(2));
assertEquals(2, onkoAlkuluku(4));
assertEquals(3, onkoAlkuluku(123));
assertEquals(1, onkoAlkuluku(7));
}
Isommissa projekteissa JUnit testit toteutetaan vähintäänkin erilliseen pakettiin ja omiin luokkiinsa, jolloin niiden ylläpitäminen ja organisoiminen on helpompaa. Molemmat tavat ovat kuitenkin täysin oikein ja esimerkiksi kurssin demojen testaaminen onnistuukin varmasti helpoiten luokan yhteyteen kirjoittamalla. Isompia kuin yhden tai muutaman luokan ohjelmia testattaessa kannattaa kuitenkin eristää testaus itse ohjelmasta.
Kurssilla on myös mahdollista käyttää yksikkötestaukseen ComTest-työkalua. ComTest on Eclipse -liitännäinen, joka generoi oman makrokielensä pohjalta täysin validia Java/JUnit -koodia. Makrokieli kirjoitetaan suoraan testattavan metodin tai luokan kommentteihin. Hieman samanlainen menetelmä on käytössä mm. Python-ohjelmointikielen doctest -kirjastossa.
ComTest pyrkii tekemään testien kirjoittamisen tekeminen vähemmän työlääksi ja helpommin ylläpidettäväksi. Ajatuksena on että kun testejä ei piiloteta eri tiedostoon, niin ohjelmoijalla on yksi askel vähemmän päästä niihin käsiksi. Lisäksi kun tämän lähestymistavan yhdistää yksinkertaiseen makrokieleen, niin samalla saadaan myös käyttöesimerkit dokumentaatioon. Yhdellä kertaa ohjelmoija voi siis periaatteessa suunnitella, testata ja dokumentoida koodinsa!
Nyt onkoAlkuluku-aliohjelman testaus muuttuu muotoon
* @example
* <pre name="test">
* onkoAlkuluku(25) === 5;
* onkoAlkuluku(2) === 1;
* onkoAlkuluku(4) === 2;
* onkoAlkuluku(123) === 3;
* onkoAlkuluku(7) === 1;
* </pre>
Testi alkaa kommenttilohkoon kirjoitetulla @example tagilla. Käytetyt<pre>tagit ovat normaalia html-koodia JavaDocdokumentaatiota varten. Niiden sisälle voi kirjoittaa ComTestmakrokieltä ja normaalia Javaa. Generoidusta JUnittestistä poistuvat tietysti rivin aloittavat kommentit, joten jos sellaiselle on tarvetta, on kommentoitava "tuplasti".
Valmiin testirungon voi tehdä kirjoittamalla kommenttilohkoon comtja painamalla näppäinyhdistelmää ctrl+välilyönti.
Seuraava hieman monimutkaisempi ohjelma käyttää hyväkseen silmukoita ja taulukoita, joten jos ne ovat hyvässä muistissa Ohjelmointi 1 kurssilta, niin seuraava on hyvää luettavaa. Muussa tapauksessa kannattaa palata esimerkkiin myöhemmin.
Ohjelma sisältää algoritmin joka palauttaa indeksin ensimmäisen taulukon lukuun, jolla on eniten esiintymiä jälkimmäisessä taulukossa. Mikäli esiintymiä kahdella luvulla on sama määrä, niin palautetaan se joka esiintyi jonossa ensimmäisenä.
/**
* Ohjelma testauksen esittelyä varten
* @author Santtu Viitanen
* @version 1.0, 4.7.2011
*/
public class Esiintymat {
/**
* Palauttaa indeksin t1 taulun arvoon, jolla on eniten esiintymiä t2
* taulussa. Mikäli kahdella luvulla on saman verran esiintymiä palautetaan
* jonossa ensimmäinen arvo. Jos t1 on tyhjä palautetaan -1
*
* @param t1 taulukko, jonka indeksi palautetaan
* @param t2 taulukko, johon verrataan
* @return indeksi t1 taulun arvoon.
*/
public static int enitenEsiintymia(int[] t1, int[] t2) {
int paras = -1; // eniten esiintymiä omaavan luvun indeksi
int parhaanEsiintymat = -1; //esiintymien määrät
for (int i = 0; i < t1.length; i++) {
int ehdokkaanEsiintymat = 0;
for (int j = 0; j < t2.length; j++)
if (t1[i] == t2[j])
ehdokkaanEsiintymat++;
if (ehdokkaanEsiintymat > parhaanEsiintymat) {
paras = i;
parhaanEsiintymat = ehdokkaanEsiintymat;
}
}
return paras;
}
}
Testipääohjelman käyttö on muuttunut jo huomattavan työlääksi
/**
* @param args ei käytössä
*/
public static void main(String[] args) {
int[] t1 = { 1, 1, 3 };
int[] t2 = { 1, 2 };
int[] t3 = { 1, 3, 3, 4 };
int[] t4 = {};
enitenEsiintymiaTesti(t1, t2, 0);
enitenEsiintymiaTesti(t1, t3, 2);
enitenEsiintymiaTesti(t1, t4, 0);
enitenEsiintymiaTesti(t4, t1, -1);
enitenEsiintymiaTesti(t4, t4, -1);
}
/**
* Testataan enitenEsiintymia-aliohjelmaa
* @see #enitenEsiintymia(int[], int[]);
* @param i oletettu tulos
*/
private static int enitenEsiintymiaTesti(int[] t1, int[] t2, int odotettu) {
int tulos = enitenEsiintymia(t1, t2);
if (tulos == odotettu)
return 0;
System.out.println("Tulokseksi tuli: " + tulos + " vaikka piti tulla: "
+ odotettu);
return 1;
}
ComTestilla funktion testaaminen muuttuu muotoon
/**
* Palauttaa indeksin t1 taulun arvoon, jolla on eniten esiintymiä t2
* taulussa. Mikäli kahdella luvulla on saman verran esiintymiä palautetaan
* jonossa ensimmäinen arvo. Jos t1 on tyhjä palautetaan -1
*
* @param t1 taulukko, jonka indeksi palautetaan
* @param t2 taulukko, johon verrataan
* @return indeksi t1 taulun arvoon.
* @example
* <pre name="test">
* int[] t1 = {1,1,3};
* int[] t2 = {1,2};
* int[] t3 = {1,3,3,4};
* int[] t4 = {};
* enitenEsiintymia(t1,t2) === 0;
* enitenEsiintymia(t1,t3) === 2;
* enitenEsiintymia(t1,t4) === 0;
* enitenEsiintymia(t4,t1) === -1;
* enitenEsiintymia(t4,t4) === -1;
* </pre>
*/
public static int enitenEsiintymia(int[] t1, int[] t2) {
…
Testit sisältävät tyypillisesti paljon toistuvia rakenteita. Tämän vuoksi ComTestmahdollistaa myös taulukkomuotoisen testaamisen. Esimerkkimme oli varsin yksinkertainen, mutta monimutkaisemmissa toteutuksissa testejä voi olla kymmeniä, jolloin taulukkomuotoinen testaus vähentää kirjoittamista ja sillä saa aikaan havainnollisemman dokumentaation.
* @example
* <pre name="test">
* int[] t1 = {1,1,3};
* int[] t2 = {1,2};
* int[] t3 = {1,3,3,4};
* int[] t4 = {};
* enitenEsiintymia($tparam1,$param2) === $tulos
* $param1 |$param2 |$tulos
* -----------------------
* t1 |t2 |0
* t1 |t3 |2
* t1 |t4 |0
* t4 |t1 |-1
* t4 |t4 |1
* </pre>
Reaalilukujen vertaamiseen käytetään aaltoviivoja
* @example
* <pre name="test">
* #TOLERANCE=0.01 // Määrää vertailun tarkkuuden
* hypotenuusa(0,0) ~~~ 0.0;
* hypotenuusa(0,1) ~~~ 1.0;
* hypotenuusa(1,1) ~~~ 1.41;
* hypotenuusa(1,2) ~~~ 2.24;
* hypotenuusa(5,6) ~~~ 7.81;
* </pre>
Muita käytettäviä makroja ovat esimerkiksi
• #CLASSIMPORT- Lisää luokkia import lauseeseen
• #STATICIMPORT- kääntäjä löytää staattisen metodin ilman viittausta luokkaan (oletuksena päällä)
Tarkemmat ohjeet ComTestinasennukseen ja käyttöön löytyvät osoitteesta
Seuraavaksi on aika sitoa opittuja asioita yhteen. Nyt osataan jo toteuttaa yksinkertaisia graafisia ohjelmia joilla on toiminnallisuutta. Otetaan pohjaksi tässä luvussa esitelty komentoriviltä toimiva Matka.java ja toteutetaan se graafisena versiona (Swing).
Kuva 7.3 Mittakaavaohjelman käyttöliittymä
Uuden JFrame ikkunan voi luoda esimerkiksi Eclipsellä. Graafisessa editorissa kuvaa vastaavan käyttöliittymän saa suurin piirtein seuraavilla toimenpiteillä
• Properties ikkunasta pääikkunalle layout GridBagLayout
• Label ja TextField komponentit oikeisiin paikkoihinsa ja niille kuvaava nimeäminen (Variable). Esimerkissä kentille on käytetty textMittakaava, textKartalta ja textMaastossa.
• Labelien tekstit kuvaa vastaaviksi ja textMittakaava kentälle oletusteksti 200000
• Käyttöliittymä on tarkoitus toimia ilman erillistä Laske-nappia, joten graafisesta näkymästä asetetaan textMittakaava ja textKartalla kentille tapahtumaksi KeyReleased.
Koska kahdella tekstikentällä on sama toiminnallisuus, niin laitetaan molemmista kutsu samaan laske()-metodiin.
/**
*
* @author Vesa Lappalainen @version 1.0, 27.1.2011
* @author Santtu Viitanen @version 1.1, 03.08.2011
*/
public class Mittakaava extends JFrame {
public static void main(String[] args) { ... }
/**
* Create the frame.
*/
public Mittakaava() {
addWindowListener(new WindowAdapter() {
...
textKartalta.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent arg0) {
laske();
}
});
textMittakaava.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent arg0) {
laske();
}
});
//Metodin loppuosa on suurimmaksi osaksi graafisessa
editorissa generoitua koodia
...
}
Tarvitsemme ohjelmassa vakiot oletusmittakaavalle, sekä kertoimelle jolla teemme muutokset.
public static final double MITTAKAAVA = 200000.0;
public static final double MM_KM = 1000.0 * 1000.0;
Mittakaavamuunnoksia olemme laskeneet jo aiemminkin. Tähän ei liity mitään graafista. Muunnetaan kartalta millimetreinä mitattu matka kilometreiksi maastossa.
public static double mittakaavamuunnos(double matka_mm, double mittakaava) {
return mittakaava*matka_mm / MM_KM;
}
Luvun hakemiseen tekstikentästä on myös hyvä olla yleiskäyttöinen metodi. Malliohjelmassa on käytetty Ali.jar-kirjastoa, josta löytyy valmis funktio reaalilukujen parsimiseen.
public static double haeLuku(JTextField text,double oletus) {
double luku = Mjonot.erotaDouble(text.getText(), oletus);
return luku;
}
Jäljellä onkin enää tekemiemme aliohjelmien ja graafisen käyttöliittymän komponenttien yhdistäminen toimivaksi kokonaisuudeksi.
public static void laitaTulos(JTextField text, double luku) {
String tulos = String.format("%5.2f",luku);
tulos = tulos.replace(',', '.');
text.setText(tulos);
}
private void laske() {
double mittakaava = haeLuku(textMittakaava,MITTAKAAVA);
double matka_mm = haeLuku(textKartalta, 0);
double matka_km = mittakaavamuunnos(matka_mm,mittakaava);
laitaTulos(textMaastossa,matka_km);
}
}
Jäljellä onkin enää tekemiemme aliohjelmien ja graafisen käyttöliittymän komponenttien yhdistäminen toimivaksi kokonaisuudeksi.
Tehtävä 7.23 Lisää toiminnallisuutta ohjelmaan
Lisää ohjelmaan seuraava toiminnallisuus selectAll()-metodin avulla.
Enter textMittakaava ja textKartalla kentissä valitsee kyseisen kartan tekstit.
Enter textMaastossa kentässä valitsee kaikkien kenttien tekstit.
Ohjat ottaako oliot?
Luokista luodut ilmentymät
kantaemosta perityt
rajapinnalla rajatut.
Itsestäänkö ilmaantuvat,
sanomatta siunaantuvat?
Viestejä hyö viskoviksi
kaiken koodin korvaajiksi.
Nyt on virhe pienen pieni
ei valta noin suuren suuri.
Taas työ itse tehtäväksi
oliot olkoonkin avuksi.
Luokat luotava lujiksi
vakaan vastuun kantajiksi
ylläpidon ystäviksi
tehtävien taitajiksi.
Oman homman hoitajaksi
tuodun tiedon taattajaksi
sisältö sen suojaamaksi
paljon piiloon pistäväksi.
Perintääkin pohdittava
katsottava koostamista
muodostajaa muotoiltava
rajapintoja raakattava.
Metodeja mietittävä
attribuutteja aateltava
viestejäkin viskeltävä
olioita ohjaillessa.