Arto Vihavainen, Matti Paksula, Matti Luukkainen, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju, Martin Pärtel
Tämä materiaali on tarkoitettu kevään 2011 Ohjelmoinnin perusteet -kurssille. Materiaali pohjautuu kevään 2010 kurssimateriaaliin.
Lue materiaalia siten, että teet samalla itse kaikki lukemasi esimerkit. Esimerkkeihin kannattaa tehdä pieniä muutoksia ja tarkkailla, miten muutokset vaikuttavat ohjelman toimintaan. Äkkiseltään voisi luulla, että esimerkkien tekeminen myös itse ja niiden muokkaaminen hidastaa opiskelua. Tämä ei kuitenkaan pidä ollenkaan paikkansa. Ohjelmoimaan ei ole vielä tietääksemme kukaan ihminen oppinut lukemalla (tai esim. luentoa kuuntelemalla). Oppiminen perustuu oleellisesti aktiiviseen tekemiseen ja rutiinin kasvattamiseen, ja esimerkkien tekeminen ja erityisesti erilaisten omien kokeilujen tekeminen on parhaita tapoja "sisäistää" luettua tekstiä.
Pyri myös tekemään tai ainakin yrittämään tehtäviä sitä mukaa kuin luet tekstiä. Jos et osaa heti tehdä jotain tehtävää, älä masennu, sillä pajassa saat ohjausta tehtävän tekemiseen.
Tekstiä ei ole tarkoitettu vain kertaalleen luettavaksi. Joudut varmasti myöhemmin palaamaan jo aiemmin lukemiisi kohtiin tai aiemmin tekemiisi tehtäviin. Tämä teksti ei sisällä kaikkea oleellista ohjelmointiin liittyvää. Itse asiassa ei ole olemassa mitään kirjaa josta löytyisi kaikki oleellinen. Eli joudut joka tapauksessa ohjelmoijan urallasi etsimään tietoa myös omatoimisesti. Kurssin harjoitukset sisältävät jo jonkun verran ohjeita, mistä suunnista ja miten hyödyllistä tietoa on mahdollista löytää.
Ohjelmointi aloitetaan seuraavasti. Talleta oheinen koodi tiedostoon nimeltä Hei.java
public class Hei { public static void main(String[] args) { System.out.println("Hei maailma"); } }
Mene komentorivillä kansioon, johon tallensit tiedoston Hei.java
. Aja komennot
javac Hei.java
java Hei
Näet tulosteen:
Hei maailma
Olet aloittanut ohjelmoinnin.
Nykyaikainen ohjelmointi tapahtuu yleensä ohjelmointiympäristössä. Ohjelmointiympäristö sisältää joukon ohjelmoijaa auttavia aputoimintoja. Ohjelmointiympäristö ei rakenna ohjelmaa ohjelmoijan puolesta, mutta se muunmuassa vinkkaa helpoista virheistä ohjelmakoodissa ja auttaa ohjelmoijaa hahmottamaan ohjelman rakennetta.
Käytämme tällä kurssilla NetBeans-nimistä ohjelmointiympäristöä. NetBeans on ilmainen ohjelmisto, voit asentaa sen omalle koneellesi täältä, valitse versio "Java SE" tai "Java".
Ohje ohjelmoinnin aloittamiseen NetBeans-ohjelmointiympäristössä löytyy täältä.
Jatkossa julkaisemme pikkuhiljaa lisää ohjeita NetBeansin käyttöön. Vaikka ohjelma tuntuisi nyt sekavalta, älä hätäile. NetBeansin käyttöön saa ohjausta pajassa, ohjelma on loppujenlopuksi hyvin helppokäyttöinen. Perusteet opit 5 minuutissa, ja kurssin myötä opit koko ajan hieman lisää ja toukokuussa olet jo todellinen NetBeans "poweruser".
Ohjelma muodostuu lähdekoodista. Tietokone suorittaa lähdekoodissa olevia komentoja pääsääntöisesti ylhäältä alaspäin ja vasemmalta oikealle. Lähdekoodi talletetaan tekstimuodossa ja suoritetaan jollakin tavalla.
Varsinaisesti ohjelma muodostuu komennoista, jotka annetaan lähdekoodissa. Tietokone suorittaa eri operaatioita, eli toimintoja, komentojen perusteella. Esimerkiksi merkkijonon, eli tekstin, "Hei maailma"-tulostuksessa tärkein komento on System.out.println
.
System.out.println("Hei maailma");
Komento System.out.println
siis tulostaa sille sulkeiden sisällä annetun merkkijonon. Pääte ln
on lyhenne sanasta line. Komento siis tulostaa rivin, eli kun annettu rivi on tulostettu, tulostaa komento myös rivinvaihdon.
Tietokone ei ymmärrä käyttämäämme ohjelmointikieltä suoraan. Siksi tarvitsemme lähdekoodin ja tietokoneen väliin kääntäjän. Ohjelmoidessamme komentoriviltä, komento javac Hei.java
kääntää Hei.java
-tiedoston tavukoodiksi, jota voidaan ajaa java-tulkin avulla. Käännetty ohjelma ajetaan komennolla java Hei
, missä Hei on käännetyn java-lähdekooditiedoston nimi.
Käyttäessämme ohjelmointiympäristöä, ohjelmointiympäristö hoitaa lähdekoodin kääntämisen. Valitessamme ohjelman suorittamisen, ohjelmointiympäristö kääntää ja suorittaa ohjelman valmiiksi. Ohjelmointiympäristöt usein suorittavat ohjelman kääntämistä myös ohjelmakoodia kirjoittaessa, jolloin yksinkertaiset virheet huomataan jo ennen ajamista.
Puolipisteellä ;
erotetaan komennot toisistaan. Kääntäjä ja tulkki ei siis ole kiinnostunut lähdekoodissa olevista riveistä, vaan voimme kirjoittaa koko ohjelman yhdelle riville.
Alla olevassa esimerkissä käytetään komentoa System.out.print
, joka on kuten komento System.out.println
. System.out.print
-komento ei tulosta rivinvaihtoa tekstin tulostamisen jälkeen.
Esimerkki puolipisteiden käytöstä
System.out.print("Hei "); System.out.print("maailma"); System.out.print("!");
Hei maailma!
Vaikka kääntäjä ja tulkki eivät tarvitse rivinvaihtoja, on niiden käyttö hyvin tärkeää muita ihmisiä ajatellen. Selkeä lähdekoodin osien erottelu vaatii rivinvaihtojen käyttöä, tätä ja muita lähdekoodin hyvään ulkomuotoon liittyviä seikkoja tullaan myös painottamaan tällä kurssilla.
Suluilla ()
ilmaistaan komennon parametrit, esimerkiksi System.out.print
-komennon parametriksi annetaan teksti hei seuraavasti: System.out.print("hei")
. Suluilla voidaan myös ilmaista laskujärjestystä, aivan kuten matematiikassakin.
Lähdekoodin kommentit
ovat kätevä tapa merkitä asioita itselle ja muille muistiin. Kommentti on mikä tahansa rivi, joka alkaa merkillä //
. Myös kaikki teksti samalla rivillä, joka tulee kommenttimerkin jälkeen tulkitaan kommentiksi.
// Tulostamme tekstin "Hei maailma" System.out.print("Hei maailma"); System.out.print(" ja kaikki sen ihmiset."); // Lisäämme samalle riville tekstiä. // System.out.print("tätä riviä ei suoriteta koska se on kommentoitu ulos");
Esimerkissä alin rivi esittelee erityisen kätevän käyttökohteen kommenteille: kirjoitettua koodia ei tarvitse poistaa jos haluaa tilapäisesti kokeilla jotain.
Kuten aiemmin huomattiin, tulostamiseen on kaksi komentoa:
System.out.print
tulostaa tekstin ilman loppurivinvaihtoaSystem.out.println
tulostaa tekstin ja loppurivinvaihdonTulostettavan tekstin osana voi olla muutamia erikoismerkkejä. Tärkein näistä on \n
, joka vaihtaa riviä. Erikoismerkkejä on muitakin.
System.out.println("Ensimmäinen\nToinen\nKolmas");
Ylläoleva tulostaa ajettaessa seuraavaa:
Ensimmäinen Toinen Kolmas
Ohjelman "OhjelmaRunko" runko on seuraavanlainen.
public class OhjelmaRunko { public static void main(String[] args) { // ohjelmakoodi } }
Ohjelma sijaitsee samannimisessä .java-päätteisessä tiedostossa. Ohjelman OhjelmaRunko täytyy sijaita tiedostossa,
jonka nimi on OhjelmaRunko.java
.
Ohjelmaa suoritettaessa suoritetaan alue, joka on rungossamme merkitty kommentilla ohjelmakoodi. Ohjelmoimme ensimmäisellä viikolla
vain tälle alueelle. Kun puhumme komennoista, esimerkiksi tulostamisesta, tarvitsee komennot myös rungon ympärilleen. Esimerkiksi
System.out.print("Tulostettava teksti");
public class OhjelmaRunko { public static void main(String[] args) { System.out.print("Tulostettava teksti"); } }
Jatkossa esimerkeissä ei erikseen näytetä pääohjelmarunkoa.
Ohjelmoinnissa eräs keskeinen käsite on muuttuja. Muuttuja kannattaa ajatella lokerona, johon voi tallettaa tietoa. Talletettavalla tiedolla on aina tyyppi. Tyyppejä ovat esimerkiksi teksti (String), kokonaisluku (int), liukuluku (double) ja totuusarvo (boolean). Muuttujaan asetetaan arvo yhtäsuuruusmerkillä (=
).
int kuukausia = 12;
Yllä olevassa asetuslauseessa asetetaan kokonaisluku-tyyppiä olevaan muuttujaan kuukausia arvo 12. Asetuslause luetaan "muuttuja kuukausia saa arvon 12"
Muuttujan arvo voidaan yhdistää merkkijonoon +
-merkillä
seuraavan esimerkin mukaisesti.
String teksti = "sisältää tekstiä"; int kokonaisluku = 123; double liukuluku = 3.141592653; boolean onkoTotta = true; System.out.println("Tekstimuuttujan arvo on " + teksti); System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku); System.out.println("Liukulukumuuttujan arvo on " + liukuluku); System.out.println("Totuusarvomuuttujan arvo on " + onkoTotta);
Tekstimuuttujan arvo on sisältää tekstiä Kokonaislukumuuttujan arvo on 123 Liukulukumuuttujan arvo on 3.141592653 Totuusarvomuuttujan arvo on true
Muuttuja säilyttää arvonsa kunnes siihen asetetaan toinen arvo. Huomaa että muuttujan tyyppi kirjoitetaan vain silloin kun muuttuja esitellään ensimmäistä kertaa.
int kokonaisluku = 123; System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku); kokonaisluku = 42; System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku);
Kokonaislukumuuttujan arvo on 123 Kokonaislukumuuttujan arvo on 42
Kun muuttujan tyyppi on kertaalleen asetettu, ei se enää muutu. Esimerkiksi tekstimuuttuja ei voi muuttua kokonaislukumuuttujaksi, eikä siihen voi asettaa kokonaislukua.
String merkkijono = "tsuppadui!"; merkkijono = 42; // Ei onnistu! :(
Liukulukuun voi asettaa kokonaisluvun, sillä kokonaisluku on myös liukuluku
double desimaali = 0.42; desimaali = 1; // Onnistuu! :)
Muuttujan nimeämistä rajoittavat tietyt ehdot. Vaikka muuttujan nimessä voidaan käyttää ääkkösiä, on parempi olla kayttamatta niita, sillä merkistökoodauksesta saattaa tulla ongelmia.
Muuttujan nimessä ei saa olla tiettyjä erikoismerkkejä, kuten huutomerkkejä (!). Välilyönti ei ole sallittu, sillä se erottaa komentojen osat toisistaan. Välilyönti kannattaa korvata alaviivalla _ tai camelCase-tyylillä, jolloin nimi muistuttaneeKamelia
. Huom! Muuttujien nimien ensimmäinen kirjain kirjoitetaan aina pienellä!
int muuttuja_alaviivalla = 3; int camelCaseMuuttuja = 7;
Numeroita voidaan käyttää muuttujan nimessä, kunhan nimi ei ala numerolla. Nimi ei myöskään voi koostua pelkistä numeroista.
int 7muuttuja = 4; // Ei sallittu! int muuttuja7 = 4; // Sallittu, mutta ei kuvaava muuttujan nimi
Muuttujan nimi ei myöskään saa olla jo entuudestaan käytössä. Tälläisiä nimiä ovat mm. aikaisemmin määritellyt muuttujat ja komennot, kuten System.out.print
ja System.out.println
.
int camelCase = 2; int camelCase = 5; // Ei sallittu -- muuttuja camelCase on jo käytössä!
Muuttuja kannattaa nimetä siten, että sen käyttötarkoitus on selvää ilman kommentteja tai miettimistä. Tällä kurssilla muuttujat pitää nimetä kuvaavasti.
Kurssin nimeämiskäytäntö on camelCase
. Esimerkiksi kuukauden ensimmäistä päivää kuvaavan muuttujan nimi on kuukaudenEnsimmainenPaiva
Laskentaoperaatiot ovat varsin suoraviivaisia: +
, -
, *
ja /
. Erikoisempana operaationa on %
, joka on jakojäännös, eli modulo. Laskentajärjestys on myös varsin suoraviivainen: operaatiot lasketaan vasemmalta oikealle sulut huomioon ottaen.
int eka = 2; // kokonaislukutyyppinen muuttuja eka saa arvon 2 int toka = 4; // kokonaislukutyyppinen muuttuja toka saa arvon 4 int summa = eka + toka; // kokonaislukutyyppinen muuttuja summa saa arvon eka + toka, eli 2 + 4 System.out.println(summa); // tulostetaan muuttujan summa arvo
int laskuSuluilla = (1 + 1) + 3 * (2 + 5); // 32 int laskuSuluitta = 1 + 1 + 3 * 2 + 5; // 13
Yllä olevan sulkuesimerkin voi suorittaa myös askeleittain.
int laskuSuluilla = (1 + 1); laskuSuluilla = laskuSuluilla + 3 * (2 + 5) // 32 int laskuSuluitta = 1 + 1; laskuSuluitta = laskuSuluitta + 3 * 2; laskuSuluitta = laskuSuluitta + 5; // 13
Laskentaoperaatioita voidaan suorittaa lähes missä tahansa kohdassa ohjelmakoodia.
int eka = 2; int toka = 4; System.out.println(eka+toka); System.out.println(2 + toka - eka - toka);
Jako ja jakojäännös ovat hieman hankalampia kuin kokonaisluvut. Liukuluku ja kokonaisluku menevät helposti sekaisin. Jos kaikki laskuoperaatiossa olevat muuttujat ovat kokonaislukuja, on tulos myös kokonaisluku.
int tulos = 3 / 2; // tulos on 1 (kokonaisluku), sillä 3 ja 2 ovat myös kokonaislukuja
Jakojäännös kertoo jakojäännöksen. Esimerkiksi laskun 7 % 2
jakojäännös on 1.
int jakojaannos = 7 % 2; // jakojaannos on 1 (kokonaisluku)
Jos jakolaskun jakaja tai jaettava (tai molemmat!) ovat liukulukuja, tulee tulokseksi myös liukuluku
double kunJaettavaOnLiukuluku = 3.0 / 2; // tulokseksi: 1.5 double kunJakajaOnLiukuluku = 3 / 2.0; // tulokseksi: 1.5
Jos jakolaskun tulos asetetaan kokonaislukutyyppiseen muuttujaan, on tulos automaattisesti kokonaisluku
int tulosKokonaislukuKoskaTyyppiKokonaisluku = 3.0 / 2; // tulos automaattisesti kokonaisluku: 1
Seuraava esimerkki tulostaa "1.5", sillä jaettavasta tehdään liukuluku kertomalla se liukuluvulla (1.0 * 3 = 3.0) ennen jakolaskua.
int jaettava = 3; int jakaja = 2; double tulos = 1.0 * jaettava / jakaja; System.out.println(tulos);
Mitä seuraava tulostaa?
int jaettava = 3; int jakaja = 2; double tulos = jaettava / jakaja * 1.0; System.out.println(tulos);
Huomioi, että nimeät muuttujat nyt ja jatkossakin yllä esiteltyjen hyvien käytäntöjen mukaan.
Tarkastellaan vielä lähemmin merkkijonojen
yhdistämistä +
-merkinnän avulla.
Jos operaatiota +
sovelletaan kahden merkkijonon välille, syntyy uusi merkkijono, jossa kaksi merkkijonoa on yhdistetty. Huomaa nokkela välilyönnin käyttö lauseen "muuttujien" osana!
String tervehdys = "Hei "; String nimi = "Matti"; String hyvästely = ", ja näkemiin!"; String lause = tervehdys + nimi + hyvästely; System.out.println(lause);
Hei Matti, ja näkemiin!
Jos toinen operaation +
kohteista on merkkijono, syntyy uusi merkkijono, jossa esimerkiksi kokonaisluku 2
on muutettu merkkijonoksi "2" ja tähän yhdistetty haluttu merkkijono.
System.out.println("tuossa on kokonaisluku --> " + 2); System.out.println( 2 + " <-- tuossa on kokonaisluku");
Edellä esitellyt laskusäännöt pätevät täälläkin:
System.out.println("Neljä: " + (2+2)); System.out.println("Mutta! kaksikymmentäkaksi: " + 2 + 2);
Edellisiä tietoja yhdistelemällä pystymme tulostamaan muuttujan arvoja ja tekstiä sekaisin:
int x = 10; System.out.println("muuttujan x arvo on: " + x); int y = 5; int z = 6; System.out.println("y on " + y + " ja z on " + 6);
Tähän asti ohjelmamme ovat olleet kovin yksipuolisia. Seuraavaksi luemme syötettä käyttäjältä. Käytämme syötteen lukemiseen erityistä Scanner-apuvälinettä.
Lisätään Scanner valmiiseen pääohjelmarunkoomme. Älä hätäile vaikka pääohjelmarunko saattaa näyttää vaikeaselkoiselta, jatkamme koodausta kuten ennenkin, eli kohtaan mikä on merkattu kommentilla ohjelmakoodi.
import java.util.Scanner; public class OhjelmaRunko { static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { // ohjelmakoodi } }
Seuraava koodi lukee käyttäjän nimen ja tulostaa tervehdyksen:
System.out.print("Mikä on nimesi? "); String nimi = lukija.nextLine(); // Luetaan käyttäjältä rivi tekstiä ja asetetaan sen arvo muuttujaan nimi System.out.println("Hei, " + nimi);
Mikä on nimesi? Matti
Hei, Matti
Seuraavassa on yllä oleva ohjelma pääohjelmarungon kanssa. Ohjelman nimi on Tervehdys. Koska ohjelman nimi on Tervehdys, täytyy sen sijaita tiedostossa Tervehdys.java
-- Käyttäessäsi NetBeans-ohjelmointiympäristöä, toimi kuten alussa olleessa ohjeessa tehtiin, mutta muuta ohjelman nimi Heistä (Hei) Tervehdykseksi (Tervehdys)
import java.util.Scanner; public class Tervehdys { static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { System.out.print("Kenelle sanotaan hei: "); String nimi = lukija.nextLine(); // Luetaan käyttäjältä rivi tekstiä ja asetetaan sen arvo muuttujaan nimi System.out.print("Hei " + nimi); } }
Kun yllä oleva ohjelma ajetaan, pääset kirjoittamaan syötteen. NetBeansin tulostusvälilehti (alhaalla) näyttää ajetun ohjelman jälkeen seuraavalta (käyttäjä syöttää nimen "Matti").
run: Kenelle sanotaan hei: Matti Hei Matti BUILD SUCCESSFUL (total time: 6 seconds)
Scanner-apuvälineemme ei ole hyvä kokonaislukujen lukemiseen, joten käytämme toista apuvälinettä merkkijonon kokonaisluvuksi muuttamisessa. Komento Integer.parseInt
muuttaa sille annetussa tekstimuuttujassa olevan kokonaisluvun kokonaislukumuuttujaksi. Komennolle annetaan tekstimuuttuja sulkuihin, ja se palauttaa kokonaisluvun joka asetetaan kokonaislukumuuttujaan.
String kolmonenMerkkijonona = "3"; int kolme = Integer.parseInt(kolmonenMerkkijonona);
Sama esimerkki, mutta luetaan kokonaisluku käyttäjältä merkkijonona.
System.out.print("Anna kokonaisluku: "); String kokonaislukuMerkkijonona = lukija.nextLine(); int kokonaisluku = Integer.parseInt(kokonaislukuMerkkijonona); System.out.println("Annoit " + kokonaisluku);
Komentoja voi usein myös ketjuttaa. Käytetään lukijan nextLine
-komennon antamaa merkkijonoa suoraan komennossa Integer.parseInt
. Tällöin lukija lukee käyttäjältä ensiksi syötteen, komento Integer.parseInt
muuttaa syötteen kokonaisluvuksi. Käytämme jatkossa alla esitettyä tapaa kokonaisluvun lukemiseen.
System.out.print("Anna kokonaisluku: "); int kokonaisluku = Integer.parseInt(lukija.nextLine()); System.out.println("Annoit " + kokonaisluku);
Kysytään seuraavaksi käyttäjältä nimi, ja sen jälkeen ikä. Tällä kertaa esimerkissä on myös ohjelmarunko mukana.
import java.util.Scanner; public class NimiJaIkaTervehdys { static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { System.out.print("Nimesi: "); String nimi = lukija.nextLine(); // Luetaan käyttäjältä rivi tekstiä System.out.print("Kuinka vanha olet: "); int ika = Integer.parseInt(lukija.nextLine()); // luetaan käyttäjältä tekstimuuttuja ja muutetaan se kokonaisluvuksi System.out.println("Nimesi on siis: " + nimi + ", ja ikäsi " + ika + ", hauska tutustua."); } }
Käyttäjän kanssa keskustelevan ohjelman runko:
import java.util.Scanner; public class OhjelmanNimi { static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { // koodi tähän } }
Merkkijonon lukeminen:
String merkkijono = lukija.nextLine();
Kokonaisluvun lukeminen:
int kokonaisluku = Integer.parseInt(lukija.nextLine());
Jotta ohjelman suoritus voisi haarautua, tarvitsemme käyttöömme valintakäskyn.
int luku = 11; if ( luku > 10 ) { System.out.println("Luku oli suurempi kuin 10"); }
Ehto ( luku > 10 )
muuntautuu totuusarvoksi true tai false. Valintakäsky if
käsittelee siis lopulta vain ja ainoastaan totuusarvoja. Yllä oleva ehtolause luetaan "jos luku on suurempi kuin 10".
Huomaa, että if
-lauseen perään ei tule puolipistettä, sillä lause ei lopu ehto-osan jälkeen.
Ehdon jälkeen avaava aaltosulku {
aloittaa lohkon (block), jonka sisältö suoritetaan jos ehto on tosi. Lohko loppuu sulkevaan aaltosulkuun }
. Lohko voi olla kuinka pitkä tahansa.
Vertailuoperaattoreita ovat seuraavat:
>
suurempi kuin>=
suurempi tai yhtäsuuri kuin<
pienempi kuin<=
pienempi tai yhtäsuuri kuin==
yhtäsuuri kuin!=
erisuuri kuinint luku = 55; if ( luku != 0 ) { System.out.println("Luku oli erisuuri kuin 0"); } if ( luku >= 1000 ) { System.out.println("Luku oli vähintään 1000"); }
Lohkon sisällä voi olla mitä tahansa koodia, myös toinen valintakäsky.
int x = 45; int luku = 55; if ( luku > 0 ) { System.out.println("Luku on positiivinen"); if ( luku>x ) { System.out.println(" ja suurempi kuin muuttujan x arvo"); System.out.println("muuttujan x arvohan on " + x); } }
Vertailuoperaattoreita voi käyttää myös ehtojen ulkopuolella. Tällöin ehdon totuusarvo asettuu totuusarvomuuttujaan.
int eka = 1 int toka = 3 boolean onkoSuurempi = eka > toka
Yllä olevassa esimerkissä totuusarvomuuttuja onkoSuurempi
sisältää nyt totuusarvon false.
Totuusarvomuuttujaa voidaan käyttää ehtolauseessa ehtona.
int eka = 1 int toka = 3 boolean onkoPienempi = eka < toka if ( onkoPienempi ) { System.out.println("1 on pienempi kuin 3!"); }
1 on pienempi kuin 3!
Huomaa, että if-komennon jälkeisen lohkon, eli {-merkkiä seuraavien rivien komentoja ei kirjoiteta samalle tasolle (eli yhtä "vasemmalle") kuin komentoa if, vaan ne sisennetään hieman oikealle. Sisentäminen tapahtuu tabulaattorimerkillä (q:n vasemmalla puolella oleva merkki). Kun lohko sulkeutuu, eli tulee }-merkki, sisennys loppuu. }-merkki on samalla tasolla kuin if.
Sisennys on oleellinen seikka ohjelmien ymmärrettävyyden kannalta. Tällä kurssilla ja kaikkialla "oikeassa elämässä" edellytetään, että koodi sisennetään asiallisesti. NetBeans auttaa sisennyksessä. Ohjelman saa sisennettyä helposti painamalla yhtä aikaa shift, alt ja f.
Jos valinnan ehto on epätotta, eli totuusarvo on false, voidaan suorittaa toinen vaihtoehtoinen lohko koodia, tämä käy sanan else
avulla.
int luku = 4; if ( luku > 5 ) { System.out.println("Lukusi on suurempi kuin viisi!"); } else { System.out.println("Lukusi on viisi tai alle!"); }
Lukusi on viisi tai alle!
Sana else if
on kuten else
, mutta lisäehdolla. else if
tulee if
-ehdon jälkeen. else if
ehtoja voi olla useita.
int luku = 3; if ( luku == 1 ) { System.out.println("Luku on yksi"); } else if ( luku == 2 ) { System.out.println("Lukuna on kaksi"); } else if ( luku == 3 ) { System.out.println("Kolme lienee lukuna!"); } else { System.out.println("Aika paljon!"); }
Kolme lienee lukuna!
Luetaan ylläoleva esimerkki: 'Jos luku on yksi, tulosta "Luku on yksi", muuten jos luku on kaksi, tulosta "Lukuna on kaksi", muuten jos lukuna on kolme, tulosta "Kolme lienee lukuna". Muulloin, tulosta "Aika paljon!"'.
Merkkijonoja, eli tekstejä, ei voi vertailla yhtäsuuri kuin (==) operaatiolla. Merkkijonojen vertailuun käytetään erillistä equals
-komentoa, joka liittyy aina verrattavaan merkkijonoon.
String teksti = "kurssi"; if ( teksti.equals("marsipaani") ) { System.out.println("Teksti-muuttujassa on teksti marsipaani."); } else { System.out.println("Teksti-muuttujassa ei ole tekstiä marsipaani."); }
Komento equals
liitetään aina siihen verrattavaan tekstimuuttujaan, "tekstimuuttuja piste equals teksti". Tekstimuuttujaa voidaan myös verrata toiseen tekstimuuttujaan.
String teksti = "kurssi"; String toinenTeksti = "pursi"; if ( teksti.equals(toinenTeksti) ) { System.out.println("Samat tekstit!"); } else { System.out.println("Ei samat tekstit!"); }
Merkkijonoja vertailtaessa on syytä varmistaa että verrattavalla tekstimuuttujalla on arvo. Jos muuttujalla ei ole arvoa, ohjelma tuottaa virheen NullPointerException, joka tarkoittaa ettei muuttujan arvoa ole asetettu tai se on tyhjä (null).
&&
ehdollinen "ja" (AND)
(tosi1) && (tosi2)
||
ehdollinen "tai" (OR)
(tosi) || (epätosi)
tai (tosi) || (tosi)
!
negaatio (not)
!(epätotta)
int vuosi = 2011; int kuukausi = 1; System.out.println("Tällä hetkellä käynnistyy kurssi:"); if ( vuosi == 2011 && kuukausi == 1 ) { System.out.println("Ohjelmoinnin perusteet"); } if ( vuosi == 2011 && kuukausi == 3 ) { System.out.println("Ohjelmoinnin jatkokurssi"); }
Tällä hetkellä käynnistyy kurssi: Ohjelmoinnin perusteet
System.out.println("Onkohan luku väliltä 5-10: "); int luku = 7; if ( luku > 4 && luku < 11 ) { System.out.println("On! :)"); } else { System.out.println("Ei ollut :(") }
Onkohan luku väliltä 5-10: On! :)
System.out.println("Onkohan luku 1, 50 tai 100: "); int luku = 50; if ( valinta == 1 || valinta == 50 || valinta == 100 ) { System.out.println("On! :)"); } else { System.out.println("Ei ollut :(") }
Onkohan luku 1, 50 tai 100: On! :)
Klassinen kirjoitusvirhe muuttujia vertailtaessa on seuraavanlainen.
int luku = 42144; if ( luku = 82133 ) { System.out.println("Ehdossa on vain yksi yhtäsuuruusmerkki kahden sijasta."); System.out.println("Kääntäjä onneksi herjaa tästä, sillä ehdossa täytyy olla totuusarvotyyppinen (boolean) arvo."); }
Huomamme usein ohjelmakoodissamme tilanteita missä sama asia toistuu usein. Esimerkiksi, jos haluamme tulostaa luvut yhdestä kymmeneen, on yksi -- kohta onneksi turha -- ratkaisu kirjoittaa jokainen käsky erikseen.
System.out.println(1); System.out.println(2); System.out.println(3); System.out.println(4); System.out.println(5); System.out.println(6); System.out.println(7); System.out.println(8); System.out.println(9); System.out.println(10);
Entä jos haluaisimme tulostaa kaikki luvut yhdestä sataan, tai vaikka luvut yhdestä käyttäjän antamaan syötteeseen asti? Tällaisissa tapauksissa käytetään toistorakenteita. Toistorakenteiden avulla voidaan toteuttaa toistuvia käskyjä.
Komento while
toistaa lohkonsa koodia niin kauan kun määritelty ehto on voimassa. Komennon
while
ehto-osa toimii kuten if
:ssä.
Kuten kaikki lohkot, while
:n lohko alkaa aukeavalla aaltosululla
{
ja loppuu sulkevaan aaltosulkuun }
.
Seuraavassa esimerkissä tulostetaan luvut 1,2,..,9,10. Kun luku
-muuttuja
saa arvokseen yli 10, while
-ehto ei ole enää voimassa ja toistaminen lopetetaan.
int luku = 1; while (luku < 11) { System.out.println(luku); luku = luku + 1; }
Lue ylläoleva "niin pitkään kuin luku on pienempi kuin 11, tulosta luku ja kasvata lukua yhdellä".
Ylläolevassa koodissa ehto-lausessa olevaa muuttujaa luku
kasvatettiin jokaisella kierroksella
yhdellä. Päivitysehto voi olla mikä tahansa.
int luku = 1024; while (luku >= 1) { System.out.println(luku); luku /= 2; // luku = luku / 2; }
Usein tahdotaan päivittää vanhojen muuttujien arvoja. Tämä onnistuu kirjoittamalla tavallinen sijoituslauseke.
int pituus = 100; pituus = pituus - 50; pituus = pituus * 2; pituus = pituus / 2; // pituus on nyt 50
Koska vanhan muuttujan arvon päivittäminen on niin yleinen operaatio, on Javassa sitä varten erityiset sijoitusoperaatiot, jotka eivät ehkä ole ainakaan aluksi niin helppolukuisia.
int pituus = 100; pituus += 10; // sama kuin pituus = pituus + 10; pituus -= 50; // sama kuin pituus = pituus - 50;
Huomaa, että muuttujan tyyppi pitää aina kertoa ennen kuin sille voidaan asettaa arvo.
Seuraava esimerkki ei toimi, sillä muuttujan pituus
tyyppiä ei ole kerrottu.
pituus = pituus + 100; // ei toimi! pituus += 100; // ei toimi!
Kun tyyppi on kerrottu, toimii laskutkin oikein.
int pituus = 0; pituus = pituus + 100; pituus += 100; // muuttujan pituus arvo on 200
Myös muille kuin yhteen- ja vähennyslaskuille on Javassa vastaavat sijoitusoperaatiot.
int pituus = 100; pituus *= 10; // sama kuin pituus = 10 * pituus; pituus /= 100; //sama kuin pituus = pituus / 100; pituus %= 3; // sama kuin pituus = pituus % 3; // muuttujan pituus arvo 1
Usein ohjelmissa esiintyy toisto jonka aikana muuttujaan lasketaan jokin toistosta riippuvainen arvo. Seuraava ohjelma laskee tulon 4*3 hieman kömpelöllä tavalla eli summana 3+3+3+3:
int tulos = 0; int i = 0; while ( i < 4 ) { tulos = tulos + 3; i++; // tarkoittaa samaa kuin i = i+1; }
Alussa tulos = 0
. Toistossa muuttujan arvo nousee joka kierroksella 3:lla. Ja koska toistoja on 4, on lopulta muuttujan arvona siis 3*4.
Käyttämällä yllä esiteltyä sijoitusoperaattoria, sama saadaan aikaan seuraavasti:
int tulos = 0; int i = 0; while ( i < 4 ) { tulos += 3; // tämä on siis sama kuin tulos = tulos + 3; i++; // tarkoittaa samaa kuin i = i+1; }
Avainsana break mahdollistaa toistosta poistumisen kesken toiston suoritusta. Yleinen while-toistolauseen
käyttötapa onkin ns. while-true -käyttötapa, jossa toistoa jatketaan näennäisesti ikuisesti
while ( true )
, mutta toiston sisällä hoidetaan toistosta poistuminen.
Esimerkiksi seuraava salasanaa kysyvä ohjelma:
while ( true ) { System.out.print("Kirjoita salasana: "); String salasana = lukija.nextLine(); if ( salasana.equals("salasana") ) { break; } System.out.println("Väärin kirjoitettu!"); } System.out.println(); // tulostetaan tyhjä rivi System.out.println("Kiitos!");
Kirjoita salasana: Mikki Väärin kirjoitettu! Kirjoita salasana: ASd Väärin kirjoitettu! Kirjoita salasana: salasana Kiitos!
Kun käyttäjä syöttää merkkijonon "salasana", ehtolause on totta, ja mennään lohkoon jossa on avainsana break. Break lopettaa toiston suorittamisen välittömästi, jolloin ohjelman suoritusta jatketaan toistolohkon jälkeiseltä riviltä.
Seuraavaksi, kysymme käyttäjän ikää. Jos ikä ei ole välillä 5-85, annetaan huomautus ja kysytään ikä uudelleen. while-lauseen ehto voi siis olla mitä tahansa totuusarvon tuottavaa.
System.out.println("ikäsi: "); int ika = Integer.parseInt(lukija.nextLine(); while( ika < 5 || ika > 85 ) { System.out.println("Valehtelet"); if ( ika < 5 ) { System.out.println("Olet niin nuori ettet osaa kirjoittaa"); } else if ( ika > 85 ) { System.out.println("Olet niin vanha ettet osaa käyttää tietokonetta"); } System.out.println("syötä ikäsi uudelleen: "); ika = Integer.parseInt(lukija.nextLine(); } System.out.println("Ikäsi on siis "+ ika);
Yksi suosituimmista ohjelmointivirheistä toistolauseissa on tehdä vahingossa ikuinen silmukka. Seuraavassa yritetään tulostaa ruudulle 10 keraa "En enää ikinä ohjelmoi ikuista silmukkaa":
int i = 0; while( i<10 ) { System.out.println("En enää ikinä ohjelmoi ikuista silmukkaa"); }
Loopin toistokertojen määrää kontrolloiva muuttuja i
on aluksi 0 ja toistoja tehdään niin kauan kuin i<10
. Käy kuitenkin hieman hassusti, i
:n arvoa ei muuteta missään, eli toistoehto pysyy ikuisesti totena.
Seuraavassa esimerkissä aloitamme laskemisen 0:sta ja toistamme while
lohkoa,
kunnes olemme lukeneet käyttäjältä neljä lukua, jolloin ehto ei ole enää voimassa.
System.out.println("Anna kolme lukua ja kerron niiden summan"); int lukuja = 0; int summa = 0; while ( lukuja < 3 ) { int luku = Integer.parseInt(lukija.nextLine()); summa = summa + luku; // tai summa += luku; lukuja += 1; // tai lukuja = lukuja + 1; } System.out.println("Lukujen summa on: " + summa);
Usein tiedämme jo etukäteen kuinka monta kertaa tahdomme silmukan suorittaa. Edellisessä esimerkissä suoritimme silmukan 3 kertaa. Tämän voi ilmaista myös käyttämällä for-silmukkaa seuraavalla tavalla.
System.out.println("Anna kolme lukua ja kerron niiden summan"); int summa = 0; for(int i = 0; i < 3; i++ ) { // i++ tarkoittaa samaa kuin i += 1 eli i = i + 1 int luku = Integer.parseInt(lukija.nextLine()); summa += luku; } System.out.println("Lukujen summa on: " + summa);
Tässä ei kannata säikähtää yksikirjaimista muuttujaa i. Sen käytöllä on tietojenkäsittelyssä ja ohjelmoinnissa pitkät perinteet.
for-lauseke muodostuu kolmesta lauseesta, jotka erotellaan toisistaan puolipisteellä.
Ensimmäinen lause int i = 0;
on ns. alustuslause,
joka suoritetaan kun for-lauseke kohdataan ensimmäisen kerran.
Toinen lauseen osa i < 3;
on ehtolause.
Se toimii kuten jo tutuksi tulleen while-lauseen ehto.
Ehto testataan ennen jokaista kierrosta ja jos se on tosi, niin runko suoritetaan kerran
ja päivityslause suoritetaan.
Viimeinen lauseen i += 1
osa on päivityslause.
Käytännössä while-lauseen pelkistetympi muoto. for-lauseen osia muokkaamalla voidaan käydä läpi mielenkiintoisempia lukualueita.
System.out.println("Tulostetaan joka kolmas luku."); for(int i = 3; i < 334; i += 3 ) { System.out.print(i + " "); } System.out.println();
Lohko alkaa avaavalla aaltosululla {
ja loppuu sulkevalla aaltosululla }
. Kuten on jo nähty, lohkoja käytetään muunmuassa ehto- ja toistolauseiden alueen rajaamisessa. Tärkeä lohkon ominaisuus on myös se, että lohkossa määritellyt muuttujat ovat voimassa vain lohkon sisällä. Seuraavassa esimerkissä määritellään ehtolausekkeeseen liittyvän lohkon sisällä tekstimuuttuja lohkonSisallaMaariteltyTeksti
, joka on olemassa vain lohkon sisällä. Lohkossa esitellyn muuttujan tulostus lohkon ulkopuolella ei siis toimi!
int luku = 5; if( luku == 5 ){ String lohkonSisallaMaariteltyTeksti = "Jea!"; } System.out.println(lohkonSisallaMaariteltyTeksti); // ei toimi!
Lohkossa voidaan käyttää ja muuttaa sen ulkopuolella määriteltyjä muuttujia.
int luku = 5; if( luku == 5 ) { luku = 6; } System.out.println(luku); // tulostaa luvun 6
Lohkon sisällä voi olla mitä tahansa koodia, eli esim for-lauseen sisällä voi olla myös toinen for tai vaikkapa while. Sisäkkäiset toistolauseet ovatkin ohjelmoinnissa aika yleisiä. Tarkastellaan seuraavaa ohjelmaa:
for(int i = 0; i < 3; i++) { System.out.print(i + ": "); for(int j = 0; j < 3; j++) { System.out.print(j + " "); } System.out.println(); }
Ohjelman tulostus on seuraava:
0: 0 1 2 1: 0 1 2 2: 0 1 2
Eli mitä ohjelmassa tapahtuukaan? Jos ajatellaan pelkkää ulommaista for:ia, on toiminnallisuus helppo ymmärtää:
for(int i = 0; i < 3; i++) { System.out.print(i + ": "); // sisemmäinen for System.out.println(); }
Eli ensin i=0
ja tulostuu 0:
ja rivinvaihto. Tämän jälkeen i kasvaa ja tulostuu ykkönen, jne., eli ulompi for saa aikaan seuraavan:
0: 1: 2:
Myös sisempi for on helppo ymmärtää erillään. Se saa aina aikaan tulosteen 0 1 2
. Kun yhdistämme nämä kaksi, huomaamme, että sisin for suorittaa tulosteensa aina juuri ennen uloimman for:in tulostamaa rivinvaihtoa.
Entä jos muutamme ohjelmaa hiukan:
for(int i = 0; i < 3; i++) { System.out.print(i + ": "); for(int j = 0; j <= i; j++) { System.out.print(j + " "); } System.out.println(); }
Sisemmän for:in toistojen määrä riippuukin nyt ulomman for:in muuttujan i
arvosta. Eli kun i=0
, tulostaa sisin looppi luvun 0, kun i=1
, tulostaa sisin looppi 0 1, jne. Koko ohjelman tulostus on siis seuraava:
0: 0 1: 0 1 2: 0 1 2
Seuraava ohjelma tulostaa lukujen 1..10 kertotaulun.
for(int i = 1; i <= 10; i++) { for(int j = 1; j <= 10; j++) { System.out.print(i*j + " "); } System.out.println(); }
Tulostus näyttää seuraavalta:
1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100
Ylimmällä rivillä luvun 1 kertotaulu. Alussa i=1
ja sisimmän loopin muuttuja j
saa arvot 1...10. Jokaisella i, j
arvoparilla tulostetaan niiden tulo. Eli alussa i=1, j=1
, sitten i=1, j=2
, ..., i=1, j=10
seuraavaksi i=2, j=1
, jne.
Muuta ohjelmaa siten, että tulostatkin sisemmässä for:issa tulon sijaan i
:n ja j
:n arvot niin näät miten ne etenevät.
Taulukko on muuttuja, joka voidaan käsittää eräänlaisena järjestettynä lokerikkona arvoille. Taulukon pituus tai koko on lokerikon paikkojen lukumäärä, eli kuinka monta arvoa taulukkoon voi laittaa. Taulukon arvoja kutsutaan taulukon alkioiksi.
Muistellaan hetki muuttujan luontia ja sen arvon asetusta. Luodaan kokonaislukutyyppinen muuttuja luku
ja asetetaan sen arvoksi 3.
int luku = 3;
Taulukon luonti tapahtuu lähes samalla tavalla, mutta muuttujan tyyppi saa liitteekseen merkinnän []
kuvaamaan taulukkoa. Kolmen alkion kokonaislukutyyppinen taulukko määritellään seuraavasti.
int[] luvut = {100, 1, 42};
Huomaa että perusasetus on täysin sama, ensiksi kerrotaan muuttujan tyyppi, sitten muuttujan nimi, ja lopuksi asetetaan muuttujalle sen arvo. Muuttujan tyyppi on kokonaislukutyyppinen taulukko int[]
, nimi on luvut
ja se saa arvokseen kolme lukua {100, 1, 42}
. Taulukko alustetaan lohkolla, jossa taulukkoon asetettavat muuttujat on esitelty pilkulla eriteltyinä.
Taulukon arvot voivat olla mitä tahansa aikaisemmin esiteltyjä tyyppejä.
String[] tekstitaulukko = {"Matti P.", "Matti V."}; double[] liukulukutaulukko = {1.20, 3.14, 100.0, 0.6666666667};
Taulukon alkioihin voidaan viitata indeksillä, joka on kokonaisluku. Indeksi kertoo paikan taulukon sisällä. Taulukon ensimmäinen alkio on paikassa nolla, seuraava yksi ja niin edelleen. Indeksi (=kokonaisluku) annetaan taulukkomuuttujan perään hakasulkeiden sisällä.
// indeksi 0 1 2 3 4 5 6 7 int[] luvut = {100,1,42,23,1,1,3200,3201}; System.out.println(luvut[0]); // tulostaa luvun taulukon indeksistä 0, eli luvun 100 System.out.println(luvut[2]); // tulostaa luvun taulukon indeksistä 2, eli luvun 42
Yllä olevan taulukon koko (eli pituus) on 8.
Arvon asettaminen taulukon alkioon tapahtuu kuten tavalliseen muuttujaan, mutta taulukkoon asettaessa täytyy myös kertoa indeksi johon arvo asetetaan. Asetus tapahtuu kuten aikaisemmin esitellyillä muuttujilla.
int[] luvut = {100,1,42}; luvut[1] = 101; luvut[0] = 1; // luvut-taulukko on nyt {1,101,42}
Jos indeksillä osoitetaan taulukon ohi, eli alkioon jota ei ole olemassa, niin saadaan virhe ArrayIndexOutOfBoundsException, joka kertoo että indeksi johon osoitimme ei ole olemassa. Taulukon ohi (indeksiin joka on pienempi kuin 0, tai indeksiin joka on suurempi tai yhtäsuuri kuin taulukon koko) ei siis saa viitata.
Taulukon läpikäyntiin soveltuu luontevasti for-silmukka.
Tällöin silmukkamuuttuja saa arvokseen vuorollaan
kunkin taulukon alkion indeksin.
Seuraavassa esimerkissä taulukon koko
(taulukko.length
) on 5,
joten muuttuja i saa arvot 0, 1, 2, 3 ja 4.
Ohjelma tulostaa taulukossa olevat luvut.
int[] luvut = {1, 8, 10, 3, 5}; for (int i = 0; i < luvut.length; i++) { System.out.println(luvut[i]); }
Usein taulukon läpikäynnissä ei ole todellista tarvetta luetella taulukon indeksejä, vaan ainoa kiinnostava asia ovat taulukon arvot. Tällöin voidaan käyttää for-each-rakennetta seuraavan esimerkin mukaisesti. Nyt silmukan rungossa annetaan vain silmukkamuuttujan nimi ja taulukon nimi kaksoispisteellä erotettuna. Silmukkamuuttuja saa arvokseen vuorollaan kunkin taulukossa olevan arvon.
int[] luvut = {1,8,10,3,5}; for (int luku : luvut) { System.out.println(luku); }
String[] nimet = {"Juhana L.", "Matti P.", "Matti L.", "Pekka M."}; for (String nimi : nimet) { System.out.println(nimi); }
Huom! For-each-tyylisellä läpikäynnillä taulukon alkoihin ei voi asettaa arvoja!
Usein emme tiedä etukäteen kuinka suurta taulukkoa tarvitsemme. Pystymme luomaan uuden taulukon muuttujan avulla seuraavalla tavalla:
int n = 99; int[] taulukko = new int[n]; //luodaan muuttujan n kokoinen taulukko if(taulukko.length == n) { System.out.println("Taulukon pituus on " + n); } else { System.out.println("Jotain kummallista tapahtui. Taulukon pituudes oli eri kuin " + n); }
Seuraavassa esimerkissä on ohjelma, joka kysyy käyttäjältä lukujen määrän ja joukon lukuja. Sitten ohjelma tulostaa luvut uudestaan samassa järjestyksessä. Käyttäjän antamat luvut tallennetaan taulukkoon.
System.out.print("Kuinka monta lukua? "); int lukuja = Integer.parseInt(lukija.nextLine()); int[] luvut = new int[lukuja]; System.out.println("Anna luvut:"); for(int i = 0; i < lukuja; i++) { luvut[i] = Integer.parseInt(lukija.nextLine()); } System.out.println("Luvut uudestaan:"); for(int i = 0; i < lukuja; i++) { System.out.println(luvut[i]); }
Eräs ohjelman suorituskerta voisi olla seuraavanlainen:
Kuinka monta lukua? 4 Anna luvut: 4 8 2 1 Luvut uudestaan: 4 2 8 1
Taulukko on myös kätevä apuväline esimerkiksi hyväksyttävien arvojen säilyttämiseen. Seuraavassa esimerkissä pidämme hyväksyttäviä lukuja taulukossa, kysymme käyttäjältä luvun ja käymme kaikki taulukon luvut läpi tarkistaen samalla, onko käyttäjän antama luku hyväksytty vai ei.
int[] hyvaksyttavatLuvut = {4,8,10}; System.out.print("Valitse 4,8 tai 10: "); int valinta = Integer.parseInt(lukija.nextLine()); boolean totteliko = false; for ( int luku : hyvaksyttavatLuvut ) { if ( luku == valinta ) { totteliko = true; } } if ( totteliko ) { System.out.println("Hyvin valittu"); } else { System.out.println("Valitsit luvun " + valinta + ", joka ei ollut sallittu."); }
Muunnelma edellisestä esimerkistä:
int[] luvut = {3,8,9,14} System.out.println("Minulla on neljä lukua väliltä 1-15."); System.out.print("Arvaa luku 1-15 välillä: "); int arvaus = Integer.parseInt(lukija.nextLine()); for (int luku : luvut ) { if ( luku == arvaus ) { System.out.print("Arvasit luvun " + luku + "!"); } }
Seuraavassa "yhdistämme" kaksi taulukkoa siten, että kahdessa taulukossa on yhtä monta alkiota ja jokainen alkio liittyy toisen taulukon vastaavan indeksin alkioon. Tällöin kummassakin taulukossa on tietoa samassa järjestyksessä.
int[] arvosanat = {3,2,1,1}; String[] nimet = {"Juhana L.", "Antti L.", "Matti L.", "Pekka M."}; int indeksi = 0; while ( indeksi < arvosanat.length ) { System.out.println( nimet[indeksi] + ": " + arvosanat[indeksi] ); indeksi = indeksi + 1; }
Olemme jo käyttäneet monia erilaisia komentoja Javassa: sijoitusta, laskutoimituksia, vertailua, if:iä, for:ia ja whileä. Ruudulle tulostaminen on tehty "komentoa" System.out.println()
käyttäen. Kahden luvun maksimi osataan laskea "komennolla" Math.max()
. Tuttuja ovat myös Integer.parseInt()
ja lukija.nextLine()
.
Huomaamme, että jälkimmäinen joukko edellä lueteltuja komentoja poikkeaa if:istä, for:ista, while:stä ym. siinä, että komennon perässä on sulut ja joskus sulkujen sisällä komennolle annettava syöte. "Sulkuihin päättyvät" eivät oikeastaan olekaan komentoja vaan metodeja.
Teknisesti ottaen metodi tarkoittaa koodinpätkää, jota voi kutsua muualta ohjelmakoodista. Koodirivi System.out.println("olen metodi!")
siis tarkoittaa, että kutsutaan metodia, joka suorittaa ruudulle tulostamisen. Metodin suorituksen jälkeen palataan siihen kohtaa missä ennen metodikutsua oltiin menossa. Metodille suluissa annettua syötettä kutsutaan metodin parametriksi.
Parametrin lisäksi metodilla voi olla paluuarvo. Esim. tuttu koodinpätkä:
String lukuMerkkijonona = lukija.nextLine(); int luku = Integer.parseInt( lukuMerkkijonona );
sisältää kaksi metodikutsua. Ensin kutsutaan metodia lukija.nextLine
. Metodilla on paluuarvonaan käyttäjän syöttämä merkkijono. Seuraavalla rivillä
kutsutaan metodia Integer.parseInt
. Metodikutsun parametrina on merkkijono ja metodin paluuarvona on merkkijonoa vastaava kokonaisluku.
On tarpeetonta tallentaa ensin luku merkkijonoksi ja sitten muuttaa se luvuksi. Edelliset metodikutsut kannattaakin yhdistää:
int luku = Integer.parseInt( lukija.nextLine() );
Tässä tapahtuu ensin metodikutsu lukija.nextLine()
. Metodikutsun paluuarvo toimii parametrina "ulommassa" kutsussa Integer.parseInt(...)
.
Metodin nimeen näyttää liittyvän piste, esim. lukija.nextLine()
. Oikeastaan tässä metodin nimi onkin pisteen oikeanpuoleinen osa, eli
nextLine()
. Pisteen vasemmanpuoleinen osa, eli tässä lukija
kertoo kenen metodista on kyse.
Eli kyseessä on lukijan metodi nextLine. Opimme hiukan myöhemmin tarkemmin mistä tässä pisteen vasemmanpuoleisessa
osassa on kyse. Tarkka lukija tietysti huomaa, että System.out.println()
:ssa on "kaksi pistettä". Metodin nimi tässä
on println, ja System.out
on se kenen metodista on kyse. Karkeasti ottaen System.out
tarkoittaa koneen näyttöä.
Tähän mennessä käyttämämme metodit ovat kaikki olleet Javan valmiita metodeita. Hetken kuluttua opimme tekemään myös omia metodeita. Ensin kuitenkin puhutaan merkkijonoista.
Tässä osiossa tutustaan Javan merkkijonoihin, eli String
:eihin.
Olemme jo käyttäneet String
-tyyppisiä muuttujia tulostuksen yhteydessä sekä
oppineet vertailemaan merkkijonoja toisiinsa. Merkkijonoja vertailtiin toisiinsa
kutsumalla sen equals()
-metodia.
String elain = "Koira"; if( elain.equals("Koira") ) { System.out.println(elain + " sanoo vuh vuh"); } else if ( elain.equals("Kissa") ) { System.out.println(elain + " sanoo miau miau"); }
Edellisen luvun käsite "kenen metodi" voi saada edellisestä lisävalaistusta. Eli kun kutsutaan
elain.equals("Koira")
, on kyse nimenomaan muuttujassa elain
olevan merkkijonon metodin equals()
kutsumisesta, parametrina on merkkijono "Koira".
Merkkijonoilta voi kysyä niiden pituutta lähes samalla tavalla kuin taulukoiltakin.
Erona on, että merkkijonon pituutta kysyessä täytyy kirjoittaa length()
taulukon length
sijaan. Sulut perässä kertovat, että kyse on metodikutsusta.
String sana1 = "banaani"; String sana2 = "kurkku"; System.out.println("Banaanin pituus on " + sana1.length()); System.out.println("Kurkku pituus on " + sana2.length());
Edellä kutsutaan metodia length()
kahdelle eri metodille. Kutsu sana1.length()
kutsuu nimenomaan sana1:n
pituuden kertovaa metodia, kun taas sana2.length()
on merkkijonon sana2 pituuden kertovan metodin kutsu.
Taas siis pisteen vasemman puoleinen osa kertoo sen kenen metodia kutsutaan.
Javassa on erillinen char
-tietotyyppi kirjaimia varten. Yksittäiseen char
-muuttujaan
voi tallentaa yhden kirjaimen. Taulukosta voidaan lukea mitä sen kussakin paikassa on. Vastaavasti
merkkijonolta voidaan kysyä sen kirjaimia paikan perusteella käyttämällä charAt()
-metodia.
String kirja = "Kalavale"; char merkki = kirja.charAt(2); System.out.println("Kirjan kolmas kirjain on " + merkki); //tulostaa "l"
Huomaa, että myös merkkijonon kirjaimia indeksoidaan alkaen paikasta 0. Esimerkiksi seuraava kaataa ohjelman.
char merkki = kirja.charAt(kirja.length()); //viimeinen merkki on yhden paikan aikaisemmin
Usein tahdotaan lukea merkkijonosta jokin tietty osa. Tämä onnistuu String-luokan (merkkijono)
substring
-metodilla. Sitä voidaan käyttää kahdella tavalla:
String kirja = "Kalavale"; System.out.println(kirja.substring(4)); //tulostaa "vale" System.out.println(kirja.substring(2,6)); //tulostaa "lava"
Koska substring
-metodin paluuarvo on String
-tyyppinen,
voidaan metodin paluuarvo ottaa talteen muuttujaan.
String kirja = "8 veljestä"; String loppuosa = kirja.substring(2); System.out.println("7 " + loppuosa);
String-luokan metodit tarjoavat myös mahdollisuuden etsiä tekstistä tiettyä sanaa.
Esimerkiksi sana "erkki" sisältyy tekstiin "merkki".
Metodi indexOf()
etsii parametrinaan annettua sanaa merkkijonosta.
Jos sana löytyi, se palauttaa sanan ensimmäisen kirjaimen indeksin. Jos
taas sanaa ei löytynyt merkkijonosta palautetaan -1.
String sana = "merkkijono"; int indeksi = sana.indexOf("erkki"); //indeksin arvoksi tulee 1 System.out.println(sana.substring(indeksi)); //tulostetaan "erkkijono" indeksi = sana.indexOf("jono"); //indeksin arvoksi tulee 6 System.out.println(sana.substring(indeksi)); //tulostetaan "jono" indeksi = sana.indexOf("kirja"); //sana "kirja" ei sisälly sanaan "merkkijono" System.out.println(indeksi); //tulostetaan -1 System.out.println(sana.substring(indeksi)); //virhe!
Olemme tähän mennessä ohjelmoineet ohjelmamme siten, että kaikki tapahtuu yhdessä jatkumossa ja koodia luetaan ylhäältä alas.
Edellä puhuttiin jo metodeista, ja mainittiin että "metodi tarkoittaa koodinpätkää, jota voi kutsua muualta ohjelmakoodista". Javan valmiita metodeja on käytetty jo oikeastaan ensimmäisestä ohjelmasta lähtien.
Javan valmiiden metodien käytön lisäksi ohjelmoija voi kirjoittaa itse metodeja joita sovellus kutsuu. Oikeastaan on hyvin poikkeuksellista jos ohjelmassa ei ole yhtään itse kirjoitettua metodia. Tästälähtien lähes jokainen kurssilla tehty ohjelma sisältääkin itsekirjoitettuja metodeja.
Ohjelmarunkoon metodit kirjoitetaan main:in aaltosulkeiden ulkopuolelle mutta kuitenkin "uloimmaisten" aaltosulkeiden sisäpuolelle, esim. alle merkittyyn kohtaan:
import java.util.Scanner; public class OhjelmaRunko { static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { // ohjelmakoodi } // omat metodit }
Luodaan runkoomme metodi tervehdi
.
import java.util.Scanner; public class OhjelmaRunko { static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { // ohjelmakoodi } // omat metodit public static void tervehdi() { System.out.println("Terveiset metodimaailmasta!"); } }
Metodin määrittely sisältää kaksi osaa. Ylimmällä rivillä on metodin nimi eli tervehdi. Nimen vasemmalla puolella tässä vaiheessa määreet public static void. Ylimmän rivin alla on aaltosulkeilla erotettu koodilohko, jonka sisälle kirjoitetaan metodin koodi, eli ne komennot joita metodi kutsuu. Metodimme ei tee muuta kuin kirjoittaa rivillisen tekstiä ruudulle.
Itsekirjoitetun metodin kutsu on helppoa, kirjoitetaan metodin nimi ja perään sulut ja puolipiste. Seuraavassa main eli pääohjelma kutsuu metodia ensin kerran ja sen jälkeen useita kertoja.
import java.util.Scanner; public class OhjelmaRunko { static Scanner lukija = new Scanner(System.in); public static void main(String[] args) { // ohjelmakoodi System.out.println("Kokeillaan pääsemmekö metodimaailmaan:"); tervehdi(); System.out.println("Näyttää siltä, kokeillaan vielä:"); tervehdi(); tervehdi(); tervehdi(); } // omat metodit public static void tervehdi() { System.out.println("Terveiset metodimaailmasta!"); } }
Ohjelman suoritus saa aikaan seuraavan tulosteen:
Kokeillaan pääsemmekö metodimaailmaan: Terveiset metodimaailmasta! Näyttää siltä, kokeillaan vielä: Terveiset metodimaailmasta! Terveiset metodimaailmasta! Terveiset metodimaailmasta!
Tässä siis huomionarvoista on, että koodin suoritus etenee siten, että pääohjelman eli main:in rivit suoritetaan ykhäältä alas yksi kerrallaan. Koodirivin ollessa metodikutsu, mennään suorittamaan metodin koodirivit jonka jälkeen palataan siihen kohtaan josta kutsu tapahtui, tai tarkemminottaen metodikutsun jälkeiselle riville.
Jos ollaan tarkkoja on pääohjelma eli main itsekin metodi. Kun ohjelma käynnistyy, kutsuu käyttöjärjestelmä main:ia. Main on siis ohjelman käynnistyspiste, jonka ylimmältä riviltä ohjelman suoritus lähtee liikkeelle. Ohjelman suoritus loppuu kun päädytään mainin loppuun.
Jatkossa kun esittelemme metodeja, emme erikseen mainitse että niiden täytyy sijaita omalla paikallaan pääohjelmassa. Metodia ei esimerkiksi voi määritellä toisen metodin sisällä.
Metodeista saa huomattavasti monikäyttöisemmän antamalla sille parametreja. Parametrit ovat muuttujia, jotka määritellään metodin ylimmällä rivillä metodin nimen jälkeen olevien sulkujen sisällä. Kun metodia kutsutaan, sen parametreille annetaan arvot kutsuvaiheessa.
Seuraavassa esimerkissä määritellään parametrillinen metodi tervehdi
, jolla on String-tyyppinen parametri nimi
public static void tervehdi(String nimi) { System.out.println("Hei " + nimi + ", terveiset metodimaailmasta!"); }
Kutsutaan metodia tervehdi
siten, että parametrin nimi
arvoksi asetetaan ensimmäisellä kutsulla Matti
ja toisella kutsulla Arto
.
public static void main(String[] args) { tervehdi("Matti"); tervehdi("Arto"); }
Hei Matti, terveiset metodimaailmasta! Hei Arto, terveiset metodimaailmasta!
Aivan kuten kutsuttaessa Javan valmista System.out.println()
-metodia, voi oman metodin kutsussa parametrina käyttää monimutkaisempaa ilmausta:
public static void main(String[] args) { String nimi1 = "Antti"; String nimi2 = "Mikkola"; tervehdi( nimi1 + " " + nimi2 ); int ika = 24; tervehdi("Juhana " + ika + " vuotta"); }
Hei Antti Mikkola, terveiset metodimaailmasta! Hei Juhana 24 vuotta, terveiset metodimaailmasta!
Molemmissa tapauksissa metodilla on edelleen vain 1 parametri. Parametrin arvo lasketaan ennen metodin kutsumista. Ensimmäisessä tapauksessa parametrin arvo saadaan merkkijonokatenaationa nimi1 + " " + nimi2
joka siis on arvoltaan Antti Mikkola ja jälkimmäisessä tapauksessa merkkijonokatenaatiosta "Juhana " + ika + " vuotta"
.
Taulukoiden käyttö parametrina onnistuu ilman ongelmia:
public static void listaaAlkiot(int[] kokonaislukutaulukko) { System.out.println("taulukon alkiot ovat: "); for( int luku : kokonaislukutaulukko) { System.out.print(luku + " "); } System.out.println(""); } public main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; listaaAlkiot(t); }
Huomaa että parametrin nimi metodin sisällä voi olla aivan vapaasti valittu, nimen ei tarvitse missään tapauksessa olla sama kuin kutsuvassa. Edellä taulukkoa kutsutaan metodin sisällä nimellä taulukko
, metodin kutsuja taas näkee saman taulukon t
-nimisenä.
Metodille voidaan määritellä useita parametreja. Tällöin metodin kutsussa parametrit annetaan samassa järjestyksessä.
public static void tervehdi(String nimi, String mistaTerveiset) { System.out.println("Hei " + nimi + ", terveiset " + mistaTerveiset); }
String kuka = "Matti"; String terveiset = "Kyröjoelta"; tervehdi(kuka, terveiset); tervehdi(kuka, terveiset + " ja Kumpulasta");
Jälkimmäisessä tervehdi
-funktion kutsussa toinen parametri muodostetaan katenoimalla muuttujaan terveiset
teksti " ja Kumpulasta"
. Tämä suoritetaan ennen varsinaista funktion suoritusta.
Hei Matti, terveiset Kyröjoelta Hei Matti, terveiset Kyröjoelta ja Kumpulasta
main ei ole suinkaan ainoa joka voi kutsua metodeita. Metodit voivat kutsua myös toisiaan. Tehdään metodi tervehdiMontaKertaa
, joka tervehtii käyttäjää useasti metodin tervehdi
avulla:
public static void tervehdi(String nimi) { System.out.println("Hei " + nimi + ", terveiset metodimaailmasta!"); } public static void tervehdiMontaKertaa(String nimi, int kerrat) { for ( int i=0; i < kerrat; i++ ) tervehdi(nimi); } public static void main(String[] args) { tervehdiMontaKertaa("Antti", 3); System.out.println("ja"); tervehdiMontaKertaa("Martin", 2); }
Tulostuu:
Hei Antti, terveiset metodimaailmasta! Hei Antti, terveiset metodimaailmasta! Hei Antti, terveiset metodimaailmasta! ja Hei Martin, terveiset metodimaailmasta! Hei Martin, terveiset metodimaailmasta!
Yritetään muuttaa metodin sisältä pääohjelman muuttujan arvoa.
public static void kasvataKolmella() { x = x + 3; }
int x = 1; kasvataKolmella();
Ohjelma ei kuitenkaan toimi, sillä metodi ei näe pääohjelman muuttujaa x
.
Yleisemminkin voi todeta, että pääohjelman muuttujat eivät näy moetodien sisään, ja metodin muuttujat eivät näy muille metodeille tai pääohjelmalle. Ainoa keino viedä metodille tietoa ulkopuolelta on parametrin avulla.
Metodi voi myös palauttaa arvon. Edellä olevissa esimerkeissä metodit eivät palauttaneet mitään. Tämä on merkitty kirjoittamalla metodin ylimmälle riville heti nimen vasemmalle puolelle void.
public static void kasvataKolmella() { ...
Arvon palauttavaa metodia määriteltäessä täytyy määritellä myös palautettavan arvon tyyppi. Paluuarvon tyyppi merkitään metodin nimen vasemmalle puolelle. Seuraavassa metodi joka palauttaa aina luvun 10. Palautus tapahtuu komennolla return:
public static int palautetaanAinaKymppi() { return 10; }
Jotta metodin kutsuja voisi käyttää paluuarvoa, tulee paluuarvo ottaa talteen muuttujaan:
int luku = palautetaanAinaKymppi(); System.out.println( "metodi palautti luvun " + luku );
Metodin paluuarvo siis sijoitetaan int-tyyppiseen muuttujaan aivan kuin mikä tahansa muukin int-arvo. Paluuarvo voi toimia myös osana mitä tahansa lauseketta:
double luku = 4*palautetaanAinaKymppi() + palautetaanAinaKymppi()/2 - 8; System.out.println( "laskutoimituksen tulos " + luku );
Kaikki muuttujatyypit, mitä olemme tähän mennessä nähneet, voidaan myös palauttaa metodista:
public static void metodiJokaEiPalautaMitaan() { // metodin runko } public static int metodiJokaPalauttaaKokonaisLuvun() { // metodin runko, tarvitsee return-komennon } public static String metodiJokaPalauttaaTekstin() { // metodin runko, tarvitsee return-komennon } public static String[] metodiJokaPalauttaaTekstiTaulukon() { // metodin runko, tarvitsee return-komennon }
Jos metodille määritellään paluuarvo, on sen myös pakko palauttaa arvo.
Seuraavassa esimerkissä määritellään metodi summan laskemiseen. Tämän jälkeen metodia käytetään laskemaan luvun 2 ja 7 yhteen, metodikutsusta saatava paluuarvo asetetaan muuttujaan lukujenSumma
.
public static int summa(int eka, int toka) { return eka + toka; }
int lukujenSumma = summa(2, 7); // lukujenSumma on nyt 9
Laajennetaan edellistä esimerkkiä siten, että käyttäjä syöttää luvut.
public static int summa(int eka, int toka) { return eka + toka; }
System.out.print("Anna ensimmäinen luku: "); int eka = Integer.parseInt(lukija.nextLine()); System.out.print("Anna toinen luku: "); int toka = Integer.parseInt(lukija.nextLine()); System.out.print("Luvut ovat yhteensä: " + summa(eka,toka) );
Kuten huomataan, metodin paluuarvoa ei tarvitse välttämättä sijoittaa muuttujaan, se voi olla osana tulostuslausetta aivan kuten mikä tahansa muukin int-arvo.
Seuraavassa esimerkissä metodia summa kutsutaan kokonaisluvuilla, jotka saadaan summa
-metodin paluuarvoina.
int a = 3; int b = 2; summa(summa(1,2), summa(a,b)); // 1) suoritetaan sisemmät metodit: // summa(1,2) = 3 ja summa(a,b) = 5 // 2) suoritetaan ulompi metodi: // summa(3,5) = 8
Seuraava metodi laskee syötteinään saamiensa luvun keskiarvon. Metodi käyttää apumuuttujia summa
ja ka
. Metodin sisäisen muuttujan määrittely tapahtuu tutulla tavalla.
public static double keskiarvo(int luku1, int luku2, int luku3) { int summa = luku1+luku2+luku3; double ka = summa/3.0; return ka; }
Metodit voivat yksittäisten arvojen lisäksi palauttaa myös muunmuassa taulukkoja. Esimerkiksi merkkijonotaulukon palauttava metodi voisi olla seuraavannäköinen.
public static String[] annaMerkkijonoTaulukko() { // palautetaan taas opejen nimet! String[] opet = {"Juhana L.", "Antti L.", "Matti L.", "Pekka M.", "Martin P."}; return opet; } public static void main(String[] args){ String[] opettajat = annaMerkkijonoTaulukko(); for ( String opettaja : opettajat) System.out.println( opettaja ); }
Metodista saadaan uudelleenkäytettävä siten, että se ei ota kantaa ohjelmaan jossa sitä käytetään. Tällöin toimivaa ja testattua metodia voidaan uudelleenkäyttää missä ohjelmassa tahansa.
Seuraavassa teemme metodin, joka käy parametrinään saadun taulukon läpi ja palauttaa true
jos kaikki arvot olivat positiivisia.
public static boolean onkoKokonaanPositiivinen(int[] taulukko) { boolean positiivinen = true; for ( int arvo : taulukko ) { if ( arvo < 0 ) { positiivinen = false; } } return positiivinen; }
int lokerikko = [1,3,-1,3]; if ( onkoKokonaanPositiivinen(lokerikko) ) { System.out.println("Lokerikko on kokonaan positiivinen"); } else { System.out.println("Lokerikko ei ole kokonaan positiivinen"); }
Kun metodissa suoritetaan return
-käsky, niin metodin suoritus loppuu välittömästi.
Käyttämällä tätä tietoa hyväksi voimme kirjoittaa edellisen onkoKokonaanPositiivinen
-metodin
hiukan suoraviivaisemmin (ja ehkä selkeämmin).
public static boolean onkoKokonaanPositiivinen(int[] taulukko) { for ( int arvo : taulukko ) { if ( arvo <= 0 ) { return false; //kaikki alkiot eivät ole positiivisia, joten tulos on varma jo nyt } } return true; //ei kohdattu yhtäkään ei-positiivista lukua, jos päästään tänne asti }
Tarkastellaan vielä muutamaa metodeihin liittyvää tärkeää yksityiskohtaa.
Hieman aiemmin materiaalissa oli esimerkki, jossa yritetään muuttaa metodin sisältä pääohjelman muuttujan arvoa:
public static void main(String[] args) { int x = 1; kasvataKolmella(); System.out.println("x on " + x); } public static void kasvataKolmella() { x = x + 3; }
Ohjelma ei kuitenkaan toimi, sillä metodi ei näe pääohjelman muuttujaa x
.
Tämä siis johtuu siitä, että pääohjelman muuttujat eivät näy metodien sisään. Ja yleisemmin: minkään metodin muuttujat eivät näy muille metodeille. Koska pääohjelma main on myös metodi, pätee sääntö myös pääohjelmalle. Ainoa keino viedä metodille tietoa ulkopuolelta on parametrin avulla.
Yritetään korjata edellinen esimerkki ja välitetään pääohjelman muuttuja x
metodille parametrina.
public static void main(String[] args) { int x = 1; kasvataKolmella(x); System.out.println(x); // tulostaa 1, eli arvo x ei muuttunut } public static void kasvataKolmella(int x) { x = x + 3; }
Ohjelma ei kuitenkaan toimi ihan odotetulla tavalla. Metodissa olevat parametrit ovat eri muuttujia kuin pääohjelmassa esitellyt muuttujat. Edellä metodi siis kasvattaa samannimistä, mutta ei samaa! parametria x
.
Kun metodille annetaan parametri, sen arvo kopioituu metodissa käytettäväksi. Yllä olevassa esimerkissä metodille kasvataKolmella
annetusta muuttujasta x
luodaan kopio, jota muokataan metodissa. Alkuperäiselle muuttujalle x
ei tehdä mitään.
Allaoleva kuva antaa vielä lisävalaisua tilanteeseen. Voidaan ajatella, että main ja metodi toimivat kumpikin omassa kohtaa muistia. Muistissa on main:in muuttujaa x
varten oma "lokero". Kun metodia kutsutaan, tehtään tälle oma x
jonka alkuarvoksi tulee main:in x
:n arvo eli 1. Molemmat x
:t ovat kuitenkin täysin erillisiä, eli kun metodi muuttaa omaa x
:äänsä, ei muutos vaikuta millään tavalla pääohjelman x
:n.
Metodista saa toki välitettyä tietoa kutsujalle käyttäen paluuarvoa, eli "returnaamalla" halutun arvon. Edellinen saadaan toimimaan muuttamalla koodia hiukan:
public static void main(String[] args) { int x = 1; x = kasvataKolmellaJaPalauta(x); System.out.println(x); // tulostaa 4, sillä x on saanut arvokseen metodin palauttaman arvon } public static int kasvataKolmellaJaPalauta(int x) { x = x + 3; return x; }
Edelleen on niin, että metodi käsittelee pääohjelman x
:n arvon kopiota. Pääohjelma sijoittaa metodin palauttaman arvon muuttujaan x
, joten muutos tulee tämän takia voimaan myös pääohjelmassa. huomaa, että edellisessä ei ole mitään merkitystä sillä mikä on parametrin nimi metodissa. Koodi toimii täysin samoin oli nimi mikä tahansa, esim.
public static void main(String[] args) { int x = 1; x = kasvataKolmellaJaPalauta(x); System.out.println(x); } public static int kasvataKolmellaJaPalauta(int kasvatettavaLuku) { kasvatettavaLuku = kasvatettavaLuku + 3; return kasvatettavaLuku; }
Eli todettiin, että metodissa olevat parametrit ovat eri muuttujia kuin metodin kutsujassa esitellyt muuttujat, ja ainoastaan parametrin arvo kopioituu kutsujasta metodiin.
Asia ei kuitenkaan ole ihan näin yksinkertainen. Jos metodille annetaan parametrina taulukko, käy niin että sama taulukko näkyy metodille ja kaikki metodin taulukkoon tekemät muutokset tulevat kaikkialla voimaan.
public static void nollaaTaulukko(int[] taulukko) { for ( int i=0; i < taulukko.length; i++ ) taulukko[i] = 0; }
int[] t = { 1, 2, 3, 4, 5 }; for ( int luku : t ) System.out.print( luku+ " " ); // tulostuu 1, 2, 3, 4, 5 System.out.println(); nollaaTaulukko(t); for ( int luku : t ) System.out.print( luku+ " " ); // tulostuu 0, 0, 0, 0, 0
Eli toisin kuin int-tyyppinen parametri, taulukko ei kopioidu vaan metodi käsittelee suoraan parametrina annettua taulukkoa. Toistaiseksi riittää tietää, että arvon kopioituminen tapahtuu muuntyyppisille parametreille kuin taulukoille.
Tilannetta valaisee allaoleva kuva. Toisin kuin int-tyyppinen muuttuja, taulukko ei sijaitsekaan samalla tapaa "lokerossa", vaan taulukon nimi, eli mainin tapauksessa t
onkin ainoastaan viite paikkaan missä taulukko sijaitsee. Yksi tapa ajatella asiaa, on että taulukko on "langan päässä", eli taulukon nimi t
on lanka jonka päästä taulukko löytyy. Kun metodikutsun parametrina on taulukko, käykin niin että metodille annetaan lanka jonka päässä on sama taulukko jonka metdoin kutsuja näkee. Eli main:illa ja metodilla on kyllä molemmilla oma lanka, mutta langan päässä on sama taulukko ja kaikki muutokset mitä metodi tekee taulukkoon tapahtuvat täsmälleen samaan taulukkoon jota pääohjelma käyttää. Viikosta 5 alkaen tulemme huomaamaan että Java:ssa hyvin moni asia on "langan päässä".
Huomaa jälleen että parametrin nimi metodin sisällä voi olla aivan vapaasti valittu, nimen ei tarvitse missään tapauksessa olla sama kuin kutsuvassa. Edellä taulukkoa kutsutaan metodin sisällä nimellä taulukko
, metodin kutsuja taas näkee saman taulukon t
-nimisenä.
Yksi maailman johtavista ohjelmistonkehittäjistä Kent Beck on lausunut mm. seuraavasti:
Yritämme jatkossa toimia näiden ohjeiden mukaan. Aletaan ottamaan nyt ensimmäisiä askelia Kent Beckin viitoittamalla tiellä.
Sisentämätöntä koodia on ikävä lukea:
public static void main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; for ( int luku : t ) } System.out.print( luku+ " " ); } System.out.println(); for ( int i=0; i < taulukko.length; i++ ) { taulukko[i] += 1; } for ( int luku : t ) { System.out.print( luku+ " " ); } System.out.println(); }
Sisennetään koodi oikein, ja erotellaan loogiset kokonaisuudet rivinvaihdoin:
public static void main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; for ( int luku : t ) { System.out.print( luku+ " " ); } System.out.println(); for ( int i=0; i < taulukko.length; i++ ) { taulukko[i] += 1; } for ( int luku : t ) { System.out.print( luku+ " " ); } System.out.println(); }
Nyt koodissa alkaa olla jo järkeä. Esim. taulukon tulostus ja arvojen kasvatus ovat omia loogisia kokonaisuuksia, joten ne on erotettu rivinvaihdolla. Koodissa on mukavaa ilmavuutta ja koneen lisäksi ihmisen alkaa jo olla miellyttävämpi lukea koodia.
Ohjelmoijan lähes pahin mahdollinen perisynti on copy paste -koodi, eli samanlaisen koodinpätkän toistuminen koodissa useaan kertaan. Esimerkissämme taulukon tulostus tapahtuu kahteen kertaan. Tulostuksen hoitava koodi on syytä erottaa omaksi metodikseen ja laittaa main-kutsumaan metodia:
public static void main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; tulostaTaulukko(t); for ( int i=0; i < taulukko.length; i++ ) { taulukko[i] += 1; } tulostaTaulukko(t); } public static void tulostaTaulukko(int[] t) { for ( int luku : t ) { System.out.print( luku+ " " ); } System.out.println(); }
Nyt koodi alkaa olla jo selkeämpää. Selvästi erillinen kokonaisuus, eli taulukon tulostus on oma helposti ymmärrettävä metodinsa. myös pääohjelman luettavuus on kasvanut. Huomaa myös, että metodit on nimetty mahdollisimman kuvaavasti, eli siten että metodin nimi sanoo sen mitä metodi tekee.
Ohjelmassa on kuitenkin vielä hiukan siistimisen varaa. Pääohjelma on vielä sikäli ikävä, että siistien metodikutsujen seassa on vielä suoraan taulukkoa käsittelevä "epäesteettinen" koodinpätkä. Erotetaan tämäkin omaksi metodikseen:
public static void main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; tulostaTaulukko(t); kasvataTaulukonAlkioitaYhdella(t); tulostaTaulukko(t); } public static void kasvataTaulukonAlkioitaYhdella(int[] t) { for ( int i=0; i < taulukko.length; i++ ) { taulukko[i] += 1; } } public static void tulostaTaulukko(int[] t) { for ( int luku : t ) { System.out.print( luku+ " " ); } System.out.println(); }
Eli vaikka copy pastea ei ollutkaan, loimme loogiselle kokonaisuudelle (taulukon arvojen kasvattaminen yhdellä) oman kuvaavasti nimetyn metodin. Lopputuloksena oleva pääohjelma on nyt erittäin ymmärrettävä, lähes suomen kieltä. Molemmat metodit ovat myös erittäin yksinkertaisia ja selkeitä ymmärtää.
Abstrahoimalla eli yleistämällä koodia hiukan lisää voisimme muokata metodin kasvataTaulukonAlkioitaYhdella(int[] t)
ottamaan toisena parametrinaan kasvatettavan lukumäärän. Metodit ja pääohjelma näyttäisivät seuraavilta:
public static void main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; tulostaTaulukko(t); kasvataAlkioita(t,1); tulostaTaulukko(t); } public static void kasvataAlkioita(int[] t, int paljonko) { for ( int i=0; i < taulukko.length; i++ ) { taulukko[i] += paljonko; } } public static void tulostaTaulukko(int[] t) { for ( int luku : t ) { System.out.print( luku+ " " ); } System.out.println(); }
Kent Beck olisi varmaan tyytyväinen aikaansaannokseemme, koodi on helposti ymmärrettävää, helposti muokattavaa eikä sisällä copy pastea.
Tarkastellaan uudelleen viikon 2 tehtävää 7.3, tehtäväkuvaus:
Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Voit olettaa, että käyttäjä antaa korkeintaan 1000 sanaa.
Anna sana: porkkana Anna sana: selleri Anna sana: nauris Anna sana: lanttu Anna sana: selleri Annoit saman sanan uudestaan!
Monilla oli hieman ongelmia tehtävän ratkaisussa. Osittain ongelmat johtuivat todennäköisesti siitä, että koko Java oli tuossa vaiheessa vielä uutta. Osin ongelmien syy saattoi olla, että oli vaikea päättää miten lähestyä tehtävää, eli miten jäsentää ongelmaa ja mistä aloittaa.
Ongelma koostuu oikeastaan kahdesta "aliongelmasta". Ensimmäinen on sanojen toistuva lukeminen käyttäjältä kunnes tietty ehto toteutuu. Tämä voitaisiin hahmotella seuraavaan tapaan ohjelmarungoksi:
public static void main(String[] args) { while ( true ) { String sana = lukija.nextLine(); if ( "pitää lopettaa" ) break; } System.out.println("Annoit saman sanan uudestaan"); }
Sanojen kysely pitää lopettaa siinä vaiheessa kun on syötetty jokin jo aiemmin syötetty sana. Päätetään tehdä metodi, joka huomaa että sana on jo syötetty. Vielä ei tiedetä miten metodi kannattaisi tehdä, joten tehdään siitä vasta runko:
public static void main(String[] args) { while ( true ) { String sana = lukija.nextLine(); if ( onJoSyotetty( sana ) ) break; } System.out.println("Annoit saman sanan uudestaan"); } public static boolean onJoSyotetty(String sana) { // tänne jotain }
Ohjelmaa on hyvä testata koko ajan, joten tehdään metodista kokeiluversio:
public static boolean onJoSyotetty(String sana) { if ( sana.equals("loppu") ) return true; return false; }
Eli toisto jatkuu nyt niin kauan kunnes syötteenä on sana loppu:
Anna sana: porkkana Anna sana: selleri Anna sana: nauris Anna sana: lanttu Anna sana: loppu Annoit saman sanan uudestaan!
Ohjelma ei siis toimi vielä kokonaisuudessaan, mutta ensimmäinen osaongelma eli ohjelman pysäyttäminen kunnes tietty ehto toteutuu on saatu toimimaan.
Toinen osaongelma on aiemmin syötettyjen sanojen muistaminen. Taulukko sopii tietysti mainiosti tähän tarkoitukseen. Tarvitaan myös muuttuja joka muistaa kuinka monta sanaa on syötetty:
public static void main(String[] args) { String[] aiemmatSanat = new String[1000]; int sanojaSyotetty = 0; // ... }
Kun uusi sana syötetään, on se lisättävä syötettyjen sanojen joukkoon. Tämä tapahtuu lisäämällä main:in while-silmukkaan taulukkoa ja sanojen laskuria päivittävät rivit:
while ( true ) { String sana = lukija.nextLine(); if ( onJoSyotetty( sana, aiemmatSanat, sanojaSyotetty ) ) break; // lisätään uusi sana aiempien sanojen taulukkoon aiemmatSanat[sanojaSyotetty] = sana; sanojaSyotetty++; }
Jälleen kannattaa testata, että ohjelma toimii edelleen. Voi olla hyödyksi esim. lisätä ohjelman loppuun testitulostus, joka varmistaa että syötetyt sanat todella menivät taulukkoon:
while ( true ) { // ... // lisätään uusi sana aiempien sanojen taulukkoon aiemmatSanat[sanojaSyotetty] = sana; sanojaSyotetty++; } // testitulostus joka varmistaa että kaikki toimii edelleen for ( int i=0; i < sanojaSyotetty; i++ ) { System.out.println( aiemmatSanat[sanojaSyotetty] ); }
Muokataan vielä äsken tekemämme metodi onJoSyotetty
tutkimaan onko kysytty sana jo syötettyjen joukossa, eli taulukon käytössä olevassa alkuosassa:
public static boolean onJoSyotetty(String sana, String[] aiemmatSanat, int sanojaSyotetty) { for ( int i=0; i < sanojaSyotetty; i++ ) { if ( sana.equals( aiemmatSanat[i] ) ) return true; } return false; } public static void main(String[] args) { String[] aiemmatSanat = new String[1000]; int sanojaSyotetty = 0; while ( true ) { String sana = lukija.nextLine(); if ( onJoSyotetty( sana, aiemmatSanat, sanojaSyotetty ) ) break; aiemmatSanat[sanojaSyotetty] = sana; sanojaSyotetty++; } System.out.println("Annoit saman sanan uudestaan"); }
Nyt koodi on valmis ja alkaa olla kohtuullisen luettava. Emme kuitenkaan ole koodiin täysin tyytyväisiä. Valitettavasti emme vielä osaa tarpeeksi Javaa tehdäksemme "lopullista" siistiä versiota. Ensi viikolla kuitenkin otamme ratkaisevan askeleen jolla saamme koodin siistiksi.
Eli ohjelmoidessasi, seuraa aina näitä neuvoja:Hyvät ja kokeneet ohjelmoijat noudattavat näitä käytänteitä sen takia että ohjelmointi olisi helpompaa, ja että ohjelmien lukeminen, ylläpitäminen ja muokkaaminen olisi helpompaa.
Hyvin usein törmätään tilanteeseen, jossa taulukollinen lukuja tai merkkijonoja on järjestettävä suuruusjärjestykseen. Viikon 2 tehtävässä 7.2 neuvottiin, että Javan valmiin kaluston avulla taulukko saadaan järjestettyä seuraavasti:
int[] taulukko = new int[10]; // laitetaan taulukkoon lukuja Arrays.sort(taulukko);
Taulukon järjestäminen on siis helppoa, Javan valmiin kaluston käyttö riittää. Ohjelmoijan yleissivistykseen kuluu yleensä ainakin yhden järjestämisalgoritmin (eli tavan järjestää taulukko) tuntemus. Tällä kurssilla tutustutaan yhteen "klassiseen" järjestämisalgoritmiin, valintajärjestämiseen. Tutustuminen tapahtuu tekemällä sarja harjoitustehtäviä.
Ohjelmissa tahdotaan satunnaisuutta syystä tai toisesta. Useimmiten kaikenlaista satunnaisuutta voidaan simuloida pyytämällä satunnaisia lukuja. Tämä onnistuu käyttäen Javasta valmiiksi löytyvää Random-apuluokkaa (luokkiin palaamme ensi viikolla). Sitä voi käyttää seuraavalla tavalla.
import java.util.Random; class Arvontaa { public static void main(String[] args) { Random arpoja = new Random(); //luodaan arpoja apuväline for(int i = 0; i < 10; ++i) { //Arvotaan ja tulostetaan jokaisella kierroksella satunnainen luku System.out.println(arpoja.nextInt(10)); } } }
Yllä olevassa koodissa luodaan ensin Random-apuväline käyttäen avainsanaa new
samoin
kuten taulukoidenkin yhteydessä. Tämän jälkeen apuvälinettä voi käyttää metodilla
nextInt
, jolle annetaan parametrina kokonaisluku n.
Metodi palauttaa satunnaisen kokonaisluvun väliltä 0..(n-1).
Ohjelman tulostus voisi olla vaikka seuraavanlainen:
2 2 4 3 4 5 6 0 7 8
Usein esiintyy tarve saada liukuluku väliltä [0..1] (esimerkiksi todennäköisyyksien yhteydessä).
Random-luokka tarjoaa myös satunnaisia liukulukuja, joita saa käskyllä
nextDouble
. Liukulukujen avulla voidaan simuloida epävarmoja tapahtumia.
Tarkastellaan seuraavia säämahdollisuuksia:
Luodaan edellä olevista arvioista sääennuste ensi viikolle.
import java.util.Random; class SaaEnnuste { public static String ennustaSaa(double tn) { if(tn <= 0.6) { return "Aurinko paistaa"; } else if (tn <= 0.9) { return "Sataa lunta"; } else { return "Sataa räntää"; } } public static void main(String[] args) { Random arpoja = new Random(); //luodaan arpoja apuväline //Käytetään taulukkoa apumuuttujana String[] paivat = { "Ma", "Ti", "Ke", "To", "Pe", "La", "Su" }; System.out.println("Seuraavan viikon sääennuste:"); for(String paiva : paivat) { //ennustetaan jokaiselle päivälle oma ennuste String saa = ennustaSaa(arpoja.nextDouble()); //arvotaan sään laatu //arvotaan lämpötila käyttäen normaalijakautunutta lukua int lampotila = (int)(4*arpoja.nextGaussian() - 3); System.out.println(paiva + ": " + saa + " " + lampotila + " astetta."); } } }
Ohjelman tulostus voisi olla esimerkiksi seuraavanlainen:
Seuraavan viikon sääennuste: Ma: Sataa lunta 1 astetta. Ti: Sataa lunta 1 astetta. Ke: Aurinko paistaa -2 astetta. To: Aurinko paistaa 0 astetta. Pe: Sataa lunta -3 astetta. La: Sataa lunta -3 astetta. Su: Aurinko paistaa -5 astetta
Edellä käytettiin kirjoitettiin seuraavasti int lampotila = (int)(4*arpoja.nextGaussian() - 3);
.
Tässä arpoja.nextGaussian()
on tavallinen metodikutsu, jonka kaltaisia olemme nähneet jo
aikaisemminkin. Mahdollisesti kiinnostavaa tässä metodissa on se, että metodin
palauttama luku on normaalijakautunut
(jos et koe mielenkiintoa satunnaisuuden eri lajeihin sillä ei ole merkitystä!).
Java-kielen kannalta kiinnostavaa edellisessä lausekkeessa on osa (int)
. Tämä kohta
lausekkeessa muuttaa suluissa
olevan liukuluvun kokonaisluvuksi. Vastaavalla menetelmällä voidaan muuttaa myös kokonaislukuja
liukuluvuiksi kirjoittamalla (double) kokonaisluku
. Tätä kutsutaan eksplisiittiseksi
tyyppimuunnokseksi (lisää tietoa löydät
täältä).
Olemme jo lähes rutinoituneita taulukon käyttäjiä. Taulukoilla on kuitenkin pari hieman ikävää puolta.
Javan valmis kalusto tarjoaa kuitenkin ratkaisun taulukkoa vaivaaviin ongelmiin, ratkaisu on ArrayList, eli "joustava taulukko". Seuraava ohjelmanpätkä ottaa käyttöönsä merkkijonoja tallentavan ArrayList:in sekä tallettaa listalle pari merkkijonoa.
import java.util.*; public class ListaOhjelma { public static void main(String[] args) { ArrayList<String> sanaLista = new ArrayList<String>(); sanaLista.add("Ensimmäinen"); sanaLista.add("Toinen"); } }
Ensimmäinen rivi luo sanaLista
nimisen merkkijonoja tallettavan arraylistin. ArrayList:in luominen muistuttaa hieman taulukon luomista. Listamuuttujan tyypin nimi on ArrayList<Integer>
, eli merkkijonoja tallettava ArrayList. Itse lista luodaan sanomalla new ArrayList<String>()
.
Huom: jotta ArrayList toimisi, on ohjelman ylälaitaan kirjoitettava import java.util.ArrayList;
Kun lista on luotu, lisätään kaksi merkkijonoa kutsumalla listan metodia add
. Toisin kuin taulukon tapauksessa, ei ArrayList:ille tarvitse kertoa mihin kohtaa listaa merkkijonot menevät. Lisätyt merkkijonot menevät automaattisesti ArrayList:in loppuun. Tila ei lopu listalla missään vaiheessa kesken, eli periaatteessa listalle saa lisätä vaikka niin monta merkkijonoa kun "koneen" muistiin mahtuu.
ArrayList tarjoaa monia hyödyllisiä metodeita:
public static void main(String[] args) { ArrayList<String> opettajat = new ArrayList<String>(); opettajat.add("Antti"); opettajat.add("Arto"); opettajat.add("Pekka"); opettajat.add("Juhana"); opettajat.add("Martin"); opettajat.add("Matti"); System.out.println("opettajien lukumäärä " + opettajat.size() ); System.out.println("listalla kolmantena " + opettajat.get(3)); opettajat.remove("Arto"); if ( opettajat.contains("Arto") ) System.out.println("Arto on opettajien listalla"); else System.out.println("Arto ei ole opettajien listalla"); }
Ensin luokaan merkkijonolista jolle lisätään 6 nimeä. size
kertoo listalla olevien merkkijonojen lukumäärän.
Merkkijonot ovat listalla siinä järjestyksessä missä ne listalle laitettiin. Metodilla get(i)
saadaan tietoon listan paikan i
sisältö.
Huom: kun metodia kutsutaan, on kutsu muotoa opettajat.size()
, eli metodin nimeä edeltää piste ja sen listan nimi kenen metodia kutsutaan.
Metodin remove
avulla voidaan listalta poistaa merkkijonoja. Jos käytetään metodia muodossa remove("merkkejä")
, poistetaan parametrina annettu merkkijono. Metodia voi käyttää myös siten, että annetaan parametriksi tietty luku. Esim. jos sanotaan remove(3)
poistuu listalla kolmantena oleva merkkijono.
Esimerkin lopussa kutsutaan metodia contains
jonka avulla kysytään listalta sisältääkö se parametrina annettavan merkkijonon. Jos sisältää, palauttaa metdoi true
. ArrayList siis tekee automaattisesti tämän vaivalloisen operaation, joka taulukkoa käyttävän pitää ohjelmoida itse!
Ohjelman tulostus:
opettajien lukumäärä 6 listalla kolmantena Juhana Arto ei ole opettajien listalla
ArrayList:in alkioiden läpikäynti for:illa on lasten leikkiä:
public static void main(String[] args) { ArrayList<String> opettajat = new ArrayList<String>(); opettajat.add("Antti"); opettajat.add("Pekka"); opettajat.add("Juhana"); opettajat.add("Martin"); opettajat.add("Matti"); for (String opettaja : opettajat) { System.out.println( opettaja ); } }
Antti Pekka Juhana Martin Matti
Tässä siis käytetään for:in "for-each"-versiota. Koska jokaisella alkiolla on ArrayList:in sisällä paikkanumero, voidaan ArrayList käydä läpi myös for:in vanhanaikaisella muodolla:
for ( int i=0; i < opettajat.size(); i++ ) { System.out.println( opettajat.get(i) ); }
ArrayList:n sisältö on helppo järjestää suuruusjärjestykseen. Suuruusjärjestys merkkijonojen yhteydessä tarkoittaa aakkosjärjestystä. Järjestäminen tapahtuu seuraavasti:
public static void main(String[] args) { ArrayList<String> opettajat = new ArrayList<String>(); // ... Collections.sort(opettajat); for (String opettaja : opettajat) { System.out.println( opettaja ); } }
Tulostuu:
Antti Arto Juhana Martin Matti Pekka
Annetaan siis lista parametriksi metodille Collections.sort
. Jotta Collections:in apuvälineet toimisivat, on ohjelman yläosassa oltava import java.util.Collections;
tai import java.util.*;
Colections:ista löytyy muutakin hyödyllistä:
shuffle
sekoittaa listan sisällön, metodista voi olla hyötyä esim. peleissäreverse
kääntää listan sisällönArrayList:eihin voi tallettaa minkä tahansa tyyppisiä arvoja. Jos talletetaan kokonaislukuja eli int
:ejä, tulee muistaa pari detaljia. int:ejä tallettava lista tulee määritellä ArrayList<Integer>
, ei int
:n sijaan tulee kirjoittaa Integer
.
Kun listalle talletetaan int
-lukuja, ei metodi remove
toimi aivan odotetulla tavalla:
public static void main(String[] args) { ArrayList<Integer> luvut = new ArrayList<Integer>(); luvut.add(4); luvut.add(8); // yrittää poistaa luvun listan kohdasta 4, eli ei toimi odotetulla tavalla! luvut.remove(4); // tämä poistaa listalta luvun 4 luvut.remove( Integer.valueOf(4) );
Eli luvut.remove(4)
yrittää poistaa listalla kohdassa 4 olevan alkion. Listalla on vain 2 alkiota, joten komento aiheuttaa virheen. Jos halutaan poistaa luku 4, täytyy käyttää hieman monimutkaisempaa muotoa: luvut.remove( Integer.valueOf(4) );
Listalle voi tallettaa myös liukulukuja eli double
:ja ja merkkejä eli char
:eja. Tällöin listat luodaan seuraavasti:
ArrayList<Double> doublet = new ArrayList<Double>(); ArrayList<Character> merkit = new ArrayList<Character>();
Järjestämisen lisäksi toinen hyvin tyypillinen ongelma johon ohjelmoija törmää, on tietyn arvon etsiminen taulkosta. Viikon 3 tehtävissä toteutimme jo metodin joka tarkastaa onko parametrina annettu luku taulukossa. Metodin toteutus oli todennäköisesti suunilleen seuraava:
public static boolean onkoTaulukossa(int[] taulukko, int etsittava ) { for ( int luku : taulukko ) { if ( luku == etsittava ) { return true; } } return false; }
Tämämänkaltainen toteutus on paras joka normaalisti pystytään tekemään. Metodin huono puoli on se, että jos taulukossa on hyvin suuri määrä lukuja, kuluu etsintään paljon aikaa, sillä jokainen taulukossa oleva luku on tarkastettava. jos taulukossa olevat luvut ovat suuruusjärjestyksessä, voidaan etsiminen tehdä huomattavasti tehokkaammin soveltamalla tekniikkaa nimeltään binäärihaku. Järjestämisen tapaan käsittelemme myös binäärihaun tehtävien kautta, eli
Metodi voi kutsua itse itseään. Tätä ilmiötä sanotaan rekursioksi. Tällöin ainoa asia, millä suorituksen kestoa ja sen loppumista voidaan kontrolloida on käyttää hyödykseen parametreja. Ohjelmoitaessa rekursiivinen metodi, pitää käsiteltävät tapaukset usein jakaa kahteen eri luokkaan, joita ovat:
Lasketaan lukujen summa 1+2+...+n rekursiivisesti.
static int summa(int n) { if ( n == 1 ) { return 1; } return n + summa(n-1); } public static void main(String[] args) { System.out.println("Lukujen 1..100 summa on " + summa(100)); }
Edellä kirjoitettu metodi laskee parametrilla k summan 1+...+k ja palauttaa sen arvon. Pelkkä ykkösen summa on ykkönen, joten se otetaan rekursion pohjaksi. Isommat summat palautetaan ykkösen summan laskemiseen kutsumalla yhdellä luvulla pienempää summaa rekursiivisesti.
Ohessa on jo ennestään tuttu taulukon lukujen summan laskeva metodi. Tällä kertaa se on toteutettu rekursiivisesti.
static int summa(int[] luvut, int kasitelty) { if(luvut.length == kasitelty) { return 0; } return luvut[kasitelty] + summa(luvut, kasitelty+1); } public static void main(String[] args) { int[] lukuja = {4, 6, -2, 7, 5} System.out.println("Lukujen summa " + summa(lukuja,0)); }
Esitelty summa
-metodi kutsuu itseään rekursiivisesti kun taulukkoa ei ole käsitelty vielä loppuun.
Kun taulukko on käsitelty, niin pätee kasitelty == luvut.length
ja palautetaan 0, joka lopettaa
rekursiivisen kutsun. Parametrina annettu kasitelty
kontrolloi rekursiivisia kutsuja. Siinä pidetään
tallessa jo käsiteltyjen lukujen lukumäärää.
Kutsumalla rekursiivista metodia eri kohdissa metodin suoritusta voidaan saada aikaan mielenkiintoinen suoritusjärjestys. Seuraava metodi tulostaa mielenkiintoisen kuvion.
static void tahtiPyramidi(int tahtia) { if(tahtia == 0) return; tahtiPyramidi(tahtia-1); for(int i = 0; i < tahtia; i++) { System.out.print("*"); } System.out.println(); tahtiPyramidi(tahtia-1); } public static void main(String[] args) { tahtiPyramidi(1); System.out.println("----------"); tahtiPyramidi(2); System.out.println("----------"); tahtiPyramidi(3); }
Ohjelman tulostus on seuraavanlainen.
* ---------- * ** * ---------- * ** * *** * ** *
Tässä kannattaa kiinnittää huomiota, että kutsuttaessa metodia tahtiPyramidi
antamalle sille kokonaisluku n, niin ensin tulostetaan lukua n-1 vastaava
kuvio. Tämän jälkeen tulostetaan n tähteä ja lopuksi tulostetaan uusiksi
lukua n-1 vastaava kuvio. Isompien lukujen kuvio koostuu siis kahdesta
kappaleesta pienempiä kuvioita, sekä ne erottavasta jonosta tähtiä.
Kuten edellä jo mainittiin, rekursiivinen metodi ratkaisee ongelman "aikaisempien" tapausten avulla.
Pieni johdanto olio-ohjelmointiin ennen aloitusta.
Proseduraalisessa ohjelmoinnissa, eli tähän asti opiskelemassamme ohjelmointityylissä ohjelma jäsennellään jakamalla se pienempiin osiin, eli metodeihin. Metodi toimii ohjelman erillisenä osana, ja sitä voi kutsua mistä tahansa ohjelmasta. Metodia kutsuttaessa ohjelman suoritus siirtyy metodin alkuun, ja suorituksen päätyttyä palataan takaisin siihen kohtaan mistä metodia kutsuttiin.
Olio-ohjelmoinnissa, kuten proseduraalisessa ohjelmoinnissa, pyritään jakamaan ohjelma pieniin osiin. Olio-ohjelmoinnissa pienet osat ovat olioita. Jokaisella oliolla on oma yksittäinen vastuunsa -- eli se sisältää joukon yhteenkuuluvaa tietoa ja toiminnallisuutta. Olio-ohjelmat koostuvat useista olioista, joiden yhteinen toiminta määrittelee järjestelmän toiminnan.
Olemme jo käyttäneet monia Javan valmiita olioita. Esim ArrayList
:it ovat olioita. Jokainen yksittäinen lista koostuu yhteenkuuluvasta tiedosta, eli olion tilasta ja listaolioihin liittyy toiminnallisuutta, eli metodt joilla voidaan muutella olion tilaa. Esim. seuraavassa ohjelmanpätkässä on kaksi ArrayList-olioa kaupungit
ja maat
:
public static void main(String[] args) { ArrayList<String> kaupungit = new ArrayList<String>(); ArrayList<String> maat = new ArrayList<String>(); maat.add("Suomi"); maat.add("Saksa"); maat.add("Hollanti"); kaupungit.add("Berliini"); kaupungit.add("Nijmegen"); kaupungit.add("Turku"); kaupungit.add("Helsinki"); System.out.println("maita " + maat.size() ); System.out.println("kaupunkeja " + kaupungit.size() ); }
Molemmat olioista, maat
ja kaupungit
elävät omaa elämäänsä, molempien "tila" on toisten olioiden tilasta riippumaton. Esim. olion maat
tila koostuu listalla olevista merkkijonoista "Suomi", "Saksa" ja "Hollanti" ja todennäköisesti myös tiedosta kuinka monta maata listalla on.
Metodia kutsuttaessa tulee pisteen vasemmalle puolelle sen olion nimi, jolle metodia kutsutaan. Eli kun kysytään monta merkkijonoa listalla maat
on, kutsu on muotoa maat.size()
eli kutsutaan maat
oliolle sen metodia size
. Metodin palauttama tulos riippuu olion maat
tilasta, eli muut listat kuten kaupungit
eivät vaikuta metodin suoritukseen millään tavalla.
Olemme jo useasti joutuneet käyttämään komentoa new
, esim. ArrayList:in, taulukon ja satunnaislukuja tuottavan Random:in luominen on tapahtunut new-komennolla. Syy on se, että kaikki näistä ovat olioita. Javassa muutamaa poikkeusta lukuunottamatta oliot luodaan aina new
-komennolla.
Yksi näistä poikkeuksista ovat merkkijonot, joiden luomiseen ei aina tarvita new
:iä. Tuttu tapa merkkijonon luomiseen on oikeastaan Javan lyhennysmerkintä new
:in käytölle. Merkkijonon voi luoda myös new:illä kuten muutkin oliot:
String m1 = "tekstiä"; // lyhennysmerkintä merkkijono-olion luomiselle String m2 = new String("lisää tekstiä");
On myös tilanteita, missä Javan valmis kalusto kutsuu new
:iä piilossa ohjelmoijalta.
On selvää että kaikki oliot eivät ole keskenään samankaltaisia. Esim. ArrayList-oliot poikkeavat ratkaisevasti String-olioista. Kaikilla ArrayList:eillä on samat metodit (add, contains, remove, size, ...) ja vastaavasi kaikilla Stringeillä on samat metodit (substring, length, charAt, ...). ArrayList- ja String-olioiden metodit eivät ole samat, nämä ovatkin erityyppisiä olioita.
Tietyn olioryhmän tyyppiä kutsutaan luokaksi, eli ArrayList
on luokka, String
on luokka, Random
on luokka, jne...
Tiettyn luokan kaikilla olioilla on samat metodit, sekä samankaltainen tila. Esim. kaikkien ArrayListien tila koostuu listalle talletetuista alkioista. String-olioiden tila taas koostuu merkkijonon muodostavista kirjaimista.
Luokka siis määrittelee minkälaisia luokan oliot ovat:
voidaan ajatella, että luokka kuvaa sen olioiden "rakennuspiirustukset".
Otetaan analogia tietokoneiden ulkopuoleisesta maailmasta. Rintamamiestalot lienevät kaikille suomalaisille tuttuja. Voidaan ajatella, että jossain on olemassa piirustukset jotka määrittelevät minkälainen rintamamiestalo on. Piirrustukset siis ovat luokka, eli ne määrittelevät luokan olioiden luonteen:
Yksittäiset oliot eli rintamamiestalot on tehty kaikki samojen piirustusten perusteella, eli ne kuuluvat samaan luokkaan. Yksittäisten olioiden tila eli ominaisuudet (esim. seinien väri, katon rakennusmateriaali ja väri, kivijalan väri, ovien rakennusmateriaali ja väri, ...) vaihtelevat. Seuraavassa yksi "rintamamiestalo-luokan olio":
Yleensä luokka määritellään jotain mielekästä kokonaisuutta varten. Joskus "mielekäs kokonaisuus" voi esim. kuvata jotain reaalimaailman asiaa. Jos tietokoneohjelman pitää käsitellä henkilötietoja, voisi olla mielekästä määritellä erillinen luokka Henkilo
joka kokoaa yhteen henkilöön liittyvät metodit ja ominaisuudet.
Aloitetaan. Avataan uusi projekti. Projektille tulee tuttuun tapaan pääohjelma joka on aluksi tyhjä:
public class Main { public static void main(String[] args) { } }
Luomme nyt projetiimme eli ohjelmaamme uuden luokan. Tämä tapahtuu valitsemalla NetBeansissa vasemmalta projects-kohdasta hiiren oikealla napilla new ja java class. Avautuvaan dialogiin annetaan luokalle nimi.
Kuiten muuttujien ja metodien nimien, myös luokkan nimen on aina oltava mahdollisimman kuvaava. Joskus ohjelmoinnin edetessä luokka elää ja muuttaa muotoaan, mutta koska uudelleen nimeäminen on helppoa (ks. NetBean-ohje), tulee luokka uudelleennimetä tälläisissä tilanteissa.
Luodaan luokka nimeltä Henkilo
. Luokasta muodostuu oma tiedostonsa. Eli ohjelma koostuu nyt
kahdesta tiedostosta, sillä pääohjelma on omassa tiedostossaan. Aluksi luokka on tyhjä:
public class Henkilo { }
Luokka siis määrittelee mitä metodeja ja ominaisuuksia luokan olioilla on. Päätetään, että jokaisella henkilöllä on nimi ja ikä. Nimi on luonnollista esittää merkkijonona, eli Stringinä, ja ikä taas kokonaislukuna. Lisätään nämä rakennuspiirustuksiimme:
public class Henkilo { private String nimi; private int ika; }
Eli määrittelimme tässä että kaikilla Henkilo
-luokan olioilla on nimi
ja ika
.
Märitettely tapahtuu hiukan kuten normaalin muuttujan määrittely. Eteen on kuitenkin nyt laitettu avainsana
private
. Tämä tarkoittaa sitä, että nimi ja ikä eivät näy suoraan olion ulkopuolelle vaan ovat
"piilossa" olion sisällä.
Luokan sisälle määriteltyjä muuttujia kutsutaan oliomuuttujiksi tai olion kentiksi tai olion attribuuteiksi. Rakkaalla lapsella on monta nimeä.
Eli olemme määritelleet että kaikilla henkilöolioilla on muuttujat nimi
ja ika
, eli henkilöiden "tila" koostuu niiden nimestä ja iästä.
Yleensä luotavalle oliolle halutaan asettaa luontivaiheessa jokin alkutila.
Itse määritellyn olion luominen tapahtuu hyvin samaan tapaan kuin Javan valmiiden olioiden
kuten ArrayList:ien luominen. Oliot siis luodaan new
-komennolla.
Olion synnyttämisen yhteydessä olisikin kätevä pystyä antamaan arvo joillekkin
olioiden muuttujille. Esim. henkilön synnytyksessä olisi kätevää pystyä antamaan nimi jo syntymähetkellä:
public static void main(String[] args) { Henkilo h = new Henkilo("Pekka"); // ... }
Tämä onnistuu määrittelemällä oliolle ns. konstruktori, eli olion luontihetkellä suoritettava "metodi". Seuraavassa on määritelty Henkilo:lle konstruktori joka asettaa iäksi 0 ja nimeksi parametrina tulevan merkkijonon:
public class Henkilo { private String nimi; private int ika; public Henkilo(String nimiAlussa) { ika = 0; nimi = nimiAlussa; } }
Konstruktori on siis nimeltään sama kuin luokan nimi. Konstruktorille parametrina
tuleva arvo tulee sulkuihin konstruktorin nimen perään. Konstruktorin voi
ajatella olevan metodi, jonka Java suorittaa kun olio luodaan sanomalla
new Henkilo("Pekka");
Aina kun jostain luokasta luodaan olio,
kutsutaan kyseisen luokan konstruktoria.
Muutama huomio: konstruktorin sisällä on komento ika = 0
. Tässä asetetaan
arvo 0 olion sisäiselle muuttujalle ika
.
Toinen komento nimi = nimiAlussa;
taas asettaa olion sisäiselle
muuttujalle nimi
arvoksi parametrina annetun merkkijonon. Olion muuttujat ika
ja nimi
näkyvät konstruktorissa ja muuallakin olion sisällä automaattisesti. Koska niissä on private-määre, niin olion ulkopuolelle ne eivät näy.
Vielä yksi huomio: jos ohjelmoija ei tee luokalle ollenkaan konstruktoria, tekee Java automaattisesti luokalle ns. oletuskonstruktorin, eli konstruktorin joka ei tee mitään. Eli jos konstruktoria ei jostian syystä tarvita, ei sellaista tarvitse ohjelmoida.
Alkaa olla korkea aika päästä käyttämään Henkilo
-olioita. Osaamme luoda olion ja alustaa olion muuttujat. Järkevään toimintaan pystyäkseen olioilla on kuitenkin oltava metodeja. Tehdään Henkilo
:lle metodi jonka avulla olio tulostaa itsensä ruudulle:
public class Henkilo { private String nimi; private int ika; public Henkilo(String nimiAlussa) { ika = 0; nimi = nimiAlussa; } public void tulostaHenkilo() { System.out.println(nimi +", ikä "+ ika + " vuotta"); } }
Metodi siis kirjoitetaan luokan sisälle, metodin nimen eteen tulee public void
sillä metodin on tarkoitus näkyä ulkomaailmalle ja metodi ei palauta mitään. Huomaa, että sana static
ei nyt ilmene missään. Olioiden yhteydessä static
-määrettä ei käytetä. Selitämme ensi viikolla hieman tarkemmin mistä tässä on kysymys.
Metodin sisällä on yksi koodirivi joka käyttää hyvakseen oliomuuttujia ika
ja nimi
. Kaikki olion muuttujat ovat siis näkyvillä ja käytettävissä metodin sisällä.
Luodaan pääohjelmassa kolme henkilöä ja pyydetään niitä tulostamaan itsensä:
public class Main { public static void main(String[] args) { Henkilo p = new Henkilo("Pekka"); Henkilo a = new Henkilo("Antti"); Henkilo m = new Henkilo("Martin"); p.tulostaHenkilo(); a.tulostaHenkilo(); m.tulostaHenkilo(); } }
Tulostuu:
Pekka, ikä 0 vuotta Antti, ikä 0 vuotta Martin, ikä 0 vuotta
Lisätään Henkilölle metodi, joka kasvattaa henkilön ikää vuodella:
public class Henkilo { // ... public void vanhene() { ika++; } }
Metodi siis kirjoitetaan edellisen metodin tapaan luokan Henkilo sisälle. Metodissa kasvatetaan oliomuuttujan ika
arvoa yhdellä.
Kutsutaan metodia ja katsotaan mitä tapahtuu:
public class Main { public static void main(String[] args) { Henkilo p = new Henkilo("Pekka"); Henkilo a = new Henkilo("Antti"); p.tulostaHenkilo(); a.tulostaHenkilo(); System.out.println(""); p.vanhene(); p.vanhene(); p.tulostaHenkilo(); a.tulostaHenkilo(); } }
Ohjelman tulostus on seuraava:
Pekka, ikä 0 vuotta Antti, ikä 0 vuotta Pekka, ikä 2 vuotta Antti, ikä 0 vuotta
Eli "syntyessään" molemmat oliot ovat nollavuotiaita. Olion p
metodia vanhene
kutsutaan kaksi kertaa. Kuten tulostus näyttää, tämä saa aikaan sen että Pekan ikä onkin 2 vuotta. Kutsumalla metodia Pekkaa vastaavalle oliolle, siis toisen henkilöolion ikä ei muutu.
Jatketaan ohjelman laajentamista. Tehdään henkilölle metodi jonka avulla voidaan selvittää onko henkilö täysi-ikäinen. Metodi palauttaa joko true tai false:
public class Henkilo { // ... public boolean taysiIkainen(){ if ( ika < 18 ) { return false; } return true; } /* huom. metodin voisi kirjoittaa lyhyemmin seuraavasti: public boolean taysiIkainen(){ return ika >= 18; } */ }
Ja testataan:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka"); Henkilo a = new Henkilo("Antti"); for ( int i=0; i < 30; i++ ) { p.vanhene(); } a.vanhene(); System.out.println(""); if ( a.taysiIkainen() ) { System.out.print("täysi-ikäinen: "); a.tulostaHenkilo(); } else { System.out.print("alaikäinen: "); a.tulostaHenkilo(); } if ( p.taysiIkainen() ) { System.out.print("täysi-ikäinen: "); p.tulostaHenkilo(); } else { System.out.print("alaikäinen: "); p.tulostaHenkilo(); } }
alaikäinen: Antti, ikä 1 vuotta täysi-ikäinen: Pekka, ikä 30 vuotta
Viritellään ratkaisua vielä hiukan. Nyt henkilön pystyy "tulostamaan" ainoastaan siten, että nimen lisäksi tulostuu ikä. On tilanteita, joissa haluamme tietoon pelkän olion nimen. Eli tehdään tarkoitusta varten oma metodi:
public class Henkilo { // ... public String getNimi() { return nimi; } }
Metodi siis vaan palauttaa oliomuuttujan nimi
kutsujalle. Metodin nimi on hieman erikoinen. Javassa on usein tapana nimetä oliomuuttjan palauttava metodi juuri näin, eli getMuuttujanNimi
. Tälläisiä metodeja kutsutaan usein "gettereiksi".
Muotoillaan pääohjelma käyttämään uutta "getteri"-metodia:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka"); Henkilo a = new Henkilo("Antti"); for ( int i=0; i < 30; i++ ) { p.vanhene(); } a.vanhene(); System.out.println(""); if ( a.taysiIkainen() ) { System.out.println( a.getNimi() + " on täysi-ikäinen" ); } else { System.out.println( a.getNimi() + " on alaikäinen" ); } if ( p.taysiIkainen() ) { System.out.println( p.getNimi() + " on täysi-ikäinen" ); } else { System.out.println( p.getNimi() + " on alaikäinen " ); } }
Tulostus alkaa olla jo aika siisti:
Antti on alaikäinen Pekka on täysi-ikäinen
Olemme syyllistyneet edellä osittain huonoon ohjelmointityyliin tekemällä metodin jonka avulla olio tulostetaan, eli metodin tulostaHenkilo
. Suositeltavampi tapa on määritellä oliolle metodi jonka palauttaa olion "merkkijonoesityksen". Merkkijonoesityksen palauttavan metodin nimen on Javassa tapana aina olla toString
. Määritellään seuraavassa henkilölle tämä metodi:
public class Henkilo { // ... public String toString() { return nimi +", ikä "+ ika + " vuotta"; } }
Eli toString
toimii samaan tyyliin kuin tulostaHenkilo
, mutta se ei itse tulosta mitään vaan palauttaa merkkijonoesityksen itsestään, jotta metodin kutsuja voi halutessaan suorittaa tulostamisen.
Metodia käytetään hieman yllättävällä tavalla:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka"); Henkilo a = new Henkilo("Antti"); for ( int i=0; i < 30; i++ ) { p.vanhene(); } a.vanhene(); System.out.println( a ); // sama kun System.out.println( a.toString() ); System.out.println( p ); // sama kun System.out.println( b.toString() ); }
Periaatteena on, että main pyytää olion merkkijonoesityksen ja tulostaa sen. Merkkijonoesityksen palauttavan toString
-metodin kutsua ei tarvitse itse kirjoittaa sillä Java lisää sen automaattisesti. Eli ohjelmoijan kirjoittaessa:
System.out.println( a );
Java täydentää suorituksen aikana kutsun muotoon:
System.out.println( a.toString() );
Eli käy niin, että pyydetään oliolta sen merkkijonoesitys ja tulostetaan se normaaliin tapaan System.out.println
-komennolla.
Poistamme nyt turhaksi käyneen tulostaOlio
-metodin.
Päätetään että haluamme laskea henkilöiden painoindeksejä. Tätä varten teemme henkilölle metodit pituuden ja painon asettamista varten, sekä metodin joka laskee painoindeksin. Henkilön uudet ja muuttuneet osat seuraavassa:
public class Henkilo { private String nimi; private int ika; private int paino; private int pituus; public Henkilo(String nimiAlussa) { ika = 0; paino = 0; pituus = 0; nimi = nimiAlussa; } public void setPituus(int uusiPituus) { pituus = uusiPituus; } public void setPaino(int uusiPaino) { paino = uusiPaino; } public double painoIndeksi(){ double pituusPerSata = pituus/100.0; return paino / ( pituusPerSata*pituusPerSata ); } // ... }
Eli henkilölle lisättiin oliomuuttujat pituus ja paino. Näille voi asettaa arvon metodeilla setPituus
ja setPaino
. Jälleen käytössä Javaan vakiintunut nimeämiskäytäntö, eli jos metodin tehtävänä on ainoastaan asettaa arvo oliomuuttujaan, on metodi tapana nimetä setMuuttujanNimi
:ksi. Seuraavassa käytämme uusia metodeja:
public static void main(String[] args) { Henkilo m = new Henkilo("Matti"); Henkilo j = new Henkilo("Juhana"); m.setPituus(180); m.setPaino(86); j.setPituus(175); j.setPaino(64); System.out.println( m.getNimi() + ", painoindeksisi on " + m.painoIndeksi() ); System.out.println( j.getNimi() + ", painoindeksisi on " + j.painoIndeksi() );
Tulostus:
Matti, painoindeksisi on 26.54320987654321 Juhana, painoindeksisi on 20.897959183673468
Desimaalien määrä edellisessä tulostuksessa on hieman liioiteltu. Yksi tapa päästä määräämään tulostettavien desimaalien määrä on seuraava:
System.out.println( m.getNimi() + ", painoindeksisi on " + String.format( "%.2f", m.painoIndeksi() ) ); System.out.println( j.getNimi() + ", painoindeksisi on " + String.format( "%.2f", j.painoIndeksi() ) );
Eli jos luku
on liukuluku, voi siitä tehdä komennolla String.format( "%.2f", luku )
merkkijonon, jossa luku on otettu mukaan kahden desimaalin tarkkuudella. Pisteen ja f:n välissä oleva parametri säätää mukaan tulevan desimaalien määrän.
Nyt tulostus on siistimpi:
Matti, painoindeksisi on 26,54 Juhana, painoindeksisi on 20,90
String.format
ei ehkä ole kaikkein monikäyttöisin tapa Javassa tulostuksen muotoiluun. Se kuitenkin lienee yksinkertaisin ja kelpaa meille hyvin nyt.
Henkilön konstruktori ja pituuden asettava metodi kirjoitettiin seuraavasti:
public class Henkilo { // ... public Henkilo(String nimiAlussa) { ika = 0; paino = 0; pituus = 0; nimi = nimiAlussa; } public void setPituus(int uusiPituus) { pituus = uusiPituus; } }
Konstruktorissa oliomuuttuja nimi
saa parametrina alkuarvon, ja metodi setPituus
asettaa oliomuuttujaan pituus
parametrina annetun arvon.
Haluaisimme kenties yksinkertaistaa parametrin nimeä, ja kirjoittaa metodin setPituus
seuraavaan tapaan:
public void setPituus(int pituus) { pituus = pituus; }
Tässä siis parametrin nimi pituus
on sama kuin oliomuuttujan nimi. Tämä ei kuitenkaan toimi. Syynä on se, että parametrin nimi "peittää" oliomuuttujan näkyvistä. Ongelma voidaan kiertää siten, että käytetään this
- eli "olio itse" -ilmausta.
Olio voi viitata omaan pituus
-nimiseen oliomuuttujaansa ilmauksella this.pituus
. Tämän voi tulikita tarkoittavan "minun itseni pituus". Tämänkaltainen omaan oliomuuttujaan viittaaminen toimii vaikka samanniminen parametri olisikin peittänyt oliomuuttujan suoran näkyvyyden. Eli metodi setPituus
voidaan kirjoittaa seuraavasti:
public void setPituus(int pituus) { this.pituus = pituus; }
this
-viittausta voi käyttää olion sisällä aina, eli konstruktorin voi kirjoittaa seuraavasti:
public class Henkilo { // ... public Henkilo(String nimi) { this.ika = 0; this.paino = 0; this.pituus = 0; this.nimi = nimiAlussa; } }
Pakkoa this
:in käytölle ei kuitenkaan ole muuten kuin jos jokin parametri peittää oliomuuttujan johon halutaan päästä käsiksi, eli konstruktori voi olla myös seuraavassa muodossa:
public class Henkilo { // ... public Henkilo(String nimi) { ika = 0; // tarkoittaa samaa kuin this.ika eli "olion itsensä muuttuja ika" paino = 0; pituus = 0; this.nimi = nimiAlussa; } }
Olio voi kutsua myös omia metodeitaan. Jos esim. halutaan, että toString-metodin palauttama merkkijonoesitys kertoisi myös henkilön painoindeksin, kannattaa toString
:istä kutsua metodia painoIndeksi
:
public String toString() { return nimi +", ikä "+ ika + " vuotta, painoindeksini on " + painoIndeksi(); }
Eli kun olio kutsuu omaa metodiaan, riittää pelkkä metodin nimi. Vaihtoehtoinen tapa on tehdä oman metodin kutsu muodossa
this.painoIndeksi()
eli korostetaan, että kutsutaan "olion itsensä" metodia painoindeksi:
public String toString() { return nimi +", ikä "+ ika + " vuotta, painoindeksini on " + this.painoIndeksi(); }
Kaikki henkilöoliot ovat luontihetkellä 0-vuotiaina, sillä konstruktori asettaa uuden henkiön ika-oliomuuttujan arvoksi 0:
public Henkilo(String nimi) { this.nimi = nimi; ika = 0; paino = 0; pituus = 0; }
Haluaisimme, myös vaihtoehtoisen tavan luoda henkilön siten, että nimen lisäksi konstruktorin parametrina annettaisiin ikä. Tämä onnistuu helposti, sillä konstruktoreja voi olla useita. Eli tehdään vaihtoehtoinen konstruktori. Vanhaa konstruktoria ei tarvise poistaa.
public Henkilo(String nimi, int ika) { this.nimi = nimi; this.ika = ika; paino = 0; pituus = 0; }
Nyt olioiden luonti onnistuu kahdella vaihtoehtoisella tavalla:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); Henkilo e = new Henkilo("Esko"); System.out.println( p ); System.out.println( e );
Pekka, ikä 24 vuotta Esko, ikä 0 vuotta
Henkilo-luokalla on nyt siis kaksi konstruktoria:
public class Henkilo { private String nimi; private int ika; private int paino; private int pituus; public Henkilo(String nimi) { this.nimi = nimi; ika = 0; paino = 0; pituus = 0; } public Henkilo(String nimi, int ika) { this.nimi = nimi; this.ika = ika; paino = 0; pituus = 0; } // ... }
Tekniikkaa jossa luokalla on kaksi konstruktoria, kutsutaan konstruktorin kuormittamiseksi. Ei kuitenkaan ole mahdollista tehdä kahta erilaista konstruktoria joilla on täysin saman tyyppiset parametrit. Edellisten lisäksi emme voi lisätä konstruktoria public Henkilo(String nimi, int paino)
sillä Javan on mahdoton erottaa tätä kaksiparametrisesta konstruktorissa jossa luku tarkoittaa ikää.
Mutta hetkinen, luvussa 17 todettiin että "copy paste"-koodi ei ole hyvä idea. Kun tarkastellaan edellä tehtyjä kuormitettuja konstruktoreita, niissä on aika paljon samaa. Emme ole oikein tyytyväisiä tilanteeseen.
Konstruktoreista ylempi on oikeastaan alemman erikoistapaus. Entä jos ylemmpi konstruktori voisi "kutsua" alempaa konstruktoria? Tämä onnistuu, sillä konstruktorin sisältä voi kutsua toista konstruktoria this
-ilmauksen avulla!
Muutetaan ylempää konstruktoria siten, että se ei itse tee mitään vaan ainoastaan kutsuu alempaa konstruktoria ja pyytää sitä asettamaan iäksi 0:
public Henkilo(String nimi) { this(nimi, 0); // suorita tässä toisen konstruktorin koodi ja laita ika-parametrin arvoksi 0 } public Henkilo(String nimi, int ika) { this.nimi = nimi; this.ika = ika; paino = 0; pituus = 0; }
Oman konstruktorin kutsu this(nimi, 0);
saattaa vaikuttaa erikoiselta. Asiaa voi vaikka ajatella siten, että kutsun kohdalle tulee "copypastena" automaattisesti alemman konstruktorin koodi, siten että ika parametrin arvoksi tulee 0.
Konstruktorien tapaan myös metodeja voi kuormittaa, eli saman nimisestä metodista voi olla useita versioita. Jälleen eri versioiden parametrien tyyppien on oltava erlaiset. Tehdään vanhene
-metodista toinen versio, joka mahdollistaa henkilön vanhentamisen parametrina olevalla vuosimäärällä:
public void vanhene() { ika++; } public void vanhene(int vuodet) { ika += vuodet; }
Seuraavassa "Pekka" syntyy 24-vuotiaana, vanhenee ensin vuoden ja sitten 10 vuotta:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); System.out.println( p ); p.vanhene(); System.out.println( p ); p.vanhene(10); System.out.println( p );
Tulostuu:
Pekka, ikä 24 vuotta Pekka, ikä 25 vuotta Pekka, ikä 35 vuotta
Henkilöllä on siis vanhene
-nimisiä metodeja 2 kappaletta. Se kumpi metodi valitaan suoritettavaksi riippuu metodikutsussa käytettyjen parametrien määrästä.
Luvussa 16 mainittiin, että taulukko on "langan päässä". Myös oliot ovat langan päässä. Mitä tämä oikein tarkoittaa? Tarkastellaan seuraavaa esimerkkiä:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); System.out.println( p ); }
Syntyy siis olio. Olioon päästään käsiksi main:in muuttujan p
avulla. Teknisesti ottaen olio ei ole muuttujan p
"sisällä" (eli lokerossa p) vaan p
viittaa syntyneeseen olioon. Toisin sanonen olio on p
:stä lähtevän "langan päässä". Kuvana asiaa voisi havainnollistaa seuraavasti:
Lisätään ohjelmaan muuttuja h
ja annetaan sille alkuarvoksi p
. Mitä nyt tapahtuu?
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); System.out.println( p ); Henkilo h = p; h.vanhene(25); System.out.println( p ); }
Tulostuu:
Pekka, ikä 24 vuotta Pekka, ikä 49 vuotta
Eli Pekka on alussa 24 vuotias, h
:ta vanhennetaan 25:llä vuodella ja sen seurauksena Pekka vanhenee! Mistä on kysymys?
Komento Henkilo h = p;
saa aikaan sen, että h
rupeaa viittaamaan samaan olioon mihin p
viittaa. Eli ei synnykään kopioa oliosta, vaan molemmissa muuttujissa on langan päässä sama olio. Kuvana:
Esimerkissä siis "vieras henkilö h
ryöstää Pekan identiteetin". Pekka kyllästyy identiteetin ryöstäjään. Seuraavassa on esimerkkiä jatkettu siten, että luodaan uusi olio ja p
alkaa viittaamaan uuteen olioon:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); System.out.println( p ); Henkilo h = p; h.vanhene(25); System.out.println( p ); p = new Henkilo("Pekka Mikkola", 24); System.out.println( p ); }
Tulostuu:
Pekka, ikä 24 vuotta Pekka, ikä 49 vuotta Pekka Mikkola, ikä 24 vuotta
Muuttuja p
viittaa siis ensin yhteen olioon, mutta rupeaa sitten viittaamaan toiseen olion.
Seuraavassa kuva tilanteesta viimeisen koodirivin jälkeen:
Jatketaan vielä esimerkkiä laittamalla h
viittaamaan "ei mihinkään", eli null
:iin:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); System.out.println( p ); Henkilo h = p; h.vanhene(25); System.out.println( p ); pekka = new Henkilo("Pekka Mikkola", 24); System.out.println( p ); h = null; System.out.println( h ); }
Viimeisen koodirivin jälkeen näyttää seuraavalta:
Alempaan olioon ei nyt viittaa kukaan. Oliosta on tullut "roska". Javan roskienkerääjä käy siivoamassa aika ajoin roskaksi joutuneet oliot. Jos näin ei tehtäisi, ne jäisivät kuluttamaan turhaan koneen muistia ohjelman suorituksen loppuun asti.
Huomaamme, että viimeisellä rivillä yritetään vielä tulostaa "ei mitään" eli null. Käy seuraavasti:
Pekka, ikä 24 vuotta Pekka, ikä 49 vuotta Pekka Mikkola, ikä 24 vuotta null
Mitä tapahtuu jos yritämme kutsua "ei minkään" metodia, esim. kysyä painoindeksiä:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); System.out.println( p ); Henkilo h = p; h.vanhene(25); h = null; System.out.println( h.painoIndeksi() ); }
Tulos:
Pekka, ikä 24 vuotta Pekka, ikä 49 vuotta Pekka Mikkola, ikä 24 vuotta Exception in thread "main" java.lang.NullPointerException at Main.main(Main.java:20) Java Result: 1
Eli käy huonosti. Tämän on ehkä ensimmäinen kerta elämässäsi kun näet tekstin NullPointerExcepton. Voimme luvata, että tulet näkemään sen vielä uudelleen.
Nyt on aika harjoitella lisää yksinkertaisten olioiden tekemistä käytännössä:
Luvussa 17 rakensimme askel askeleelta viikon 2 tehtävälle 7.3 ratkaisua. Tehtäväkuvaus oli seuraava:
Tee ohjelma, joka kysyy käyttäjältä sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Voit olettaa, että käyttäjä antaa korkeintaan 1000 sanaa.
Anna sana: porkkana Anna sana: selleri Anna sana: nauris Anna sana: lanttu Anna sana: selleri Annoit saman sanan uudestaan!
Päädyimme luvussa 17 seuraavaan ratkaisuun:
public static boolean onJoSyotetty(String sana, String[] aiemmatSanat, int sanojaSyotetty) { for ( int i=0; i < sanojaSyotetty; i++ ) { if ( sana.equals( aiemmatSanat[i] ) ) return true; } return false; } public static void main(String[] args) { String[] aiemmatSanat = new String[1000]; int sanojaSyotetty = 0; while ( true ) { String sana = lukija.nextLine(); if ( onJoSyotetty( sana, aiemmatSanat, sanojaSyotetty ) ) break; aiemmatSanat[sanojaSyotetty] = sana; sanojaSyotetty++; } System.out.println("Annoit saman sanan uudestaan"); }
Totesimme, että vaikka ratkaisu alkaa olla ymmärrettävyydeltään ihan kohtuullinen, jokin siinä edelleen jäi vaivaamaan.
Pääohjelman käyttämät apumuuttujat, eli taulukko aiemmatSanat
ja kokonaisluku sanojaSyotetty
ovat todella ikäviä "matalan tason" detaljeja pääohjelman kannalta. Pääohjelman kannaltahan on oleellista, että muistetaan niiden sanojen joukko jotka on nähty jo aiemmin. Sanojen joukko, tämähän on selkeä erillinen "käsite", tai abstraktio. Tälläiset selkeät käsitteet ovat potentiaalisia olioita ja huomattuaan koodissaan "käsitteen" viisas ohjelmoija eristää sen luokaksi.
Eli päätämme tehdä luokan Sanajoukko
. Haluaisimme, että pääohjelma näyttää seuraavalta:
public static void main(String[] args) { Sanajoukko aiemmatSanat = new Sanajoukko(); while (true) { String sana = lukija.nextLine(); if ( aiemmatSanat.sisaltaa(sana) ) { break; } aiemmatSanat.lisaa(sana); } System.out.println("Annoit saman sanan uudestaan"); }
Pääohjelman, eli sanajoukon käyttäjän kannalta Sanajoukko olisi hyvä jos sillä olisi metodit boolean sisaltaa(String sana)
jolla tarkastetaan sisältyykö annettu sana jo sanajoukkoon ja void lisaa(String sana)
jolla annettu sana lisätään joukkoon.
Huomaamme, että näin kirjoitettuna pääohjelman luettavuus on huomattavasti parempi kuin taulukkoa käyttäen. Pääohjelma on lähes suomen kieltä!
Luokan Sanajoukko
runko näyttää seuraavanlaiselta:
public class Sanajoukko { // sopivia oliomuuttujia public Sanajoukko() { // ... } public boolean sisaltaa(String sana) { // ... } public void lisaa(String sana) { // ... } }
Voimme toteuttaa sanajoukon siirtämällä aiemman ratkaisumme taulukon sanajoukon oliomuuttujaksi:
public class Sanajoukko { private String[] joukonSanat; private int sanojaSyotetty; public Sanajoukko() { joukonSanat = new String[1000]; sanojaSyotetty = 0; } // ... }
Oliomuuttujien alustus tapahtuu tuttuun tapaan konstruktorissa.
Uuden sanan lisääminen on helppoa. Koska sanojaSyotetty
muistaa monta sanaa taulukossa jo on, ja taulukon indeksit alkavat nollasta, tulee uusi sana juuri tähän paikkaan. Muuttujan arvo pitää muistaa vielä kasvattaa:
public class Sanajoukko { private String[] joukonSanat; private int sanojaSyotetty; public Sanajoukko() { joukonSanat = new String[1000]; sanojaSyotetty = 0; } public void lisaa(String sana) { joukonSanat[ sanojaSyotetty ] = sana; sanojaSyotetty++; } // ... }
Ja vielä metodi, jolla tarkistetaan onko sana jo joukossa. Eli käydään sanat muistavan taulukon alkuosaa läpi syötettyjen sanojen verran ja palautetaan true jos etsitty sana löytyy:
public class Sanajoukko { private String[] joukonSanat; private int sanojaSyotetty; public Sanajoukko() { joukonSanat = new String[1000]; sanojaSyotetty = 0; } public boolean sisaltaa(String sana) { for ( int i=0; i < sanojaSyotetty; i++ ) { if ( joukonSanat[i].equals(sana) ) return true; } return true; } public void lisaa(String sana) { joukonSanat[ sanojaSyotetty ] = sana; sanojaSyotetty++; } }
Ratkaisu on nyt varsin elegantti. Erillinen käsite on saatu erotettua ja pääohjelma näyttää siistiltä. Kaikki "likaiset yksityiskohdat" (taulukko ja lukumäärä sanoista) on saatu siivottua eli kapseloitua olion sisälle.
Yhtäkkiä mieleemme palaa toissa viikolla esitelty ArrayList. Sanojen muistaminenhan voidaan toteuttaa helposti ArrayList:illa! Päätämme korvata luokan Sanajoukko
sisällä olevan taulukon listalla:
import java.util.ArrayList; public class Sanajoukko { private ArrayListjoukonSanat; public Sanajoukko() { joukonSanat = new ArrayList (); } public boolean sisaltaa(String sana) { return joukonSanat.contains(sana); } public void lisaa(String sana) { joukonSanat.add(sana); }
Näin päädytään ratkaisuun jossa Sanajoukko
on ainoastaan ArrayList:in "wräpperi". Onko tässä järkeä? Kenties. Voimme nimittäin halutessamme tehdä Sanajoukolle muitakin muutoksia. Ennen pitkään saatamme esim. huomata, että sanajoukko pitää tallettaa tiedostoon. Jos tekisimme nämä muutokset Sanajoukon koodiin, ei pääohjelmaa tarvitsisi muuttaa mitenkään.
Voi olla, että jatkossa ohjelmaa halutaan laajentaa siten, että Sanajoukko
-luokan olisi osattava uusia asiota. Jos esim. pääohjelmassa haluttaisiin tietää kuinka moni syötetyistä sanoista oli palindromi, voidaan sanajoukkoa laajentaa metodilla palindromeja
:
public static void main(String[] args) { Sanajoukko aiemmatSanat = new Sanajoukko(); while (true) { String sana = lukija.nextLine(); if ( aiemmatSanat.sisaltaa(sana) ) { break; } aiemmatSanat.lisaa(sana); } System.out.println("Annoit saman sanan uudestaan"); System.out.println("Sanoistasi " + aiemmatSanat.palindromeja() + "oli palindromeja"); }
Pääohjelma säilyy siistinä, viimeisellä rivillä kutsutaan uutta hyvin nimettyä metodia, palindromien laskeminen jää Sanajoukko
-olion huoleksi. Metodin toteutus voisi olla seuraavanlainen:
public class Sanajoukko { private ArrayListjoukonSanat; // ... public int palindromeja() { int palindromit = 0; for ( String sana : joukonSanat ) { if ( onPalindromi(sana) ) palindromit++; } return palindromit; } private boolean onPalindromi(String sana){ int loppu = sana.length()-1; for ( int i=0; i < sana.length()/2; i++ ){ if ( sana.charAt(i) != sana.charAt(loppu-i) ) return false; } return true; }
Eli metodi palindromeja
käy läpi kaikki joukon sanat ja apumetodin onPalindromi
avulla laskee paindromien määrän.
Koska apumetodi onPalindromi
on tarkoitettu ainoastaan olion sisäiseen käyttöön, on sen näkyvyysmääreeksi määritelty private
. Olion käyttäjän siis ei ole mahdollista kutsua metodia, metodia voi kutsua ainoastaan olion sisältä.
Jos sanojen talletukseen olisi käytetty suoraan ArrayList:iä, olisi palindromien laskeminen vaatinut pääohjelman koodiin suurempia muutoksia kuin Sanajoukko-luokan käyttö. Joskus on toki tilanteita, joissa kannattaa käyttää suoraan valmista kalustoa eikä oman luokan määrittelylle ole tarvetta. Kuitenkin jos koodin luettavuus alkaa kärsimään, on selkeistä käsitteistä syytä tehdä omat luokkansa. Kuten näimme tämä parhaassa tapauksessa mahdollistaa ohjelman laajennetatuutta ja muokattavuutta.
Olemme nähneet että metodien parametrina voi olla esim. int, double, String
tai taulukko. Taulukot ja merkkijonot ovat olioita, joten kuten arvata saattaa, metodi voi saada
parametriksi minkä tahansa tyyppisen olion. Demonstroidaan tätä esimerkillä.
Painonvartijoihin hyväksytään jäseniksi henkilöitä joiden painoindeksi ylittää jonkun annetun rajan. Kaikissa painonvartijayhdistyksissä raja ei ole sama. Tehdään painonvartijayhdistystä vastaava luokka. Olioa luotaessa annetaan konstruktorille parametriksi pienin painoindeksi millä yhdistyksen jäseneksi pääsee.
public class PainonvartijaYhdistys { private double alinPainoindeksi; public PainonvartijaYhdistys(double indeksiRaja) { this.alinPainoindeksi = indeksiRaja; } }
Tehdään sitten metodi jonka avulla voidaan tarkastaa hyväksytäänkö tietty henkilö yhdistyksen jäseneksi, eli onko henkilön painoindeksi tarpeeksi suuri. Metodi palauttaa true jos hyväksytään, false jos ei.
Huom! tässä käytetään materiaalin luvussa
23 luotua Henkilo
-oliota.
public class PainonvartijaYhdistys { // ... public boolean hyvaksytaanJaseneksi(Henkilo henkilo) { if ( henkilo.painoIndeksi() < alinPainoindeksi ) { return false; } return true; } }
Testipääohjelma:
public static void main(String[] args) { Henkilo m = new Henkilo("Matti"); m.setPaino(86); m.setPituus(180); Henkilo j = new Henkilo("Juhana"); j.setPaino(64); j.setPituus(172); PainonvartijaYhdistys kumpulanLaski = new PainonvartijaYhdistys(25); if ( kumpulanLaski.hyvaksytaanJaseneksi(m) ) { System.out.println( m.getNimi() + " pääsee jäseneksi"); } else { System.out.println( m.getNimi() + " ei pääse jäseneksi"); } if ( kumpulanLaski.hyvaksytaanJaseneksi(j) ) { System.out.println( j.getNimi() + " pääsee jäseneksi"); } else { System.out.println( j.getNimi() + " ei pääse jäseneksi"); }
Ohjelma tulostaa:
Matti pääsee jäseneksi Juhana ei pääse jäseneksi
Jatkamme edelleen luokan Henkilo
parissa.
Haluamme vertailla kahden henkilön ikää. Vertailu voidaan hoitaa usealla tavalla. Henkilölle voitaisiin määritellä getterimetodi getIka
ja kahden henkilön iän vertailu tapauhtuisi tällöin seuraavasti:
Henkilo h1 = new Henkilo("Pekka"); Henkilo h2 = new Henkilo("Juhana") if ( h1.getIka() > h2.getIka() ) { System.out.println( h1.getNimi() + " on vanhempi kuin " + h2.getNimi() ); }
Opettelemme kuitenkin nyt hieman "oliohenkisemmän" tavan kahden henkilön ikävertailun tekemiseen.
Teemme Henkilö-luokalle metodin boolean vanhempiKuin(Henkilo verrattava)
jonka avulla tiettyä henkilö-olioa voi verrata parametrina annettuun henkilöön iän perusteella.
Metodia on tarkoitus käyttää seuraavaan tyyliin:
public static void main(String[] args) { Henkilo p = new Henkilo("Pekka", 24); Henkilo a = new Henkilo("Antti", 22); if ( p.vanhempiKuin(a) ) { // sama kun p.vanhempiKuin(a)==true System.out.println( p.getNimi() + " on vanhempi kuin " + a.getNimi() ); } else { System.out.println( p.getNimi() + " ei ole vanhempi kuin " + a.getNimi() ); }
Tässä siis kysytään Pekalta onko hän Anttia vanhempi, Pekka vastaa true jos on ja false muuten.
Eli teknisesti ottaen kutsutaan "Pekkaa" vastaavan olion johon p
viittaa metodia, ja
parametriksi annetaan "Anttia" vastaavan olion viite a
.
Ohjelman kuuluu tulostaa
Pekka on vanhempi kuin Antti
Metodi saa siis parametrikseen henkilöolion (tarkemmin sanottuna viitteen henkilöolioon, eli "langan päässä"
olevan henkilön) ja vertaa omaa ikäänsä this.ika
verrattavaksi annetun henkilön
ikään verrattava.ika
. Toteutus näyttää seuraavalta:
public class Henkilo { // ... public boolean vanhempiKuin(Henkilo verrattava) { if ( this.ika > verrattava.ika ) { return true; } return false; } }
Vaikka ika
onkin olion yksityinen (private
) oliomuuttuja,
pystymme kirjoittamaan lukemaan muuttujan arvon kirjoittamalla verrattava.ika
.
Tämä johtuu siitä, että private
-muuttujat ovat luettavissa kaikissa
metodeissa, jotka kyseinen luokka sisältää. Huomaa, että syntaksi (kirjoitusasu) vastaa
tässä jonkin olion metodin kutsumista. Toisin kuin metodia kutsuttaessa, viittaamme olion kenttään,
jolloin metodikutsun osoittavaia sulkeita ei kirjoiteta.
Toinen esimerkki samasta teemasta. Tehdään luokka, jonka avulla voidaan esittää päiväyksiä.
Olion sisällä päiväys esitetään kolmella oliomuutujalla. Tehdään myös metodi jolla voi vertailla onko päivämäärä aiemmin kuin parametrina annettu päivämäärä:
public class Paivays { private int pv; private int kk; private int vv; public Paivays(int pv, int kk, int vv) { this.pv = pv; this.kk = kk; this.vv = vv; } public String toString() { return pv+"."+kk+"."+vv; } public boolean aiemmin(Paivays verrattava){ // ensin verrataan vuosia if ( this.vv < verrattava.vv ) return true; // jos vuodet ovat samat, verrataan kuukausia if ( this.vv == verrattava.vv && this.kk < verrattava.kk ) return true; // vuodet ja kuukaudet samoja, verrataan päivää if ( this.vv == verrattava.vv && this.kk == verrattava.kk && this.pv < verrattava.pv ) return true; return false; } }
Vertailu on nyt hiukan ikävä toimenpide, sillä on huomioitava monia vaihtoehtoja. Käyttöesimerkki:
public static void main(String[] args) { Paivays p1 = new Paivays(14, 2, 2011); Paivays p2 = new Paivays(21, 2, 2011); Paivays p3 = new Paivays(1, 3, 2011); Paivays p4 = new Paivays(31, 12, 2010); System.out.println( p1 + " aiemmin kuin " + p2 + ": " + p1.aiemmin(p2)); System.out.println( p2 + " aiemmin kuin " + p1 + ": " + p2.aiemmin(p1)); System.out.println( p2 + " aiemmin kuin " + p3 + ": " + p2.aiemmin(p3)); System.out.println( p3 + " aiemmin kuin " + p2 + ": " + p3.aiemmin(p2)); System.out.println( p4 + " aiemmin kuin " + p1 + ": " + p4.aiemmin(p1)); System.out.println( p1 + " aiemmin kuin " + p4 + ": " + p1.aiemmin(p4)); }
14.2.2011 aiemmin kuin 21.2.2011: true 21.2.2011 aiemmin kuin 14.2.2011: false 21.2.2011 aiemmin kuin 1.3.2011: true 1.3.2011 aiemmin kuin 21.2.2011: false 31.12.2010 aiemmin kuin 14.2.2011: true 14.2.2011 aiemmin kuin 31.12.2010: false
Viime viikolla tutustuttiin helppokäyttöiseen ArrayList
:iin. Listoille pystyi helposti laittamaan esim. merkkijonoja ja listalla olevien merkkijonojen läpikäynti, etsiminen, poistaminen, järjestäminen ym. olivat vaivattomia toimenpiteitä.
ArrayList:eihin voidaan laittaa minkä tahansa tyyppisiä oliota. Luodaan seuraavassa henkiölista, eli tyyppiä ArrayList<Henkilo>
oleva ArrayList ja laitetaan sinne muutama henkilöolio:
public static void main(String[] args) { ArrayList<Henkilo> opettajat = new ArrayList<Henkilo>(); // voidaan ensin ottaa henkilö muuttujaan Henkilo opettaja = new Henkilo("Juhana"); // ja lisätä se sitten listalle opettajat.add(opettaja); // tai voidaan myös luoda olio lisättäessä: opettajat.add( new Henkilo("Matti") ); opettajat.add( new Henkilo("Martin") ); System.out.println("opettajat vastasyntyneenä: "); for ( Henkilo hlo : opettajat ) { System.out.println( hlo ); } for ( Henkilo hlo : opettajat ) { for ( int i=0; i < 30; i++ ) { hlo.vanhene(); } } System.out.println("30 vuoden kuluttua: "); for ( Henkilo hlo : opettajat ) { System.out.println( hlo ); }
Ohjelman tulostus:
opettajat vastasyntyneenä: Juhana, ikä 0 vuotta Matti, ikä 0 vuotta Martin, ikä 0 vuotta 30 vuoden kuluttua: Juhana, ikä 30 vuotta Matti, ikä 30 vuotta Martin, ikä 30 vuotta
Luvussa 24 ohjelmoidut Sanajoukko
-oliot tallettivat sisälleen merkkijonoja. Aiemmin mainittiin, että myös merkkijonot ovat olioita. Olioiden sisällä siis voi olla olioita, ei pelkästään merkkijonoja vaan myös itse määriteltyjä oliota.
Lisätään henkilöille syntymäpäivä. Syntymäpäivä on luonnollista esittaa äsken tehdyn Paivays-olion avulla:
public class Henkilo { private String nimi; private int ika; private int paino; private int pituus; private Paivays syntymaPaiva; // ...
Tehdään henkilölle uusi konstruktori, joka mahdollistaa syntymäpäivän asettamisen:
public Henkilo(String nimi, int pp, int kk, int vv) { this.nimi = nimi; this.paino = 0; this.pituus = 0; this.syntymaPaiva = new Paivays(pp, kk, vv); }
Eli konstruktorin parametrina annetaan erikseen päiväyksen osat (päivä, kuukausi, vuosi), niistä
luodaan päiväysolio joka sijoitetaan oliomuuttujaan syntymaPaiva
.
Muokataan toString
siten, että iän sijaan se näyttää syntymäpäivän:
public String toString() { return nimi +", syntynyt "+ syntymaPaiva; }
Ja kokeillaan miten uusittu Henkilö-luokka toimii:
public static void main(String[] args) { Henkilo m = new Henkilo("Martin", 24, 4, 1983); Henkilo j = new Henkilo("Juhana", 17, 9, 1985); System.out.println( m ); System.out.println( j ); }
Tulostuu:
Martin, syntynyt 24.4.1983 Juhana, syntynyt 17.9.1985
Luvussa 23.16 todettiin, että oliot ovat "langan päässä". Kertaa nyt luku 23.16.
Henkilö-oliolla on oliomuuttujat nimi
joka on merkkijono-olio ja syntymaPaiva
joka on Päiväys-olio. Henkilön oliomuuttujat siis ovat molemmat olioita, eli teknisesti ottaen ne eivät sijaitse henkilö-olion sisällä vaan ovat "langan päässä", ts. henkilöllä on oliomuuttujissa tallessa viite niihin. Kuvana:
Pääohjelmalla on nyt siis langan päässä kaksi Henkilö-olioa. Henkilöllä on nimi ja syntymäpäivä. Koska molemmat ovat olioita, ovat ne henkilölä langan päässä.
Syntymäpäivä vaikuttaa hyvältä laajennukselta Henkilö-luokkaan. Huomaamme kuitenkin, että oliomuuttuja ikä
uhkaa jäädä turhaksi ja lienee syytä poistaa se. Iän pystyy nimittäin tarvittaessa selvittämään helposti nykyisen päivämäärän ja syntymäpäivän perusteella. Javassa nykyinen päivä selviää esim. seuraavasti:
int p = Calendar.getInstance().get(Calendar.DATE); int k = Calendar.getInstance().get(Calendar.MONTH) + 1; // tammikuun numero 0 joten lisätään 1 int v = Calendar.getInstance().get(Calendar.YEAR); System.out.println("tänään on " + p + "." + k + "."+ v );
Kun ikä poistetaan, täytyy vanhempiKuin
-metodi muuttaa toimimaan syntymäpäiviä
vertaamalla. Teemme muutoksen harjoitustehtävänä,
Laajennetaan PainonvartijaYhdistys
-oliota siten, että yhdistys tallettaa ArrayList:iin kaikki jäsenensä. Listalle tulee siis Henkilö-olioita. Yhdistykselle annetaan laajennetussa versiossa konstruktorin parametrina nimi:
public class PainonvartijaYhdistys { private double alinPainoindeksi; private String nimi; private ArrayList<Henkilo> jasenet; public PainonvartijaYhdistys(String nimi, double alinPainoindeksi) { this.alinPainoindeksi = alinPainoindeksi; this.nimi = nimi; jasenet = new ArrayList<Henkilo>(); } //.. }
Tehdään metodi jolla henkilö liitetään yhdistykseen. Metodi ei liitä yhdistykseen kuin tarpeeksi suuren painoindeksi omaavat henkilöt. Tehdään myös toString jossa tulostetaan jäsenten nimet:
public class PainonvartijaYhdistys { // ... public boolean hyvaksytaanJaseneksi(Henkilo henkilo) { if ( henkilo.painoIndeksi() < alinPainoindeksi ) { return false; } return true; } public void lisaaJaseneksi(Henkilo henkilo) { if ( !hyvaksytaanJaseneksi(henkilo) ) // sama kuin hyvaksytaanJaseneksi(henkilo) == false return; jasenet.add(henkilo); } public String toString() { String jasenetMerkkijonona = ""; for ( Henkilo jasen : jasenet ) { jasenetMerkkijonona += " " + jasen.getNimi() + "\n"; } return "Painonvartijayhdistys " + nimi + " jäsenet: \n" + jasenetMerkkijonona; } }
Metodi lisaaJaseneksi
siis käyttää aiemmin tehtyä metodia hyvaksytaanJaseneksi
.
Kokeillaan laajentunutta painonvartijayhdistystä:
public static void main(String[] args) { PainonvartijaYhdistys painonVartija = new PainonvartijaYhdistys("Kumpulan paino", 25); Henkilo m = new Henkilo("Matti"); m.setPaino(86); m.setPituus(180); painonVartija.lisaaJaseneksi(m); Henkilo j = new Henkilo("Juhana"); j.setPaino(64); j.setPituus(172); painonVartija.lisaaJaseneksi(j); Henkilo h = new Henkilo("Harri"); h.setPaino(104); h.setPituus(182); painonVartija.lisaaJaseneksi(h); Henkilo p = new Henkilo("Petri"); p.setPaino(112); p.setPituus(173); painonVartija.lisaaJaseneksi(p); System.out.println( painonVartija ); }
Tulostuksesta huomaamme, että Juhanaa ei kelpuutettu jäseneksi:
Painonvartijayhdistys Kumpluan paino jäsenet: Matti Harri Petri
Olemme nähneet metodeja jotka palauttavat totuusarvoja, lukuja, taulukkoja ja merkkijonoja. On helppoa arvata, että metodi voi palauttaa minka tahansa tyyppisen olion. Tehdään painovartijayhdistykselle metodi, jolla saadaan tietoon yhdistyksen suurimman painoindeksin omaava henkilö.
public class PainonvartijaYhdistys { // ... public Henkilo suurinPainoindeksinen() { Henkilo painavinTahanAsti = jasenet.get(0); for ( Henkilo henkilo : jasenet) { if ( henkilo.painoIndeksi() > painavinTahanAsti.painoIndeksi() ) painavinTahanAsti = henkilo; } return painavinTahanAsti; }
Logiikaltaan metodi toimii samaan tapaan kuin suurimman luvun etsiminen taulukosta. Käytössä on apumuuttuja painavinTahanAsti
joka laitetaan aluksi viittaamaan listan ensimmäiseen henkilöön. Sen jälkeen käydään lista läpi ja katsotaan tuleeko vastaan suuremman painoindeksin omaavia henkilöitä, jos tulee, niin otetaan viite talteen muuttujaan painavinTahanAsti
. Lopuksi palautetaan muuttujan arvo eli viite henkilöolioon.
Tehdään lisäys edelliseen pääohjelmaan. Pääohjelma ottaa vastaan metodin palauttaman viitteen muuttujaan painavin
.
public static void main(String[] args) { PainonvartijaYhdistys painonVartija = new PainonvartijaYhdistys("Kumpluan paino", 25); // .. Henkilo painavin = painonVartija.suurinPainoindeksinen(); System.out.print("suurin painoindeksinen jäsen: " + painavin.getNimi() ); System.out.println(" painoindeksi " + String.format( "%.2f", painavin.painoIndeksi() ) ); }
Tulostuu:
suurin painoindeksinen jäsen: Petri painoindeksi 37,42
Kun aloimme ohjelmoida olioilla, materiaalissa oli neuvo jättää sana static pois olioiden metodien määrittelystä. Viikkoon 4 asti taas kaikissa metodeissa esiintyi määre static. Mistä on kysymys?
Luvussa 16 oli esimerkkinä metodi nollaaTaulukko
joka toimi nimensä mukaisesti eli asettaa nollan parametrina saamansa taulukon kaikkien lokeroiden arvoksi.
Seuraavassa ohjelma joka sisältää metodin määrittelyn sekä sitä käyttävän pääohjelman.
public class Ohjelma { public static void nollaaTaulukko(int[] taulukko) { for ( int i=0; i < taulukko.length; i++ ) taulukko[i] = 0; } public static void main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; for ( int luku : t ) System.out.print( luku+ " " ); // tulostuu 1, 2, 3, 4, 5 System.out.println(); nollaaTaulukko(t); for ( int luku : t ) System.out.print( luku+ " " ); // tulostuu 0, 0, 0, 0, 0 } }
Huomaamme, että metodin määrittelyssä on nyt määre static
. Syynä on se, että metodi ei liity mihinkään olioon vaan kyseessä on ns. luokkametodi, luokkametodeja kutsutaan yleensä staattisiksi metodeiksi. Toisin kuin olioiden metodit (jotka eivät ole static), staattiseen metodiin ei liity mitään olioa. Staattinen metodi ei siis voi viitata this
-määreellä olioon itseensä toisin kuin olioimetodit.
Staattiselle metodille voi toi antaa olion parametriksi. Staattinen metodi ei voi käsitellä mitään muita lukuja, merkkijonoja, taulukoita tai oliota kuin parametrina sille annettuja (tähänkin on eräs poikkeus mutta ei puhuta siitä nyt).
Koska staattinen metodi ei liity mihinkään olioon, ei sitä kutsuta oliometodien tapaan olionNimi.metodinNimi()
, vaan ylläolevan esimerkin tapaan käytetään pelkkää staattisen metodin nimeä.
Jos staattisen metodin koodi on eri luokan sisällä kuin sitä kutsuva metodi, tapahtuu kutsu muodossa LuokanNimi.staattisenMetodinNimi()
. Edellinen esimerkki alla muutettuna siten, että pääohjelma ja metodi ovat omissa luokissaan (eli käytännössä myös eri tiedostoissa):
public class Ohjelma { public static void main(String[] args) { int[] t = { 1, 2, 3, 4, 5 }; for ( int luku : t ) System.out.print( luku+ " " ); // tulostuu 1, 2, 3, 4, 5 System.out.println(); TaulukonKasittely.nollaaTaulukko(t); for ( int luku : t ) System.out.print( luku+ " " ); // tulostuu 0, 0, 0, 0, 0 } } public class TaulukonKasittely { public static void nollaaTaulukko(int[] taulukko) { for ( int i=0; i < taulukko.length; i++ ) taulukko[i] = 0; } }
Toisen luokan sisällä määriteltyä staattista metodia siis kutsutaan nyt muodossa TaulukonKasittely.nollaaTaulukko(t);
.
Kaikki olion tilaa käsittelevät metodit tulee määritellä normaaleina oliometodeina. Esim. edellä määrittelemiemme luokkien Henkilo, Paivays, PainonvartijaYhdistys
kaikki metodit tulee määritellä normaaleina oliometodeina eli ei static:eina.
Palataan vielä luokkaan Henkilo
. Seuraavassa on osa luokan määritelmästä. Kaikkiin oliomuuttujiin viitataan nyt this
-määreen avulla sillä halutaan korostaa, että metodeissa käsitellään olion "sisällä" olevia oliomuuttujia.
public class Henkilo { private String nimi; private int ika; public Henkilo(String nimi) { this.ika = 0; this.nimi = nimi; } public boolean taysiIkainen(){ if ( this.ika < 18 ) { return false; } return true; } public void vanhene() { this.ika++; } public String getNimi() { return this.nimi; } }
Koska metodit käsittelevät olioa, ei niitä voi määrittää static:eiksi eli "olioon kuulumattomiksi". Jos näin yritetään tehdä, ei metodi toimi:
public class Henkilo { //... public static void vanhene() { this.ika++; } }
Seurauksena on virheilmoitus non static variable ika can not be referenced from static context, joka tarkoittaa "suomeksi" että staattinen metodi ei pysty käsittelemään oliomuuttujaa.
Eli milloin staattista metodia sitten kannattaa käyttää? Tarkastellaan luvusta 23 tuttua henkilöolioita käsittelevää esimerkkiä:
public class Ohjelma { public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); Henkilo juhana = new Henkilo("Juhana"); for ( int i=0; i < 30; i++ ) { pekka.vanhene(); juhana.vanhene(); } antti.vanhene(); if ( antti.taysiIkainen() ) { System.out.println( antti.getNimi() + " on täysi-ikäinen" ); } else { System.out.println( antti.getNimi() + " on alaikäinen" ); } if ( pekka.taysiIkainen() ) { System.out.println( pekka.getNimi() + " on täysi-ikäinen" ); } else { System.out.println( pekka.getNimi() + " on alaikäinen " ); } if ( juhana.taysiIkainen() ) { System.out.println( juhana.getNimi() + " on täysi-ikäinen" ); } else { System.out.println( juhana.getNimi() + " on alaikäinen " ); } } }
Huomaamme, että henkilöiden täysi-ikäisyyden ilmottamiseen liittyy koodinpätkä joka on copy-pastettu peräkkäin kolme kertaa. Todella rumaa!
Henkilön täysi-ikäisyyden ilmoittaminen on mainio kohde staattiselle metodille. Eli kirjoitetaan ohjelma uudelleen metodia hyödyntäen:
public class Main { private static void ilmoitaTaysiIkaisyys(Henkilo henkilo) { if ( henkilo.taysiIkainen() ) { System.out.println(henkilo.getNimi() + " on täysi-ikäinen"); } else { System.out.println(henkilo.getNimi() + " on alaikäinen"); } } public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); Henkilo juhana = new Henkilo("Juhana"); for ( int i=0; i < 30; i++ ) { pekka.vanhene(); juhana.vanhene(); } antti.vanhene(); ilmoitaTaysiIkaisyys(antti); ilmoitaTaysiIkaisyys(pekka); ilmoitaTaysiIkaisyys(juhana); }
Metodi ilmoitaTaysiIkaisyys
on määritelty staattiseksi, eli se ei liity mihinkään olioon,
mutta metodi saa parametrikseen henkilöolion. Metodia ei ole nyt määritelty
Henkilö-luokan sisälle sillä vaikka se käsittelee parametrinaan saamaan henkilöolioa, se on juuri
kirjoitetun pääohjelman apumetodi, jonka avulla main on saatu kirjoitettua selkeämmin.
Usein tahdotaan myös tehdä olioista loogisesti riippumattomista apuri-metodeista staattisia.
Tästä sopiva esimerkki olisi luvun 24 Sanajoukko
-esimerkissä esiintynyt
onPalindromi
-metodi. Kuten koodista nähdään, ei metodissa viitata lainkaan oliomuuttujiin.
public class Sanajoukko { /* Muu luokan määrittely */ private static boolean onPalindromi(String sana){ int loppu = sana.length()-1; for ( int i=0; i < sana.length()/2; i++ ){ if ( sana.charAt(i) != sana.charAt(loppu-i) ) return false; } return true; } }
Mikäli et vielä täysin sisäistänyt staattisten ja ei-staattisten metodien eroja,
niin kannattaa pohtia millaista olisi käyttää jo tutun Collections
-luokan
metodeja sort
ja shuffle
, mikäli ne eivät olisi staattisia.
Osaamme jo laittaa olioita ArrayList:iin. Joskus on tarvetta laittaa olioita taulukkoon. Seuraava esimerkki näyttää miten tämä onnistuu:
public static void main(String[] args) { Henkilo[] henkilot = new Henkilo[3]; henkilot[0] = new Henkilo("Pekka"); henkilot[1] = new Henkilo("Antti"); henkilot[2] = new Henkilo("Juhana"); for ( int i=0; i < 30; i++ ) { henkilot[0].vanhene(); henkilot[1].vanhene(); henkilot[2].vanhene(); } for ( Henkilo h : henkilot ) ilmoitaTaysiIkaisyys(h); }
Alussa luodaan taulukko johon mahtuu 3 henkilöolioa. Laitetaan Pekka lokeroon 0, Antti lokeroon 1 ja Juhana lokeroon 2. Vanhennetaan kaikkia 30 vuotta ja tarkastetaan kaikkien täysi-ikäisyys edellisen luvun metodia hyödyntäen.
Sama esimerkki ArrayListien avulla:
public static void main(String[] args) { ArrayList<Henkilo> henkilot = new ArrayList<Henkilo>(); henkilot.add( new Henkilo("Pekka") ); henkilot.add( new Henkilo("Antti") ); henkilot.add( new Henkilo("Juhana") ); for ( int i=0; i < 30; i++ ) { for ( Henkilo h : henkilot ) h.vanhene(); // tai henkilot.get(0).vanhene(); // henkilot.get(1).vanhene(); // ... } for ( Henkilo h : henkilot ) ilmoitaTaysiIkaisyys(h); }