(Muutettu viimeksi 15.9.2011, sivu perustettu 17.8.2011.)
Tässä luvussa tutustutaan algoritmin ideaan ja perustavanlaatuisten algoritmien laadintaan. Algoritmi on toimintaohje jonkin ongelman ratkaisemiseen tai vaikkapa jonkin palvelun toteuttamiseen. Tietokone aina suorittaa jotakin algoritmia, kun se tekeee jotakin. Myös useimmat ohjelmointikielet perustuvat algoritmin kirjoittamiseen – toisin sanoen ohjelmoija ajattelee laativansa ajassa etenevää toimintaohjetta. Tosin on toisenlaisiakin ohjelmointikieliä...
Algoritmisen ohjelmoinnin peruskäsitteitä ovat muuttuja (variable) ja sijoitusoperaatio (assignment operation).
Muuttuja tarkoittaa matematiikassa jotakin melko syvällistä: esimerkiksi ilmauksessa a+b=b+a väitetään vaikkapa minkä tahansa lukuparin summan olevan riippumaton laskentajärjestyksestä. Tilastotieteessä muuttuja voi tarkoittaa "selittävää tekijää"; esimerkiksi myytyjen jäätelöiden määrä voisi olla muuttujana hukkumiskuolemien määrää selitettäessä.
Algoritmisessa ohjelmoinnissa muuttuja on paljon konkreettisempi asia: muuttuja on arvon säilytyspaikka, "laatikko, jonka kyljessä on nimi". Laatikon sisältöa voi tutkia ja sisältöä voi muuttaa. Uuden sisällön asettamista laatikkoon kutsutaan sijoittamiseksi, sijoitusoperaatioksi tai sijoituslauseeksi.
Sijoitus korvaa muuttujan vanhan sisällön uudella. Uusi arvo voi olla jonkin laskutoimituksen, ns. lausekkeen (expression) arvo. Tuo lauseke voi sisältää vakioita, muuttujien arvoja ja laskutoimituksia.
Sijoitusoperaation ilmauksena algoritmeja kirjoitettaessa joissain ohjelmointikielissä käytetään merkkijonoa ":=". Myös ilmausta "<-" näkee joskus. Valitettavasti eräissä ohjelmointikielissä (mm. Javassa!) sijoitusoperaation merkkinä käytetään yhtäsuuruusmerkkiä "=". Näissä kielissä matematiikasta tuttuun yhtäsuuruuden vertailuun joudutaan siten käyttämään matematiikan kielestä poikkeavaa ilmausta, esimerkiksi kahta yhtäsuuruusmerkkiä peräkkäin "==".
Esimerkki:
eka, toka ja kolm ovat muuttujia eka = 7 toka = 9 kolm = 2 eka = toka toka = kolm * 2 kolm = eka + toka kolm = kolm + 1
Huom: Sijoitusoperaatio kannattaa heti alussa opetella lukemaan "saa arvokseen", ei "on", vaikka ohjelmointikielessä Javan tapaan käytettäisiin yhtäsuuruusmerkkiä sijoitusoperaation ilmauksena! Erityisen hyvin tuo viimeinen esimerkki osoittaa, miksi.
kolm = kolm + 1
Ilmaus "kolm on kolm plus yksi" on looginen epätotuus. Sitävastoin "kolm saa arvokseen kolm plus yksi" ilmaisee, mistä on kysymys: muuttujan vanhaan arvoon lisätään yksi ja saatu summa sijoitetaan muuttujan uudeksi arvoksi.
Algoritmia siis suoritetaan ajassa: "ensin tehdään sitä, sitten tätä". Algoritmin muuttujien arvot kullakin ajan hetkellä muodostavat algoritmin ns. tilan (state). Ja oikeastaan kaikki tietokoneohjelmat ovat lopulta vain algoritmeja, jotka etenevät tilasta toiseen. Jos ohjelmassa näyttäisi olevan jotakin "viisautta", kyse on siitä, että ohjelman laatija on osannut rakentaa algoritmilleen sellaisen tilojen ketjun, joka ohjelman käyttäjästä vaikuttaa viisaalta.
Edellisen esimerkin tilat, kun algoritmi suoritetaan:
eka, toka ja kolm ovat muuttujia eka = 7 toka = 9 kolm = 2 tila: |__7____| |__9____| |__2____| eka toka kolm eka = toka tila: |__9____| |__9____| |__2____| eka toka kolm toka = kolm * 2 tila: |__9____| |__4____| |__2____| eka toka kolm kolm = eka + toka tila: |__9____| |__4____| |__13___| eka toka kolm kolm = kolm + 1 tila: |__9____| |__4____| |__14___| eka toka kolm
Algoritmi liitetään ympäristöönsä ns. syöttö- ja tulostusoperaatioin. Syöttöoperaation eli lukemisen seurauksena jokin muuttuja saa arvon ohjelman käyttäjältä tai vaikkapa jostakin tiedostosta. Lukeminen siis muuttaa ohjelman tilaa. Tulostusoperaatio, kirjoittaminen puolestaan laskee jonkin arvon ja siirtää tuloksen jollekin tulostuslaitteelle. Tällöin algoritmin tila ei muutu.
Syöttötietojen ajatellaan olevan syöttöjonossa, josta ne yksi kerrallaan luetaan. Tulostietojen ajatellaan puolestaan olevan tulosjonossa, johon ne yksi kerrallaan kirjoitetaan. Nämä jonot voivat ihan konkreettisestikin olla "jonoja" jossakin tiedostossa, mutta esimerkiksi vuorovaikutteisen ohjelman syötteiden ja tulosteiden "jonot" ovat pikemminkin jonoja ajassa.
Esimerkki:
eka, toka ja kolm ovat muuttujia kirjoita viesti: "Anna kaksi lukua" eka = lue syöttöluku toka = lue syöttöluku kolm = eka + toka kirjoita viesti: "Niiden summa on" kirjoita viesti: kolm
Jos tämän ohjelman syöttöjonossa on luvut 3 ja 4, muuttujaan eka luetaan arvo 3, muuttujaan toka arvo 4. Sitten muuttujaan kolm sijoitetaan summa eka+toka eli arvo 7. Lopuksi ohjelma kirjoittaa tulosjonoon arvon 7. Näin "jonot" ovat siis ohjelman käyttäjän näkökulmasta toimintoja näytön ja näppäimistön äärellä.
Peräkkäin kirjoitetut operaatiot siis suoritetaan peräkkäin - "vasemmalta oikealle, ylhäältä alaspäin". Alkeisoperaatioiden (sijoitus, lukeminen, kirjoittaminen) suorittamista ohjataan ns. rakenteisin operaatioin. Rakenteiset operaatiot ovat "rakenteisia" siksi, että ne sisältävät muita operaatioita.
Monet rakenteiset operaatiot perustuvat siihen, että väitetään jotakin algoritmin tilasta, ja jos väite on tosi, "tehdään jotakin", jos epätosi, "tehdään jotakin muuta". Esimerkkejä tällaisista ovat valinta ja toisto:
Esimerkki valinnasta:
luku on muuttuja kirjoita viesti "Anna luku. Tutkin, onko se positiivinen" luku = lue syöttöluku jos (luku>0) // valinta kirjoita viesti "Luku on positiivinen." muuten kirjoita viesti "Luku ei ole positiivinen.")
Esimerkki toistosta:
määrä ja monesko ovat muuttujia kirjoita viesti "Moneenko kertaan haluat saada onnittelut?" määrä = lue syöttöluku monesko = 0 niin pitkään kun (monesko < määrä) toista seuraavia operaatoita kirjoita viesti "Onnea!" monesko = monesko + 1
Mitä seuraava ohjelmointivirheen sisältävä ohjelma tekee kun sille syötetään positiivinen luku? Entä kun sille syötetään negatiivinen luku? Simuloi ja opi! Mitä opit?
määrä ja monesko ovat muuttujia kirjoita viesti "Moneenko kertaan haluat saada onnittelut?" määrä = lue syöttöluku monesko = 0 niin pitkään kun (monesko määrä) toista seuraavia operaatoita kirjoita viesti "Onnea!" monesko = monesko + 1
Huom: Rakenteiset operaatiot ovat siis "rakenteisia" siksi, että niiden rakenneosina on muita lauseita. Noita rakenneosia voidaan kutsua alialgoritmeiksi: "Toistolause toistaa alialgoritmia..."
Lady Ada Lovelace, lordi Byronin tytär, keksi valinnan ja toiston 1800-luvun lopulla. Hän laati ohjelmia Charles Babbagen suunnittelemaan mekaaniseen tietokoneeseen, jossa ohjelmat suoritettiin suoraan reikäkorttipakalta. Lady Ada keksi täydentää konekieltä operaatioilla, jotka jostakin ehdosta riippuen hyppäävät joidenkin korttien ohi tai jotka siirtävät jo ohitetun korttipakan osan uudelleen luettavaksi!
Ehdollisuus - algoritmin tilasta riippuva toiminnan ohjaus - on keskeistä algoritmeja laadittaessa. Ilman ehdollisuutta algoritmi toimisi samoin kaikilla syöttötiedoilla! Alialgoritmien nimeäminen ja nimettyjen alialgoritmien kutsuminen on myös tärkeä väline: Kerran ohjelmoitua alialgoritmia ei tarvitse kirjoittaa uudelleen ja uudelleen.
Aloitetaan ohjelmointi kansainväliseen tapaan.
Kirjoita seuraava Java-ohjelma tekstitiedostoon
HelloOhjelma.java
.
public class HelloOhjelma { public static void main(String[] args) { System.out.println("Hello world!"); } }
Java on tyyliltään huomattavan monisanaista. Tällä on hyvät puolensa, mutta toisaalta seurauksena on, että aloittelija joutuu kirjoittamaan paljon sellaista, mikä toistaiseksi vaikuttaa pelkältä loitsulta. Jos yhtään lohduttaa, niin jatkokurssin loppupuoleen mennessä noiden "loitsujen" merkityskin selviää.
Oleellinen toiminnallisuus tässä esimerkissä on kohta:
println("Hello world!");
Tämä lause luetaan "print line". Se tulostaa kuvaruudulle – ohjelman käynnistysikkunaan tms. – yhden rivin, jonka sisältö on sulkeiden sisällä olevan merkkijonon Hello world!
Ohjelman "loitsut" lyhyesti sanottuna tarkoittavat seuraavaa:
public class HelloOhjelma
:
Suoritettava Java-ohjelma on julkinen luokka, jolle tässä on annettu
nimi HelloOhjelma
. Lähdekielinen ohjelma on tekstitiedossa,
jonka nimen alkuosan on oltava sama kuin julkisen luokan nimi:
HelloOhjelma.java
.
public static void main(String[] args)
: Ohjelman suoritus alkaa
ns. pääohjelmasta, joka Javassa on aina tämän esimerkin mukainen.
Pääohjelmaa tavataan kutsua "main-metodiksi".
System.out.
: [Älä vielä yritäkään ymmärtää seuraavaa!]
Luokassa System
on julkinen kenttä out
, joka
viittaa tulostusolioon, jolla on mm. aksessori println
.
Toisinaan tekstitiedostoon kirjoitettua lähdekielistä ohjelmaa kutsutaan koodiksi. Nimitys on kotoisin 1950-luvulta, jolloin konekielikäskyjä "koodattiin" symbolisilla nimillä.
Java-ohjelma voidaan suorittaa ilman erityistä ohjelmankehitysympäristöä seuraavaan tapaan käyttöjärjestelmän komentotulkissa ("terminal"):
HelloOhjelma.java
.
Ohjelman nimen alkuosan pitää olla sama kuin luokan nimi, esimerkissä
siis HelloOhjelma
. Muista että luokkanimet on Javassa tapana kirjoittaa
suurella alkukirjaimella.
javac HelloOhjelma.javaJos ohjelmassa on muotovirheitä, kääntäjä antaa niistä enemmän tai vähemmän osuvia virheilmoituksia. Korjaa virheet tiedostossa
HelloOhjelma.java
ja
yritä kääntää uudelleen. Sitten kun ohjelma kelpaa kääntäjälle, kääntäjä tuottaa
ohjelman välikielivastineen tiedostoksi HelloOhjelma.class
.
java HelloOhjelmaHuomaa, että välikielisen ohjelmatiedoston nimen loppuosaa
.class
ei mainita tulkin käynnistyskomennossa.
Muokkaa alkuperäistä tiedostoasi: muuta tulostettavaa tekstiä ja lisää uusia tulostuslauseita. Talleta tiedosto ja suorita taas. Osaat siis jo tehdä ohjelmia, jotka tulostavat, mitä ikinä maailmassa voidaankaan pyytää tulostamaan.
Tietokone ei kerta kaikkiaan "osaa" suorittaa mitään muuta algoritmia kuin sellaista, joka on kirjoitettu koneen sisäisellä omalla konekielellä. Tuo kieli kuitenkin on useimpiin ohjelmointitehtäviin auttamattoman kömpelöä ja koneenläheistä.
Ohjelma kirjoitetaan useimmiten jollakin konekieltä korkeamman tason kielellä, lausekielellä. Toisinaan näin kirjoitettua ohjelmaa kutsutaan lähdekoodiksi. Lähdekoodi talletetaan tekstimuodossa ja suoritetaan jollakin tavalla.
Tietokone ei siis osaa suorittaa mitään muita kuin konekielellä ilmaistuja algoritmeja. Siksi tarvitaan kääntäjä tai tulkki, joka muokkaa lähdekielisen ohjelman tavalla tai toisella konekielioperaatoiden jonoksi.
---> Lisätietoa kääntäjistä ja tulkeista
Ohjelmointiympäristö eli IDE sisältää kääntäjän, editorin, yksinkertaisen virhetarkastuksen ja monia muita hyödyllisiä välineitä ohjelmoinnin helpottamiseen. Ja koska kaikki nuo ovat samassa ohjelmassa, kyseessä todellakin on "integroitu" ympäristö.
Kurssilla käytetään NetBeans-ohjelmointiympäristöä, joka löytyy valmiiksi asennettuna laitoksen koneista, Linuxissa Applications/Programming-alasvetovalikosta: NetBeans-aloittelijan ohje
File -> New Project -> Java -> Java Application -> NextKirjoita kenttään Project Name (toistaiseksi) ohjelman luokan nimi
HelloOhjelma
. Kirjoita sama nimi myös kenttään
Create Main Class. Paina nappulaa Finish.
KUVA!
(Jos käynnityksen jälkeen yläpalkin alla näkyy Start Page, sulje
se klikkamalla rastia, ks. kuva.)
Niin yllättävältä kuin se saattaakin kuulostaa, ohjelmat eivät ihan aina toimi ihan oikein, vaikka ne kääntäjälle kelpaisivatkin... ;-)
Yksi tapa yrittää löytää syitä ohjelman virheelliseen toimintaan on ohjelman suorituksen pysäyttäminen ennalta määrättyihin kohtiin ja muuttujien arvojen tutkiminen tuolla hetkellä.
NetBeansilla tällaista analyysia voi tehdä seuraavaan tapaan:
NetBeansilla laaditusta Java-ohjelmasta on olemassa myös ihan tavallinen tekstitiedostoversio, jota voi perinteseen tapaan kääntää ja suorittaa käyttöjärjestelmän komentotulkissa.
Linuxissa NetBeansilla laaditut ohjelmat yms. oletusarvoisesti löytyvät
kotihakemiston alihakemistosta NetBeansProjects
.
Esimerkkitapauksessamme hakemistopolku on
NetBeansProjects/HelloOhjelma2/src/
Tuossa hakemistossa ohjelma on omalla nimellään tekstiedostona
HelloOhjelma.java
.
Algoritmisessa ohjelmoinnissa muuttuja on keskeinen käsite. Muuttuja on nimetty "laatikko", jossa säilytetään jotakin arvoa. Ja kuten nimi "muuttuja" antaa ymmärtää, tuota arvoa voi muuttaa. Algoritmin tila tarkoittaa algoritmin muuttujien arvoja "tällä hetkellä".
Tietokoneessa kaikki tiedot, muuttujien arvot, esitetään bittijonoina ja koneen kannalta bittijono on vain bittijono. Mutta ohjelmoijan kannalta jotkin bittijonot esittävät kokonaislukuja, jotkin desimaalilukuja, jotkin merkkejä, jotkin totuusarvoja, jotkin merkkijonoja, jotkin vaikkapa kokonaislukumatriiseja, jne... Ohjelmoijan kannalta siis muuttujien arvoilla ja samalla itse muuttujilla on jokin tyyppi (type). Vanhanaikaisilla ohjelmointikielillä ohjelmoidessa tyypillinen virhe oli vahingossa käsitellä vaikkapa desimaaliluvun bittiesitystä kokonaisluvun tavoin, kokonaislukua totuusarvon tavoin jne.
Tuollaisia virheitä oli vaikea välttää, mutta sitten keksittiin hyvä ajatus: Miksei panna tietokonetta tarkistamaan, että arvoja käytetään ohjelmoijan tarkoittamalla tavalla. Juuri tuollainen mekaaninen tarkistustyöhän sopii vallan erinomaisesti juuri koneelle.
Edellytys oli, että ohjelmoija ilmoittaa ennalta, mihin aikoo mitäkin muuttujaa käyttää. Kieliin otettiin mukaan muuttujien määrittelyt: esitellään ennalta, millaiseen tarkoitukseen kutakin muuttujaa käytetään. Sitten ohjelmointijärjestelmä (kääntäjä ja käännetty ohjelma) pyrkii tarkastamaan ja varmistamaan, ettei ohjelmoija yritä sijoittaa muuttujalle vääräntyyppistä arvoa. Tätä nimitetään vahvaksi tyypitykseksi.
Tavanomaisia tyyppejä ovat esimerkiksi merkkijono, kokonaisluku, "liukuluku" eli desimaaliluku ja totuusarvo.
Esimerkkejä muuttujien määrittelystä ja arvojen muuttamisesta
public class Muuttujia { public static void main(String[] args) { String teksti = "kissantassu"; // String on merkkijonotyyppi int kokonaisluku = 1234; // int on kokonaislukutyyppi double liukuluku = 3.14; // double on liukulukutyyppi boolean onkoTotta = true; // boolean on totuusarvotyyppi System.out.println(teksti); System.out.println(kokonaisluku); System.out.println(liukuluku); System.out.println(onkoTotta); teksti = "ketunhäntä"; kokonaisluku = -666; liukuluku = 1.2; onkoTotta = false; System.out.println(teksti); System.out.println(kokonaisluku); System.out.println(liukuluku); System.out.println(onkoTotta); } }
Ohjelma tulostaa komentotulkin ikkunaan:
kissantassu 1234 3.14 true ketunhäntä -666 1.2 false
Kuten esimerkissä nähdään, muuttujan arvoa siis voi muuttaa. Mikä on algoritmin tila ennen ensimmäisiä tulostuksia? Entä ennen toisia?
Ohjelmia laadittaessa monenlaisille itse määritellyille asioille annetaan itse päätettäviä
nimiä eli tunnuksia (identifier).
Esimerkiksi juuri muuttujien nimet ovat näitä tunnuksia.
Javassa tunnukset alkavat aina kirjaimella ja voivat sisältää
kirjaimia ja numeromerkkejä. (Myös alaviiva "_
lasketaan "kirjaimeksi".)
Vaikka skandinaaviset "ääkkösemmekin" kelpaavat tunnuksissa käytettäviksi, niitä on paras välttää, koska maailmalla on käytössä pari hyvin yleistä toisistaan poikkeavaa tapaa koodata nuo rakkaat kirjaimemme, ks. Wikipedia-artikkeli merkkikoodauksesta.
Nimessä ei siis saa olla erikoismerkkejä, kuten vaikkapa huutomerkkejä (!).
Välilyönti ei ole sallittu, sillä se erottaa kielen rakenteita toisistaan.
Välilyönti kannattaa korvata alaviivalla tai
camelCase
tyylillä, jolloin nimi muistuttaaKamelia
.
Muuttujan nimi ei myöskään saa olla jo entuudestaan käytössä. Tälläisiä nimiä ovat mm. ns. varatut sanat, joilla ilmaistaan kielen rakenteita.
Muuttuja kannattaa nimetä siten, että sen käyttötarkoitus on selvää ilman selityksiä tai miettimistä. Itse asiassa tällä kurssilla muuttujat pitää nimetä kuvaavasti!
Kunnollisia muuttujien nimiä:
Huonompia muuttujien nimiä:
Virheellisiä muuttujien nimiä:
Kurssilla suositaan (Javan tapaan) Camel Case -nimiä:
kuukaudenEnsimmainenPaiva
, jne...
Ja säästetään isolla kirjaimella alkavat tunnukset Javan luokkanimille.
Ohjelmatekstiin on mahdollista, hyödyllistä ja välttämätöntä liittää selittäviä tekstejä eli kommentteja. Miksi?
Javassa on käytössä kaksi kommentointitapaa, monirivinen kommentti ja loppurivikommentti:
/* Tämä ohjelma esittelee muuttujan määrittelyä, arvon tulostamista ja arvon muuttamista. */ int kokonaisluku = 1234; // määritellään ja annetaan alkuarvo System.out.println(kokonaisluku); // tulostetaan /* ja sitten muutetaan muuttujan arvoa: */ kokonaisluku = -666; System.out.println(kokonaisluku); // muuttuiko?
Tulostukseen on käytettävissä jo nähdyn
println
-operaation lisäksi operaatio
print
.
Ensin mainittu vaihtaa riviä tulostettuaan parametrinsa, jälkimmäinen ei
vaihda.
public class Kissoja { public static void main(String[] args) { System.out.print("kissa"); System.out.println("kävelee"); // tulostaa: kissakävelee System.out.println("kissa"); System.out.println("kävelee"); // tulostaa: kissa // kävelee int i = 4321; System.out.print("Muuttujan i arvo on "); System.out.print(i); System.out.println("."); // tulostaa Muuttujan i arvo on 4321. } }
Ohjelman tulostus:
kissakävelee kissa kävelee Muuttujan i arvo on 4321.
Tuttu yhteenlaskuoperaatio +
sovellettuna kahdelle
merkkijonolle synnyttää uuden merkkijonon, jossa
"yhteenlaskettavat" merkkijonot liitetään toisiinsa. Hienolla
nimellä tätä kutsutaan merkkijonojen katenoinniksi.
System.out.println("kissa" + "kävelee") // tulostaa: kissakävelee System.out.println("kissa " + "kävelee") // tulostaa: kissa kävelee System.out.println("kissa" + " kävelee") // tulostaa: kissa kävelee
Eipä tästä vielä paljon iloa ole, mutta kun kuulee ilosanoman, että merkkijonoja ja lukuarvoja voi katenoida, alkaa tulostusoperaatoiden kirjoittaminen helpottua:
public class Katenointeja { public static void main(String[] args) { int i = 4321; System.out.println("Muuttujan i arvo on " + i + "."); // tulostaa Muuttujan i arvo on 4321. System.out.println("Yhtä suurempi arvo on " + (i+1) + "."); // tulostaa Yhtä suurempi arvo on 4322. // HUOM: SULUT I:N KASVATUKSEN YMPÄRILLÄ VÄLTTÄMÄTTÖMÄT, SILLÄ System.out.println("Yhtä suurempi arvo on " + i+1 + "."); // tulostaa Yhtä suurempi arvo on 43211. System.out.println("ahaa " + i + 7); // ahaa 43217 System.out.println("ahaa " + (i + 7)); // ahaa 4328 System.out.println(i + 7 + " ahaa"); // 4328 ahaa System.out.println("Merkkijonon sisäänkin saa lainausmerkin: \". Mukavaa."); System.out.println("Sarkain eli tabulointikin onnistuu: \t Kas näin."); System.out.println("Rivinvaihtojakin voi kirjoittaa: \n Tässä yksi. \n\nJa pari lisää."); } }
Ohjelman tulostus:
Muuttujan i arvo on 4321. Yhtä suurempi arvo on 4322. Yhtä suurempi arvo on 43211. ahaa 43217 ahaa 4328 4328 ahaa Merkkijonon sisäänkin saa lainausmerkin: ". Mukavaa. Sarkain eli tabulointikin onnistuu: Kas näin. Rivinvaihtojakin voi kirjoittaa: Tässä yksi. Ja pari lisää.
Huomaa siis Javan knoppologia:
int i=68; int j=2; System.out.println("arvo on " + i + j + 3); // arvo on 6823 System.out.println(i + j + 3 + " on arvo"); // 73 on arvo System.out.println("arvo on " + (i + j + 3)); // arvo on 73 System.out.println(i + j + (3 + " on arvo")); // 703 on arvo
Merkkijonokatenointi on toki käytettävissä myös merkkijonomuuttujien käsittelyssä:
String kissa = "topi"; String hanta = "katti"; String koko = kissa + hanta; System.out.println(koko); // tulostaa topikatti
Ohjelmoinnissa – ja tietojenkäsittelytieteessä yleensäkin – laskennaksi kutsutaan melkeinpä mitä tahansa toimintaa, jossa arvoista tavalla tai toisella muokataan uusia arvoja. Nyt tarkastellaan numeerista laskentaa eli aritmetiikkaa.
Aritmeettiset operaatiot ovat tuttuja: +
, -
,
*
ja /
.
Erikoisempi operaatio on %
, joka on jäännösjako,
jakolaskun jakojäännös eli modulo.
Myös laskentajärjestys on tuttu: kertolaskut ennen yhteenlaskuja,
suluissa olevat ennen muita, ...
int eka = 2; int toka = 4; int kolm = 6; int arvo = eka + toka * kolm; System.out.println(arvo); // tulostaa 26
int sulut = (1+1) + 3 * (2+5) // 23 int suluitta = 1+1 + 3 * 2+5 // 13
Laskutoimituksia kirjoitetaan usein myös tulostuslauseeseen:
System.out.println((1+1) + 3 * (2+5)); // tulostaa 23 System.out.println(5 / 2) // tulostaa 2 System.out.println(19 / 5) // tulostaa 3 System.out.println(5 % 2) // tulostaa 1 System.out.println(19 % 5) // tulostaa 4
Desimaalilukujen (tai reaalilukujen) esittämiseen tietokoneissa käytetään ns. liukulukuja (floating point number). Yksinkertaistaen sanottuna ne esittävät reaaliluvun likiarvon kahtena kokonaislukuna: "merkitsevät numerot" ja "pisteen paikka". Tärkeää on muistaa, että tietokoneen liukuluku on aina likiarvo, ei "tarkka arvo". Asian voisi selittää vaikkapa sillä, että "merkiteseviä numeroita" voi tietokoneessa olla vain äärellinen määrä.
Javan liukuluvun tyyppi on double:
int x = 0; // x on kokonaislukutyyppinen double y = 0.0; // y on liukuluku System.out.println(x + " " + y) // tulostaa 0 0.0 x = 123; y = 1.23; println(x + " " + y) // tulostaa 123 1.23 // x = y; // VIRHE: double-arvoa ei voi sijoittaa int-muuttujaan // mutta y = x; System.out.println(x + " " + y) // tulostaa 123 123.0
Jakolaskuoperaation "/
" merkitys – onko kyse kokonaisjakolaskusta
vai liukulukujakolaskusta – määräytyy siten, että jos jaettava, jakaja
tai molemmat ovat liukulukuja, kyseessä on liukulukujakolasku.
Kokonaisjako on kyseessä vain, jos sekä jakaja että jaettava ovat kokonaislukuja:
System.out.println(5 / 2) // 2 System.out.println(5.0 / 2) // 2.5 System.out.println(5 / 2.0) // 2.5 System.out.println(5.0 / 2.0) // 2.5
Joskus ohjelmoija haluaa kahden kokonaisluvun jakolaskun tulokseksi liukuluvun. Yksinkertainen tapa on ennen jakolaskua muuttaa jaettava liukuluvuksi kertomalla se luvulla 1.0:
int summa = 55; int lukumaara = 20; System.out.println("keskiarvo on " + summa/lukumaara); // 2 System.out.println("keskiarvo on " + 1.0*summa/lukumaara); // 2.75 // mutta System.out.println("keskiarvo on " + summa/lukumaara*1.0); // 2.0 // tosin toki System.out.println("keskiarvo on " + summa/(lukumaara*1.0)); // 2.0
Ohjelman tulostus:
keskiarvo on 2 keskiarvo on 2.75 keskiarvo on 2.0 keskiarvo on 2.75
---> Lisätietoa kokonaisluvuista, liukuluvuista ja tyyppimuunnoksista
Usein muuttujan uusi arvo lasketaan sen vanhasta vanhasta arvosta:
int pituus = 98; pituus = pituus + 2; pituus = pituus - 50; pituus = pituus * 2; pituus = pituus / 2; System.out.println(pituus); // 50
Tällaisessa melko tavanomaisessa tilanteessa voi käyttää erityisiä sijoitusoperaatioita +=, -=, *=, /=. Näiden avulla yllä näkyvät sijoitukset voi ohjelmoida hieman lyhyemmin:
pituus += 2; pituus -= 50; pituus *= 2; pituus /= 2; System.out.println(pituus); // 50
Useimmiten on kuitenkin selkeämpää kirjoittaa sijoitusoperaatiot tavanomaisessa muodossa!
Kokonaislukumuuttujan kasvattaminen tai vähentäminen yhdellä on algoritmeissa niin tuiki tavanomaista, että tähän tarkoitukseen Javassa ja sen edeltäjissä on käytössä erityiset ilmaukset i++, ++i, i-- ja --i:
int i = 1; i++; // seuraus sama kuin sijoituksella i = i + 1; ++i; // seuraus sama kuin sijoituksella i = i + 1; i--; // seuraus sama kuin sijoituksella i = i - 1; --i; // seuraus sama kuin sijoituksella i = i - 1;
Näitä käytetään usein.
Nykyään useimmat tavalliselle tietokoneen käyttäjälle tarkoitetut ohjelmat perustuvat graafiseen käyttöliittymään (GUI) ja tapahtumaohjattuun ohjelmointiin. Tällaisen käyttöliittymän ohjelmoinnin ideaan tutustutaan jatkokurssilla.
Hyvin perinteinen ja "vanhanaikainen" käyttäjävuorovaikutus perustuu siihen, että sekä ohjelman tulosteet ja käyttäjän antamat syötteet toteutetaan käyttöjärjestelmän komentotulkissa, sellaisessa ikkunassa, jossa mm. kurssin alun ohjelmaesimerkki käännettiin ja suoritettiin. Myös tutuksi tullut System.out.println() tulostaa juuri tuohon ikkunaan. Javassa on toki välineitä myös lukea syötteet tästä komentotulkki-ikkunasta. Näiden käytössä on omat pikku kömpelyytensä, mutta tällaiseen "perinneohjelmointiinkin" tutustutaan kurssilla aikanaan.
Mutta aluksi käytämme ohjelman käyttäjän kanssa keskusteluun ponnahdusikkunoita (pop up windows). Olen toteuttanut Javan kirjastoluokkana joukon helppokäyttöisiä operaatioita ikkunoiden ponnahdutteluun: Pop.java. Tällainen välinekirjasto on siis eräänlainen työkalupakki.
Saadaksesi ponnahdusikkunatyökalut käyttöösi riittää sijoittaa tuo lähdekielinen Java-luokka samaan hakemistoon kuin luokka, jossa välineitä haluat käyttää. Kääntäjä osaa kääntääkin tuon työkaluluokan tarvittaessa automaattisesti. Muitakin – tyylikkäämpiä? – tapoja ottaa nämä välineet käyttöön on toki olemassa. NetBeansin käyttäjät saavat harjoitusten ohjaajilta neuvot, miten asia tuossa ympäristössä hoidellaan.
Seuraavassa luetellaan ensin lyhyesti "poppikoneen" osaamat operaatiot. Myöhemmin opetellaan operaatioiden monipuolisempaa käyttöä. (Operaatioilla on myös englantilaiset nimet, jos jotakuta suomi ei miellytä.)
Ohjelman käyttäjälle voi kirjoittaa erilaisia ilmoituksia operaatiolla ilmoita(viesti). Komennolle annetaan ns. parametrina (eräänlaisena lähtötietona) haluttu ilmoitus merkkijonona eli String-arvona aivan samaan tapaan kuin jo on opittu kirjoittamaan komentotulkin ikkunaan System.out.println-operaatiolla.
Pop.ilmoita("Ilmoitus on tällainen." + "\n" + "Se on kuitattava ok-nappulalla.");
Lauseen suoritus ponnauttaa näkyville ikkunan:
Rivejä voi olla enemmänkin:
Pop.ilmoita("Ilmoituksessa voi olla useitakin rivejä" + "\n" + "Rivinvaidot pitää itse ilmaista \\n-merkillä." + "\n" + "Pitkiäkin rivejä voi kirjoittaa. " + "Pitkiäkin rivejä voi kirjoittaa. " + "Pitkiäkin rivejä voi kirjoittaa. " + "\n" + "Tuohon tyyliin.." );
Merkkijonoa pyydetään operaatiolla kysyString(kysymys). Esimerkiksi:
String vastaus = Pop.kysyString("Anna jokin merkkijono!"); Pop.ilmoita("Se oli: " + vastaus);
Kun käyttäjä on kirjoittanut merkkijonon, hän voi painaa OK-nappulaa tai näppäimistön enter-näppäintä, jolloin ohjelma saa annetun merkkijonon käyttöönsä. (Cancel-nappulasta myöhemmin.)
Kokonaislukujen lukeminen:
int luku = Pop.kysyInt("Anna kokonaisluku."); Pop.ilmoita("Luku oli " + luku);
Liukulukujen lukeminen:
double liuku = Pop.kysyDouble("Anna desimaaliluku."); Pop.ilmoita("Luku oli " + liuku);
Laaditaan valaisevaksi esimerkiksi pieni tekoälyohjelma:
public class Viisas { public static void main(String[] args) { String nimi; int ika; double pituus; nimi = Pop.kysyString("Mikä on nimesi?"); ika = Pop.kysyInt("Ja ikäsi: "); pituus = Pop.kysyDouble("Entä pituutesi? "); Pop.ilmoita("Moi " + nimi + "!\n" + "Tiedän että olet " + ika + "-vuotias \n" + "ja että olet " + pituus + " senttiä pitkä.\n" + "Enkö olekin viisas!"); } }
Käyttöesimerkki:
Vaihtoehtoja voi kysellä operaatiolla valitseNappula(kysymys, valinnat). Ensimmäinen parametri on kysymys. Seuraavat parametrit määräävät ponnahdusikkunaan tulevat nappulat. Näitä on yksi tai enemmän.
Tuttu "kyllä vai ei" -kysymys voidaan ohjelmoida näin:
int vast = Pop.valitseNappula("Kyllä vai ei?", "kyllä", "ei"); Pop.ilmoita("Vastauksen indeksi on " + vast);
Operaatio palauttaa nappulan ns. indeksin, tiedon siitä, monettako nappulaa painettiin. Huomaa että nappulat on numeroitu nollasta alkaen.
Esimerkki kolmen nappulan käytöstä:
int lemmikki = Pop.valitseNappula("Mikä on lemmikkisi?", "kissa", "hiiri", "koira"); Pop.ilmoita("Valintasi indeksi on " + lemmikki);
Valitaan koira:
Valintavaihtoehtoja voi olla hyvinkin paljon:
int maa = Pop.valitseNappula("Mikä maa?", "Suomi", "Ruotsi", "Venäjä", "Norja", "Tanska", "Islanti", "Saksa", "Ranska", "Hollanti", "Belgia", "Puola", "Itävalta", "Sveitsi", "Italia", "Espanja"); Pop.ilmoita("Valintasi indeksi on " + maa);
Valitaan vaikkapa Islanti:
Tällaisessa tapauksessa on kuitenkin ehkä järkevämpää käyttää seuraavaksi esiteltävää ponnahdusikkunan vaihtoehtoa.
Vaihtoehtoja voi kysellä myös alasvetovalikon tarjoavalla ponnahdusikkunalla. Operaatio on valitseString(kysymys, valinnat). Kuten nappulavaihtoehdossa myös tässä ensimmäinen parametri on kysymys ja seuraavat parametrit määräävät valintavaihtoehdot.
String lempi = Pop.valitseString("Mikä on lemmikkisi?", "kissa", "hiiri", "koira"); Pop.ilmoita("Valintasi on " + lempi);
Operaatio palauttaa arvonaan nappulavaihtoehdosta poiketen valitun vaihtoehdon merkkijonona eli String-arvona.
Valitaan taas koira:
Kun vaihtoehtoja on paljon, alasvetovalikko on luonnollinen ratkaisu:
String v = Pop.valitseString("Mikä maa?", "Suomi", "Ruotsi", "Venäjä", "Norja", "Tanska", "Islanti", "Saksa", "Ranska", "Hollanti", "Belgia", "Puola", "Itävalta", "Sveitsi", "Italia", "Espanja"); Pop.ilmoita("Valintasi on " + v);
Tähän mennessä nähdyt ohjelmat ovat olleet vähän tylsiä: ne on suoritettu aina samalla tavoin ensimmäisestä lauseesta viimeiseen. Valintalauseen avulla algoritmin toiminta saadaan toimimaan vaihtoehtoisilla tavoilla riippuen vaikkapa syötteiden arvosta.
Useimmissa ohjelmointikielissä valintalause on muodoltaan if-lause:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) Pop.ilmoita("Luku on positiivinen.");
If-lauseen ehto (luku > 0)
on väittämä algoritmin tilasta. Kun algoritmi etenee tuon ehdon laskentaan, tutkitaan juuri sillä hetkellä,
sattuuko muuttujan luku arvo olemaan positiivinen. Jos sattuu, suoritetaan
if-lauseen alilause Pop.ilmoita("Luku on positiivinen.")
.
Tällöin ehdon totuusarvo on true
. Jos ehto ei ole tosi eli jos väittämä
ei pidä paikkaansa (false
), alilausetta ei suoriteta.
If-lauseeseen voi liittää myös else-osan, jos haluaa jotakin tehtäväksi myös tilanteessa, jossa ehto on epätosi:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) Pop.ilmoita("Luku on positiivinen."); else Pop.ilmoita("Luku ei ole positiivinen.");
Usein if-lauseita ketjutetaan:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) Pop.ilmoita("Luku on positiivinen."); else if (luku < 0) Pop.ilmoita("Luku on negatiivinen."); else Pop.ilmoita("Ahaa, luku on nolla."); // ei ollut positiivinen eikä negatiivinen...
Loogisilla lausekkeilla esitetään väitteitä ohjelman tilasta eli ohjelman muuttujien arvoista. Väitteiden totuusarvoa - totta tai ei totta - käytetään ohjaamaan algoritmin suoritusta. Loogisia lausekkeita (eli totuusarvoisia lausekkeita) muodostetaan tavallisimmin erilaisista vertailuista, joita mahdollisesti liitetään toisiinsa loogisilla operaatioilla.
Tavallisimpia vertailuoperaatioita:
>
suurempi kuin>=
suurempi tai yhtäsuuri kuin<
pienempi kuin<=
pienempi tai yhtäsuuri kuin==
yhtäsuuri kuin!=
erisuuri kuinKaikki vertailut tuottavat totuusarvon true tai false.
On syytä pitää mielessa sijoitusoperaation "=" ja vertailuoperaation "==" ero:
i = 1; // "i saa arvokseen yksi" i == 1 // "onko i:n arvo yksi"
Suuremmuutta tai pienemmyyttä tutkivilla operaatioilla voi vertailla vain numeerisia lausekkeita, yhtäsuuruus ja erisuuruus ovat käytettävissä monien muidenkin arvojen vertailussa.
Esimerkki: Olkoon ohjelmassa vaikkapa seuraavat määrittelyt:
int i = 7; int d = 3.14; boolean b = false;
Tässä tilanteessa looginen lauseke
(i < 10) on arvoltaan true (d > 10.0) on arvoltaan false (8 == (i+1)) on arvoltaan true (b != true) on arvoltaan true // boolean muuttujan arvo voi olla vain true tai false
Huom: Liukulukujen (so. desimaalilukujen) vertailussa ei ole syytä käyttää yhtäsuuruusvertailuja (== ja !=), koska ns. pyöristysvirheiden takia yhtäsuuruusvertailut voivat antaa odottamattomia tuloksia. Tällaisessa tilanteessa on parempi tutkia lukujen erotuksen itseisarvon pienuutta. Sen voi tehdä vaikkapa seuraavaan tyyliin:
double a, b; // .. muuttujat saavat arvot, ei tiedetä kumpi on suurempi if (Math.abs(a-b) < 0.001) // Math.abs laskee itseisarvon // tilanne että arvot ovat riittävän samat else // tilanne että arvot eivät riittävän samat
Huom: String-arvojen eli merkkijonojen vertailu ei Javalla onnistu yhtä vaivattomasti kuin numeeristen arvojen vertailu. Yritetään:
String s = "hupsista"; ... if (s != "hupsista") ... sallittu, mutta voi olla true tai false if (s == "hupsista") ... sallittu, mutta voi olla true tai false
Näin saa kyllä kirjoittaa – se on "oikein" – mutta tarkoittaa luultavasti
jotain muuta kuin ohjelmoija tahtoi tarkoittaa:
Javassa kaikkien olioarvojen yhtäsuuruus- ja erisuuruusvertailut tarkoittavat
vastausta kysymykseen "onko kyseessä yksi ja sama olio". Ja on täysin mahdollista,
että muuttuja s viittaa eri merkkijono-olioon, jonka sisältö sattuu
olemaan myös "hupsista"
! Tässä tilanteessa ehto luultavasi siis tarkoittaa jotakin
muuta kuin ohjelmoija tahtoi tarkoittaa.
Java-ohjelmassa merkkijonojen samansisältöisyyttä tutkitaan String-luokan aksessorimetodilla equals:
if (s.equals("hupsista")) ... on arvoltaan true // NÄIN VERTAILLAAN if (s.equals("muumi")) ... on arvoltaan false // MERKKIJONOJEN SAMUUTTA!
Olioista ja niiden vertailuista lisää myöhemmin. Toistaiseksi riittää vain muistaa operaatio equals.
Usein on tarpeen liittää algoritmin tilaa koskevia väittämiä yhteen monimutkaisemmiksi väittämiksi. Käytettävissä ovat mm. seuraavat loogiset operaatiot:
&&
ehdollinen "ja" (and)
(ehto_1) && (ehto_2)
on tosi, jos molemmat
ehdot ovat totta, muuten se on epätosi
||
ehdollinen "tai" (or)
(ehto_1) || (ehto_2)
on tosi, jos jompikumpi
ehto on tosi tai jos molemmat
ehdot ovat tosia, muuten se on epätosi
!
negaatio (not)
!(ehto)
on epätosi, jos (ehto)
on tosi,
muuten se on tosi
Näiden operaatioiden operandit eli laskettavat voivat olla vain totuusarvoisia lausekkeita, tavallisimmin ne ovat vertailuja. Operaatioiden ehdollisuus tarkoittaa sitä, että operaation jälkimmäinen laskettava lasketaan vain, jos totuusarvo ei selviä jo ensimmäisestä!
int luku = Pop.kysyInt("Anna luku väliltä 5-10."); if ( 5 <= luku && luku <= 10 ) // HUOM: ILMAUS (5 <= luku <= 10) EI OLE LUVALLINEN! Pop.ilmoita("Oikein!"); else Pop.ilmoita("Luku ei kelpaa.");
int vastaus = Pop.kysyInt("Valitse joko 1, 50 tai 100."); if ( vastaus == 1 || vastaus == 50 || vastaus == 100 ) Pop.ilmoita("Valitsit ohjeen mukaisesti."); else Pop.ilmoita("Valitsit väärin.");
Loogisien operaatioiden totuustaulukko (a ja b voivat itse olla mitä tahansa loogisia lausekkeita, esimerkiksi vertailuja):
a | b || a && b | a || b | !a -------------------------------------------------------- true | true || true | true | false true | false || false | true | false false | true || false | true | true false | false || false | false | true
Javassa on myös "jompikumpi mutta eivät molemmat" -operaatio, ns. "xor" eli "poissulkeva tai". Sen merkki on "^". Tuo merkki ei siis Javassa tarkoita potenssiin korottamista.
Hyvin usein if-lauseen alilauseeksi if-osaan tai else-osaan halutaan useampi kuin yksi lause.
Tällöin nuo lauseet kootaan yhdeksi lauseeksi, lohkoksi, aaltosulkeiden
"{
" ja "}
" väliin:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) { Pop.ilmoita("Luku on positiivinen."); Pop.ilmoita("Kaksinkertaisena se on " + 2*luku); } else { Pop.ilmoita("Luku ei ole positiivinen."); Pop.ilmoita("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku); }
Huomaa että nuo aaltosulkeet ovat välttämättömät! Pelkkä sisentäminen ei riitä. Seuraava on siis väärin:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) // TÄMÄ ON VIRHEELLINEN! Pop.ilmoita("Luku on positiivinen."); // AALTOSULUT PUUTTUVAT! Pop.ilmoita("Kaksinkertaisena se on " + 2*luku); else Pop.ilmoita("Luku ei ole positiivinen."); Pop.ilmoita("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku);
Jotkut suosivat seuraava ulkoasua:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) { Pop.ilmoita("Luku on positiivinen."); Pop.ilmoita("Kaksinkertaisena se on " + 2*luku); } else { Pop.ilmoita("Luku ei ole positiivinen."); Pop.ilmoita("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku); }
Myös seuraavaa else-layout on mahdollinen:
if (luku > 0) { ... } else { ... }
Useimmissa ohjelmointikielissä ohjelman ulkoasulla ei ole kääntäjälle viestiä. Niinpä kääntäjälle kelpaa jopa seuraava sotku — sinänsä täysin toimiva ohjelma:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) {Pop.ilmoita("Luku on positiivinen."); Pop.ilmoita("Kaksinkertaisena se on " + 2*luku); } else {Pop.ilmoita("Luku ei ole positiivinen."); Pop.ilmoita("Mutta itsellään kerrottuna se ei ainakaan ole negatiivinen: " + luku*luku); }
Mutta ohjelmoijalle ohjelman selkeä ja johdonmukainen ulkoasu on välttämätön! Miksi?
Samaan tapaan kuin väittämät algorimin tilasta ohjaavat valintaa if-lauseissa, väittämiä käytetään myös ohjaamaan algoritmin jonkin osan toistamista.
Ehdollinen toistolause, while
-lause, toistaa alilausettaan
yhä uudelleen ja uudelleen kunnes toistoehto ei enää ole totta.
Toki on mahdollista, että ehto alkujaankin on epätosi, jolloin toistoja
ei ole ainuttakaan:
int lukumaara = Pop.kysyInt("Moneenko kertaan haluat onnentoivotuksen?"); while (lukumaara > 0) { Pop.ilmoita("Onnea!"); lukumaara = lukumaara - 1; }
Toistolauseen alussa tutkitaan, onko väittämä (lukumaara > 0)
totta.
Jos se on, koko toistettava alialgoritmi suoritetaan. Sen jälkeen taas tutkitaan,
onko ehto voimassa, jne...
Huomaa että toistettavan alialgoritmin on syytä vaikuttaa jotenkin
toistoehdon arvoon! Miksi?
Toinen esimerkki: Vaaditaan että käyttäjä syöttää parittoman luvun. Luku on pariton, jos se ei ole parillinen. Luku on parillinen, jos jakojäännös kahdella jaettaessa on nolla (vaihteeksi kokonainen ohjelma):
public class Pariton { public static void main(String[] args) { int luku = Pop.kysyInt("Anna pariton luku."); while (luku % 2 == 0) luku = Pop.kysyInt("Eihän luku " + luku + " ole pariton!\n" + "Yritä uudelleen"); Pop.ilmoita("Kyllä! Luku " + luku + " todellakin on pariton."); } }
Mikään ei estä käyttämästä tulostukseen myös System.out.println-operaatiota. Ehkäpä onnentoivotusohjelma tällaisena olisi luontevampi?
int lukumaara = Pop.kysyInt("Moneenko kertaan haluat onnentoivotuksen?"); while (lukumaara > 0) { System.out.println("Onnea!"); lukumaara = lukumaara - 1; }
Kun ohjelmalle syöttää vaikkapa luvun neljä, ohjelman käynnistysikkunaan tulostuu seuraavaa:
Onnea! Onnea! Onnea! Onnea!
Laaditaan vielä hyötyohjelma, jolla voi tupalata (kertoa kahdella) minkä tahansa luvun paitsi nollan. Nolla toimii ns. lopetusmerkkinä, jolla ohjelmalle ilmaistaan halu lopettaa lukujen tuplaaminen:
public class Tuplaa { public static void main(String[] args) { int luku = Pop.kysyInt("Lukujen tuplausohjelma:\n" + "Anna luku! Nolla lopettaa."); while (luku != 0) { Pop.ilmoita("Luku " + luku + " tuplana on " + luku*2); luku = Pop.kysyInt("Anna luku! Nolla lopettaa."); } Pop.ilmoita("Moi!"); } }
Alkuehtoinen toisto tarkoittaa siis sitä, että toistamista vahtiva ehto lasketaan aina ennen jokaista toistokertaa. Usein on luontevaa toimia toisin: suorittaa aina yksi toisto ja sen jälkeen tutkia, tarvitaanko uusi toisto. Tähän tarkoituksiin useimmissa ohjelmointkielissä on käytössä ns. loppuehtoinen toisto. Javassa se on nimeltään do-while-lause.
Loppuehtoinen toisto on erityisen luonteva syöttötietojen tarkastamisessa. Edellä nähty ohjelma, joka vaati paritonta syöttölukua sujuu loppuehtoisella toistolla seuraavaan tapaan:
public class Pariton { public static void main(String[] args) { int luku; do { luku = Pop.kysyInt("Anna pariton luku."); if (luku % 2 == 0) luku = Pop.kysyInt("Eihän luku " + luku + " ole pariton!\n" + "Yritä uudelleen"); } while (luku % 2 == 0); Pop.ilmoita("Kyllä! Luku " + luku + " todellakin on pariton."); } }
Tässä ohjelmassa moni ohjelmoija näkee pienen vian: ehto (luku % 2 == 0) lasketaan joka toistokerralla kahteen kertaan. Tietokonehan todella tekee töitä myös ehtoja laskiessaan.
Tässä esimerkissä onkin oivallinen tilaisuus käyttää totuusarvoista boolean-muuttujaa:
public class Pariton { public static void main(String[] args) { int luku; boolean parillinen; do { luku = Pop.kysyInt("Anna pariton luku."); parillinen = (luku % 2 == 0); if (parillinen) luku = Pop.kysyInt("Eihän luku " + luku + " ole pariton!\n" + "Yritä uudelleen"); } while (parillinen); Pop.ilmoita("Kyllä! Luku " + luku + " todellakin on pariton."); } }
Alkuehtoinen ja loppuehtoinen toisto löytyvät miltei kaikista ohjelmointikielistä. Ne molemmat perustuvat toistettavan alialgoritmin suorittamiseen aina kokonaan.
Melko usein algoritmeissa kuitenkin esiintyy tilanne, jossa toistettava alialgoritmi on luontevinta lopettaa "puolessa välissä" eli viimeistä toistokertaa ei suoritekaan loppuun asti vaan keskeytetään heti, kun havaitaan toiston tarve päättyneeksi. Tällaista toistorakennetta kutsutaan joskus "1.5-kertaiseksi toistoksi."
Harvoissa ohjelmointikielissä on erityistä omaa lausetyyppiään tähän tarkoitukseen, mutta esimerkiksi Javassa vastaava toiminnallisuus voidaan ohjelmoida käyttäen näennäisesti "ikuista" toistoa while (true) ja erityistä keskeytyslausetta break.
Tarkastellaan esimerkkinä edellä nähtyjä parittomuustarkistuksia alku- ja loppuehtoisella toistolla ohjelmoituna.
Alkuehtoinen toisto:
int luku = Pop.kysyInt("Anna pariton luku."); while (luku % 2 == 0) luku = Pop.kysyInt("Eihän luku " + luku + " ole pariton!\n" + "Yritä uudelleen"); Pop.ilmoita("Kyllä! Luku " + luku + " todellakin on pariton.");
Loppuehtoinen toisto:
int luku; do { luku = Pop.kysyInt("Anna pariton luku."); if (luku % 2 == 0) luku = Pop.kysyInt("Eihän luku " + luku + " ole pariton!\n" + "Yritä uudelleen"); } while (luku % 2 == 0); Pop.ilmoita("Kyllä! Luku " + luku + " todellakin on pariton.");
"Puolitoistakertaisella" toistolla sama voidaan ohjelmoida tähän tyyliin:
int luku; while (true) { ///// toisto keskeytetään break-lauseella luku = Pop.kysyInt("Anna pariton luku."); if (luku % 2 != 0) break; /////////// toiston keskeytys ///////////// Pop.ilmoita("Eihän luku " + luku + " ole pariton!\n" + "Yritä uudelleen"); } Pop.ilmoita("Kyllä! Luku " + luku + " todellakin on pariton.");
Ohjelman toiminnan hahmottamisen helpottamiseksi on syytä kommentein kororostaa keskytyskohta!
Nyt kun ehtojen avulla on opittu ohjaamaan algoritmin toimintaa, on aika alkaa tutkia ponnahdusikkunoista saatavia syötteitä tarkemmin.
Edellä nähdyt ponnahdusikkunaoperaatiot ovat ns. metodeita, nimettyjä algoritmeja. Tällaisia opitaan pian tekemään itsekin, mutta jo nyt on syytä tarkastella näiden metodeiden ns. otsakkeita, joista näkee, millä tavoin metodeita kutsutaan eli miten ne käynnistetään.
public void ilmoita(...); public String kysyString(String kysymys); public int kysyInt(String kysymys); public double kysyDouble(String kysymys); public int valitseNappula(String kysymys, String... valinnat); public String valitseString(String kysymys, String... valinnat);
Metodin ns. paluuarvon tyyppi näkyy ennen metodin nimeä: String, int, double. Sana void tarkoittaa, että metodilla ei ole paluuarvoa, se on ikäänkuin pelkkä käsky tai komento tehdä jotakin; "näytä ilmoitus ponnahdusikkunassa", tms. Niinpä metodista ilmoita ei tässä yhteydessä sen enempää.
Koska metodi kysyString palauttaa String-arvon eli merkkijonon, metodin palauttamaa arvoa voidaan tutkia seuraavaan tapaan:
String vastaus = Pop.kysyString("Mikä oli Jaakobin poikien isän nimi?"); if (vastaus.equals("Jaakob")) Pop.ilmoita("Vastauksesi " + vastaus + " on oikein!"); else Pop.ilmoita("Vastauksesi " + vastaus + " on väärin!");
Käyttöesimerkki:
Metodi kysyString sallii myös tyhjän merkkijonon syöttämisen. Painetaan siis OK tai enter kirjoittamatta mitään tekstiikenttään. Silloinkin ohjelma tulostaa:
Jos tyhjää merkkijonoa ei haluta kelpuuttaa vastaukseksi, asia voidaan hoidella seuraavaan tyyliin:
String vastaus = Pop.kysyString("Anna merkkijono"); if (vastaus.equals("")) // käsittele virhetilanne else // merkkijono ei ollut tyhjä.
Vaihtoehtoisesti voidaan tutkia merkkijonon pituutta erityisellä length-metodilla:
String vastaus = Pop.kysyString("Anna merkkijono"); if (vastaus.length() == 0) // käsittele virhetilanne else // merkkijono ei ollut tyhjä.
Jaakob-esimerkin tapauksessa voitaisiin ohjelmoida vaikkapa seuraavaan tapaan:
String vastaus = Pop.kysyString("Mikä oli Jaakobin poikien isän nimi?"); if (vastaus.equals("")) Pop.ilmoita("Tyhjästä on paha nyhjästä!"); else if (vastaus.equals("Jaakob")) Pop.ilmoita("Vastauksesi " + vastaus + " on oikein!"); else Pop.ilmoita("Vastauksesi " + vastaus + " on väärin!");
Kuten kaikissa kunnon ikkunoissa, myös kysyString-ikkunassa on yläpalkissa keskeytysrasti. Jos sitä tai Cancel-nappia painetaan, metodi ei palauta edes tyhjää String-arvoa vaan erikoisarvon null, joka tarkoittaa, ettei mitään varsinaista arvoa saatu. Koska null "ei ole mitään", siihen ei voi soveltaa esimerkiksi String-luokan metodia equals eikä mitään muutakaan metodia. Jos yrittää, huonosti käy: Jaakob-ohjelman käynnistysikkunaan ilmestyy jokin seuraavanlainen virheilmoitus ja ohjelman suoritus päättyy siihen paikkaan:
Exception in thread "main" java.lang.NullPointerException at koe.main(koe.java:8)
Ei ole kovin mukavaa, kun ohjelma ns. "kaatuu"! Jos kaatumisen tässä tilanteessa tahtoo estää, se käy vaikkapa tähän tapaan:
String vastaus = Pop.kysyString("Anna merkkijono"); if (vastaus == null) // käsittele tilanne, jossa ei saatu mitään merkkijonoa else // saatiin String, jolle voi soveltaa kaikkia String-operaatioita
Jaakob-esimerkki voisi näyttää vaikka tältä:
String vastaus = Pop.kysyString("Mikä oli Jaakobin poikien isän nimi?"); if (vastaus == null) Pop.ilmoita("Keskeytys!"); // Riippuu tilanteesta, mitä sitten tehdään! else if (vastaus.equals("")) Pop.ilmoita("Tyhjästä on paha nyhjästä!"); else if (vastaus.equals("Jaakob")) Pop.ilmoita("Vastauksesi " + vastaus + " on oikein!"); else Pop.ilmoita("Vastauksesi " + vastaus + " on väärin!");
Klikataan Cancel-nappulaa tai keskeytysrastia:
Numeerisia syötteitä kyselevät operaatiot kysyInt ja kysyDouble ovat sitkeitä; ne eivät anna periksi ennen kunnon luvun syöttämistä. Edes Cancel tai yläpalkin keskeytysrasti eivät lopeta niiden intoa saada oikeanlainen numeerinen syöte. Kokeile!
Kuten jo edellä nähtiin, if-lauseella on (tietenkin) paljon käyttöä lukujen vertailussa:
int luku = Pop.kysyInt("Anna kokonaisluku!"); if (luku > 0) Pop.ilmoita("Luku on positiivinen."); else if (luku < 0) Pop.ilmoita("Luku on negatiivinen."); else Pop.ilmoita("Ahaa, luku on nolla."); // ei ollut positiivinen eikä negatiivinen...
Metodi valitseNappula on tyypiltään int. Idea on että ponnahdusikkunassa näkyvät nappulat on numeroitu nollasta alkaen vasemmalta oikealle. Metodi palauttaa arvonaan painetun nappulan järjestysnumeron eli indeksin.
Kuten edellä jo nähtiin valinnan "koira" indeksi oli 2:
Indeksiä voi tietenkin tutkia ja ohjata ohjelman toimintaa sen mukaan:
int lemmikki = Pop.valitseNappula("Mikä on lemmikkisi?", "kissa", "hiiri", "koira"); if (lemmikki == 0) Pop.ilmoita("Miau"); else if (lemmikki == 2) Pop.ilmoita("Hau"); // hiiri on hiljaa
Kun nyt valitaan "koira", saadaan ilmoitus:
Jos käyttäjä keskeyttää ikkunan käytön keskeytysrastilla, metodi palauttaa arvonaan luvun -1. Sitäkin voi tietysti tutkia:
int lemmikki = Pop.valitseNappula("Mikä on lemmikkisi?", "kissa", "hiiri", "koira"); if (lemmikki == -1) Pop.ilmoita("Okei, okei, et siis valitse mitään!"); if (lemmikki == 0) Pop.ilmoita("Miau"); else if (lemmikki == 2) Pop.ilmoita("Hau");
Keskeytetään:
Metodi valitseString puolestaan on tyyppiä String. Edellisestä metodista poiketen se palauttaa arvonaan valitun merkkijonon:
String maa = Pop.valitseString("Mikä maa on Suomen naapuri?", "Suomi", "Ruotsi", "Venäjä", "Norja", "Tanska", "Islanti", "Saksa", "Ranska", "Hollanti", "Belgia", "Puola", "Itävalta", "Sveitsi", "Italia", "Espanja"); if (maa.equals("Ruotsi") || maa.equals("Venäjä") || maa.equals("Norja") ) Pop.ilmoita(maa + " on Suomen naapuri."); else Pop.ilmoita(maa + " ei ole Suomen naapuri.");
Käyttöesimerkki:
Valitaan Ruotsi:
Valitaan Sveitsi:
Jos painetaan Cancel-nappulaa tai keskeytysrastia, operaatio valitseString palauttaa arvonaan jo aiemmin nähdyn erikoisarvon null, joka on siis ilmaus siitä, että mitään ei valittu. Kun tuolle arvolle yritetään soveltaa equals-metodia, ohjelman suoritus päättyy virheeseen:
Exception in thread "main" java.lang.NullPointerException at koe.main(koe.java:12)
Virhetilanteeseen joutuminen voidaan välttää vaikkapa seuraavaan tapaan:
String maa = Pop.valitseString("Mikä maa on Suomen naapuri?", "Suomi", "Ruotsi", "Venäjä", "Norja", "Tanska", "Islanti", "Saksa", "Ranska", "Hollanti", "Belgia", "Puola", "Itävalta", "Sveitsi", "Italia", "Espanja"); if (maa == null) // Riippuu tilanteesta, mitä sitten ihan oikeasti tehdään! Pop.ilmoita("Ai ettei kiinnosta vastata vai!?"); else if (maa.equals("Ruotsi") || maa.equals("Venäjä") || maa.equals("Norja") ) Pop.ilmoita(maa + " on Suomen naapuri."); else Pop.ilmoita(maa + " ei ole Suomen naapuri.");
Useimmissa ohjelmointikielissä on erityinen lause, joka soveltuu erityisen hyvin ns. taulukon alkioiden läpikäymiseen. Taulukoista kohta lisää. Monissa kielissä tuo lause on nimeltään for-lause. Niin myös Javassa:
for (int i = 5; i < 11; i = i+1) System.out.print(i + " "); // 5 6 7 8 9 10 System.out.println(); // rivinvaihto for (int i = 10; i > 4; i = i-1) System.out.print(i + " "); // 10 9 8 7 6 5 System.out.println(); // rivinvaihto
Selityksiä:
for (int i = 5; i < 11; i = i+1)
true
, toistettava alialgoritmi suoritetaan, jos false
,
alialgoritmia ei toisteta.
for (int i = 5; i < 11; ++i) for (int i = 10; i > 4; --i)Tai
for (int i = 5; i < 11; i++) for (int i = 10; i > 4; i--)
(Näillä kahdella tavalla ei tässä tilanteessa ole merkityseroa. On olemassa tilannne jossa merkitykset eroavat, mutta se tilanne ei liity hyvään ohjelmointityyliin.)
//Tulostetaan luvun 3.14:n kahtakymmentä pienemmät monikerrat: for (double piit = 3.14; piit < 20; piit+=3.14) System.out.println(piit);Kun tämä suoritetaan, saadaan tulostus:
3.14 6.28 9.42 12.56 15.700000000000001 18.84
[Liukuluvut ovat aina likiarvoja ja yllä tapahtuu ns. pyöristysvirhe (---> lisätietoa pyöristysvirheestä).]
Vaihtelevaan määrään syöttötietoja voidaan varautua eri tavoin. Tarkastellaan esimerkkinä ohjelmaa, joka laskee syötteinä saamiensa kokonaislukujen summan. Tehdään ohjelmasta kolme erilaista versiota:
public class Summa1 { public static void main(String[] args) { int lukuja = Pop.kysyInt("Monenko luvun summa lasketaan?"); int summa = 0; for (int i= 1; i<=lukuja; i++) { int luku = Pop.kysyInt("Anna " + i + ". luku? "); summa = summa + luku; } Pop.ilmoita("Lukujen summa on " + summa); } }
public class Summa2 { public static void main(String[] args) { int summa = 0; int lukuja = 0; Pop.ilmoita("Lukujen summan laskenta. Negatiivinen lopettaa."); while (true) { ///// toisto keskeytetään break-lauseella int lukutarjokas = Pop.kysyInt("Anna " + (lukuja+1) + ". luku. Negatiivinen lopettaa."); // Huom: laskurin korjaus! if (lukutarjokas < 0 ) break; /////////// toiston keskeytys ///////////// summa = summa + lukutarjokas; // viimeksi luettu siis oli varsinainen syöte lukuja = lukuja + 1; } Pop.ilmoita("Lukuja oli " + lukuja + ". Summa on " + summa); } }
public class Summa3 { public static void main(String[] args) { int summa = 0; int lukuja = 0; while (true) { ///// toisto keskeytetään break-lauseella int vastaus = Pop.valitseNappula("Onko vielä lukuja? ", "kyllä", "ei"); if (vastaus != 0) break; /////////// toiston keskeytys ///////////// lukuja = lukuja + 1; int luku = Pop.kysyInt("Anna " + lukuja + ". luku?"); summa = summa + luku; } Pop.ilmoita("Lukuja oli " + lukuja + ". Summa on " + summa); } }
Tähän mennessä tavatut muuttujat ovat olleet yksittäisiä "laatikoita, joihin mahtuu vain yksi arvo kerrallaan". Tällaisilla saa toki ohjelmoitua kaikki mahdolliset algoritmit. Mutta työlääksi ja tylsäksi se voi käydä...
Esimerkki: Laadi ohjelma, joka lukee viisi lukua ja tulostaa ne käänteisessä järjestyksessä:
int eka = Pop.kysyInt("Anna 1. luku: "); int toka = Pop.kysyInt("Anna 2. luku: "); int kolm = Pop.kysyInt("Anna 3. luku: "); int nelj = Pop.kysyInt("Anna 4. luku: "); int viid = Pop.kysyInt("Anna 5. luku: "); System.out.println("Luvut käänteisessä järjestyksessä:"); System.out.println(viid); System.out.println(nelj); System.out.println(kolm); System.out.println(toka); System.out.println(eka);
Kun ohjelmalle syötetään vaikkapa luvut 2, 4, 6, 8, 9, se tulostaa:
Luvut käänteisessä järjestyksessä: 9 8 6 4 2
Kyllähän homma siis onnistuu, mutta vaikkapa jo sadan luvun tapauksessa ohjelmoija joutuisi näkemään melkoisesti vaivaa. Puhumattakaan kymmennetuhannen tai miljoonan luvun tapauksesta. Ja aivan turhaan!
Taulukko (array) on muuttuja, joka sisältää monta "lokeroa", joihin jokaiseen voidaan tallettaa yksi arvo. Taulukko on siis ikään kuin monen muuttujan "lokerikko".
Ja nyt tulee koko homman idis: Taulukkomuuttujan yksittäiseen lokeroon voidaan viitata lokeron järjestysnumerolla! Ja tuo järjestysnumero voi olla muuttujan arvona. Muista, että muuttujan arvoa voi muuttaa!
Javassa ja monessa muussa ohjelmointikielessä taulukkomuuttujan lokerot on numeroitu nollasta alkaen. Lokeroiden numeroita on tapana kutsua indekseiksi ja lokeroita taulukon alkioiksi.
Äskeisen esimerkin voi taulukkoa käyttäen ohjelmoida vaikkapa seuraavasti:
public class EkaVikaksiJne { public static void main(String[] args) { int[] luvut = new int[5]; // tämä selitetään kohta... int lokeronNumero = 0; while (lokeronNumero < 5) { luvut[lokeronNumero] = Pop.kysyInt("Anna " + (lokeronNumero+1) + ". luku: "); // pyydetään 1., 2., ..., siksi tuo +1 lokeronNumero = lokeronNumero +1; } System.out.println("Luvut käänteisessä järjestyksessä:"); lokeronNumero = 4; while (lokeronNumero >= 0) { System.out.println(luvut[lokeronNumero]); lokeronNumero = lokeronNumero - 1; } } }
Yllä ilmaus int[] luvut = new int[5];
on
Java-kielen tapa määritellä
taulukkomuuttuja, jonka alkiot ovat tyypiltään kokonaislukuja ja jonka
koko eli alkioiden lukumäärä on viisi.
Ilmaus luvut[lokeronNumero]
on taulukon indeksointi;
se tarkoittaa taulukkomuuttujan alkiota, jonka järjestysnumero on
lokeronNumero
.
Jos muuttujan arvoa vastaavaa alkiota ei taulukossa ole, ohjelman
suoritus keskeytyy virheeseen. Kelvollinen indeksi on siis ohjelmoijan
vastuulla.
Taulukon pituuden voi tarkistaa ilmauksella luvut.length
:
int[] t1 = new int[5]; int[] t2 = new int[500]; int[] t3 = new int[1000000]; System.out.println(t1.length); // 5 System.out.println(t2.length); // 500 System.out.println(t3.length); // 1000000
Kuten edellä mainittiin, askeltava for-lause soveltuu erityisen hyvin taulukon alkioiden läpikäymiseen.
Esimerkkinä viimeistellään ja yleistetään yllä nähty esimerkki EkaVikaksiJne.java, jossa syöttöluvut tulostetaan käänteisessä järjestyksessä. Tässä kehittyneemmässa versiossa lukujen lukumäärä kysytään käyttäjältä:
public class EkaVikaksiJnePlus { public static void main(String[] args) { int koko = Pop.kysyInt("Montako lukua haluat tulostaa käännetyssä järjestyksessä?"); int[] luvut = new int[koko]; // HUOM: taulukon koko saa olla siis myös muuttuja! for (int lokeronNumero=0; lokeronNumero < luvut.length; ++lokeronNumero) luvut[lokeronNumero] = Pop.kysyInt("Anna " + (lokeronNumero+1) + ". luku: "); System.out.println("Luvut käänteisessä järjestyksessä:"); for (int lokeronNumero = luvut.length-1; lokeronNumero >= 0; --lokeronNumero) System.out.println(luvut[lokeronNumero]); } }
Edellä opittu for-lauseen muoto taulukoiden käsittelyssä perustuu
indeksointiin, siihen että askelmuuttuja
(vaikkapa int i
) käy läpi taulukon indeksit ja
toistettava alialgoritmi viittaa taulukon alkiohin indeksointi-ilmauksella
(vaikkapa taulu[i]
). Tällä menettelyllä taulukon alkioiden
arvoja voidaan tutkia ja myös muuttaa.
Jos on tarve vain tutkia taulukon alkiot yksi kerrallaan alusta loppuun, käytettävissä on myös ns. "for-each"-toisto, jossa taulukon alkioiden tyyppinen muuttuja saa yksi kerrallaan arvokseen taulukon alkioiden arvot. Lauseen muoto on:
for (taulukon_alkion_tyyppi alkio: taulukko) { // tänne saadaan yksi kerrallaan käyttöön // taulukon alkioiden arvot muuttujassa alkio }
Esimerkkinä taulukon alkioiden summan laskenta.
public class PiinAlkua { public static void main(String[] args) { int[] taulu = {3, 1, 4, 5, 9, 2, 6, 5, 3, 5}; // näinkin voi luoda taulukon! int summa = 0; for (int alkio : taulu) summa = summa + alkio; Pop.ilmoita("Alkioiden summa: " + summa); } }
Huom: Tätä for-toiston muotoa käyttäen alkioiden arvoja ei siis
voi muuttaa. Muutostarpeen tullessa indeksointia "omin käsin" ei voi
välttää. Alkioiden idekseistä ei myöskään ole mitään tietoa.
Jos on tiedettävä, on syytä tällöikin ideksoida "omin käsin"
Siis:
for (taulukon_alkion_tyyppi alkio: taulukko) { // tällä ei ole tietoa alkion indeksistä // ja vaikka muutujan alkio arvoa muuttaisi, // taulukossa mikään ei muutu }
Huom: Tämä sama väline on käytettävissä myös Javan monien ns. kokoelmien alkioiden arvojen järjestelmälliseen läpikäyntiin.
Jo aiemmin nähtiin esimerkki merkkijonomuuttujasta:
String teksti = "kissantassu"; System.out.println(teksti); teksti = "ketunhäntä"; System.out.println(teksti);
Merkkijonot kelpaavat myös taulukon alkioiksi. Tällöin
alkiotyypin nimi on String
.:
String[] nimet = new String[3]; nimet[0] = "Aku"; nimet[1] = "Iines"; nimet[2] = "Hessu"; for (int indeksi=0; indeksi < nimet.length; ++indeksi) System.out.println(nimet[indeksi]);
Muista että merkkijonoja vertaillaam
equals
-operaatiolla!
Yhtäsuuruusmerkki ==
tarkoittaa jotakin muuta...
if (teksti.equals("kissantassu")) ...
Taulukon voi määritellä myös luettelemalla alkioiden alkuarvot seuraavaan tapaan:
public class NimiIka { public static void main(String[] args) { String[] henkilot = {"Matti", "Maija", "Pekka", "Liisa"}; int[] ika = { 29, 22, 19, 27}; String kuka = Pop.kysyString("Kenen henkilön iän haluat tietää?"); // etsitään indeksi: int indeksi = 0; while (indeksi < henkilot.length && !henkilot[indeksi].equals(kuka)) indeksi = indeksi + 1; // Nyt joko löytyi tai indeksi kasvoi yli: if ( indeksi == henkilot.length ) Pop.ilmoita("Henkilöä " + kuka + " ei löytynyt."); else { int haetunIka = ika[indeksi]; Pop.ilmoita("Henkilön " + kuka + " ikä on " + haetunIka + "."); } } }
Tässä esimerkissä on paljon muutakin opittavaa kuin tuo taulukon määrittely alkuarvot luettelemalla. Mitä? Mieti, tutki ja opi!
Ohjelmia laadittaessa esiintyy usein tilanne, että jokin toiminto - alialgoritmi - täytyy suorittaa ohjelman useassa eri kohdassa. Alialgoritmi voidaan tietenkin kirjoittaa uudelleen ja uudelleen, mutta tällä on joitakin huonoja seurauksia:
Jo varhaisissa ohjelmointikielissä käytettiin aliohjelmia, nimettyjä algoritmeja, jotka ensin määritellään ja nimetään. Tällaista nimettyä apuvälinettä voidaan sitten nimellä kutsua siellä missä tuota algoritmia tarvitaan, esimerkiksi pääohjelmassa tai jossakin aliohjelmassa. Kutsuminen tarkoittaa siis, että aliohjelma käydään suorittamassa ja suorituksen jälkeen automaattisesti palataan kutsua seuraavaan ohjelmakohtaan.
Mahdollisuus nimetä ja kutsua alialgoritmeja on jo sinänsä arvokasta. Näin voidaan välttyä "koodin kertaamiselta" ja samalla selkeyttää ohjelmaa. Tärkeä lisäominaisuus on mahdollisuus antaa aliohjelmille lähtötietoja, ns. parametreja, jotka ohjaavat alialgoritmin suoritusta.
Nimetyt alialgoritmit voivat olla "komentoja tehdä jotakin" samaan tapaan
kuin vaikkapa println
osaa kirjoittaa parametrinsa ohjelman
käyttäjän kuvaruudulle. Samoin Pop.ilmoita(...) vain "komentaa"
ohjelman näyttämään tiedotuksen ponnahdusikkunassa.
Alialgoritmeja voi kirjoittaa myös sellaisiksi, että ne laskevat jonkin arvon ja palauttavat tuon arvon. Esimerkkejä tällaisista ovat vaikkapa trigonometriset funktiot, jotka useimmista ohjelmointikielistäkin löytyvät. Myös kurssilla käytettävät tietoja lukevat ponnahdusikkunat palauttavat arvon ja ovat sikäli funktion kaltaisia: esimerkiksi Pop.kysyInt(...) palauttaa arvonaan käyttäjän syöttämän kokonaisluvun.
Java-ohjelmoijat kutsuvat nimettyjä aliohjelmia useimmiten metodeiksi (method). Nimitys tulee metodien yhdestä tärkeästä käyttotavasta: olioita ja niiden tilaa käsitellään metodein. Tästä lisää myöhemmin.
Javassa arvoa palauttamattomia metodeita kutsutaan void-metodeiksi. Arvon palauttavat metodit saavat nimensä palautettavan arvon tyypin mukaan: int-metodi, double-metodi, String-metodi, jne.
Toistaiseksi metodeita klassiseen malliin "pääohjelman pikku apulaisiksi". Olio-ohjelmoinnin puolella sitten opitaan toinen tärkeä käyttötapa metodeille.
Ensin esimerkki esimerkki "komennon kaltaisesta" void-metodista:
public class Moi { private static void tervehdi() { // määrittely System.out.println("Moi!"); } public static void main(String[] args) { System.out.println("Kutsutaanpa tervehdystä:"); tervehdi(); // kutsu System.out.println("Kutsutaanpa kolmasti:"); tervehdi(); tervehdi(); tervehdi(); } }
Ohjelman tulostus:
Kutsutaanpa tervehdystä: Moi! Kutsutaanpa kolmasti: Moi! Moi! Moi!
Metodin määrittely ei vielä johda sen algoritmin suorittamiseen. Vasta kutsu – nimi mainiten – johtaa algoritmin suorittamiseen.
Arvon palauttava aliohjelma määritellään samaan tapaan, mutta määrittelyssä metodille annetaan paluuarvon tyyppi ilmauksen void sijasta:
public class Moi2 { private static int onnenluku() { // määrittely return 14; } public static void main(String[] args) { System.out.println("Onnenluku = " + onnenluku()); // kutsu System.out.println("Ja kolmeen kertaan: " + (onnenluku() + onnenluku() + onnenluku())); } }
Ohjelman tulostus:
Onnenluku = 14 Ja kolmeen kertaan: 42
Huom: Näissä kahdessa esimerkissä metodeilla ei ole parametreja. Silti sekä määrittelyssä että kutsussa niiden nimen yhteyteen on kirjoitettava "tyhjät" kaarisulut.
Ja sitten muutamia huomioita Javan tyylistä
private
) ja luokkaan,
ei olioon, liittyviksi (static
). Nämä asiat selviävät
aikanaan, ei huolta.
void
-määreellä, joka
juuri sanoo, että "ei arvoa".
int
.
Metodille siis voi antaa lähtötietoja ns. parametrien avulla. Idea on seuraava:
private static void tervehdi(String kuka) { ... // muodollisen parametrin määrittely
System.out.println("Moi " + kuka + "!"); // muodollisen parametrin käyttö algoritmissa
tervehdi("Aku"); // kutsussa annetaan todellinen parametri
Annettava arvo voi olla myös muuttujan arvo tai laskutoimituksen tulos:
int monesko = 7; String nimi = "Jaska Jokunen"; tervehdi(nimi + ", perijä numero " + (monesko+2)); // tulostus: Moi Jaska Jokunen, perijä numero 9!
Yleistetty tervehtimisalgoritmi ja sitä esittelevä ohjelma kokonaisuudessaan:
public class Moi3 { private static void tervehdi(String kuka) { // määrittely System.out.println("Moi " + kuka + "!"); } public static void main(String[] args) { System.out.println("Kutsutaanpa tervehdystä:"); tervehdi("Aku"); // kutsu System.out.println("Kutsutaanpa kolmasti:"); tervehdi("Iines"); tervehdi("Minni"); tervehdi("Mikki"); int monesko = 7; String nimi = "Jaska Jokunen"; tervehdi(nimi + ", perijä numero " + (monesko+2)); // tulostus: Moi Jaska Jokunen, perijä numero 9! } }
Ohjelman tulostus:
Kutsutaanpa tervehdystä: Moi Aku! Kutsutaanpa kolmasti: Moi Iines! Moi Minni! Moi Mikki! Moi Jaska Jokunen, perijä numero 9!
Tietenkin myös arvon palauttavalle aliohjelmalle voi antaa parametreja:
public class Tuplaus { private static int tuplaa(int luku) { // määrittely return 2 * luku; } public static void main(String[] args) { System.out.println("Kolme tuplana = " + tuplaa(3)); // kutsu int i = 8; int j = tuplaa(i); System.out.println( tuplaa(j) + tuplaa(2) ); System.out.println(tuplaa(tuplaa(tuplaa(1)))); } }
Ohjelman tulostus:
Kolme tuplana = 6 36 8
Parametreja voi toki olla useampiakin:
public class Onnea { private static void onnittele(String kuka, int kertaa) { for (int i = 0; i < kertaa; ++i) System.out.println( "Onnea " + kuka + "!"); } public static void main(String[] args) { onnittele("Liisa", 3); onnittele("Pekka", 2); } }
Ohjelman tulostus:
Onnea Liisa! Onnea Liisa! Onnea Liisa! Onnea Pekka! Onnea Pekka!
public class Nelis { private static int neliosumma(int a, int b) { int summa = a + b; int nelio = summa * summa; return nelio; } public static void main(String[] args) { System.out.println(neliosumma(1, 1)); System.out.println(neliosumma(3, 2)); } }
Ohjelman tulostus:
4 25
Metodin sisällä eli metodin lohkossa määritellyt muuttujat eivät milloinkaan voi näkyä metodin lohkon ulkopuolelle. Tällaisia muuttujia kutsutaan funktion paikallisiksi muuttujiksi. Ne ovat käytettävissä vain metodin omassa algoritmissa.
Metodin paikalliset muuttujat ja muodolliset parametrit itse asiassa syntyvät, kun metodia kutsutaan ja metodi käynnistyy. Paikallisille muuttujille ja muodollisille parametreille varattu tila vapautetaan, kun metodin suoritus päättyy. Koska paikalliset muuttujat eivät säily suorituskerrasta toiseen, niiden avulla myöskään ei voi muistaa mitään metodin seuraavalle suorituskerralle.
Metodin muodolliset parametrit ovat muuten samassa asemassa kuin paikalliset muuttujat, mutta niillä on aina alkuarvonaan kulloisessakin kutsussa annettujen todellisten parametrien arvo. Arvoa on lupa myös muuttaa, mutta muutos ei näy metodin ulkopuolelle, koska muodollisten parametrienkin tila vapautetaan metodin suorituksen päättyessä.
Java-kielessä pätee sääntö: Mistään aliohjelmasta ei koskaan voi nähdä minkään toisen aliohjelman paikallista muuttujaa.
Myös taulukon voi välittää parametrina, mikä usein on näppärää:
public class ArvotPaikalleen { private static void asetaJokaAlkiolle(int[] taulu, int arvo) { for (int i = 0; i < taulu.length; ++i) taulu[i] = arvo; } public static void main(String[] args) { int[] t = new int[10]; // int-taulukon alkioden alkuarvo on 0 System.out.println(t[7]); // 0 asetaJokaAlkiolle(t, 100); System.out.println(t[7]); // 100 t = new int[1000000]; // uusi arvo t:lle, siis kokonaan uusi taulukko! asetaJokaAlkiolle(t, 9876); System.out.println(t[765432]); // 9876 } }
Ohjelman tulostus:
0 100 9876
Esimerkki peräkkäishausta: metodi etsii taulukosta kysyttyä arvoa ja palauttaa vastaavan indeksin ensimmäisestä löytyneestä. Jos päästään taulukon loppuun löytämättä haettua arvoa, palautetaan arvo -1, joka ei kelpaa taulukon indeksiksi. Se viestii, ettei haettua arvoa löytynyt:
public class Perakkaishakuesimerkki { private static int mikaOnArvonIndeksi(int etsittava, int[] taulu) { for (int i = 0; i < taulu.length; ++i) if (taulu[i] == etsittava) return i; // jos tänne päästään, ei löytynyt return -1; // -1 ei voi olla indeksi } public static void main(String[] args) { int[] t = {2, 4, 6, 8, 10, 12, 14}; System.out.println("Luvun 8 indeksi on " + mikaOnArvonIndeksi(8, t)); System.out.println("Luvun 7 indeksi on " + mikaOnArvonIndeksi(7, t)); if (mikaOnArvonIndeksi(8, t) != -1) System.out.println("8 löytyy"); else System.out.println("8 ei löydy"); if (mikaOnArvonIndeksi(7, t) != -1) System.out.println("7 löytyy"); else System.out.println("7 ei löydy"); } }
Ohjelman tulostus:
Luvun 8 indeksi on 3 Luvun 7 indeksi on -1 8 löytyy 7 ei löydy
Metodin tyyppinä eli metodin palauttamana arvona voi olla myös taulukko, tarkemmin sanottuna viite taulukko-olioon.
Laaditaan pikku työkalu, jolla voi pyytää käyttäjältä taulukon alkioille arvot. Metodille annetaan parametrina taulukon koko. Metodi luo taulukon ja kyselee sen alkoille arvot. Lopuksi metodi palauttaa valmiin taulukon kutsujalleen.
public class TaulukkometodiA { private static int[] kyseleTaulukko(int koko) { // huomaa tyyppi! int[] tulos = new int[koko]; // luodaan taulukko Pop.ilmoita("Anna " + koko + " lukua."); for (int i=0; i < tulos.length; ++i) tulos[i] = Pop.kysyInt("Anna " + (i+1) + ". luku."); return tulos; // palautetaan valmis taulukko } // pääohjelma public static void main(String[] args) { int[] b = kyseleTaulukko(7); // 1. käyttöesimerkki for (int i=0; i < b.length; ++i) System.out.print(b[i]+" "); System.out.println(); b = kyseleTaulukko(3); // 2. käyttöesimerkki for (int i=0; i < b.length; ++i) System.out.print(b[i]+" "); System.out.println(); } }
Kun tälle ohjelmalle syötetään vaikkapa ensin luvut 8, 7, 6, 5, 4, 3, 2 ja sitten 5, 3, 1, se odotetusti tulostaa:
8 7 6 5 4 3 2 5 3 1
Toinen vaihtoehto tällaisen palvelun toteuttamiseen on void-metodi, joka saa taulukon parametrina:
public class TaulukkometodiB { private static void kyseleTaulukko(int[] tulos) { Pop.ilmoita("Anna " + tulos.length + " lukua."); for (int i=0; i < tulos.length; ++i) tulos[i] = Pop.kysyInt("Anna " + (i+1) + ". luku."); } // pääohjelma public static void main(String[] args) { int[] b = new int[7]; // nyt taulukko luodaan pääohjelmassa! kyseleTaulukko(b); // 1. käyttöesimerkki for (int i=0; i < b.length; ++i) System.out.print(b[i]+" "); System.out.println(); b = new int[3]; kyseleTaulukko(b); // 2. käyttöesimerkki for (int i=0; i < b.length; ++i) System.out.print(b[i]+" "); System.out.println(); } }
Huomaa että kun taulukko välitetään parametrina, todellisuudessa parametrina välitetään viite taulukko-olioon. Siksi kutsutun metodin toimenpiteet taulukolle kohdistuvat pääohjelman luomaan taulukkoon!