Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.

Ohjelmoinnin perusteet

Arto Vihavainen ja Matti Luukkainen

1. Huomautus lukijalle

Tämä materiaali on tarkoitettu Helsingin yliopiston tietojenkäsittelytieteen laitoksen syksyn 2012 Ohjelmoinnin perusteet- ja jatkokurssille. Materiaali pohjautuu keväiden 2012, 2011 ja 2010 kurssimateriaaleihin, joiden sisältöön ovat vaikuttaneet Matti Paksula, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju, Martin Pärtel, Joel Kaasinen ja Mikael Nousiainen

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. Esimerkkien ja erityisesti erilaisten omien kokeilujen tekeminen on parhaita tapoja "sisäistää" luettua tekstiä.

Pyri 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ä saat ohjausta tehtävän tekemiseen pajassa (tai MOOC-kurssilaisena verkossa).

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

Muutamiin kohtiin olemme myös liittäneet screencasteja joita katsomalla voi pelkän valmiin koodin lukemisen sijaan seurata miten ohjelma muodostuu.

Viikko1

2. Ohjelma ja lähdekoodi

2.1 Lähdekoodi

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.

2.2 Komennot

Varsinaisesti ohjelma muodostuu lähdekoodiin kirjoitetuista komennoista. 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 tulostaa sille sulkeiden sisällä annetun merkkijonon. Pääte ln on lyhenne sanasta line. Komento siis tulostaa rivin, eli kun annettu merkkijono on tulostettu, tulostaa komento myös rivinvaihdon.

2.3 Kääntäjä ja tulkki

Tietokone ei suoraan ymmärrä käyttämäämme ohjelmointikieltä. Tarvitsemme lähdekoodin ja tietokoneen väliin kääntäjän. Ohjelmoidessamme komentoriviä käyttäen, komento javac Hei.java kääntää Hei.java-tiedoston tavukoodiksi, jota voidaan ajaa java-tulkin avulla. Käännetty ohjelma ajetaan komentoriviltä komennolla java Hei, missä Hei on käännetyn java-lähdekooditiedoston nimi.

Käyttäessämme modernia ohjelmointiympäristöä (tästä myöhemmin lisää), ohjelmointiympäristö hoitaa lähdekoodin kääntämisen. Valitessamme ohjelman suorittamisen, ohjelmointiympäristö kääntää ja suorittaa ohjelman. Ohjelmointiympäristöt kääntävät ohjelmakoodia ohjelmoijan sitä kirjoittaessa, jolloin yksinkertaiset virheet huomataan ennen ohjelman suoritusta.

2.4 Komennon osia

2.4.1 Puolipiste

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 ilman rivinvaihtoa. System.out.print-komento ei siis 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 luettavuuteen liittyviä seikkoja tullaan painottamaan tällä kurssilla.

2.4.2 Komennoille lähdetettävät "tiedot" eli parametrit

Komennon käsittelemä tieto eli komennon parametrit lähetetään komennolle lisäämällä ne komennon nimen perässä olevien sulkujen () sisään. Esimerkiksi System.out.print -komennon parametriksi annetaan teksti hei seuraavasti: System.out.print("hei").

2.4.3 Kommentit

Lähdekoodin kommentit ovat kätevä tapa merkitä asioita itselle ja muille muistiin. Kommentti on mikä tahansa rivi, joka alkaa kahdella vinoviivalla //. Kaikki kommenttimerkkiä seuraava samalla rivillä oleva teksti tulkitaan kommentiksi.

2.4.4 Esimerkki kommenttien käytöstä


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

2.5 Lisää tulostamisesta

Kuten aiemmin huomattiin, tulostamiseen on kaksi komentoa:

Tulostettavan tekstin osana voi olla erikoismerkkejä. Tärkein näistä on \n, joka vaihtaa riviä. Erikoismerkkejä on muitakin.

System.out.println("Ensimmäinen\nToinen\nKolmas");

Ylläoleva tulostaa suoritettaessa seuraavaa:

Ensimmäinen
Toinen
Kolmas

2.6 Pääohjelmarunko

Ohjelman "Esimerkki" runko on seuraavanlainen.

public class Esimerkki {
    public static void main(String[] args) { 
        // ohjelmakoodi
    }
}

Ohjelma sijaitsee samannimisessä .java-päätteisessä tiedostossa. Ohjelman Esimerkki täytyy siis sijaita tiedostossa, jonka nimi on Esimerkki.java.

Ohjelmaa suoritettaessa suoritetaan alue, joka on rungossamme merkitty kommentilla ohjelmakoodi. Ohjelmoimme ensimmäisellä viikolla vain tälle alueelle. Kun puhumme komennoista, esimerkiksi tulostamisesta, tulee komennot kirjoittaa ohjelmarungon sisälle. Esimerkiksi System.out.print("Tulostettava teksti");

public class Esimerkki {
    public static void main(String[] args) {
        System.out.print("Tulostettava teksti");
    }
}

Jatkossa esimerkeissä ei erikseen näytetä pääohjelmarunkoa.

2.7 Tutustuminen ohjelmointiympäristöön

Nykyaikainen ohjelmointi tapahtuu lähes poikkeuksetta 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öä. 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. NetBeans on loppujenlopuksi hyvin helppokäyttöinen. Perusteet opit 5 minuutissa, ja kurssin myötä opit koko ajan hieman lisää ja kesäkuussa olet jo todellinen NetBeans "poweruser".

Ennen kuin tiedät mitä teet, toimi täsmälleen ohjeen kuvaamalla tavalla. Useimmat seuraavista tehtävänannoista näyttävät mitä pyydetyn tehtävän tulisi tulostaa ruudulle toimiakseen oikein.

HUOM: älä tee tehtäviä siten että pelkästään kirjoitat koodia ja painelet testinappia. Suorita myös koodia normaaliin tapaan (vihreällä nuolella) ja katso mitä ruudulle tulostuu. Erityisesti jos ohjelma ei meinaa mennä testeistä läpi, kannattaa varmistaa normaalisti suorittamalla että ohjelma toimii silmämääräisesti niinkuin sen pitäisi.

Seuraavissa tehtävissä harjoitellaan tuntuman saamista NetBeansiin ja ruudulle tulostamista.

Muista ensin katsoa ohje NetBeansin käyttöön täältä

Jos et ole vielä käynyt vastaamassa kyselyyn, tee se NYT klikkaamalla tästä

Näet tehtävänannon klikkaamalla tehtävän nimeä

Tehtävä 1: Nimi

Tehtävä 2: Hei Maailma! (Ja Mualima!)

Tehtävä 3: Kuusi

3. Muuttuja ja sijoitus

3.1 Muuttujat ja tietotyypit

Ohjelmoinnissa eräs keskeinen käsite on muuttuja. Muuttuja kannattaa ajatella lokerona, johon voi tallettaa tietoa. Muuttujaan talletettavalla tiedolla on aina tyyppi. Tyyppejä ovat esimerkiksi teksti eli merkkijono (String), kokonaisluku (int), liukuluku (double) ja totuusarvo (boolean). Muuttujaan asetetaan arvo yhtäsuuruusmerkillä (=).

int kuukausia = 12;

Yllä olevassa asetuslauseessa asetetaan kokonaisluku-tyyppiä (int) olevaan muuttujaan nimeltä 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);

Tulostus:

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 kun muuttuja esitellään ohjelmassa ensimmäistä kertaa.

int kokonaisluku = 123;
System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku);

kokonaisluku = 42;
System.out.println("Kokonaislukumuuttujan arvo on " + kokonaisluku);

Tulostuu:

Kokonaislukumuuttujan arvo on 123
Kokonaislukumuuttujan arvo on 42

3.2 Muuttujan tyyppi pysyy

Kun muuttujan tyyppi on kertaalleen määritelty, ei se enää muutu. Esimerkiksi merkkijonomuuttuja 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 liukuluku = 0.42;
liukuluku = 1; // Onnistuu! :)

Tehtävä 4: Muuttuvat muuttujat

3.3 Sallittu ja kuvaava muuttujan nimi

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 camelCase-tyylillä, jolloin nimi muistuttaneeKamelia. Huom! Muuttujien nimien ensimmäinen kirjain kirjoitetaan aina pienellä:

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.

3.3.1 Sallittuja muuttujien nimiä

3.3.2 Virheellisiä muuttujien nimiä

4. Laskentaa

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. Kuitenkin * ja / laskentaan ennen + ja - operaatioita. Tässä vielä tarkemmin laskujärjestyksestä javassa. Linkin takana oleva materiaali ei ole kuitenkaan aloittelijan kannalta kovin oleellista.

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);   // 23
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)   // 23

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

4.1 Liukuluvut eli desimaaliluvut

Kokonaislukujen jako ja jakojäännös ovat hieman hankalampia. 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
int eka = 3:
int toka = 2;
double tulos = eka / toka;      // nytkin tulos on 1, sillä eka ja toka ovat kokonaislukuja

Jakojäännös-operaation (%) avulla saadaan selville jakojäännös. 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

Kokonaisluku voidaan tarvittaessa muuttaa liukuluvuksi lisäämällä sen eteen tyyppimuunnosoperaatio (double):

int eka = 3;
int toka = 2;

double tulos1 = (double)eka / toka;  // tulokseksi: 1.5

double tulos2 = eka / (double)toka;  // tulokseksi: 1.5

double tulos3 = (double)(eka / toka);  // tulokseksi: 1

Jälkimmäisessä tulos pyöristyy väärin sillä laskuoperaatio kokonaisluvuilla suoritetaan ennen tyyppimuunnosta.

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.

Tehtävä 5: Sekunnit vuodessa

5. Katenointi eli merkkijonojen yhdistäminen

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);
Neljä: 4
Mutta! kaksikymmentäkaksi: 22

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 " + z);

Tämä ohjelma tulostaa tietenkin:

muuttujan x arvo on: 10
y on 5 ja z on 6

Tehtävä 6: Yhteenlasku

Tehtävä 7: Kertolasku

6. Käyttäjän syötteen lukeminen

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 {

    public static void main(String[] args) {
        Scanner lukija = new Scanner(System.in);

        // ohjelmakoodi
    }
}

6.1 Merkkijonon lukeminen

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 se 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 {

    public static void main(String[] args) {
        Scanner lukija = new Scanner(System.in);

        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)

6.2 Kokonaisluvun lukeminen

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.

Käytännössä kytkemme kaksi komentoa yhteen. Ensin luemme käyttäjältä rivin, jonka annamme heti komennolle Integer.parseInt.

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 {
    public static void main(String[] args) {
        Scanner lukija = new Scanner(System.in);

        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.");
    }
}

6.3 Yhteenveto

Käyttäjän kanssa keskustelevan ohjelman runko:

import java.util.Scanner;
public class OhjelmanNimi {
    public static void main(String[] args) {
        Scanner lukija = new Scanner(System.in);

        // koodi tähän
    }
}

Merkkijonon lukeminen:

String merkkijono = lukija.nextLine();

Kokonaisluvun lukeminen:

int kokonaisluku = Integer.parseInt(lukija.nextLine());

Tehtävä 8: Summaaja

Tehtävä 9: Jakaja

Tehtävä 10: Ympyrän kehän pituus

Tehtävä 11: Suurempi luku

Tehtävä 12: Ikien summa

Tehtävä 13: NHL-tilastot, osa1

7. Valinta ja totuusarvot

Tähän mennessä tekemämme ohjelmat ovat edenneet suoraviivaisesti käskystä toiseen, toimien jokaisella suorituskerralla samalla tavalla. Jotta ohjelman suoritus voisi haarautua erilaisille suorituspoluille käyttäjän esim. käyttäjän antaman syötteen perusteella, 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:

int 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!

7.1 Koodin sisennys

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.

7.2 else

Jos valinnan ehto on epätotta, eli totuusarvo on false, voidaan suorittaa toinen vaihtoehtoinen lohko koodia, tämä käy komennon 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!

Tehtävä 14: Positiivinen luku

Tehtävä 15: Täysi-ikäisyys

Tehtävä 16: Pariton vai parillinen?

7.3 else if

Jos valittavissa olevia vaihtoehtoja on enemmän kuin kaksi kannattaa käyttää else if-komentoa, joka 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!"'.

7.4 Merkkijonojen vertailu

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

Tehtävä 17: Suurempi luku

Tehtävä 18: Arvosanat ja pisteet

7.5 Loogiset operaatiot

Valinnan ehto voi olla myös monimutkaisempi, yksittäisten loogisten operaatioiden avulla koostettu ehto. Loogisia operaatioita ovat:

Seuraavassa yhdistetään &&:lla eli ja-operaatiolla kaksi yksittäistä ehtoa, sillä halutaan tarkistaa, onko muuttujassa oleva luku suurempi kuin 4 ja pienempi kuin 11, eli siis välillä 5-10:

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! :)

Seuraavassa annetaan ||:n eli tai-operaation avulla kaksi vaihtoehtoa, onko luku pienempi kuin 0 tai suurempi kuin 100. Ehto toteutuu jos luku täyttää jomman kumman ehdon:

System.out.println("Onkohan luku pienempi kuin 0 tai suurempi kuin 100");
int luku = 145;

if ( luku < 0 || luku > 100 ) {
    System.out.println("On! :)");
} else {
    System.out.println("Ei ollut :(")
}
Onkohan luku pienempi kuin 0 tai suurempi kuin 100
On! :)

Seuraavassa käännetään !:n eli negaatio-operaation avulla ehdon arvo päinvastaiseksi:

System.out.println("Eihän merkkijono ole 'maito'");
String merkkijono = "piimä";

if ( !(merkkijono.equals("maito")) ) {  // tosi jos ehto merkkijono.equals("maito") on epätosi
    System.out.println("ei ollut!");
} else {
    System.out.println("oli")
}
ei ollut!

Monimutkaisten ehtojen muodostamisessa tarvitaan usein sulkuja:

int luku = 99;

if ( (luku > 0 && luku < 10) || luku > 100 ) {
    System.out.println("luku oli joko yhden ja yhdeksän väliltä tai yli sata"); 
} else {
    System.out.println("luku oli 0 tai pienempi tai väliltä 10-99");
}
luku oli 0 tai pienempi tai väliltä 10-99

Tehtävä 19: Iän tarkistus

Tehtävä 20: Käyttäjätunnukset

Tehtävä 21: Karkausvuosi

8. Toiston alkeet

Valintakomennon avulla saamme ohjelman toimintaan mukaan ehdollisuutta, eli esim. jos käyttäjätunnus ja salasana ovat oikein, päästetään käyttäjä kirjautumaan ohjelmaan ja muuten ei.

Ehdollisuuden lisäksi tarvitsemme toistoa: käyttäjätunnusta ja salasanaa pitää pystyä kysymään uudelleen niin kauan kunnes oikea käyttäjätunnus/salasana-pari on annettu.

Yksinkertaisin toiston muoto on ikuinen toisto. Seuraava ohjelma tulostaa merkkijonoa osaan ohjelmoida! ikuisesti eli "äärettömän monta kertaa":

while ( true ) {
    System.out.println("osaan ohjelmoida!");
}

Komento while( true ) saa sen aikaan, että siihen liittyvää lohkoa, eli {}:lla ympäröityjä komentoja suoritetaan äärettömän monta kertaa.

Ikuinen toisto ei yleensä ole se mitä halutaan. Toisto voidaan keskeyttää esim. komennolla break.

while ( true ) {
    System.out.println("osaan ohjelmoida!");

    System.out.print("jatketaanko (ei lopettaa)? ");
    String komento = lukija.nextLine();
    if ( komento.equals("ei") ) {
        break;
    }
}

System.out.println("kiitos ja kuulemiin.");

Nyt toisto etenee siten että ensin tulostuu osaan ohjelmoida! ja tämän jälkeen ohjelma kysyy käyttäjältä jatketaanko vielä. Jos käyttäjä vastaa ei, suoritetaan komento break jonka ansiosta toisto lopetetaan ja suoritetaan komento joka tulostaa kiitos ja kuulemiin.

osaan ohjelmoida!
jatketaanko (ei lopettaa)? joo
osaan ohjelmoida!
jatketaanko (ei lopettaa)? jawohl
osaan ohjelmoida!
jatketaanko (ei lopettaa)? ei
kiitos ja kuulemiin.

Toiston sisällä voi tehdä erilaisia asioita. Seuraavassa yksinkertainen laskin. Laskin kysyy käyttäjältä komentoa. Komennossa lopetus suoritetaan break ja toisto loppuu. Tämän jälkeen kysytään kahta lukua. Jos komento oli summa lasketaan lukujen summa ja tulostetaan se. Jos komento oli erotus toimitaan vastaavasti. Muussa tapauksessa ilmoitetaan että komento on tuntematon.

System.out.println("tervetuloa käyttämään laskinta");

while ( true ) {    
    System.out.print("anna komento (summa, erotus, lopetus): ");
    String komento = lukija.nextLine();
    if ( komento.equals("lopetus") ) {
        break;
    }

    System.out.print("anna luvut ");
    int eka = Integer.parseInt(lukija.nextLine());
    int toka = Integer.parseInt(lukija.nextLine());

    if ( komento.equals("summa") ) {     
        int summa = eka+toka;
        System.out.println( "lukujen summa " + summa );
    } else if ( komento.equals("erotus") ) {     
        int erotus = eka-toka;
        System.out.println( "lukujen erotus " + erotus );
    } else {     
        System.out.println( "tuntematon komento" );
    } 

}
System.out.println("kiitos ja kuulemiin.");

Screencast joka näyttää miten ohjelma syntyy:

Tehtävä 22: Salasana

Tehtävä 23: Lämpötiloja

Tehtävä 24: NHL-tilastot, osa 2

Viikko 2

9. Muuttuvat muuttujat

Olemassaolevan muuttujan arvoa halutaan usein muuttaa. Tämä onnistuu tavallisen sijoituslausekkeen avulla. Seuraavassa muuttujan ika arvoa kasvatetaan yhdellä:

int ika = 1;

System.out.println( ika );    // tulostuu 1
ika = ika + 1;                // ika:n uusi arvo on ika:n vanha arvo plus yksi    
System.out.println( ika );    // tulostuu 2

Komento ika = ika + 1 siis kasvattaa muuttujan ika arvoa yhdellä. Muuttujan arvon kasvattaminen yhdellä onnistuu myös seuraavasti:

int ika = 1;

System.out.println( ika );    // tulostuu 1
ika++;                        // tarkoittaa samaa kuin ika = ika + 1;
System.out.println( ika );    // tulostuu 2

Toinen esimerkki:

int pituus = 100;

System.out.println( pituus );   // tulostuu 100
pituus = pituus - 50;    
System.out.println( pituus );   // tulostuu 50
pituus = pituus * 2;
System.out.println( pituus );   // tulostuu 100 
pituus = pituus / 4;
System.out.println( pituus );   // tulostuu 25
pituus--;                       // sama kuin pituus = pituus-1;
System.out.println( pituus );   // tulostuu 24

Tehtävä 25: Kolmen luvun summa

Tehtävä 26: Useamman luvun summa

10. Lisää toistoa

Opimme aiemmin toteuttamaan while(true)-komennon avulla ohjelman, joka toistaa tiettyä komentosarjaa komennon break suorittamiseen asti.

Komento break ei ole ainoa tapa lopettaa toistoa. Toistokomennon yleinen muoto on while (ehto), jossa ehtona voi olla mikä tahansa totuusarvoinen lauseke, eli ehto voi olla täsmälleen samaa muotoa kuin if-komentojen ehdot.

Seuraavassa esimerkissä tulostetaan luvut 1, 2, ..., 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++ tarkoittaa samaa kuin luku = luku + 1
}

Lue ylläoleva "niin pitkään kuin luku on pienempi kuin 11, tulosta luku ja kasvata lukua yhdellä".

Ylläolevassa koodissa ehdossa olevaa muuttujaa luku kasvatettiin jokaisella kierroksella yhdellä. Päivitys voi olla mikä tahansa, eli ehdossa olevan muuttujan ei tarvitse suinkaan aina kasvaa yhdellä, esim:

int luku = 1024;

while (luku >= 1) {
    System.out.println(luku);
    luku = luku / 2;
}

Screencast aiheesta:

Muutama NetBeans-vihje
  • Kaikki NetBeans-vihjeet löytyvät täältä
  • Koodin automaattinen täydennys

    Jos ohjelmassasi on käytössä esim. muuttuja String sukunimi;, ei koko muuttujan nimeä tarvitse välttämättä joka kerta kirjoittaa kokonaan. Kokeile mitä tapahtuu kun kirjoitat s ja sen jälkeen painat yhtäaikaa ctrl ja välilyönti. HUOM: laitoksen koneilla automaattinen täydennys saadaan aikaan painamalla yhtä aikaa ctrl, alt ja välilyönti. Vastaavalla tavalla NetBeans osaa täydentää muitakin nimiä, esim. komennon while aikaansaamiseksi riittää kirjoittaa w ja painaa ctrl+välilyönti...

  • sout

    Muista että saat ruudulle tekstin System.out.println("") kirjoittamalla sout ja painamalla tabulaattoria eli q:n vasemmalla puolella olevaa näppäintä

Tee seuraavat tehtävät while-komennon avulla:

Tehtävä 27: Yhdestä sataan

Tehtävä 28: Sadasta yhteen

Tehtävä 29: Parilliset luvut

Tehtävä 30: Lukuun asti

Tehtävä 31: Alaraja ja yläraja

10.1 Sijoitusoperaatiot

Koska vanhan muuttujan arvon muuttaminen on hyvin yleinen operaatio, on Javassa sitä varten erityiset sijoitusoperaatiot.

int pituus = 100;

pituus += 10;  // sama kuin pituus = pituus + 10;
pituus -= 50;  // sama kuin pituus = pituus - 50;

Olemassaolevan muuttujan arvoa muuttava sijoitusoperaatio merkitään muuttuja muutostyyppi= muutos, esimerkiksi muuttuja += 5. Huomaa, että muuttujan tyyppi pitää aina kertoa ennen kuin sille voidaan asettaa arvo. Muuttuja tulee siis aina esitellä ennen kuin se on käytettävissä. Muuttujan esittely tapahtuu kertomalla muuttujan tyyppi ja nimi.

Seuraava esimerkki ei toimi, sillä muuttujan pituus tyyppiä ei ole kerrottu.

pituus = pituus + 100;   // ei toimi!
pituus += 100;           // ei toimi!

Kun tyyppi on kerrottu, laskutkin toimivat 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;
}

Tehtävä 32: Lukusarjan summa

Tehtävä 33: Rajoitetun lukusarjan summa

Tehtävä 34: Kertoma

Tehtävä 35: Potenssien summa

10.2 Ikuinen silmukka

Yksi suosituimmista ohjelmointivirheistä toistolauseissa on tehdä vahingossa ikuinen silmukka. Seuraavassa yritetään tulostaa ruudulle 10 kertaa "En enää ikinä ohjelmoi ikuista silmukkaa":

int i = 0;

while( i<10 ) {
    System.out.println("En enää ikinä ohjelmoi ikuista silmukkaa");
}

Toistokertojen määrää kontrolloiva muuttuja i on aluksi 0 ja toistoja on tarkoitus tehdä niin kauan kuin i<10. Käy kuitenkin hieman hassusti: muuttujan i arvoa ei muuteta missään, joten toistoehto pysyy ikuisesti totena.

10.3 while ja lopetus

Olemme tehneet peräkkäin jo muutamia toistotehtäviä, joissa toistoehto on suunnilleen muotoa:

int i = 1;
while ( i < 10 ) {
    // ...
    i++;
}

Ylläolevassa rungossa muuttuja i muistaa toistokertojen määrän, ja lopetusehto perustuu i:n vertailuun.

Muistellaan taas toiston lopetusta. Toiston lopetuksen ei tarvitse perustua toistokertojen laskemiseen. Katsotaan esimerkkiä jossa kysytään käyttäjän ikää. Jos ikä ei ole välillä 5-85, annetaan huomautus ja kysytään ikä uudelleen. Toistolauseen 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 ) {   // ikä pienempi kuin 5 TAI suurempi kuin 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);

Ohjelman olisi voinut tehdä myös vanhaa tuttua while(true) -rakennetta käyttäen:

System.out.println("ikäsi: ");

while( true ) {   
    int ika = Integer.parseInt(lukija.nextLine());

    if ( ika >= 5 && ika <= 85 ) {   // ikä 5:n JA 85:n välillä
        break;                              // lopetetaan toisto
    }

    System.out.println("Valehtelet");
    if ( ika < 5 ) {
        System.out.println("Olet niin nuori ettet osaa kirjoittaa");   
    } else {  // ikä siis yli 85
        System.out.println("Olet niin vanha ettet osaa käyttää tietokonetta");   
    } 

    System.out.println("syötä ikäsi uudelleen: ");
}

System.out.println("Ikäsi on siis "+ ika);

Tehtävä 36: Silmukat, lopetus ja muistaminen

Huomio: Ohjelmien tekeminen pienissä paloissa

Edeltävissä tehtävissä tehtiin oikeastaan ainoastaan yksi ohjelma, mutta ohjelman rakentaminen tapahtui hyvin pienissä paloissa. Tämä on ehdottoman suositeltava tapa AINA kun ohjelmoit.

Eli kun teet ohjelmaa, oli se sitten harjoitustehtävä tai oma projektisi, kannattaa edetä hyvin pienissä paloissa. Älä koskaan yritä ratkaista koko ongelmaa kerralla. Aloita jollain helpolla asialla jonka tiedät varmasti osaavasi. Esim. edellisessä tehtäväsarjassa keskityttiin aluksi pelkästään siihen, että osataan pysäyttää ohjelma käyttäjän syöttäessä luvun -1. Kun yksi ohjelman osa on saatu toimimaan, voidaan siirtyä ratkaisemaan jotain seuraavaa varsinaisen ongelman osaongelmaa.

Osa kurssin tehtäviä on edellisten tapaan valmiiksi osiin pilkottuja. Usein osat pitää vielä pilkkoa ohjelmoinnin kannalta vieläkin pienempiin paloihin. Kannattaa tehdä siten, että suoritat ohjelman lähes jokaisen uuden koodirivin jälkeen. Tällöin varmistat, että ratkaisu on etenemässä haluttuun suuntaan.

Lisätietoa: toisto for-lauseen avulla

Edellä esitellyn while-toistolauseen lisäksi Javassa toiston voi hoitaa myös for-toistolauseen avulla. Varsinaisesti rupeamme käyttämään for:ia vasta hieman myöhemmin kurssilla, mutta jo tässä vaiheessa mainittakoon, että toistolause

int i = 0;
while ( i < 10 ) {
    System.out.println( i );
    i++;
}

tehtäisiin for:in avulla seuraavasti:

for ( int i = 0; i < 10 ; i ++ ) {
    System.out.println( i );
}

eli for-komento sisältää samalla rivillä indeksimuuttujan alustuksen, toistoehton ja indeksimuuttujan kasvatuksen puolipistein eroteltuna. Voit unohtaa for:in olemassaolon toistaiseksi jos et halua sekoittaa päätäsi sen olemassaololla.

11. Metodit

Olemme käyttäneet useita erilaisia komentoja Javassa: sijoitusta, laskutoimituksia, vertailuja, if:iä 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 lukija.nextLine() ja sen kanssa usein nähty Integer.parseInt().

Huomaamme, että jälkimmäinen joukko edellä lueteltuja komentoja poikkeaa if:istä ja 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 metodille annettava parametri!") 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ä:

int luku = Integer.parseInt( lukija.nextLine() );

sisältää kaksi metodikutsua. Ensin kutsutaan sisempänä olevaa metodia lukija.nextLine. Metodilla on paluuarvonaan käyttäjän syöttämä merkkijono. Seuraavaksi kutsutaan metodia Integer.parseInt. Metodikutsun parametrina on merkkijono jonka lukija.nextLine:n kutsu palautti ja metodin paluuarvona on merkkijonoa vastaava kokonaisluku.

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. Nyt opettelemme tekemään omia metodeita.

12. Omat metodit

Olemme tähän mennessä ohjelmoineet ohjelmamme siten, että kaikki tapahtuu yhdessä jatkumossa ja koodia luetaan ylhäältä alas.

Edellä 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, joko mainin ylä- tai alapuolelle

import java.util.Scanner;

public class OhjelmaRunko {
    // omia metodeja tänne

    public static void main(String[] args) {
        Scanner lukija = new Scanner(System.in);
        // ohjelmakoodi
    }

    // omia metodeja tai tänne
}

Luodaan metodi tervehdi.

public static void tervehdi() {
    System.out.println("Terveiset metodimaailmasta!");
}

Ja asetetaan se metodeille kuuluvalle paikalle.

import java.util.Scanner;

public class OhjelmaRunko {
    public static void main(String[] args) {
        Scanner lukija = new Scanner(System.in);
        // ohjelmakoodi
    }

    // omat metodit
    public static void tervehdi() {
        System.out.println("Terveiset metodimaailmasta!");
    }
}

Metodin määrittely sisältää kaksi osaa. Metodimäärittelyn ensimmäisellä rivillä on metodin nimi eli tervehdi. Nimen vasemmalla puolella tässä vaiheessa määreet public static void. Metodin nimen sisältävän rivin alla on aaltosulkeilla erotettu koodilohko, jonka sisälle kirjoitetaan metodin koodi, eli ne komennot jotka metodia kutsuttaessa suoritetaan. Metodimme tervehdi 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 {
    public static void main(String[] args) {
        Scanner lukija = new Scanner(System.in);

        // 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!

Huomionarvoista tässä on koodin suoritusjärjestys. Koodin suoritus etenee siten, että pääohjelman -- eli main:in -- rivit suoritetaan ylhäältä alas yksi kerrallaan. Koodirivin ollessa metodikutsu, mennään suorittamaan metodin koodirivit, jonka jälkeen palataan josta metodin kutsu tapahtui. Tarkemmin ottaen 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. Metodi 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. Metodia ei esimerkiksi voi määritellä toisen metodin sisällä.

Tehtävä 37: Tekstin tulostus

Tehtävä 38: Monta tulostusta

12.1 Metodin parametrit

Metodista 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".

12.2 Monta parametria

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  

12.3 Metodi kutsuu toista metodia

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) {
    int i = 0;
    while ( i < kerrat ) {
        tervehdi(nimi);
        i++;
    }

}

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!

Screencast aiheesta:

Tehtävä 39: Tulostelua

Tehtävä 40: Tulostelua Like A Boss

Tehtävä 41: Numerovisa

Tehtävä 42: Tekstikäyttöliittymä hirsipuu-pelille

Viikko 3

13. Lisää metodeista

Edellisellä viikolla opettelimme kirjoittamaan omia metodeja. Metodien avulla voimme jäsennellä ohjelmaa pienemmiksi hyvin nimetyiksi. selkeän tehtävän omaaviksi loogisiksi kokonaisuuksiksi. Tämä sekä helpottaa ongelmanratkaisua että parantaa ohjelman luettavuutta niin ohjelmoijan kuin ohjelmaa myöhemmin mahdollisesti ylläpitävänkin osalta. Tästä lähtien oikeastaan jokainen tekemämme ohjelma sisältää metodeita.

Jatketaan edelleen metodien parissa.

13.1 Metodit ja muuttujien näkyvyys

Yritetään muuttaa metodin sisältä pääohjelman muuttujan arvoa.

// pääohjelma
public static void main(String[] args) {
    int luku = 1;
    kasvataKolmella();
}

// metodi
public static void kasvataKolmella() {
    luku = luku + 3;
}

Ohjelma ei kuitenkaan toimi, sillä metodi ei näe pääohjelman muuttujaa luku.

Yleisemminkin voi todeta, että pääohjelman muuttujat eivät näy metodien sisään, ja metodin muuttujat eivät näy muille metodeille tai pääohjelmalle. Ainoa keino viedä metodille tietoa ulkopuolelta on parametrin avulla.

// pääohjelma
public static void main(String[] args) {
    int luku = 1;
    System.out.println("Pääohjelman muuttujan luku arvo: " + luku);
    kasvataKolmella(luku);
    System.out.println("Pääohjelman muuttujan luku arvo: " + luku);
}

// metodi
public static void kasvataKolmella(int luku) {
    System.out.println("Metodin parametrin luku arvo: " + luku);
    luku = luku + 3;
    System.out.println("Metodin parametrin luku arvo: " + luku);
}

Yllä metodilla kasvataKolmella on parametri luku. Parametri luku kopioidaan metodin käyttöön. Kun yllä oleva ohjelma suoritetaan, nähdään seuraavanlainen tulostus.

Pääohjelman muuttujan luku arvo: 1
Metodin parametrin luku arvo: 1
Metodin parametrin luku arvo: 4
Pääohjelman muuttujan luku arvo: 1

Parametrina annettu luku siis kopioitiin metodin käyttöön. Jotta saisimme luvun uuden arvon myös pääohjelmaan, tulee metodin palauttaa arvo.

13.2 Metodin paluuarvot

Metodi voi palauttaa arvon. Tähän mennessä kurssilla olleissa esimerkeissä ja tehtävissä 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 kokonaisluvun 10, metodin tyyppi on int. Palautus tapahtuu komennolla return:

public static int palautetaanAinaKymppi() {
    return 10;
}

Jotta metodin palauttamaa arvoa voisi käyttää, tulee paluuarvo ottaa talteen muuttujaan:

public static void main(String[] args) {
    int luku = palautetaanAinaKymppi();

    System.out.println( "metodi palautti luvun " + luku );
}

Metodin paluuarvo 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 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 double metodiJokaPalauttaaLiukuluvun() {
    // metodin runko, tarvitsee return-komennon
}

Jos metodille määritellään paluuarvo, on sen myös pakko palauttaa arvo, esimerkiksi seuraava metodi on virheellinen.

public static String virheellinenMetodi() {
    System.out.println("Väitän palauttavani merkkijonon, mutten palauta sitä.");
}

Seuraavassa esimerkissä määritellään metodi summan laskemiseen. Tämän jälkeen metodia käytetään laskemaan luvut 2 ja 7 yhteen. Metodikutsusta saatava paluuarvo asetetaan muuttujaan lukujenSumma.

public static int summa(int eka, int toka) {
    return eka + toka;
}

Metodin kutsu:

int lukujenSumma = summa(2, 7);
// lukujenSumma on nyt 9

Laajennetaan edellistä esimerkkiä siten, että käyttäjä syöttää luvut.

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    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) );
}

public static int summa(int eka, int toka) {
    return 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.

Huomaa, että metodin parametrien nimillä ja metodin kutsujan puolella määritellyillä muuttujan nimillä ei ole mitään tekemistä keskenään. Edellisessä esimerkissä sekä pääohjelman muuttujat että metodin parametrit olivat "sattumalta" nimetty samoin (eli eka ja toka). Seuraava toimisi aivan yhtä hyvin:

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    System.out.print("Anna ensimmäinen luku: ");
    int luku1 = Integer.parseInt( lukija.nextLine() );

    System.out.print("Anna toinen luku: ");
    int luku2 = Integer.parseInt( lukija.nextLine() );

    System.out.print("Luvut ovat yhteensä: " + summa(luku1, luku2) );
}

public static int summa(int eka, int toka) {
    return eka + toka;
}

Nyt pääohjelman muuttujan luku1 arvo kopioituu metodin parametrin eka arvoksi ja pääohjelman muuttujan luku2 arvo kopioituu metodin parametrin toka arvoksi.

Seuraavassa esimerkissä metodia summa kutsutaan kokonaisluvuilla, jotka saadaan summa-metodin paluuarvoina.

int eka = 3;
int toka = 2;

int monenLuvunSumma = summa(summa(1, 2), summa(eka, toka));
// 1) suoritetaan sisemmät metodit:
//    summa(1, 2) = 3   ja summa(eka, toka) = 5
// 2) suoritetaan ulompi metodi:
//    summa(3, 5) = 8 
// 3) muuttujan monenLuvunSumma arvoksi siis tulee 8

13.3 Metodin omat muuttujat

Seuraava metodi laskee syötteinään saamiensa lukujen 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;
}

Metodin kutsu voi tapahtua esim seuraavasti

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    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("ja kolmas luku: ");
    int kolmas = Integer.parseInt( lukija.nextLine() );

    double keskiarvonTulos = keskiarvo(eka, toka, kolmas);

    System.out.print("Lukujen keskiarvo: " + keskiarvonTulos );
}

Huomaa että metodin sisäiset muuttujat summa ja ka eivät näy pääohjelmaan. Yksi yleinen aloittelijan virhe olisikin yrittää käyttää metodia seuraavasi:

public static void main(String[] args) {
    // annetaan arvot muuttujille eka, toka ja kolmas

    keskiarvo(eka, toka, kolmas);

    // yritetään käyttää metodin sisäistä muuttujaa, EI TOIMI!
    System.out.print("Lukujen keskiarvo: " + ka );
}

Myös seuraavanlaista virhettä näkee usein:

public static void main(String[] args) {
    // annetaan arvot muuttujille eka, toka ja kolmas

    keskiarvo(eka, toka, kolmas);

    // yritetään käyttää pelkkää metodin nimeä, EI TOIMI!
    System.out.print("Lukujen keskiarvo: " + keskiarvo );
}

Eli tässä yritettiin käyttää pelkkää metodin nimeä muuttujamaisesti. Toimiva tapa metodin tuloksen sijoittamisen apumuuttujaan lisäksi on suorittaa metodikutsu suoraan tulostuslauseen sisällä:

public static void main(String[] args) {
    int eka = 3;
    int toka = 8;
    int kolmas = 4; 

    // kutsutaan metodia tulostuslauseessa, TOIMII!
    System.out.print("Lukujen keskiarvo: " + keskiarvo(eka, toka, kolmas) );
}

Tässä siis ensin tapahtuu metodikutsu joka palauttaa arvon 5.0 joka sitten tulostetaan tulostuskomennon avulla.

Screencast aiheesta:

Tehtävä 43: Lukujen summa

Tehtävä 44: Pienin

Tehtävä 45: Suurin

Tehtävä 46: Lukujen keskiarvo

14. Merkkijonot

Tässä luvussa tutustaan tarkemmin Javan merkkijonoihin, eli String:eihin. Olemme jo käyttäneet String-tyyppisiä muuttujia tulostuksen yhteydessä sekä oppineet vertailemaan merkkijonoja toisiinsa. Merkkijonoja vertailtiin toisiinsa kutsumalla merkkijonon 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");
}

Merkkijonoilta voi kysyä niiden pituutta kirjoittamalla merkkijonon perään .length() eli kutsumalla merkkijonolle sen pituuden kertovaa metodia.

String banaani = "banaani";
String kurkku = "kurkku";
String yhdessa = banaani + kurkku;

System.out.println("Banaanin pituus on " + banaani.length());
System.out.println("Kurkku pituus on " + kurkku.length());
System.out.println("Sanan " + yhdessa + " pituus on " + yhdessa.length());

Edellä kutsutaan metodia length() kolmelle eri merkkijonolle. Kutsu banaani.length() kutsuu nimenomaan merkkijonon banaani pituuden kertovaa metodia, kun taas kurkku.length() on merkkijonon kurkku pituuden kertovan metodin kutsu, jne.Pisteen vasemman puoleinen osa kertoo kenen metodia kutsutaan.

Javassa on erillinen char-tietotyyppi kirjaimia varten. Yksittäiseen char-muuttujaan voi tallentaa yhden kirjaimen. Merkkijonolta voidaan kysyä sen kirjaimia kirjaimen paikan perusteella käyttämällä charAt()-metodia. Huomaa että laskeminen alkaa nollasta!

String kirja = "Kalavale";

char merkki = kirja.charAt(3);
System.out.println("Kirjan neljäs kirjain on " + merkki); //tulostaa "a"
  

Koska merkkijonon kirjaimet numeroidaan (eli teknisemmin ilmaistuna merkkijonoja indeksoidaan) alkaen paikasta 0, on merkkijonon viimeisen kirjaimen numero eli indeksi "merkkijonon pituus miinus yksi", eli kirja.charAt(kirja.length()-1). Esimerkiksi seuraava kaataa ohjelman: yritämme hakea kirjainta kohdasta jota ei ole olemassa.

char merkki = kirja.charAt(kirja.length());
  
NetBeans-vihje
  • Kaikki NetBeans-vihjeet löytyvät täältä
  • Uudelleennimentä

    Muuttujat, metodit ja ensi viikolla opittavat luokat kannattaa nimetä kuvaavasti. Usein käy, että valittu nimi on hieman epäkuvaava ja tulee tarve nimen muuttamiselle. NetBeans:issa tämä on todella helppoa. Maalaa huono nimi jostain kohtaa koodiasi hiirellä. Paina (yhtäaikaa) ctrl ja r ja kirjoita muuttujalle/metodille uusi nimi.

Tehtävä 47: Nimen pituus

Tehtävä 48: Ensimmäinen kirjain

Tehtävä 49: Viimeinen kirjain

Tehtävä 50: Ensimmäiset kirjaimet erikseen

Tehtävä 51: Kirjaimet erikseen

Tehtävä 52: Nimen kääntäminen

14.1 Muita merkkijonojen metodeja

Merkkijonosta halutaan usein lukea jokin tietty osa. Tämä onnistuu mekkkijonojen eli String-luokan metodilla substring. Sitä voidaan käyttää kahdella tavalla: yksiparametrisenä palauttamaan merkkijonon loppuosa tai kaksiparametrisena palauttamaan parametrien valitsema osajono merkkijonosta:

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 String-tyyppiseen muuttujaan kirja..

String kirja = "8 veljestä";

String loppuosa = kirja.substring(2);
System.out.println("7 " + loppuosa); // tulostaa: 7 veljestä

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, eli paikan (muista että paikkanumerointi alkaa nollasta!). Jos taas sanaa ei löytynyt merkkijonosta palautetaan arvo -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!

Screencast aiheesta:

Tehtävä 53: Alkuosa

Tehtävä 54: Loppuosa

Tehtävä 55: Sana sanassa

Tehtävä 56: Merkkijonon kääntäminen

15. Oliot ja metodit

Merkkijonot poikkeavat luonteeltaan hieman esim. kokonaisluvuista. Kokonaisluvut ovat "pelkkiä arvoja", niiden avulla voi laskea ja niitä voi tulostella ruudulle:

int x = 1;
int y = 3;

y = 3*x + 2;

System.out.println( "y:n arvo nyt: " + y );
  

Merkkijonot ovat hieman "älykkäämpiä" ja tietävät esimerkiksi pituutensa:

String sana1 = "Ohjelmointi";
String sana2 = "Java";

System.out.println( "merkkijonon "+ sana1 +" pituus: " + sana1.length() );

System.out.println( "merkkijonon "+ sana2 +" pituus: " + sana2.length() );
  

Tulostuu:

merkkijonon Ohjelmointi pituus on 11
merkkijonon Java pituus on 4

Pituus saadaan selville kutsumalla merkkijonon metodia length(). Merkkijonoilla on joukko muitakin metodeja. Kokonaisluvuilla eli int:eillä ei ole metodeja ollenkaan, ne eivät itsessään "osaa" mitään.

Merkkijonot ovat olioita eli "asioita joihin liittyy metodeja sekä arvo". Jatkossa tulemme näkemään hyvin paljon muitakin olioita kuin merkkijonoja.

Kuten edellisestä esimerkistä huomaamme, kutsutaan olion metodia lisäämällä olion nimen perään piste ja metodikutsu:

sana1.length()    // kutsutaan merkkijono-olion sana1 metodia length()
sana2.length()    // kutsutaan merkkijono-olion sana2 metodia length()

Metodikutsu kohdistuu nimenomaan sihen olioon, mille metodia kutsutaan. Yllä kutsumme ensin sana1-nimisen merkkijonon length()-metodia, sitten merkkijonon sana2 metodia length().

Vanha tuttumme lukija on myös olio:

Scanner lukija = new Scanner(System.in);

Lukijat ja merkkijonot ovat molemmat oliota, ne ovat kuitenkin varsin erilaisia. Lukijoilla on mm. metodi nextLine() jota merkkijonoilla ei ole. Javassa oliot "synnytetään" eli luodaan melkein aina komennolla new, merkkijonot muodostavat tässä suhteessa poikkeuksen! -- Merkkijonoja voi luoda kahdella tavalla:

String banaani = new String("Banaani");
String porkkana = "porkkana";

Kumpikin ylläolevista riveistä luo uuden merkkijono-olion. Merkkijonojen luonnissa new-komentoa käytetään hyvin harvoin.

Olion "tyyppiä" sanotaan luokaksi. Merkkijonojen luokka on String, lukijoiden luokka taas on Scanner. Opimme jatkossa luokista ja olioista paljon lisää.

16. ArrayList eli "oliosäiliö"

Ohjelmoidessa tulee usein vastaan tilanteita joissa haluaisimme pitää muistissa esimerkiksi useita erilaisia merkkijonoja. Todella huono tapa olisi määritellä jokaiselle oma muuttujansa:

String sana1;
String sana2;
String sana3;
// ...
String sana10;

Tämä olisi aivan kelvoton ratkaisu ja sen huonoutta ei kannata oikeastaan edes perustella -- ajattele ylläoleva esimerkki vaikkapa sadalla tai tuhannella sanalla.

Java, kuten kaikki modernit ohjelmointikielet, tarjoaa erilaisia apuvälineitä joiden avulla on helppo säilyttää ohjelmassa monia olioita. Tutustumme nyt Javan ehkä eniten käytettyyn oliosäiliöön ArrayList:iin.

Seuraava ohjelmanpätkä ottaa käyttöönsä merkkijono-olioita tallentavan ArrayList:in sekä tallettaa listalle pari merkkijonoa.

import java.util.ArrayList;

public class ListaOhjelma {

    public static void main(String[] args) {
        ArrayList<String> sanaLista = new ArrayList<String>();

        sanaLista.add("Ensimmäinen");
        sanaLista.add("Toinen");
    }
}

Yllä olevan pääohjelman ensimmäinen rivi luo sanaLista-nimisen merkkijonoja tallettavan arraylistin. Listamuuttujan tyypin nimi on ArrayList<String>, eli merkkijonoja tallettava ArrayList. Itse lista luodaan sanomalla new ArrayList<String>();.

Huom: Jotta ArrayList toimisi, on ohjelman ylälaitaan kirjoitettava import java.util.ArrayList; tai import java.util.*;

Kun lista on luotu, siihen lisätään kaksi merkkijonoa kutsumalla listan metodia add. Tila ei lopu listalla missään vaiheessa kesken, eli periaatteessa listalle saa lisätä niin monta merkkijonoa kun "koneen" muistiin mahtuu.

Sisäisesti ArrayList on nimensä mukaisesti lista. Lisätyt merkkijonot menevät automaattisesti ArrayList:in loppuun.

16.1 ArrayList:in metodeja

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 ensimmäisenä " + opettajat.get(0));
    System.out.println("listalla kolmantena " + opettajat.get(2));

    opettajat.remove("Arto");

    if ( opettajat.contains("Arto") ) {
        System.out.println("Arto on opettajien listalla");
    } else {
        System.out.println("Arto ei ole opettajien listalla");
    }
}

Ensin luodaan merkkijonolista jolle lisätään 6 nimeä. size kertoo listalla olevien merkkijonojen lukumäärän. Huom: kun metodia kutsutaan, on kutsu muotoa opettajat.size(), eli metodin nimeä edeltää piste ja sen listan nimi kenen metodia kutsutaan.

Merkkijonot ovat listalla siinä järjestyksessä missä ne listalle laitettiin. Metodilla get(i) saadaan tietoon listan paikan i sisältö. Listan alkioiden paikkanumerointi alkaa nollasta, eli ensimmäisenä lisätty on paikassa numero 0, toisen a lisätty paikassa numero 1 jne.

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 neljäntenä oleva merkkijono.

Esimerkin lopussa kutsutaan metodia contains jonka avulla kysytään listalta sisältääkö se parametrina annettavan merkkijonon. Jos sisältää, palauttaa metodi arvon true.

Ohjelman tulostus:

opettajien lukumäärä 6
listalla ensimmäisena Antti
listalla kolmantena Pekka
Arto ei ole opettajien listalla

Huom! Metodit remove ja contains olettavat että listaan tallennetuilla olioilla on metodi equals. Palaamme tähän myöhemmin kurssilla.

16.2 ArrayList:in läpikäynti

Seuraavassa esimerkissä lisätään listalle 4 nimeä ja tulostetaan listan sisältö:

public static void main(String[] args) {
    ArrayList<String> opettajat = new ArrayList<String>();

    opettajat.add("Antti");
    opettajat.add("Pekka");
    opettajat.add("Juhana");
    opettajat.add("Martin");

    System.out.println( opettajat.get(0) );
    System.out.println( opettajat.get(1) );
    System.out.println( opettajat.get(2) );
    System.out.println( opettajat.get(3) );
}

Ratkaisu on kuitenkin erittäin kömpelö. Entäs jos listalla olisi enemmän alkiota? Tai vähemmän? Entäs jos ei olisi edes tiedossa listalla olevien alkioiden määrää?

Tehdään ensin välivaiheen versio jossa pidetään:

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");

    int paikka = 0;
    System.out.println( opettajat.get(paikka) );
    paikka++;
    System.out.println( opettajat.get(paikka) );  // paikka = 1
    paikka++;
    System.out.println( opettajat.get(paikka) );  // paikka = 2
    paikka++;
    System.out.println( opettajat.get(paikka) );  // paikka = 3
}

Vanhan tutun while-komennon avulla voimme kasvataa muuttujaa paikka niin kauan kunnes se kasvaa liian suureksi:

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");

    int paikka = 0;
    while ( paikka < opettajat.size() )  // muistatko miksi paikka <= opettajat.size() ei toimi?
        System.out.println( opettajat.get(paikka) );
        paikka++;
    }
}

Nyt tulostus toimii riippumatta listalla olevien alkioiden määrästä.

While-toistolauseen käyttö ja listan paikkojen "indeksointi" ei kuitenkaan ole yleensä järkevin tapa listan läpikäyntiin. Suositeltavampi on seuraavassa esiteltävä for-each -toistolauseke.

16.3 for-each

Vaikka komennosta käytetään nimitystä for-each, komennon nimi on pelkästään for. for:ista on olemassa kaksi versiota, perinteinen (jonka nopea esittely oli jo viimeviikolla mutta mitä alame varsinaisesti käyttämään vasta viikolla 6) ja "for-each" jota käytämme nyt.

ArrayList:in alkioiden läpikäynti for-each: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 );
    }
}

Kuten huomaame, ei listalla olevien merkkijonojen paikkanumeroista tarvitse välittää, for käy listan sisällön läpi "automaattisesti".

Komennon for aaltosulkujen sisällä olevassa koodissa käytetään muuttujaa opettaja, joka on määritelty for-rivillä kaksoispisteen vasemmalla puolella. Käy niin, että kukin listalla opettajat oleva merkkijono tulee vuorollaan muuttujan opettaja arvoksi. Eli kun for:iin mennään, on opettaja ensin Antti, forin toisella toistolla opettaja on Pekka, jne

Vaikka for-komento voi tuntua aluksi hieman oudolta sitä kannattaa ehdottomasti totutella käyttämään!

Screencast aiheesta:

Tehtävä 57: Sanat

Tehtävä 58: Toistuva sana

16.4 Listan järjestäminen, kääntäminen ja sekoittaminen

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.*;

Collections:ista löytyy muutakin hyödyllistä:

Tehtävä 59: Sanat käänteisesti

Tehtävä 60: Sanat aakkosjärjestyksessä

16.5 ArrayList metodin parametrina

ArrayList:in voi antaa metodille parametrina:

public static void tulosta(ArrayList<String> lista) {
    for (String sana : lista) {
        System.out.println( sana );
    }
}

public static void main(String[] args) {
    ArrayList<String> lista = new ArrayList<String>();
    lista.add("Java");
    lista.add("Python");
    lista.add("Ruby");
    lista.add("C++");

    tulosta(lista);
}

Parametrin tyyppi siis määritellään listaksi täsmälleen samalla tavalla eli ArrayList<String> kuin listamuuttujan määrittely tapahtuu.

Huomaa, että parametrin nimellä ei ole merkitystä:

public static void tulosta(ArrayList<String> tulostettava) {
    for (String sana : tulostettava) {
        System.out.println( sana );
    }
}

public static void main(String[] args) {
    ArrayList<String> ohjelmointikielet = new ArrayList<String>();
    ohjelmointikielet.add("Java");
    ohjelmointikielet.add("Python");
    ohjelmointikielet.add("Ruby");
    ohjelmointikielet.add("C++");

    ArrayList<String> maat = new ArrayList<String>();
    maat.add("Suomi");
    maat.add("Ruotsi");
    maat.add("Norja");

    tulosta(ohjelmointikielet);    // annetaan metodille parametriksi lista ohjelmointikielet

    tulosta(maat);                 // annetaan metodille parametriksi lista maat
}

Ohjelmassa on nyt kaksi listaa ohjelmointikielet ja maat. Metodille annetaan ensin tulostettavaksi lista ohjelmointikielet. Metodi tulosta käyttää parametriksi saamastaan listasta sisäisesti nimellä tulostettava! Seuraavaksi metodille annetaan tulostettavaksi lista maat. Jälleen metodi käyttää parametrinaan saamasta listasta sisäisesti nimeä tulostettava.

Tehtävä 61: Listan alkioiden lukumäärä

Metodin sisällä on mahdollisuus vaikuttaa parametrina saadun listan sisältöön. Seuraavassa esimerkissä metodi poistaEnsimmainen nimensä mukaisesti poistaa listalla ensimmäisenä olevan merkkijonon (mitähän tapahtuu jos listalla ei ole mitään?).

public static void tulosta(ArrayList<String> tulostettava) {
    for (String sana : tulostettava) {
        System.out.println( sana );
    }
}

public static void poistaEnsimmainen(ArrayList<String> lista) {
    lista.remove(0);  // poistetaan listalta ensimmäinen eli "nollas"
}

public static void main(String[] args) {
    ArrayList<String> ohjelmointikielet = new ArrayList<String>();
    ohjelmointikielet.add("Pascal");
    ohjelmointikielet.add("Java");
    ohjelmointikielet.add("Python");
    ohjelmointikielet.add("Ruby");
    ohjelmointikielet.add("C++");


    tulosta(ohjelmointikielet);

    poistaEnsimmainen(ohjelmointikielet);

    System.out.println();  // tulostetaan tyhjä rivi

    tulosta(ohjelmointikielet);
}

Tulostuu:

Pascal
Java
Python
Ruby
C++

Java
Python
Ruby
C++

Vastaavalla tavalla metodi voisi esim. lisätä parametrina saamaansa listaan lisää merkkijonoja.

Tehtävä 62: Poista viimeinen

Kuten edellisen tehtävän esimerkkitulostuksesta näemme, voi ArrayList:in tulostaa sellaisenaan. Tulostusmuoto ei kuitenkaan yleensä ole halutun kaltainen ja tulostus joudutaan hoitamaan itse esim. for-komennon avulla.

16.6 Lukuja ArrayList:issä

ArrayList: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>, eli 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>();
    

Tehtävä 63: Lukujen summa

Tehtävä 64: Lukujen keskiarvo

Tehtävä 65: Suurin

Tehtävä 66: Varianssi

17. Totuusarvojen käyttö

Totuusarvoinen eli boolean-muuttuja voi saada vain kaksi arvoa true tai false. Seuraavassa esimerkki booleanin käytöstä:

int luku1 = 1;
int luku2 = 5;

boolean ekaSuurempi = true;

if (luku1 <= luku2) {
    ekaSuurempi = false;
}

if (ekaSuurempi==true) {
    System.out.println("luku1 suurempi");
} else {
    System.out.println("luku1 ei ollut suurempi");
}

Eli ensin asetetaan totuusarvon ekaSuurempi arvoksi tosi eli true. Ensimmäinen if tarkastaa onko luku1 pienempi tai yhtä pieni kuin luku2. Jos näin on, vaihdetaan totuusarvon arvoksi epätosi eli false. Myöhempi if valitsee tulostuksen totuusarvoon perustuen.

Totuusarvon käyttö ehtolauseessa on itseasiassa edellistä esimerkkiä yksinkertaisempaa, jälkimmäinen if voidaan kirjoittaa seuraavasti:

if (ekaSuurempi) {  // tarkoittaa samaa kuin ekaSuurempi==true
    System.out.println("luku1 suurempi");
} else {
    System.out.println("luku1 ei ollut suurempi");
}

Eli jos halutaan tarkistaa että booleanmuuttujan arvo on tosi, eli ole tarvetta kirjoittaa ==true, pelkkä muuttujan nimi riittää!

Epätoden tarkastaminen onnistuu negaatio-operaation eli huutomerkin avulla:

if (!ekaSuurempi) {  // tarkoittaa samaa kuin ekaSuurempi==false
    System.out.println("luku1 ei ollut suurempi");
} else {
    System.out.println("luku1 suurempi");
}

17.1 totuusarvon palauttava metodi

Totuusarvot ovat erityisen käteviä jonkun asian voimassaolon tarkistavien metodien paluuarvoina. Tehdään metodi joka tarkastaa sisältääkö sen parametrina saama lista ainoastaan positiivisia lukuja (tulkitaan 0 positiiviseksi). Tieto positiivisuudesta palautetaan totuusarvona.

public static boolean kaikkiPositiivisia(ArrayList<Integer> luvut) {
    boolean eiNegatiivisia = true;

    for (int luku : luvut) {
        if (luku < 0) {
            eiNegatiivisia = false;
        }
    }
    // jos jonkun listan luvuista arvo oli pienempi kuin 0, on eiNegatiivisia nyt false
    return eiNegatiivisia;
}

Metodilla on totuusarvoinen apumuuttuja eiNegatiivisia. Apumuuttujan arvoksi asetetaan ensin true. Metodi käy läpi kaikki listan luvut. Jos jonkun (siis vähintään yhden) luvun arvo on pienempi kuin nolla, asetetaan apumuuttujan arvoksi false. Lopuksi palautetaan apumuuttujan arvo. Apumuuttuja on edelleen true jos yhtään negatiivista lukua ei löytynyt, muuten false.

Metodia käytetään seuraavasti:

public static void main(String[] args) {

    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(3);
    luvut.add(1);
    luvut.add(-1);

    boolean vastaus = kaikkiPositiivisia(luvut);

    if (vastaus) {  // tarkoittaa siis samaa kuin vastaus == true
        System.out.println("luvut positiivisia");
    } else {
        System.out.println("joukossa oli ainakin yksi negatiivinen");
    }
}

Vastauksen tallettaminen ensin muuttujaan ei yleensä ole tarpeen, ja metodikutsu voidaan kirjottaa suoraan ehdoksi:

ArrayList<Integer> luvut = new ArrayList<Integer>();
luvut.add(4);
luvut.add(7);
luvut.add(12);
luvut.add(9);

if (kaikkiPositiivisia(luvut)) {
    System.out.println("luvut positiivisia");
} else {
    System.out.println("joukossa oli ainakin yksi negatiivinen");
}

17.2 Komento return ja metodin lopetus

Metodin suoritus loppuu välittömästi kun metodissa suoritetaan return-käsky. Käyttämällä tätä tietoa hyväksi voimme kirjoittaa kaikkiPositiivisia-metodin hiukan suoraviivaisemmin ja selkeämmin.

public static boolean kaikkiPositiivisia(ArrayList<Integer> luvut) {
    for (int luku : luvut) {
        if (luku < 0) {
            return false;
        }
    }

    // jos tultiin tänne asti, ei yhtään negatiivista löytynyt
    // siispä palautetaan true
    return true;
    }
}

Eli jos lukujen listaa läpikäydessä törmätään negatiiviseen lukuun, voidaan metodista poistua heti palauttamalla false. Jos listalla ei ole yhtään negatiivista, päädytään loppuun ja voidaan palauttaa true. Olemme päässeet metodissa kokonaan eroon apumuuttujan käytöstä!

Tehtävä 67: Onko luku listalla monta kertaa

Tehtävä 68: Palindromi

17.3 ArrayListin kopioiminen

Joskus on tarpeen muodostaa ArrayLististä kopio, johon voi tehdä muutoksia vaikuttamatta alkuperäisen ArrayListin sisältöön. Kopion voi muodostaa luomalla uuden ArrayListin käyttäen vanhaa ArrayListiä parametrina:

ArrayList<String> nimet = new ArrayList<String>();
nimet.add("Kyösti");
nimet.add("Risto");
nimet.add("Carl");
nimet.add("Urho");

//luodaan kopio nimet-listasta
ArrayList<String> kopio = new ArrayList<String>(nimet);
//järjestetään kopio
Collections.sort(kopio);

System.out.println(kopio);  //tulostuu [Carl, Kyösti, Risto, Urho]
System.out.println(nimet);  //tulostuu [Kyösti, Risto, Carl, Urho]

Tehtävä 69: KaikkiEri

18. Metodit ja parametrien kopioituminen

Tarkastellaan muutamaa metodeihin liittyvää tärkeää yksityiskohtaa.

Luvussa 15 oli esimerkki, jossa yritettiin muuttaa pääohjelmassa olevan muuttujan arvoa metodin sisällä.

public static void main(String[] args) {
    int luku = 1;
    kasvataKolmella();
    System.out.println("luku on " + luku);
}

public static void kasvataKolmella() {
    luku = luku + 3;
}

Ohjelma ei toimi, sillä metodi ei pääse käsiksi pääohjelman muuttujaan luku.

Tämä 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 välittämällä pääohjelman muuttuja luku parametrina metodille.

public static void main(String[] args) {
    int luku = 1;
    kasvataKolmella(luku);
    System.out.println(luku);  // tulostaa 1, eli arvo luku ei muuttunut
}

public static void kasvataKolmella(int luku) {
    luku = luku + 3;
}

Ohjelma ei toimi toivotulla tavalla. Metodissa olevat parametrit ovat eri muuttujia kuin pääohjelmassa esitellyt muuttujat. Edellä metodi siis kasvattaa samannimistä, mutta ei samaa parametria luku.

Kun metodille annetaan parametri, parametrin arvo kopioidaan uuteen muuttujaan metodissa käytettäväksi. Yllä olevassa esimerkissä metodille kasvataKolmella annetusta muuttujasta luku luodaan kopio, jota metodin sisällä lopulta käsitellään. Metodi käsittelee siis pääohjelmassa olevan muuttujan kopiota, ei alkuperäistä muuttujaa -- pääohjelmametodissa olevalle muuttujalle luku ei tehdä mitään.

Voidaan ajatella, että pääohjelmametodi main ja metodi kasvataKolmella toimivat kumpikin omassa kohtaa tietokoneen muistia. Allaolevassa kuvassa on main:in muuttujaa luku varten oma "lokero". Kun metodia kutsutaan, tehdään tälle oma muuttuja luku jonka arvoksi kopioituu main:in luku-muuttujan arvo eli 1. Molemmat luku-nimiset muuttujat ovat kuitenkin täysin erillisiä, eli kun metodissa kasvataKolmella muutetaan sen luku-muuttujan arvoa, ei muutos vaikuta millään tavalla pääohjelman muuttujaan luku.

Allaoleva kuva antaa lisävalaisua tilanteeseen.

Metodista saa toki välitettyä tietoa kutsujalle käyttäen paluuarvoa, eli palauttamalla arvon return-komennolla. Edellinen saadaan toimimaan muuttamalla koodia hiukan:

public static void main(String[] args) {
    int luku = 1;
    luku = kasvataKolmellaJaPalauta(luku);

    System.out.println(luku);  // tulostaa 4, sillä luku on saanut arvokseen metodin palauttaman arvon
}

public static int kasvataKolmellaJaPalauta(int luku) {
    luku = luku + 3;

    return luku;
}

Edelleen on niin, että metodi käsittelee pääohjelman luku-muuttujan arvon kopiota. Pääohjelmassa metodin palauttama arvo sijoitetaan muuttujaan luku, joten muutos tulee tämän takia voimaan myös pääohjelmassa. Huomaa, että edellisessä ei ole mitään merkitystä sillä, mikä nimi metodin parametrilla on. Koodi toimii täysin samoin oli nimi mikä tahansa, esim.

public static void main(String[] args) {
    int luku = 1;
    luku = kasvataKolmellaJaPalauta(luku);

    System.out.println(luku);
}

public static int kasvataKolmellaJaPalauta(int kasvatettavaLuku) {
    kasvatettavaLuku = kasvatettavaLuku + 3;

    return kasvatettavaLuku;
}

Huomasimme että metodissa olevat parametrit ovat eri muuttujia kuin metodin kutsujassa esitellyt muuttujat. Ainoastaan parametrin arvo kopioituu kutsujasta metodiin.

Asia ei kuitenkaan ole ihan näin yksinkertainen. Jos metodille annetaan parametrina ArrayList, käy niin että sama lista näkyy metodille ja kaikki metodin listalle tekemät muutokset tulevat kaikkialla voimaan.

public static void poistaAlussaOleva(ArrayList<Integer> lista) {
    lista.remove(0); // poistaa paikassa 0 olevan luvun
}
ArrayList<Integer> luvut = new ArrayList<Integer>();
luvut.add(4);
luvut.add(3);
luvut.add(7);
luvut.add(3);

System.out.println(luvut); // tulostuu [4,3,7,3]

poistaAlussaOleva(luvut);

System.out.println(luvut); // tulostuu [3,7,3]

Toisin kuin int-tyyppinen parametri, lista ei kopioidu vaan metodi käsittelee suoraan parametrina annettua listaa.

Tilannetta valaisee allaoleva kuva. Toisin kuin int-tyyppinen muuttuja, ArrayList ei sijaitsekaan samalla tapaa "lokerossa", vaan muuttujan nimi, eli mainin tapauksessa luvut onkin ainoastaan viite paikkaan missä ArrayList sijaitsee. Yksi tapa ajatella asiaa, on että ArrayList on "langan päässä", eli listan nimi luvut on lanka jonka toisesta päästä lista löytyy. Kun metodikutsun parametrina on ArrayList, käykin niin että metodille annetaan "lanka" jonka päässä on sama lista jonka metodin kutsuja näkee. Eli main:illa ja metodilla on kyllä molemmilla oma lanka, mutta langan päässä on sama lista ja kaikki muutokset mitä metodi tekee listaan tapahtuvat täsmälleen samaan listaan jota pääohjelma käyttää. Tästä viikosta 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 metodissa oleva nimi. Edellä listaa kutsutaan metodin sisällä nimellä lista, metodin kutsuja taas näkee saman listan luvut-nimisenä.

Miksi int-parametrista ainoastaan arvo kopioituu metodille mutta parametrin ollessa ArrayList metodi käsittelee suoraan listan sisältöä? Javassa ainoastaan alkeistietotyyppisten eli tyyppien int, double, char, boolean (ja muutamien muiden joita emme ole käsitelleet) arvot kopioidaan metodille. Muun tyyppisten parametrien tapauksessa metodille kopioidaan viite, ja metodista käsitellään viitteen takana olevaa parametria suoraan. Ei-alkeistyyppiset muuttujat -- eli viittaustyyppiset muuttujat ovat siis edellisen kuvan tapaan "langan päässä" -- metodille välitetään lanka parametriin, ja näin metodi käsittelee parametria suoraan.

Tehtävä 70: ArrayListien yhdistäminen

Tehtävä 71: Joukkoyhdistäminen

19. Ohjeita koodin kirjoittamiseen ja ongelmanratkaisuun

Yksi maailman johtavista ohjelmistonkehittäjistä, Kent Beck, on lausunut mm. seuraavasti:

Otamme viimeistään nyt ensimmäisiä askelia Kent Beckin viitoittamalla tiellä.

19.1 Oikein sisennetty ja "hengittävä" koodi

Tarkastellaan koodia joka ensin lisää listalle lukuja ja tulostaa listan sisällön. Tämän jälkeen listalta poistetaan kaikki tietyn luvun esiintymät ja tulostetaan lista uudelleen.

Kirjoitetaan koodi ensin huonosti ja jätetään se sisentämättä:

public static void main(String[] args) {
ArrayList<Integer> luvut = new ArrayList<Integer>();
luvut.add(4);
luvut.add(3);
luvut.add(7);
luvut.add(3);
System.out.println("luvut alussa:");

for (int luku : luvut) {
System.out.println(luku);
}

while (luvut.contains(Integer.valueOf(3))) {
luvut.remove(Integer.valueOf(3));
}

System.out.println("luvut poiston jälkeen:");

for (int luku : luvut) {
System.out.println(luku);
}
}

Vaikka sisentämätön koodi toimii, on sitä hyvin ikävä lukea. Sisennetään koodi oikein (NetBeansissa sisennyksen saa korjattua automaattisesti painamalla alt+shift+f), ja erotellaan loogiset kokonaisuudet rivinvaihdoin:

public static void main(String[] args) {
    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(4);
    luvut.add(3);
    luvut.add(7);
    luvut.add(3);

    System.out.println("luvut alussa:");

    // tässä tulostetaan luvut
    for (int luku : luvut) {
        System.out.println(luku);
    }

    // tarkastetaan onko listalla luku 3
    while (luvut.contains(Integer.valueOf(3))) {
        luvut.remove(Integer.valueOf(3));  // jos löytyi, niin poistetaan se
    }
    // tehdään tämä whilessä jotta saadaan kaikki kolmoset poistetua!

    System.out.println("luvut poiston jälkeen:");

    // tässä tulostetaan luvut
    for (int luku : luvut) {
        System.out.println(luku);
    }
}

Nyt koodissa alkaa olla jo järkeä. Esimerkiksi tulostus ja kolmosten poisto ovat omia loogisia kokonaisuuksia, joten ne on erotettu rivinvaihdolla. Koodissa on ilmavuutta ja koodin lukeminen alkaa olla miellyttävämpää.

Koodiin on vieläpä kirjoitettu kommentteja selventämään muutaman kohdan toimintaa.

19.2 Copy-pasten eliminointi metodeilla

Ohjelmoijan lähes pahin mahdollinen perisynti on copy-paste -koodi, eli samanlaisen koodinpätkän toistaminen koodissa useaan kertaan. Esimerkissämme listan tulostus tapahtuu kahteen kertaan. Tulostuksen hoitava koodi on syytä erottaa omaksi metodikseen ja kutsua uutta metodia pääohjelmasta:

public static void main(String[] args) {
    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(4);
    luvut.add(3);
    luvut.add(7);
    luvut.add(3);

    System.out.println("luvut alussa:");

    // tässä tulostetaan luvut
    tulosta(luvut);

    while (luvut.contains(Integer.valueOf(3))) {
      luvut.remove(Integer.valueOf(3));
    }

    System.out.println("luvut poiston jälkeen:");

    // tässä tulostetaan luvut
    tulosta(luvut);
}

public static void tulosta(ArrayList<Integer> luvut) {
    for (int luku : luvut) {
        System.out.println( luku );
    }
}

19.3 Erillisten tehtävien erottaminen omiksi, selkeästi nimetyiksi metodeiksi

Koodi alkaa olla jo selkeämpää. Selvästi erillinen kokonaisuus, eli listan tulostus on oma helposti ymmärrettävä metodinsa. Uuden metodin esittelyn myötä myös pääohjelman luettavuus on kasvanut. Huomaa että uusi metodi on nimetty mahdollisimman kuvaavasti, eli siten että metodin nimi kertoo mitä metodi tekee. Ohjelmaan kirjoitetut kommentit tässä tulostetaan luvut ovatkin tarpeettomia, joten poistetaan ne.

Ohjelmassa on vielä hiukan siistimisen varaa. Pääohjelma on vielä sikäli ikävä, että siistien metodikutsujen seassa on vielä suoraan listaa käsittelevä "epäesteettinen" koodinpätkä. Erotetaan tämäkin omaksi metodikseen:

public static void main(String[] args) {
    ArrayList<Integer> luvut = new ArrayList<Integer>();
    luvut.add(4);
    luvut.add(3);
    luvut.add(7);
    luvut.add(3);

    System.out.println("luvut alussa:");
    tulosta(luvut);

    poista(luvut, 3);

    System.out.println("luvut poiston jälkeen:");
    tulosta(luvut);
}

public static void tulosta(ArrayList<Integer> luvut) {
    for (int luku : luvut) {
        System.out.println( luku );
    }
}

public static void poista(ArrayList<Integer> luvut, int poistettava) {
    while (luvut.contains(Integer.valueOf(poistettava))) {
        luvut.remove(Integer.valueOf(poistettava));
    }
}

Loimme yllä loogiselle kokonaisuudelle -- tietyn luvun kaikkien esiintymien poistolle -- 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ää.

Kent Beck olisi varmaan tyytyväinen aikaansaannokseemme, koodi on helposti ymmärrettävää, helposti muokattavaa eikä sisällä copy-pastea.

Viikko 4

20. Olio-ohjelmointi

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.

20.1 Olio

Olemme käyttäneet jo monia Javan valmiita olioita. Esimerkiksi ArrayList:it ovat olioita. Jokainen yksittäinen lista koostuu yhteenkuuluvasta tiedosta, eli olion tilasta. Listaolioihin liittyy toiminnallisuutta, eli metodt joilla olion tilaa voidaan muuttaa. Esimerkiksi 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() );
}

Sekä maat-olio että kaupungit-olio elää 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.

Olioon liittyvää metodikutsua tehdessä (esimerkiksi maat.add("Suomi");) pisteen vasemmalle puolelle tulee sen olion nimi, jolle metodia kutsutaan, oikealle metodin nimi. 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 oliot kuten kaupungit eivät vaikuta metodin suoritukseen millään tavalla.

Olemme jo useasti käyttämään komentoa new. Esimerkiksi listan (ArrayList) ja lukijan (Scanner) luominen on tapahtunut new-komennolla. Syy on se, että kumpikin näistä on Luokkia, joista olio luodaan. Java-kielessä oliot luodaan aina new-komennolla muutamaa poikkeusta lukuunottamatta.

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 teksti = "tekstiä";       // lyhennysmerkintä merkkijono-olion luomiselle
String toinenTeksti = new String("lisää tekstiä");   

On myös tilanteita, missä Javan valmis kalusto kutsuu new:iä piilossa ohjelmoijalta.

20.2 Luokka

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 String-olioilla on samat metodit (substring, length, charAt, ...). ArrayList- ja String-olioiden metodit eivät ole samat sillä ne ovat eri tyyppisiä olioita.

Tietyn olioryhmän tyyppiä kutsutaan luokaksi. ArrayList on luokka, String on luokka, Scanner on luokka, jne... Oliot taas ovat luokasta tehtyjä ilmentymiä.

Luokan kaikilla olioilla on samat metodit, sekä samankaltainen tila. Esim. kaikkien ArrayList-olioiden tila koostuu listalle tallennetuista alkioista. String-olioiden tila taas koostuu merkkijonon muodostavista kirjaimista.

20.3 Luokka ja sen oliot

Luokka määrittelee minkälaisia luokan oliot ovat:

Luokka kuvaa siitä luotavien 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 ovat luokka, eli ne määrittelevät luokasta luotavien olioiden luonteen:

Yksittäiset oliot eli rintamamiestalot on tehty samojen piirustusten perusteella, eli ne ovat saman luokan ilmentymiä. 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":

Luokasta luodaan olio aina kutsumalla olion luovaa metodia eli konstruktoria komennon new avulla. Esimerkiksi Scanner-luokasta luodaan uusi ilmentymä eli olio kun kutsutaan new Scanner(..):

  Scanner lukija = new Scanner(System.in);

Konstruktorit saavat parametreja kuten muutkin metodit.

Tehtävä 72: Tilejä

20.4 Oman luokan määritteleminen - oliomuuttujat

Luokka määritellään jotain mielekästä kokonaisuutta varten. Usein "mielekäs kokonaisuus" kuvaa 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. Oletetaan että meillä on projektirunko jossa on tyhjä pääohjelma:

public class Main {

    public static void main(String[] args) {

    }

}
  

Luomme nyt projektiimme eli ohjelmaamme uuden luokan. Tämä tapahtuu valitsemalla NetBeansissa vasemmalta projects-kohdasta hiiren oikealla napilla new ja java class. Avautuvaan dialogiin annetaan luokalle nimi.

Kuten muuttujien ja metodien nimien, myös luokan nimen on aina oltava mahdollisimman kuvaava. Joskus ohjelmoinnin edetessä luokka elää ja muuttaa muotoaan. Tällaisissa tilanteissa luokan voi nimetä uudelleen ( ks. NetBeans ohje ).

Luodaan luokka nimeltä Henkilo. Luokasta muodostuu oma tiedostonsa Henkilo.java. Eli ohjelma koostuu nyt kahdesta tiedostosta, sillä pääohjelma on omassa tiedostossaan. Aluksi luokka on tyhjä:

public class Henkilo {

}

Luokan tulee määritellä 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;
}

Määrittelimme yllä 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ä. Olion sisälle piilottamista kutsutaan kapseloinniksi.

Luokan sisälle määriteltyjä muuttujia kutsutaan oliomuuttujiksi tai olion kentiksi tai olion attribuuteiksi. Rakkaalla lapsella on monta nimeä.

Eli olemme määritelleet rakennuspiirrustukset -- luokan -- henkilöoliolle. Kaikilla henkilöolioilla on muuttujat nimi ja ika. Henkilöiden "tila" koostuu niiden nimeen ja ikään asetetuista arvoista.

20.5 Oman luokan määritteleminen - konstruktori eli tilan alustus

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 joillekin olioiden muuttujille. Esim. henkilön synnytyksessä olisi kätevää pystyä antamaan nimi jo syntymähetkellä:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka");
    // ...
}

Tämä onnistuu määrittelemällä olion synnyttävä metodi, eli konstruktori. Seuraavassa on määritelty Henkilo-luokalle konstruktori, joka luo uuden Henkilo-olion. Konstruktorissa luotavan henkilön iäksi asetetaan 0 ja nimeksi parametrina tuleva merkkijono:

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimiAlussa) {
        this.ika = 0;
        this.nimi = nimiAlussa;
      }
}

Konstruktori on siis nimeltään sama kuin luokan nimi. Yllä luokka (class) on Henkilo, ja konstruktori public Henkilo(String nimiAlussa). 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 this.ika = 0. Tässä asetetaan arvo 0 juuri tämän olion, eli "this"-olion sisäiselle muuttujalle ika. Toinen komento this.nimi = nimiAlussa; taas asettaa juuri tämän olion sisäiselle muuttujalle nimi arvoksi parametrina annetun merkkijonon. Olion muuttujat ika ja nimi näkyvät konstruktorissa ja muuallakin olion sisällä automaattisesti. Niihin viitataan this-etuliitteellä. Koska niissä on private-määre, niin olion ulkopuolelle ne eivät näy.

Vielä yksi huomio: jos ohjelmoija ei tee luokalle konstruktoria, tekee Java automaattisesti luokalle oletuskonstruktorin. Oletuskonstruktori on konstruktori joka ei tee mitään. Eli jos konstruktoria ei jostain syystä tarvita, ei sellaista tarvitse ohjelmoida.

20.6 Oman luokan määritteleminen - metodit

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 oltava myös metodeja. Tehdään Henkilo-luokalle metodi jonka avulla olio tulostaa itsensä ruudulle:

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimiAlussa) {
        this.ika = 0;
        this.nimi = nimiAlussa;
    }

    public void tulostaHenkilo() {
        System.out.println(this.nimi + ", ikä " + this.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 tulostaHenkilo sisällä on yksi koodirivi joka käyttää hyvakseen oliomuuttujia nimi ja ika. Juuri tämän olion muuttujiin viitataan taas etuliitteellä this. 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 pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");
        Henkilo martin = new Henkilo("Martin");

        pekka.tulostaHenkilo();
        antti.tulostaHenkilo();
        martin.tulostaHenkilo();
    }
}

Tulostuu:

Pekka, ikä 0 vuotta
Antti, ikä 0 vuotta
Martin, ikä 0 vuotta

Sama screencastina:

Tehtävä 73: Tuote

20.7 lisää metodeja

Lisätään aiemmin rakentamallemme Henkilölle metodi, joka kasvattaa henkilön ikää vuodella:

public class Henkilo {
    // ...

    public void vanhene() {
      this.ika = this.ika + 1;
    }
}

Metodi siis kirjoitetaan tulostaHenkilo-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 pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");

        pekka.tulostaHenkilo();
        antti.tulostaHenkilo();

        System.out.println("");

        pekka.vanhene();
        pekka.vanhene();

        pekka.tulostaHenkilo();
        antti.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 (konstruktorissa suoritetaan mm. rivi this.ika = 0;). Olion pekka metodia vanhene kutsutaan kaksi kertaa. Kuten tulostus näyttää, tämä saa aikaan sen että Pekan ikä vanhenemisen jälkeen 2 vuotta. Kutsumalla metodia Pekkaa vastaavalle oliolle, toisen henkilöolion ikä ei muutu.

Jokaisella oliolla on siis oma sisäinen tilansa.

Aivan kuiten edellisellä viikolla käsittelemämme olioihin liittymättömät metodit, myös olioihin liittyvät metodit voivat palauttaa arvon. Lisätään Henkilölle metodi joka palauttaa henkilön iän:

public class Henkilo {
    // ...

    public int palautaIka() {
        return this.ika;
    }
}

Eli koska kyseessä olioon liittyvä metodi, ei määrittelyssä ole sanaa static. Olioiden arvon palauttavia metodeja käytetään kuten mitä tahansa arvon palauttavia metodeja:

public class Main {

    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");

        pekka.vanhene();
        pekka.vanhene();

        antti.vanhene();

        System.out.println( "Pekan ikä: "+pekka.palautaIka() );
        System.out.println( "Antin ikä: "+antti.palautaIka() );

        int yht = pekka.palautaIka() + antti.palautaIka();

        System.out.println( "Pekka ja Antti yhteensä "+yht+ " vuotta" );
    }
}

Ohjelman tulostus on seuraava:

Pekan ikä 2
Antin ikä 1

Pekka ja Antti yhteensä 3 vuotta

Tehtävä 74: Kertoja

Tehtävä 75: Vähenevä laskuri

Tehtävä 76: Ruokalista

20.8 Henkilo-luokka laajenee

Jatketaan taas Henkilo-luokan laajentamista. Luokan tämänhetkinen versio on seuraava:

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimiAlussa) {
        this.ika = 0;
        this.nimi = nimiAlussa;
    }

    public void tulostaHenkilo() {
        System.out.println(this.nimi + ", ikä " + this.ika + " vuotta");
    }

    public void vanhene() {
        this.ika = this.ika + 1;
    }
}

Tehdään henkilölle metodi, jonka avulla voidaan selvittää onko henkilö täysi-ikäinen. Metodi palauttaa totuusarvon -- joko true tai false:

public class Henkilo {
  // ...

  public boolean taysiIkainen(){
      if ( this.ika < 18 ) {
          return false;
      }

      return true;
    }

    /*
    huom. metodin voisi kirjoittaa lyhyemmin seuraavasti:

    public boolean taysiIkainen(){
        return this.ika >= 18;
    }
    */
}

Ja testataan:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka");
    Henkilo antti = new Henkilo("Antti");

    int i = 0;
    while ( i < 30 ) {
        pekka.vanhene();
        i++;
    }

    antti.vanhene();

    System.out.println("");

    if ( antti.taysiIkainen() ) {
        System.out.print("täysi-ikäinen: ");
        antti.tulostaHenkilo();
    } else {
        System.out.print("alaikäinen: ");
        antti.tulostaHenkilo();
    }

    if ( pekka.taysiIkainen() ) {
        System.out.print("täysi-ikäinen: ");
        pekka.tulostaHenkilo();
    } else {
        System.out.print("alaikäinen: ");
        pekka.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 this.nimi;
      }
}

Metodi getNimi palauttaa oliomuuttujan nimi kutsujalle. Metodin nimi on hieman erikoinen. Javassa on usein tapana nimetä oliomuuttujan 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 pekka = new Henkilo("Pekka");
    Henkilo antti = new Henkilo("Antti");

    int i = 0;
    while ( i < 30 ) {
        pekka.vanhene();
        i++;
    }

    antti.vanhene();

    System.out.println("");

    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 " );
    }
}

Tulostus alkaa olla jo aika siisti:

Antti on alaikäinen
Pekka on täysi-ikäinen

20.9 toString

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 nimi on Javassa aina toString. Määritellään seuraavassa henkilölle tämä metodi:

public class Henkilo {
    // ...

    public String toString() {
        return this.nimi + ", ikä " + this.ika + " vuotta";
    }
}

Metodi toString toimii kuten tulostaHenkilo, mutta se ei itse tulosta mitään vaan palauttaa merkkijonoesityksen, jotta metodin kutsuja voi halutessaan suorittaa tulostamisen.

Metodia käytetään hieman yllättävällä tavalla:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka");
    Henkilo antti = new Henkilo("Antti");

    int i = 0;
    while ( i < 30 ) {
        pekka.vanhene();
        i++;
    }

    antti.vanhene();

    System.out.println( antti ); // sama kun System.out.println( antti.toString() );
    System.out.println( pekka ); // sama kun System.out.println( pekka.toString() );
}

Periaatteena on, että System.out.println-metodi pyytää olion merkkijonoesityksen ja tulostaa sen. Merkkijonoesityksen palauttavan toString-metodin kutsua ei tarvitse kirjoittaa itse, sillä Java lisää sen automaattisesti. Ohjelmoijan kirjoittaessa:

System.out.println( antti );

Java täydentää suorituksen aikana kutsun muotoon:

System.out.println( antti.toString() ); 

Käy niin, että oliolta pyydetään sen merkkijonoesitys. Olion palauttama merkkijonoesitys tulostetaan normaaliin tapaan System.out.println-komennolla.

Voimme nyt poistaa turhaksi käyneen tulostaOlio-metodin.

Olioscreencastin toinen osa:

Tehtävä 77: Lyyra-kortti

20.10 Lisää metodeja

Jatketaan taas Henkilo-luokan parissa. 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) {
        this.ika = 0;
        this.paino = 0;
        this.pituus = 0;
        this.nimi = nimiAlussa;
    }

    public void setPituus(int uusiPituus) {
        this.pituus = uusiPituus;
    }

    public void setPaino(int uusiPaino) {
        this.paino = uusiPaino;
    }

    public double painoIndeksi(){
        double pituusPerSata = this.pituus / 100.0;
        return this.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. Arvon asettavia metodeja kutsutaan usein "settereiksi". Seuraavassa käytämme uusia metodeja:

public static void main(String[] args) {
    Henkilo matti = new Henkilo("Matti");
    Henkilo juhana = new Henkilo("Juhana");

    matti.setPituus(180);
    matti.setPaino(86);

    juhana.setPituus(175);
    juhana.setPaino(64);

    System.out.println( matti.getNimi() + ", painoindeksisi on " + matti.painoIndeksi() );
    System.out.println( juhana.getNimi() + ", painoindeksisi on " + juhana.painoIndeksi() );
}

Tulostus:

Matti, painoindeksisi on 26.54320987654321
Juhana, painoindeksisi on 20.897959183673468

20.11 Parametrilla ja oliomuuttujalla sama nimi!

Edellä metodissa setPituus asetetaan oliomuuttujaan pituus parametrin uusiPituus arvo:

public void setPituus(int uusiPituus) {
    this.pituus = uusiPituus;
}

Parametrin nimi voisi olla myös sama kuin oliomuuttujan nimi, eli seuraava toimisi myös:

public void setPituus(int pituus) {
    this.pituus = pituus;
}

Nyt metodissa pituus tarkottaa nimenomaan pituus-nimistä parametria ja this.pituus saman nimistä oliomuuttujaa. Esim. seuraava ei toimisi sillä koodi ei viittaa ollenkaan oliomuuttujaan pituus:

public void setPituus(int pituus) {
    // EI TOIMI ÄLÄ TEE NÄIN!!!
    pituus = pituus;
}

20.12 Liukuluvun "siisti" tulostaminen

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( matti.getNimi() + ", painoindeksisi on " + String.format( "%.2f", matti.painoIndeksi() ) );
System.out.println( juhana.getNimi() + ", painoindeksisi on " + String.format( "%.2f", juhana.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 numero 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.

Tehtävä 78: Kello laskurin avulla

Tärkeitä kommentteja liittyen olioiden käyttöön. Lue nämä ehdottomasti.

Olio-ohjelmoinnissa on kyse pitkälti käsitteiden eristämisestä omiksi kokonaisuuksikseen tai toisin ajatellen abstraktioiden muodostamisesta. Voisi ajatella, että on turha luoda oliota jonka sisällä on ainoastaan luku, eli että saman voisi tehdä suoraan int-muuttujilla. Asia ei kuitenkaan ole näin. Jos kello koostuu pelkästään kolmesta int-muuttujasta joita kasvatellaan, muuttuu ohjelma lukijan kannalta epäselvemmäksi, koodista on vaikea "nähdä" mistä on kysymys. Aiemmin materiaalissa mainitsimme jo kokeneen ja kuuluisan ohjelmoijan Kent Beckin neuvon "Any fool can write code that a computer can understand. Good programmers write code that humans can understand", eli koska viisari on oikeastaan oma selkeä käsitteensä, on siitä ohjelman ymmärrettävyyden parantamiseksi hyvä tehdä oma luokka, eli YlhaaltaRajoitettuLaskuri.

Käsitteen erottaminen omaksi luokaksi on monellakin tapaa hyvä idea. Ensinnäkin tiettyjä yksityiskohtia (esim. laskurin pyörähtäminen) saadaan piilotettua luokan sisään (eli abstrahoitua). Sen sijaan että kirjoitetaan if-lause ja sijoitusoperaatio, riittää, että laskurin käyttäjä kutsuu selkeästi nimettyä metodia seuraava(). Aikaansaatu laskuri sopii kellon lisäksi ehkä muidenkin ohjelmien rakennuspalikaksi, eli selkeästä käsitteestä tehty luokka voi olla monikäyttöinen. Suuri etu saavutetaan myös sillä, että koska laskurin toteutuksen yksityiskohdat eivät näy laskurin käyttäjille, voidaan yksityiskohtia tarvittaessa muuttaa.

Totesimme että kello sisältää kolme viisaria, eli koostuu kolmesta käsitteestä. Oikeastaan kello on itsekin käsite ja teemme ensi viikolla luokan Kello, jotta voimme luoda selkeitä Kello-olioita. Kello tulee siis olemaan olio jonka toiminta perustuu "yksinkertaisimpiin" olioihin eli viisareihin. Tämä on juuri olio-ohjelmoinnin suuri idea: ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista.

Nyt otamme varovaisia ensiaskelia oliomaailmassa. Kurssin lopussa oliot alkavat kuitenkin olla jo selkärangassa ja nyt ehkä käsittämättömältä tuntuva lausahdus, ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista alkaa tuntua meistä ehkä järkeenkäyvältä ja itsestäänselvältä.

20.13 Oman metodin kutsu

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 olion omaa metodia painoIndeksi:

public String toString() {
    return this.nimi + ", ikä " + this.ika + " vuotta, painoindeksini on " + this.painoIndeksi();
}

Eli kun olio kutsuu omaa metodiaan, riittää etuliite this ja pelkkä metodin nimi. Vaihtoehtoinen tapa on tehdä oman metodin kutsu muodossa painoIndeksi() jolloin ei korosteta, että kutsutaan "olion itsensä" metodia painoindeksi:

public String toString() {
    return this.nimi + ", ikä " + this.ika + " vuotta, painoindeksini on " + painoIndeksi();
}

Olioscreencastin kolmas osa:

Nyt on aika harjoitella lisää yksinkertaisten olioiden tekemistä käytännössä:

Tehtävä 79: Lukutilasto

21. Satunnaisuus

Ohjelmoidessa satunnaisuutta tarvitaan silloin tällöin. Satunnaisuutta, esimerkiksi sään arvaamattomuutta tai tietokoneen tietokonepeleissä tekemien yllättäviä siirtoja, voidaan useimmiten simuloida tietokoneelta pyydettävien satunnaislukujen avulla. Satunnaislukujen pyytäminen onnistuu käyttäen Javasta löytyvää Random-luokkaa. Random-luokkaa voi käyttää seuraavalla tavalla.

import java.util.Random;

    public class Arvontaa {
        public static void main(String[] args) {
        Random arpoja = new Random(); // luodaan arpoja apuväline
        int i = 0;

        while (i < 10) {
            // Arvotaan ja tulostetaan jokaisella kierroksella satunnainen luku
            System.out.println(arpoja.nextInt(10)); 
            i++;
        }
    }
}

Yllä olevassa koodissa luodaan ensin Random-luokan ilmentymä käyttäen avainsanaa new -- samoin kuin muitakin olioita luodessa. Random-olio tarjoaa metodin nextInt, jolle annetaan parametrina kokonaisluku. Metodi palauttaa satunnaisen kokonaisluvun väliltä 0..(annettu kokonaisluku - 1).

Ohjelman tulostus voisi olla vaikka seuraavanlainen:

2
2
4
3
4
5
6
0
7
8

Tarvitsemme liukulukuja esimerkiksi todennäköisyyslaskennan yhteydessä. Tietokoneella todennäköisyyksiä simuloidaan yleensä väliltä [0..1] olevilla luvuilla. Random-oliolta saa satunnaisia liukulukuja metodilla nextDouble. Tarkastellaan seuraavia säämahdollisuuksia:

Luodaan edellä olevista arvioista sääennustaja.

import java.util.ArrayList;
import java.util.Random;

public class SaaEnnustaja {
    private Random random;

    public SaaEnnustaja() {
        this.random = new Random();
    } 

    public String ennustaSaa() {
        double todennakoisyys = this.random.nextDouble();

        if (todennakoisyys <= 0.1) {
            return "Sataa räntää";
        } else if (todennakoisyys <= 0.4) { // 0.1 + 0.3
            return "Sataa lunta";
        } else { // loput, 1.0 - 0.4 = 0.6
            return "Aurinko paistaa";
        } 
    }

    public int ennustaLampotila() {
        return (int) ( 4 * this.random.nextGaussian() - 3 ); 
    }
}

Metodi ennustaLampotila on monella tapaa mielenkiintoinen. Metodin sisällä tehtävä kutsu this.random.nextGaussian() on tavallinen metodikutsu, jonka kaltaisia olemme nähneet aikaisemminkin. Kiinnostavaa tässä Random-luokan ilmentymän tarjoamassa metodissa on se, että metodin palauttama luku on normaalijakautunut (jos et koe mielenkiintoa satunnaisuuden eri lajeihin se ei haittaa!).

public int ennustaLampotila() {
    return (int) ( 4 * this.random.nextGaussian() - 3 ); 
}

Edellisessä lausekkeessa kiinnostava on myös 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.

Luodaan vielä pääohjelma josta luokkaa SaaEnnustaja käytetään.

public class Ohjelma {

    public static void main(String[] args) {
        SaaEnnustaja ennustaja = new SaaEnnustaja();

        // käytetään listaa apuvälineenä
        ArrayList<String> paivat = new ArrayList<String>();
        Collections.addAll(paivat, "Ma", "Ti", "Ke", "To", "Pe", "La", "Su");

        System.out.println("Seuraavan viikon sääennuste:");
        for(String paiva : paivat) {
            String saaEnnuste = ennustaja.ennustaSaa();
            int lampotilaEnnuste = ennustaja.ennustaLampotila(); 

            System.out.println(paiva + ": " + saaEnnuste + " " + lampotilaEnnuste + " 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

Tehtävä 80: Nopan heittäminen

Tehtävä 81: Salasanan arpoja

Tehtävä 82: Lottoarvonta

Tehtävä 83: Viikon huipennus: hirsipuun logiikka

Viikko 5

22. Lisää luokista ja olioista

22.1 Useita konstruktoreja

Palataan jälleen henkilöitä käsittelevän luokan pariin. Luokka Henkilo näyttää tällä hetkellä seuraavalta:

public class Henkilo {

    private String nimi;
    private int ika;
    private int pituus;
    private int paino;

    public Henkilo(String nimi) {
        this.nimi = nimi;
        this.ika = 0;
        this.paino = 0;
        this.pituus = 0;
    }

    public void tulostaHenkilo() {
        System.out.println(this.nimi + " olen " + this.ika + " vuotta vanha");
    }

    public void vanhene() {
        this.ika++;
    }

    public boolean taysiIkainen(){
        if ( this.ika < 18 ) {
            return false;
        }

        return true;
    }

    public double painoindeksi(){
        double pituusMetreina = this.pituus/100.0;

        return this.paino / (pituusMetreina*pituusMetreina);
    }

    public String toString(){
        return this.nimi + " olen " + this.ika + " vuotta vanha, painoindeksini on " + this.painoindeksi();
    }

    public void setPituus(int pituus){
        this.pituus = pituus;
    }

    public int getPituus(){
        return this.pituus;
    }

    public int getPaino() {
        return this.paino;
    }

    public void setPaino(int paino) {
        this.paino = paino;
    }

    public String getNimi(){
        return this.nimi;
    }
}

Kaikki henkilöoliot ovat luontihetkellä 0-vuotiaita, sillä konstruktori asettaa uuden henkilön ika-oliomuuttujan arvoksi 0:

public Henkilo(String nimi) {
    this.nimi = nimi;
    this.ika = 0;
    this.paino = 0;
    this.pituus = 0;
}

Haluaisimme myös luoda henkilöitä siten, että nimen lisäksi konstruktorin parametrina annettaisiin ikä. Tämä onnistuu helposti, sillä konstruktoreja voi olla useita. Tehdään vaihtoehtoinen konstruktori. Vanhaa konstruktoria ei tarvise poistaa.

public Henkilo(String nimi) {
    this.nimi = nimi;
    this.ika = 0;
    this.paino = 0;
    this.pituus = 0;
}

public Henkilo(String nimi, int ika) {
    this.nimi = nimi;
    this.ika = ika;
    this.paino = 0;
    this.pituus = 0;
}

Nyt olioiden luonti onnistuu kahdella vaihtoehtoisella tavalla:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka", 24);
    Henkilo esko = new Henkilo("Esko");

    System.out.println( pekka );
    System.out.println( esko );
}
Pekka, ikä 24 vuotta
Esko, ikä 0 vuotta

Tekniikkaa jossa luokalla on kaksi konstruktoria, kutsutaan konstruktorin kuormittamiseksi. Luokalla voi siis olla useita konstruktoreja, jotka poikkeavat toisistaanparametriensa määrältä tai tyypeiltä. Ei kuitenkaan ole mahdollista tehdä kahta erilaista konstruktoria joilla on täysin saman tyyppiset parametrit. Emme siis voi edellisten lisäksi lisätä konstruktoria public Henkilo(String nimi, int paino) sillä Javan on mahdoton erottaa tätä kaksiparametrisesta konstruktorissa, jossa luku tarkoittaa ikää.

22.2 Oman konstruktorin kutsuminen

Mutta hetkinen, luvussa 21 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 ylempi konstruktori voisi "kutsua" alempaa konstruktoria? Tämä onnistuu, sillä konstruktorin sisältä voi kutsua toista konstruktoria juuri tähän olioon liittyvän 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;
    this.paino = 0;
    this.pituus = 0;
}

Oman konstruktorin kutsu this(nimi, 0); saattaa vaikuttaa erikoiselta. Asiaa voi vaikka ajatella siten, että kutsun kohdalle tulee "copy-pastena" automaattisesti alemman konstruktorin koodi, siten että ika parametrin arvoksi tulee 0.

22.3 Metodin kuormittaminen

Konstruktorien tapaan myös metodeja voi kuormittaa, eli samannimisestä metodista voi olla useita versioita. Jälleen eri versioiden parametrien tyyppien on oltava erilaiset. Tehdään vanhene-metodista toinen versio, joka mahdollistaa henkilön vanhentamisen parametrina olevalla vuosimäärällä:

public void vanhene() {
    this.ika = this.ika + 1;
}

public void vanhene(int vuodet) {
    this.ika = this.ika + vuodet;
}

Seuraavassa "Pekka" syntyy 24-vuotiaana, vanhenee ensin vuoden ja sitten 10 vuotta:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka", 24);

    System.out.println( pekka );
    pekka.vanhene();
    System.out.println( pekka );
    pekka.vanhene(10);
    System.out.println( pekka );
}

Tulostuu:

Pekka, ikä 24 vuotta
Pekka, ikä 25 vuotta
Pekka, ikä 35 vuotta

Henkilöllä on siis vanhene-nimisiä metodeja 2 kappaletta. Se kumpi metodeista valitaan suoritettavaksi, riippuu metodikutsussa käytettyjen parametrien määrästä. Metodin vanhene voi myös toteuttaa metodin vanhene(int vuodet) avulla:

public void vanhene() {
    this.vanhene(1);
}

public void vanhene(int vuodet) {
    this.ika = this.ika + vuodet;
}

Tehtävä 84: Kuormitettu laskuri

22.4 Olio on langan päässä

Luvussa 20 mainittiin, että ArrayList 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 pekka = new Henkilo("Pekka", 24);

    System.out.println( pekka );
}

Kun suoritamme lauseen Henkilo pekka = new Henkilo("Pekka", 24); syntyy olio. Olioon päästään käsiksi muuttujan pekka avulla. Teknisesti ottaen olio ei ole muuttujan pekka "sisällä" (eli lokerossa pekka) vaan pekka viittaa syntyneeseen olioon. Toisin sanonen olio on pekka-nimisestä muuttujasta lähtevän "langan päässä". Kuvana asiaa voisi havainnollistaa seuraavasti:

Lisätään ohjelmaan Henkilo-tyyppinen muuttuja henkilo ja annetaan sille alkuarvoksi pekka. Mitä nyt tapahtuu?

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka", 24);

    System.out.println( pekka );

    Henkilo henkilo = pekka;
    henkilo.vanhene(25);

    System.out.println( pekka );
}

Tulostuu:

Pekka, ikä 24 vuotta
Pekka, ikä 49 vuotta

Eli Pekka on alussa 24-vuotias, henkilo-muuttujaan liittyvän langan päässä olevaa Henkilö-oliota vanhennetaan 25:llä vuodella ja sen seurauksena Pekka vanhenee! Mistä on kysymys?

Komento Henkilo henkilo = pekka; saa aikaan sen, että henkilo rupeaa viittaamaan samaan olioon kuin mihin pekka viittaa. Eli ei synnykään kopiota oliosta, vaan molemmissa muuttujissa on langan päässä sama olio. Komennossa Henkilo henkilo = pekka; syntyy kopio langasta. Kuvana (Huom: kuvassa p ja h tarkottavat pääohjelman muuttujia pekka ja henkilo. Kuvien muuttujanimiä on lyhennelty myös muutamassa seuraavassa kuvassa.):

Esimerkissä "vieras henkilö henkilo ryöstää Pekan identiteetin". Seuraavassa esimerkkiä on jatkettu siten, että luodaan uusi olio ja pekka alkaa viittaamaan uuteen olioon:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka", 24);

    System.out.println( pekka );

    Henkilo henkilo = pekka;
    henkilo.vanhene(25);

    System.out.println( pekka );

    pekka = new Henkilo("Pekka Mikkola", 24);
    System.out.println( pekka );
}

Tulostuu:

Pekka, ikä 24 vuotta
Pekka, ikä 49 vuotta
Pekka Mikkola, ikä 24 vuotta

Muuttuja pekka viittaa siis ensin yhteen olioon, mutta rupeaa sitten viittaamaan toiseen olion. Seuraavassa kuva tilanteesta viimeisen koodirivin jälkeen:

Jatketaan vielä esimerkkiä laittamalla henkilo viittaamaan "ei mihinkään", eli null:iin:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka", 24);

    System.out.println( pekka );

    Henkilo henkilo = pekka;
    henkilo.vanhene(25);

    System.out.println( pekka );

    pekka = new Henkilo("Pekka Mikkola", 24);
    System.out.println( pekka );

    henkilo = null;
    System.out.println( henkilo );
}

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, jäisivät ne 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, esimerkiksi metodia painoIndeksi:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka", 24);

    System.out.println( pekka );

    Henkilo henkilo = null;
    System.out.println( henkilo.painoIndeksi() );
}

Tulos:

Pekka, 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 NullPointerException. Voimme luvata, että tulet näkemään sen vielä uudelleen. NullPointerException on poikkeustila, joka syntyy kun null-arvoisen olion metodeja yritetään kutsua.

22.5 Metodin parametrina olio

Olemme nähneet että metodien parametrina voi olla esim. int, double, String tai ArrayList. ArrayListit 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 konstruktorille annetaan parametriksi pienin painoindeksi, jolla 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 parametrina annettu henkilö hyväksytään, false jos ei.

public class PainonvartijaYhdistys {
    // ...

    public boolean hyvaksytaanJaseneksi(Henkilo henkilo) {
        if ( henkilo.painoIndeksi() < this.alinPainoindeksi ) {
            return false;
        }
        return true;
        }
}

Painonvartijayhdistys-olion metodi hyvaksytaanJaseneksi saa siis parametriksi Henkilo-olion (tarkemmin sanottuna "langan" henkilöön) ja kutsuu parametrina saamansa henkilön metodia painoIndeksi.

Seuraavassa testipääohjelma jossa painonvartijayhdistyksen metodille annetaan ensin parametriksi henkilöolio matti ja sen jälkeen henkilöolio juhana:

public static void main(String[] args) {
    Henkilo matti = new Henkilo("Matti");
    matti.setPaino(86);
    matti.setPituus(180);

    Henkilo juhana = new Henkilo("Juhana");
    juhana.setPaino(64);
    juhana.setPituus(172);

    PainonvartijaYhdistys kumpulanPaino = new PainonvartijaYhdistys(25);

    if ( kumpulanPaino.hyvaksytaanJaseneksi(matti) ) {
        System.out.println( matti.getNimi() + " pääsee jäseneksi");
    } else {
        System.out.println( matti.getNimi() + " ei pääse jäseneksi");
    }

    if ( kumpulanPaino.hyvaksytaanJaseneksi(juhana) ) {
        System.out.println( juhana.getNimi() + " pääsee jäseneksi");
    } else {
        System.out.println( juhana.getNimi() + " ei pääse jäseneksi");
    }
}

Ohjelma tulostaa:

Matti pääsee jäseneksi
Juhana ei pääse jäseneksi

Muutama NetBeans-vihje

  • Kaikki NetBeans-vihjeet löytyvät täältä
  • Konstruktorien, getterien ja setterien automaattinen generointi

    Mene luokan koodilohkon sisäpuolelle mutta kaikkien metodien ulkopuolelle ja paina yhtä aikaa ctrl ja välilyönti. Jos luokallasi on esim. oliomuuttuja saldo, tarjoaa NetBeans mahdollisuuden generoida oliomuuttujalle getteri- ja setterimetodit sekä konstruktorin joka asettaa oliomuuttujalle alkuarvon. HUOM: laitoksen koneilla tämä saadaan aikaan painamalla yhtä aikaa ctrl, alt ja välilyönti.

Tehtävä 85: Kasvatuslaitos

Tehtävä 86: Lyyra-kortti ja Kassapääte

22.6 Metodin parametrina toinen samantyyppinen olio

Jatkamme edelleen luokan Henkilo parissa. Kuten muistamme, henkilöt tietävät ikänsä:

public class Henkilo {

    private String nimi;
    private int ika;
    private int pituus;
    private int paino;

    // ...
}

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 pekka = new Henkilo("Pekka");
Henkilo juhana = new Henkilo("Juhana") 

if ( pekka.getIka() > juhana.getIka() ) {
    System.out.println( pekka.getNimi() + " on vanhempi kuin " + juhana.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 pekka = new Henkilo("Pekka", 24);
    Henkilo antti = new Henkilo("Antti", 22);

    if ( pekka.vanhempiKuin(antti) ) {  //  sama kun pekka.vanhempiKuin(antti)==true
        System.out.println( pekka.getNimi() + " on vanhempi kuin " + antti.getNimi() );
    } else {
        System.out.println( pekka.getNimi() + " ei ole vanhempi kuin " + antti.getNimi() );
    }
}

Tässä siis kysytään Pekalta onko hän Anttia vanhempi, Pekka vastaa true jos on ja false muuten. Käytännössä kutsutaan "Pekkaa" vastaavan olion johon pekka viittaa metodia vanhempiKuin, jolle annetaan parametriksi "Anttia" vastaavan olion viite antti.

Ohjelman tulostaa:

Pekka on vanhempi kuin Antti

Metodi saa 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 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 osoittavia sulkeita ei kirjoiteta.

22.7 Päiväys oliona

Toinen esimerkki samasta teemasta. Tehdään luokka, jonka avulla voidaan esittää päiväyksiä.

Olion sisällä päiväys esitetään kolmella oliomuuttujalla. 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 paiva;
    private int kuukausi;
    private int vuosi;

    public Paivays(int paiva, int kuukausi, int vuosi) {
        this.paiva = paiva;
        this.kuukausi = kuukausi;
        this.vuosi = vuosi;
    }

    public String toString() {
        return this.paiva + "." + this.kuukausi + "." + this.vuosi;
    }

    public boolean aiemmin(Paivays verrattava) {
        // ensin verrataan vuosia
        if ( this.vuosi < verrattava.vuosi ) {
            return true;
        }

        // jos vuodet ovat samat, verrataan kuukausia
        if ( this.vuosi == verrattava.vuosi && this.kuukausi < verrattava.kuukausi ) {
            return true;
        }

        // vuodet ja kuukaudet samoja, verrataan päivää
        if ( this.vuosi == verrattava.vuosi && this.kuukausi == verrattava.kuukausi &&
            this.paiva < verrattava.paiva ) {
            return true;
        }

        return false;
    }
}

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

Tehtävä 87: Asuntovertailu

22.8 Olioita listalla

Olemme käyttäneet ArrayList:ejä jo monessa esimerkissä ja tehtävässä. ArrayList-olioon pystyy lisäämään esimerkiksi merkkijonoja ja listalla olevien merkkijonojen läpikäynti, etsiminen, poistaminen, järjestäminen ym. ovat vaivattomia toimenpiteitä.

ArrayList:eihin voidaan laittaa minkä tahansa tyyppisiä oliota. Luodaan seuraavassa henkilö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 ) {
        hlo.vanhene( 30 );
    }

    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

Tehtävä 88: Opiskelija-luokka

22.9 Olion sisällä olio

Olioiden sisällä voi olla olioita, ei pelkästään merkkijonoja vaan myös itse määriteltyjä oliota. Jatketaan taas Henkilo-luokan parissa ja lisätään henkilölle syntymäpäivä. Syntymäpäivä on luonnollista esittää aiemmin 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 paiva, int kuukausi, int vuosi) {
    this.nimi = nimi;
    this.paino = 0;
    this.pituus = 0;
    this.syntymaPaiva = new Paivays(paiva, kuukausi, vuosi);
}

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 this.nimi + ", syntynyt " + this.syntymaPaiva;
}

Ja kokeillaan miten uusittu Henkilö-luokka toimii:

public static void main(String[] args) {
    Henkilo martin = new Henkilo("Martin", 24, 4, 1983);

    Henkilo juhana = new Henkilo("Juhana", 17, 9, 1985);

    System.out.println( martin );
    System.out.println( juhana );
  }

Tulostuu:

Martin, syntynyt 24.4.1983
Juhana, syntynyt 17.9.1985

Luvussa 24.4 todettiin, että oliot ovat "langan päässä". Kertaa nyt luku 24.4.

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öllä 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 paiva = Calendar.getInstance().get(Calendar.DATE);
int kuukausi = Calendar.getInstance().get(Calendar.MONTH) + 1; // tammikuun numero 0 joten  lisätään 1
int vuosi = Calendar.getInstance().get(Calendar.YEAR);
System.out.println("tänään on " + paiva + "." + kuukausi + "." + vuosi );

Kun ikä poistetaan, täytyy vanhempiKuin-metodi muuttaa toimimaan syntymäpäiviä vertaamalla. Teemme muutoksen harjoitustehtävänä.

Tehtävä 89: Kellosta olio

22.10 Olion sisällä listallinen olioita

Laajennetaan PainonvartijaYhdistys-oliota siten, että yhdistys tallettaa kaikki jäsenensä ArrayList-olioon. Listalle tulee siis Henkilo-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;
        this.jasenet = new ArrayList<Henkilo>();
    }

//..
} 

Tehdään metodi jolla henkilö liitetään yhdistykseen. Metodi ei liitä yhdistykseen kuin tarpeeksi suuren painoindeksin omaavat henkilöt. Tehdään myös toString jossa tulostetaan jäsenten nimet:

public class PainonvartijaYhdistys {
  // ...

    public boolean hyvaksytaanJaseneksi(Henkilo henkilo) {
        if ( henkilo.painoIndeksi() < this.alinPainoindeksi ) {
            return false;
        }

        return true;
    }

    public void lisaaJaseneksi(Henkilo henkilo) {
        if ( !hyvaksytaanJaseneksi(henkilo) ) { // sama kuin hyvaksytaanJaseneksi(henkilo) == false
            return;
        }

        this.jasenet.add(henkilo);
    }

    public String toString() {
        String jasenetMerkkijonona = "";

        for ( Henkilo jasen : this.jasenet ) {
            jasenetMerkkijonona += "  " + jasen.getNimi() + "\n";
        }

        return "Painonvartijayhdistys " + this.nimi + " jäsenet: \n" + jasenetMerkkijonona;
    }
}

Metodi lisaaJaseneksi käyttää aiemmin tehtyä metodia hyvaksytaanJaseneksi.

Kokeillaan laajentunutta painonvartijayhdistystä:

public static void main(String[] args) {
    PainonvartijaYhdistys painonVartija = new PainonvartijaYhdistys("Kumpulan paino", 25);

    Henkilo matti = new Henkilo("Matti");
    matti.setPaino(86);
    matti.setPituus(180);
    painonVartija.lisaaJaseneksi(matti);

    Henkilo juhana = new Henkilo("Juhana");
    juhana.setPaino(64);
    juhana.setPituus(172);
    painonVartija.lisaaJaseneksi(juhana);

    Henkilo harri = new Henkilo("Harri");
    harri.setPaino(104);
    harri.setPituus(182);
    painonVartija.lisaaJaseneksi(harri);

    Henkilo petri = new Henkilo("Petri");
    petri.setPaino(112);
    petri.setPituus(173);
    painonVartija.lisaaJaseneksi(petri);

    System.out.println( painonVartija );
}

Tulostuksesta huomaamme, että Juhanaa ei kelpuutettu jäseneksi:

Painonvartijayhdistys Kumpulan paino jäsenet: 
Matti
Harri
Petri

Tehtävä 90: Joukkueet ja pelaajat

22.11 Metodi palauttaa olion

Olemme nähneet metodeja jotka palauttavat totuusarvoja, lukuja, listoja ja merkkijonoja. On helppoa arvata, että metodi voi palauttaa minkä tahansa tyyppisen olion. Tehdään painovartijayhdistykselle metodi, jolla saadaan tietoon yhdistyksen suurimman painoindeksin omaava henkilö.

public class PainonvartijaYhdistys {
    // ...

    public Henkilo suurinPainoindeksinen() {
        // jos jasenlista on tyhjä, palautetaan null-viite
        if ( this.jasenet.isEmpty() ) {
            return null;
        }

        Henkilo painavinTahanAsti = this.jasenet.get(0);

        for ( Henkilo henkilo : this.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

22.12 Metodi palauttaa luomansa olion

Edellisessä esimerkissä metodi palautti yhden painonVartija-olion sisältämistä Henkilo-olioista. On myös mahdollista, että metodi palauttaa kokonaan uuden olion. Seuraavassa yksinkertainen laskuri, jolla on metodi kloonaa, jonka avulla laskurista voidaan tehdä klooni, eli uusi laskurio-olio, jolla on luomishetkellä sama arvo kuin kloonattavalla laskurilla:

public Laskuri {
    private int arvo;

    public Laskuri(){
        this(0);
    }

    public Laskuri(int alkuarvo){
        this.arvo = alkuarvo;
    }

    public void kasvata(){
        this.arvo++;
    }

    public String toString(){
        return "arvo: "+arvo;
    }

    public Laskuri kloonaa(){
        // luodaan uusi laskuriolio, joka saa alkuarvokseen kloonattavan laskurin arvon
        Laskuri klooni = new Laskuri(this.arvo);

        // palautetaan klooni kutsujalle
        return klooni;
    }
}

Seuraavassa käyttöesimerkki:

Laskuri laskuri = new Laskuri();
laskuri.kasvata();
laskuri.kasvata();

System.out.println(laskuri);         // tulostuu 2

Laskuri klooni = laskuri.kloonaa();  

System.out.println(laskuri);         // tulostuu 2
System.out.println(klooni);          // tulostuu 2

laskuri.kasvata();
laskuri.kasvata();
laskuri.kasvata();
laskuri.kasvata();

System.out.println(laskuri);         // tulostuu 6
System.out.println(klooni);          // tulostuu 2

klooni.kasvata();

System.out.println(laskuri);         // tulostuu 6
System.out.println(klooni);          // tulostuu 3

Kloonattavan ja kloonin arvo on siis kloonauksen tapahduttua sama. Kyseessä on kuitenkin kaksi erillistä olioa, eli jatkossa kun toista laskureista kasvatetaan, ei kasvatus vaikuta toisen arvoon millään tavalla.

Tehtävä 91: Päiväys

Tehtävä 92: Päivämäärien erotus

Tehtävä 93: Luokan Henkilö muokkaaminen

Viikko 6

23. Nopea kertaus

Aloitetaan viikko muutamalla viime viikon tärkeimpiä teemoja kertaavalla tehtävällä. Kertaa tarvittaessa luku 24.10 ennen tehtävää 94 ja luvut 24.6 ja 24.12 ennen tehtävää 95.

MUISTUTUS kun lisäät ohjelmaasi ArrayList:in, Scanner:in tai Random:in ei Java tunnista luokkaa ellet "importoi" sitä lisäämällä ohjelmatiedoston alkuun:

import java.util.ArrayList;    // importoi ArrayListin
import java.util.*;            // importoi kaikki java.util:sissa olevat työkalut, mm. ArrayListin, Scannerin ja Randomin
  

Tehtävä 94: Puhelinmuistio

Tehtävä 95: Raha

23.1 Merkkijonot ovat immutaabeleja

Javan String-oliot ovat Raha-luokan olioiden tyyliin muuttumattomia, eli immutaabelena. Jos esim. merkkijonon perään katenoidaan eli liitetään +-operaatiolla uusi merkkijono, ei alkuperäistä merkkijonoa pidennetä vaan syntyy uusi merkkijono-olio:

String jono = "koe";
jono + "häntä";

System.out.println( jono );  // koe

Merkkijonoa ei siis voi muuttaa, mutta voimme ottaa katenoimalla syntyvän uuden merkkijonon talteen vanhaan muuttujaan:

String jono = "koe";
jono = jono + "häntä";   // tai jono += "häntä";

System.out.println( jono );  // koehäntä
  

Nyt siis muuttuja jono viittaa uuteen merkkijono-olioon, joka luotiin yhdistämällä muuttujan aiemmin viittaama merkkijono "koe" ja merkkijono "häntä". Merkkijono-olioon "koe" ei enää viitata.

24. Taulukko

Olemme käyttäneet kurssin aikana lukemattomia kertoja ArrayList:ejä erilaisten olioiden säilömiseen. ArrayList on helppokäyttöinen sillä se tarjoaa paljon valmiita työvälineitä jotka helpottavat ohjelmoijan elämää: muun muassa automaattisen listan kasvatuksen, jonka ansioista listalta ei lopu tila kesken (ellei lista kasva niin suureksi että se käyttää loppuun ohjelmalle varatun muistimäärän).

ArrayListin helppokäyttöisyydesta huolimatta ohjelmissa on joskus tarvetta ArrayListin esi-isälle eli taulukolle.

Taulukko on olio, 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. ArrayLististä poiketen taulukon kokoa (eli sen alkioiden määrää) ei voi muuttaa, taulukon kasvattaminen vaatii siis aina uuden taulukon luomista ja vanhassa olevien alkioiden kopiointia uuteen.

Taulukon voi luoda kahdella eri tavalla. Tutustutaan ensin tapaan jossa taulukolle annetaan sisältö luomisen yhteydessä. Kolmen alkion kokonaislukutyyppinen taulukko määritellään seuraavasti:

int[] luvut = {100, 1, 42};

Taulukko-olion tyyppi merkitään int[], joka tarkoittaa taulukkoa, jonka alkiot ovat tyyppiä int. Taulukko-olion nimi on esimerkissä luvut ja se sisältää kolme lukua {100, 1, 42}. Taulukko alustetaan lohkolla, jossa taulukkoon asetettavat arvot on esitelty pilkulla eriteltyinä.

Taulukon arvot voivat olla mitä tahansa aikaisemmin nähtyjä muuttujatyyppejä. Alla on esitelty ensin merkkijonoja sisältävä taulukko, jonka jälkeen esitellään liukulukuja sisältävä taulukko.

String[] merkkijonotaulukko = {"Matti P.", "Matti V."};
double[] liukulukutaulukko = {1.20, 3.14, 100.0, 0.6666666667};

Taulukon alkioihin viitataan indeksillä, joka on kokonaisluku. Indeksi kertoo alkion paikan taulukossa. Taulukon ensimmäinen alkio on paikassa nolla, seuraava kohdassa yksi ja niin edelleen. Taulukon tietyssä indeksissä olevaa arvoa tutkittaessa indeksi annetaan taulukko-olion nimen 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.

Huomaat todennäköisesti että ArrayListin metodi get käyttäytyy hyvin samalla tavalla kuin taulukon tietystä indeksistä haku. Taulukon kohdalla vain syntaksi, eli merkintätapa, on erilainen.

Yksittäisen arvon asettaminen taulukon tiettyyn paikkaan tapahtuu kuten arvon asetus tavalliseen muuttujaan, mutta taulukkoon asetettaessa paikka eli indeksi tulee kertoa. Indeksi kerrotaan hakasulkeiden sisällä.

int[] luvut = {100,1,42};

luvut[0] = 1;    // asetetaan luku 1 indeksiin 0
luvut[1] = 101;  // asetetaan luku 101 indeksiin 1

// luvut-taulukko on nyt {1,101,42}

Jos indeksillä osoitetaan taulukon ohi, eli alkioon jota ei ole olemassa, niin saadaan virheilmoitus ArrayIndexOutOfBoundsException, joka kertoo että indeksiä johon osoitimme ei ole olemassa. Taulukon ohi, eli indeksiin joka on pienempi kuin 0 tai suurempi tai yhtäsuuri kuin taulukon koko ei siis saa viitata.

Huomaamme, että taulukko on selvästi sukua ArrayList:ille. Aivan kuten listoilla, myös taulukossa alkiot ovat tietyssä paikassa!

24.1 Taulukon läpikäynti

Taulukko-olion koon saa selville kirjoittamalla koodiin taulukko.length, huomaa että ilmauksessa ei tule käyttää sulkuja eli taulukko.length() ei toimi!

Taulukon alkioiden läpikäynti on helppo toteuttaa while-komennon avulla:

int[] luvut = {1, 8, 10, 3, 5};

int i = 0;
while (i < luvut.length ) {
    System.out.println(luvut[i]);
    i++;
}
  

Esimerkissä käydään muuttujan i avulla läpi indeksit 0, 1, 2, 3, ja 4, ja tulostetaan taulukon kussakin indeksissä olevan muuttujan arvo. Ensin siis tulostuu luvut[0], sitten luvut[1] jne. Muuttujan i kasvatus loppuu kun koko taulukko on käyty läpi, eli kun sen arvo on yhtäsuuri kuin taulukon pituus.

Taulukon läpikäynnissä ei ole aina todellista tarvetta taulukon indeksien luetteluun, vaan ainoa kiinnostava asia ovat taulukon arvot. Tällöin voidaan käyttää aiemmin tutuksi tullutta for-each-rakennetta arvojen läpikäyntiin. Nyt toistolauseen rungossa annetaan vain muuttujan nimi, johon kukin taulukon arvo asetetaan vuorollaan, ja taulukon nimi kaksoispisteellä erotettuna.

    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 alkioihin ei voi asettaa arvoja! Seuraavaksi nähtävällä for-lauseen toisella muodolla sekin onnistuu.

24.2 for-komennon toinen muoto

Olemme toistaiseksi käyttäneet toistolauseissa whileä tai for-lauseen ns. "for-each"-muotoa. Toistolauseesta for on olemassa myös toinen muoto, joka on kätevä erityisesti taulukoiden käsittelyn yhteydessä. Seuraavassa tulostetaan for-toistolauseen avulla luvut 0, 1 ja 2:

for (int i = 0; i < 3; i++ ) {
    System.out.println(i);
}

Esimerkin for toimii täsmälleen samalla tavalla kuin alla oleva while:

int i = 0;  // toistossa käytettävän muuttujan alustus
while ( i < 3 ) {  // toistoehto
    System.out.println(i);
    i++;   // toistossa käytettävän muuttujan päivitys
}

for-komento, kuten yllä esitelty for (int i = 0; i < 3; i++ ) sisältää kolme osaa: looppimuuttujien alustus; toistoehto; looppimuuttujien päivitys:

for on hieman while:ä selkeämpi tapa toteuttaa toistoja joissa toistojen määrä perustuu esim. laskurin kasvatukseen. Taulukkojen läpikäynnissä tilanne on yleensä juuri tämä. Seuraavassa tulostetaan taulukon luvut sisältö for:illa

int[] luvut = {1, 3, 5, 9, 17, 31, 57, 105};

for(int i = 3; i < 7; i++) {
    System.out.println(luvut[i]);
}

Forilla voidaan aloittaa läpikäynti luonnollisesti muualtakin kuin nollasta ja läpikäynti voi edetä "ylhäältä alas". Esimerkiksi taulukon paikoissa 6, 5, 4, ja 3 olevat alkiot voidaan tulostaa seuraavasti:

int[] luvut = {1, 3, 5, 9, 17, 31, 57, 105};

for(int i = 6; i>2 ; i--) {
    System.out.println(luvut[i]);
}

24.3 for ja taulukon pituus

Kaikkien taulukon alkioiden läpikäynti for:in avulla onnistuu seuraavasti:

int[] luvut = {1, 8, 10, 3, 5};

for (int i = 0; i < luvut.length; i++ ) {
    System.out.println(luvut[i]);
}

Huomaa, että toistoehdossa i < luvut.length verrataan looppimuuttujan arvoa taulukolta kysyttyyn pituuteen. Ehtoa ei kannata missään tapauksessa "kovakoodata" tyyliin i < 5 sillä yleensä taulukon pituudesta ei ole etukäteen varmuutta.

24.4 Taulukko parametrina

Taulukkoja voidaan käyttää metodin parametrina aivan kuten muitakin olioita. Huomaa, että kuten kaikkien olioiden tapauksessa, metodi saa parametrina viitteen taulukkoon, eli kaikki metodissa tapahtuvat taulukon sisältöön vaikuttavat muutokset näkyvät myös pääohjelmassa.

public static void listaaAlkiot(int[] kokonaislukuTaulukko) {

    System.out.println("taulukon alkiot ovat: ");
    for( int luku : kokonaislukuTaulukko) {
        System.out.print(luku + " ");  
    }

    System.out.println("");
}

public static void  main(String[] args) {
    int[] luvut = { 1, 2, 3, 4, 5 };
    listaaAlkiot(luvut);
}

Kuten jo tiedämme, 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ä kokonaislukuTaulukko, metodin kutsuja taas näkee saman taulukon luvut-nimisenä.

Tehtävä 96: Taulukon lukujen summa

Tehtävä 97: Tyylikäs tulostus

24.5 Uuden taulukon luonti

Jos taulukon koko ei ohjelmassa ole aina sama, eli se riippuu esim. käyttäjän syötteestä, ei äsken esitelty taulukon luontitapa kelpaa. Taulukko on mahdollista luoda myös siten, että sen koko määritellään muuttujan avulla:

int alkioita = 99;
int[] taulukko = new int[alkioita];

Yllä luodaan int-tyyppinen taulukko, jossa on 99 paikkaa. Tällä vaihtoehtoisella tavalla taulukon luominen tapahtuu siis kuten muidenkin olioiden luominen, eli new-komennolla. Komentoa new seuraa taulukon sisältämien muuttujien tyyppi, ja hakasuluissa taulukon koko.

int alkioita = 99;
int[] taulukko = new int[alkioita]; //luodaan muuttujan alkioita sisältämän arvon kokoinen taulukko

if(taulukko.length == alkioita) {
    System.out.println("Taulukon pituus on " + alkioita);
} else {
    System.out.println("Jotain epätodellista tapahtui. Taulukon pituus on eri kuin " + alkioita);
}

Seuraavassa esimerkissä on ohjelma, joka kysyy käyttäjältä lukujen määrän ja joukon lukuja. Tämän jälkeen 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
8
2
1

24.6 Taulukko paluuarvona

Koska metodit voivat palauttaa olioita, voivat ne palauttaa myös taulukkoja. Eräs merkkijonotaulukon palauttava metodi on seuraavannäköinen -- huomaa että taulukkoihin voi aivan hyvin siis laittaa myös olioita.

public static String[] annaMerkkijonoTaulukko() {
    String[] opet = new String[3];

    opet[0] = "Bonus";
    opet[1] = "Ihq";
    opet[2] = "Lennon";

    return opet;
}

public static void main(String[] args){
    String[] opettajat = annaMerkkijonoTaulukko();

    for ( String opettaja : opettajat){
        System.out.println( opettaja );
    }
}

Tehtävä 98: Taulukon kopiointi ja kääntaminen

25. Lohkoista ja sisäkkäisistä toistolauseista

Aaltosululla { alkavaa ja aaltosululla } koodia sisältävää aluetta siis sanotaan lohkoksi. Kuten on jo nähty, lohkoja käytetään muun muassa ehto- ja toistolauseiden alueen rajaamisessa. Tärkeä lohkon ominaisuus on 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 toimi!

int luku = 5;

if( luku == 5 ){
    String lohkonSisallaMaariteltyTeksti = "Jea!";
}

System.out.println(lohkonSisallaMaariteltyTeksti); // ei toimi!

Lohkossa voidaan toki 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. Esimerkiksi for-toistolauseen määrittelemän lohkon sisällä voi olla toinen for-toistolauseke tai vaikkapa while-toistolauseke. 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.

25.1 for-toistolauseen ulkopuolella määriteltyjen muuttujien käyttö toiston ehtona

Tutkitaan seuraavaa muunnosta edelliseen esimerkkiin:

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 sisempi toistolauseke 0 1, jne. Koko ohjelman tulostus on 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.

Kertotaulu-ohjelman voi toki pilkkoa pienempiin osiin. Voimme määritellä metodit public void tulostaKertotaulunRivi(int kerroin, int montakokertaa) ja public void tulostaKertotaulu(int mihinAsti), jolloin ohjelman rakenne olisi esimerkiksi seuraavanlainen seuraavanlainen:

public class Kertotaulu {

    public void tulosta(int mihinAsti) {
        for(int i = 1; i <= mihinAsti; i++) {
            tulostaKertotaulunRivi(i, mihinAsti);

            System.out.println();
        }
    }


    public void tulostaKertotaulunRivi(int kerroin, int montakokertaa) {
        for(int i = 1; i <= montakokertaa; i++) {
            System.out.print(i * kerroin + " ");
        }
    }
}

Nyt kutsu new Kertotaulu().tulosta(5); tulostaa allaolevan kertotaulun.

1 2 3 4 5    
2 4 6 8 10   
3 6 9 12 15  
4 8 12 16 20 
5 10 15 20 25

Tehtävä 99: Taulukko tähtinä

Tehtävä 100: Tähtitaivas

26. static:illa vai ilman?

Kun aloimme olioden käytön, materiaalissa oli neuvo jättää sana static pois olioiden metodien määrittelystä. Viikkoon 3 asti taas kaikissa metodeissa esiintyi määre static. Mistä on kysymys?

Seuraavassa esimerkissä on metodi nollaaTaulukko joka toimii nimensä mukaisesti eli asettaa nollan parametrina saamansa taulukon kaikkien lokeroiden arvoksi.

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[] luvut = { 1, 2, 3, 4, 5 };

        for ( int luku : luvut ) {
            System.out.print( luku + " " );  // tulostuu 1, 2, 3, 4, 5
        }

        System.out.println();

        nollaaTaulukko(luvut);

        for ( int luku : luvut ) {
            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 luokkametodi. Luokkametodeja kutsutaan usein myös staattisiksi metodeiksi. Toisin kuin olioiden metodit (joilla ei ole määrettä static), staattiseen metodiin ei liity mitään olioa. Staattinen metodi ei siis voi viitata this-määreellä olioon itseensä toisin kuin oliometodit.

Staattiselle metodille voi toki antaa olion parametrina. Staattinen metodi ei voi käsitellä mitään muita lukuja, merkkijonoja, taulukoita tai oliota kuin niitä, jotka on sille parametrina annettu. Toisin sanoin, staattisen metodin kutsuja antaa metodille aina käsiteltävät arvot ja oliot.

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 eri tiedostoissa):

public class Ohjelma {
    public static void main(String[] args) {
        int[] luvut = { 1, 2, 3, 4, 5 };

        for ( int luku : luvut ) {
            System.out.print( luku + " " );  // tulostuu 1, 2, 3, 4, 5
        }

        System.out.println();

        TaulukonKasittely.nollaaTaulukko(luvut);

        for ( int luku : luvut ) {
            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 kutsutaan nyt muodossa TaulukonKasittely.nollaaTaulukko(parametri);.

26.1 Milloin staattisia metodeja tulisi käyttää

Kaikki olion tilaa käsittelevät metodit tulee määritellä normaaleina oliometodeina. Esim. edellisillä viikoilla määrittelemiemme luokkien Henkilo, Paivays, Kello, Joukkue, ... 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 this-määreen avulla sillä korostamme, 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 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. Kirjoitetaan ohjelma uudelleen metodia hyödyntäen:

public class Main {

    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);
    }

    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");
        }
    }
}

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.

Tehtävä 101: Kirjaston tietojärjestelmä

27. Tehtäviä joissa saat itse keksiä ohjelmalle sopivan rakenteen

Tehtävä 102: Arvosanajakauma

Tehtävä 103: Lintubongarin tietokanta

28. Debuggeri ja muuta hyödyllistä

Ohjelmien muuttuessa monimutkaisemmiksi, tulee virheiden löytämisestäkin koko ajan haastavampaa. NetBeansiin integroitu debuggeri voi olla avuksi virheiden löytämisessä. Seuraavalla screencastilla esitellään debuggerin käyttöä. Screencast esittelee myös miten projekteja voidaan luoda, avata ja sulkea sekä miten ohjelmia voidaan suorittaa NetBeansin ulkopuolella.

29. Taulukon järjestäminen

Palaamme jälleen taulukkojen pariin.

29.1 Taulukon järjestäminen Javan valmiilla työkaluilla.

Kuten olemme nähneet, Javassa on valmiina paljon kaikenlaista hyödyllistä. Esimerkiksi ArrayListin käsittelyyn löytyi useita hyödyllisiä apumetodeja luokasta Collections. Taulukoille löytyy vastaavia apuvälineitä luokasta Arrays. Taulukon saa järjestettyä komennolla Arrays.sort(taulukko).

Huom: Komennon käyttäminen vaatii, että ohjelmatiedoston yläosassa lukee seuraava määrittely:

  import java.util.Arrays;

Jos unohdat import-rivin, NetBeans tarjoaa apua sen kirjoittamiseen. Kokeile klikata punaisella alleviivatun koodirivin vasempaan laitaan ilmestyvää lampun kuvaa.

Seuraava ohjelma luo taulukon ja järjestää taulukossa olevat luvut Arrays.sort -komennon avulla.

int[] luvut = {-3, -111, 7, 42};
Arrays.sort(luvut);
for(int luku: luvut) {
    System.out.println(luku);
} 
-111
-3
7
42

29.2 Järjestämisalgoritmin toteuttaminen

Taulukon järjestäminen on helppoa Javan valmiin kaluston avulla. Ohjelmoijan yleissivistykseen kuuluu kuitenkin ainakin yhden järjestämisalgoritmin (eli tavan järjestää taulukko) tuntemus. Tutustutaan yhteen "klassiseen" järjestämisalgoritmiin, valintajärjestämiseen. Tutustuminen tapahtuu harjoitustehtävien avulla.

Tehtävä 104: Järjestäminen

30. Etsintä

Järjestämisen lisäksi toinen hyvin tyypillinen ongelma johon ohjelmoija törmää, on tietyn arvon etsiminen taulukosta. Olemme aiemmin jo toteuttaneet metodeja, jotka etsivät arvoja listoista ja taulukoista. Taulukkojen tapauksessa lukuja ja merkkijonoja voi etsiä seuraavasti:

public static boolean onkoTaulukossa(int[] taulukko, int etsittava) {
    for ( int luku : taulukko ) {
        if ( luku == etsittava )  {
            return true;
        }
    }

    return false;
}

public static boolean onkoSanaTaulukossa(String[] taulukko, String etsittava) {
    for ( String sana: taulukko ) {
        if ( sana.equals(etsittava) )  {
            return true;
        }
    }

    return false;
}

Tämänkaltainen toteutus on paras mihin olemme tähän asti pystyneet. Metodin huono puoli on se, että jos taulukossa on hyvin suuri määrä lukuja, kuluu etsintään paljon aikaa. Metodi käy läpi pahimmassa tapauksessa jokaisen taulukossa olevan alkion. Tämä tarkoittaa sitä, että taulukon, jossa on esimerkiksi 16777216 alkiota, läpikäynti vaatii 16777216 alkion tutkiskelua.

Toisaalta, jos taulukossa olevat luvut ovat suuruusjärjestyksessä, voidaan etsiminen tehdä huomattavasti tehokkaammin soveltamalla tekniikkaa nimeltään binäärihaku. Tutkitaan binäärihaun ideaa seuraavan taulukon kautta:

// indeksit   0   1   2   3    4   5    6   7   8   9  10
// luvut     -7  -3   3   7   11  15   17  21  24  28  30

Oletetaan että haluamme löytää luvun 17. Hyödynnetään tietoa siitä että taulukon arvot ovat järjestyksessä sen sijaan, että kävisimme taulukon lukuja läpi taulukon alusta lähtien. Tutkitaan taulukon keskimmäistä alkiota. Taulukon puolessa välissä olevan alkion indeksi on isoin indeksi 10 jaettuna kahdella eli 5. Keskimmäinen alkio on merkattu seuraavaan tähdellä:

                                   *
// indeksit   0   1   2   3    4   5    6   7   8   9  10
// luvut     -7  -3   3   7   11  15   17  21  24  28  30

Puolessa välissä on luku 15, joka ei ollut hakemamme luku. Etsimme lukua 17, joten koska taulukon alkiot ovat suuruusjärjestyksessä, ei etsitty luku voi missään tapauksessa olla luvun 15 vasemmalla puolella. Voimme siis päätellä että kaikki indeksit, jotka ovat pienempiä tai yhtäsuuria kuin 5, eivät missään nimessä sisällä hakemaamme arvoa.

Alue, jolta etsimme haettavaa lukua voidaan nyt rajata lukuihin, jotka sijaitsevat indeksin 5 oikealla puolella, eli indekseihin välillä [6, 10] (6, 7, 8, 9, 10). Seuraavassa on merkitty harmaalla se osa taulukkoa jossa etsitty ei voi olla:

// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Tutkitaan seuraavaksi jäljellä olevan etsintäalueen, eli indeksien 6-10 keskimmäistä indeksiä. Keskimmäinen indeksi löytyy ottamalla etsintäalueen pienimmän ja suurimman indeksin summa ja jakamalla se kahdella, eli (6+10)/2 = 16/2 = 8. Indeksi 8 on merkitty alle tähdellä.

                                                 *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Indeksissä 8 oleva luku on 24, joka ei ollut hakemamme luku. Koska luvut taulukossa ovat suuruusjärjestyksessä, ei etsittävä luku voi missään nimessä olla luvun 24 oikealla puolella. Voimme siis päätellä että kaikki indeksit, jotka ovat suurempia tai yhtäsuuria kuin 8, eivät missään nimessä sisällä hakemaamme arvoa. Etsintäalue rajautuu taas, harmaat alueet on käsitelty:

// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Etsintä jatkuu. Tutkitaan jäljellä olevan etsintäalueen, eli indeksien 6-7, keskimmäistä indeksiä. Keskimmäinen indeksi löytyy taas ottamalla etsintäalueen pienimmän ja suurimman indeksin summa ja jakamalla se kahdella, eli (6+7)/2 = 6,5, joka pyöristyy alaspäin luvuksi 6. Kohta on merkitty alle tähdellä.

                                         *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Indeksissä 6 on luku 17, joka on sama kuin hakemamme luku. Voimme lopettaa haun ja ilmoittaa että etsitty luku on taulukossa. Jos luku ei olisi ollut taulukossa -- esimerkiksi jos haettava luku olisi ollut 16, etsintäalue olisi jäänyt lopulta tyhjäksi.

                                         *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Jotta binäärihaun idea tulee sinulle tutuksi, simuloi kynällä ja paperilla miten binäärihaku toimii kun taulukkona on alla oleva taulukko ja haet ensin lukua 33, sitten lukua 1.

// indeksit   0   1   2   3   4   5   6   7   8   9  10  11  12  13
// luvut     -5  -2   3   5   8  11  14  20  22  26  29  33  38  41

Binäärihaun avulla haemme alkiota puolittamalla aina tutkittavan alueen kahteen osaan. Tämä mahdollistaa hyvin tehokkaan hakemisen. Esimerkiksi taulukko, jossa on 16 alkiota, voidaan jakaa kahteen osaan korkeintaan 4 kertaa, eli 16 -> 8 -> 4 -> 2 -> 1. Toisaalta, taulukko, jossa on 16777216 alkiota, voidaan jakaa kahteen osaan korkeintaan 24 kertaa. Tämä tarkoittaa sitä, että binäärihaun avulla 16777216-alkioisessa taulukossa tarvitsee tarkastella korkeintaan 24 alkiota haetun alkion löytymiseksi.

Binäärihaun tehokkuutta voi tutkia logaritmien avulla. Kaksikantainen logaritmi (log2) luvusta 16777216 on 24 -- voimme siis laskea kaksikantaisen logaritmin avulla kuinka monta kertaa jonkun luvun voi puolittaa. Vastaavasti luvun 4294967296 kaksikantainen logaritmi, (log2 4294967296) on 32. Tämä tarkoittaa että 4294967296 eri arvoa sisältävästä järjestyksessä olevasta taulukosta hakeminen vaatisi korkeintaan 32 eri alkion tarkastamista. Tehokkuus on oleellinen osa tietojenkäsittelytiedettä. Esimerkiksi tietojenkäsittelytieteen ensimmäisen vuoden kurssi Tietorakenteet keskittyy tehokkaiden tietorakenteiden toteuttamiseen.

Tehtävä 105: Arvauspeli

Tehtävä 106: Binäärihaun toteutus

31. Taulukoista ja olioista

Jos tarvetta on, taulukkoon voi luonnollisesti laittaa minkä tahansa tyyppisen olion. Seuraavassa esimerkki taulukosta johon talletetaan Henkilo-olioita:

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 henkilo : henkilot ) {       
        ilmoitaTaysiIkaisyys(henkilo);
    }
}

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 henkilo : henkilot ) {
            henkilo.vanhene();
        }

        // tai henkilot.get(0).vanhene();
        //     henkilot.get(1).vanhene();
        //     ...
    }

    for ( Henkilo henkilo : henkilot ) {
        ilmoitaTaysiIkaisyys(henkilo);
    }
}

Useimmissa sovellustilanteissa taulukon sijaan kannattaa käyttää ArrayListiä. Voi kuitenkin olla joitain tilanteita joissa taulukko riittää ja on hieman yksinkertaisempi käyttää.

Viikko koostuu aina seitsemästä päivästä. Viikko-olio olisikin mielekästä koostaa tasan seitsemästä päiväoliosta. Koska päiva-olioita on aina täsmälleen 7, sopii taulukko tilanteeseen hyvin:

public class Paiva {
      private String nimi;
      // ...
}

public class Viikko {
    private Paiva[] paivat;

    public Viikko(){
        paivat = new Paiva[7];
        paivat[0] = new Paiva("Maanantai");
        paivat[1] = new Paiva("Tiistai"); 
        // ...
    }
}

32. Kurssipalaute

Tehtävä 107: Kurssipalaute

Tehtävä 108: Palaute