Tehtävät
Neljännen osan tavoitteet

Tuntee käsitteet olio, konstruktori ja olion metodit. Luo ainakin yhden oman luokan. Luo olioita tekemistään luokista. Ymmärtää static-määreellä ja ilman static-määrettä olevien metodien eron. Tietää olio-ohjelmoinnin perusperiaatteet ja luo luokkia, jotka kuvaavat annettua ongelma-aluetta.

CrowdSorcerer ja listat

Tässä kohtaa kertaat listoja ja pääset taas pohtimaan tehtävää tulevia sukupolvia varten. Jos et käyttänyt CrowdSorcereria kurssin toisessa osassa, käy katsomassa CrowdSorcererin opasvideo toisen osan materiaalista.

Suunnittele oma tehtävä: listat

Suunnittele tehtävä, joka harjoituttaa listojen käsittelyä ja tietojen hakemista niistä.

Tee tehtävään valmiiksi lista tai listoja, jotka sisältävät oman valintasi mukaan joko merkkijonoja, kokonailukuja tai liukulukuja. Täytä listan arvot valmiiksi.

Ohjeista tulevaa tehtävän ratkaisijaa kysymään käyttäjältä komentoa, jonka jälkeen listalta haetaan komennon perusteella jotakin tietoa, joka sen jälkeen tuolostetaan. Jos annettu käsky ei ole sallittujen listalla, tulee ohjelman tulostaa jokin virheviesti.

Esimerkiksi yksi tälläinen tehtävä voisi sisältää listan kokonaislukuja, ja käskyt voisivat olla: "suurin", "pienin" ja "keskiarvo". Kun tuleva tehtävän ratkaisija antaa käskyn "keskiarvo", ohjelma tulostaa listan lukujen keskiarvon ja niin edelleen. Keksi kuitenkin tehtävällesi omat sallitut käskyt.

Muista merkitä ainakin käskyyn reagointiin liittyvät rivit malliratkaisuriveiksi -- näin ratkaisu ei tule suoraan tehtäväpohjaan. Vastaavasti älä merkitse listan luontia tai sen arvoja lisäävää koodia malliratkaisuriveiksi, sillä se on tarkoitus jättää tehtäväpohjaan.

Ongelma listan importtauksessa

Joillakin tuli tässä ongelmaksi, että ei voinut lisätä importteja koska niille tarkoitetut rivit olivat lukittuja. Jos sinulla crowdsorcererin aivan ensimmäiset rivit ovat lukittuja, kopio ensiksi työsi johonkin toiseen ohjelmaan ja sitten paina tästä napista nollataksesi Crowdsorcererin:

Olio-ohjelmointi

Tutustuimme edellisessä osassa ensimmäistä kertaa termeihin olio ja luokka. Aloitamme nyt matkan olio-ohjelmoinnin pariin.

Olio-ohjelmoinnissa on kyse ratkaistavassa ongelmassa esiintyvien käsitteiden eristämisestä omiksi kokonaisuuksikseen sekä näiden kokonaisuuksien käyttämistä ongelman ratkaisemisessa. Kun ongelmaan liittyvät käsitteet on tunnistettu, niistä voidaan myös keskustella. Toisin ajatellen, ratkaistavasta ongelmasta muodostetaan abstraktioita, joiden avulla ongelmaa on helpompi käsitellä.

Kun ongelmasta tunnistetaan käsitteitä, voidaan niitä vastaavia rakenteita luoda myös ohjelmaan. Näitä rakenteita ja niistä luotavia yksittäisiä ilmentymiä eli olioita käytetään ongelman ratkaisemisessa. Nyt ehkä käsittämättömältä tuntuva lausahdus ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista alkaa hiljalleen kurssin edetessä tuntua järkeenkäyvältä ja jopa itsestäänselvältä.

Luokka ja Olio

Olemme käyttäneet jo joitakin Javan tarjoamia luokkia ja olioita. Luokka määrittelee olioiden ominaisuudet eli niihin liittyvät tiedot eli oliomuuttujat ja niiden tarjoamat komennot eli metodit. Oliomuuttujien arvot määrittelevät yksittäisen olion sisäisen tilan ja metodit taas olion tarjoamat toiminnallisuudet. Olio luodaan luokkaan kirjoitetun määrittelyn perusteella. Samasta luokasta voidaan luoda useampia olioita, joilla jokaisella on eri tila eli jokaisella on omat oliomuuttujien arvot. Jokaisella oliolla on myös metodit, jotka olion luomiseen käytetyssä luokassa on määritelty.

Metodi on luokkaan kirjoitettu lähdekoodista koostuva kokonaisuus, jolle on annettu nimi, ja jota voidaan kutsua. Metodi liittyy aina tiettyyn luokkaan, ja sitä käytetään usein luokasta tehdyn olion sisäisen tilan muokkaamiseen.

Esimerkiksi Scanner on Javan tarjoama luokka, josta luotuja olioita olemme hyödyntäneet ohjelmissamme. Alla ohjelmassa luodaan Scanner-olio nimeltä lukija, jota käytetään kokonaislukumuuttujien lukemiseen.

// luodaan Scanner-luokasta olio, jonka nimeksi tulee lukija
Scanner lukija = new Scanner(System.in);

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

    if (luku == 0) {
        break;
    }

    System.out.println("Luettu " + luku);
}

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(..). Konstruktorit saavat parametreja kuten muutkin metodit.

Luokan ja olion suhde

Luokka kuvaa siitä luotavien olioiden "rakennuspiirustukset". Otetaan analogia tietokoneiden ulkopuoleisesta maailmasta. Rintamamiestalot lienevät monille tuttuja. Voidaan ajatella, että jossain on olemassa piirustukset jotka määrittelevät minkälainen rintamamiestalo on. Piirrustukset ovat luokka, eli ne määrittelevät minkälaisia olioita luokasta voidaan luoda:

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":

Visuaalinen johdanto oliohin

Osoitteessa https://emo-2014.herokuapp.com on visuaalinen johdanto olio-ohjelmointiin. Johdannon läpikäynti vie korkeintaan 15 minuuttia -- katso kyseinen johdanto nyt.

Tehtäväpohjan mukana tulee valmis luokka Tili. Luokan Tili olio esittää pankkitiliä, jolla on saldo (eli jossa on jokin määrä rahaa). Tilejä käytetään näin:

Tili artonTili = new Tili("Arton tili", 100.00);
Tili artonSveitsilainenTili = new Tili("Arton tili Sveitsissä", 1000000.00);

System.out.println("Alkutilanne");
System.out.println(artonTili);
System.out.println(artonSveitsilainenTili);

artonTili.otto(20);
System.out.println("Arton tilin saldo on nyt: " + artonTili.saldo());
artonSveitsilainenTili.pano(200);
System.out.println("Arton toisen tilin saldo on nyt: " + artonSveitsilainenTili.saldo());

System.out.println("Lopputilanne");
System.out.println(artonTili);
System.out.println(artonSveitsilainenTili);

Tee ohjelma, joka luo tilin jonka saldo on 100.0, panee tilille 20.0 ja tulostaa tilin. Huom! tee kaikki nämä operaatiot täsmälleen tässä järjestyksessä.

Tässäkin tehtävässä on käytössä edellisessä tehtävässä mukana ollut luokka Tili.

Tee ohjelma joka:

  1. Luo tilin nimeltä "Matin tili" saldolla 1000
  2. Luo tilin nimeltä "Oma tili" saldolla 0
  3. Nostaa matin tililtä 100.0
  4. Panee omalle tilille 100.0
  5. Tulostaa molemmat tilit

Luokan luominen

Luokka määrittelee minkälaisia luokasta luotavat oliot ovat:

  • olion muuttujat määrittelevät minkälainen olion sisäinen tila on
  • olion metodit määrittelevät mitä toiminnallisuuksia olio tarjoaa

Tutustutaan seuraavaksi oman luokan luomiseen sekä luokkaan liittyvien oliomuuttujien määrittelyyn.

Luokka määritellään kuvaamaan jotain mielekästä kokonaisuutta. Usein "mielekäs kokonaisuus" kuvaa jotain reaalimaailman asiaa tai käsitettä. 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) {

    }
}
Uuden luokan luominen

Uuden luokan luominen NetBeansissa tapahtuu valitsemalla 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. Usein ohjelmoinnin edetessä luokka elää ja muuttaa muotoaan, joten on myös mahdollista että luokka nimetään uudelleen.

Luokkien, muuttujien ja metodien nimissä ei tyypillisesti käytetä ääkkösiä. Vältä niiden käyttöä myös tässä.

Luodaan luokka nimeltä Henkilo. Luokkaa varten luodaan erillinen tiedosto nimeltä Henkilo.java. Ohjelmamme koostuu nyt siis kahdesta erillisestä tiedostosta, sillä myös pääohjelma on omassa tiedostossaan. Aluksi Henkilo.java -tiedosto sisältää luokan määrittelyn public class Henkilo sekä luokan sisällön rajaavat aaltosulut.

public class Henkilo {

}

Luokkaa kuvaamaan voi piirtää myös luokkakaavion, jonka merkintätekniikkaan tutustutaan tässä samalla. Henkilo-niminen luokka, jossa ei ole mitään sisällä näyttää seuraavalta:

Luokka määrittelee luokasta luotavien olioiden ominaisuudet ja toiminnallisuudet. Päätetään, että jokaisella henkilöoliolla 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ä jokaisella Henkilo-luokasta luotavalla oliolla on nimi ja ika. Luokan sisälle määriteltyjä muuttujia kutsutaan oliomuuttujiksi tai olion kentiksi tai olion attribuuteiksi. Muitakin nimiä tuntuu löytyvän.

Oliomuuttujat kirjoitetaan luokan määrittelyä "public class Henkilo {" seuraaville riveille. Jokaisen muuttujan eteen asetetaan avainsana private. Avainsana private tarkoittaa sitä, että muuttujat ovat "piilossa" olion sisällä. Tätä kutsutaan kapseloinniksi.

Luokkaakaaviossa luokkaan liittyvät muuttujat määritellään muodossa "muuttujanNimi: muuttujanTyyppi". Miinusmerkki ennen muuttujan nimeä kertoo, että muuttuja on kapseloitu (sillä on avainsana private).

Olemme nyt määritelleet rakennuspiirustukset -- luokan -- henkilöoliolle. Jokaisella uudella henkilöolioilla on muuttujat nimi ja ika, joissa voi olla oliokohtainen arvo. Henkilöiden "tila" koostuu niiden nimeen ja ikään asetetuista arvoista.

Uuden luokan saa lisättyä NetBeansissa seuraavasti: Ruudun vasemmalla reunalla on projektilistaus (Projects). Paina projektin nimen kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) Koira.

Tässä tehtävässä harjoittelet luokan luomista. Luo tehtäväpohjaan luokka nimeltä Koira ja lisää sille oliomuuttujat private String nimi, private String rotu ja private int ika. Luokkakaaviona luokka näyttää seuraavalta:

[Koira|-nimi:String;-rotu:String;-ika:int]

Luokalla ei vielä oikeastaan tee mitään, mutta tämän askeleen harjoittelusta on hyötyä myöhempää ajatellen.

Konstruktorin määrittely

Konstruktoria käytetään olion luomiseen.

Luotavalle oliolle halutaan asettaa alkutila. Itse määritellyn olion luominen tapahtuu hyvin samaan tapaan kuin olioiden luominen Javan valmiista luokista kuten ArrayLististä. Oliot luodaan new-komennolla. Olion luomisen yhteydessä on kätevää pystyä antamaan arvot luotavan olion muuttujille. Esimerkiksi uutta henkilö-oliota luotaessa olisi kätevää pystyä antamaan oliolle nimi:

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

Tämä onnistuu määrittelemällä olion luova metodi eli konstruktori. Konstruktori määritellään oliomuuttujien jälkeen. Seuraavassa esimerkissä Henkilo-luokalle on määritelty konstruktori, jota voidaan käyttää uuden Henkilo-olion luomiseen. Konstruktori asettaa luotavan olion iäksi 0 ja nimeksi konstruktorin parametrina annettavan merkkijonon:

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

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

Konstruktorin nimi on aina sama kuin luokan nimi. Yllä luokka (class) on Henkilo, joten konstruktorin nimeksi tulee Henkilo. Konstruktorille annetaan lisäksi parametrina luotavan henkilööolion nimi. Parametri asetetaan sulkuihin konstruktorin nimen perään. Parametreja mahdollisesti sisältävien sulkujen jälkeen tulee aaltosulut, joiden sisälle määritellään lähdekoodi, jonka ohjelma suorittaa konstruktorikutsun (esim. new Henkilo("Ada")) yhteydessä.

Oliot luodaan aina konstruktorin avulla.

Muutama huomio: konstruktorin sisällä on lauseke this.ika = 0. Lausekkeessa asetetaan juuri luotavan olion (eli "tämän" olion) oliomuuttujan ika arvoksi 0. Toinen lauseke this.nimi = nimiAlussa; taas asettaa juuri tämän olion sisäiselle muuttujalle nimi arvoksi parametrina annetun merkkijonon.

Koska oliomuuttujat on määritelty konstruktorin aaltosulkujen ulkopuolella, voi niitä käyttää myös konstruktorin sisällä.

Nyt luokkakaavioon on merkitty luokan nimen ja muuttujien lisäksi myös konstruktori. Konstruktori saa public näkyvyysmääreen takia eteen plussan, jonka lisäksi siitä merkitään sen nimi ja parametrin tyypit (tässä + Henkilo(String)).

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

Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Jos haluat että luokan nimi on Luokkahuone, aseta luokan nimeksi (Class Name) Luokkahuone.

Luo luokka nimeltä Luokkahuone. Luokkahuoneella on oliomuuttujina private String koodi, esimerkiksi "B221", ja private int istumapaikat, esimerkiksi 30. Luo tämän jälkeen konstruktori public Luokkahuone(String luokanKoodi, int istumapaikkojenMaara), minkä avulla oliomuuttujiin asetetaan arvot.

[Luokkahuone|-koodi:String;-istumapaikat:int|+Luokkahuone(String‚ int)]

Tälläkään luokalla ei vielä oikeastaan tee mitään, mutta seuraavassa tehtävässä luokastamme tehty olio osaa jo tulostaa tekstiä.

Metodien määrittely

Alkaa olla korkea aika päästä käyttämään Henkilo-luokasta luotuja olioita. Osaamme luoda olion ja alustaa olion muuttujat. Toimintaan pystyäkseen olioilla on oltava myös metodeja. Metodi on luokkaan kirjoitettu lähdekoodista koostuva kokonaisuus, jolle on annettu nimi, ja jota voidaan kutsua. Metodi liittyy aina tiettyyn luokkaan, ja sitä käytetään usein luokasta tehdyn olion sisäisen tilan muokkaamiseen.

Tehdään luokalle Henkilo metodi, jota käytetään olion tietojen tulostamiseen.

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 kirjoitetaan luokan sisälle konstruktorin alapuolelle. Metodin nimen eteen tulee public void sillä metodin on tarkoitus näkyä ulkomaailmalle ("public") ja metodi ei palauta arvoa ("void").

Oliot ja määre static

Aiemmin toteuttamissamme metodeissa on ollut käytössä määre static. Määre static viittaa siihen, että metodi ei liity olioon ja sen avulla ei voi käsitellä oliolle määriteltyjä muuttujia.

Metodeistamme puuttuu jatkossa määre static jos ne käsittelevät olioiden tietoa. Jos taas metodit eivät käsittele olioihin liittyvää tietoa, niissä voidaan käyttää määrettä static.

Luokkakaavioon on merkitty luokan nimen, oliomuuttujien ja konstruktorin lisäksi nyt myös metodi tulostaHenkilo. Koska metodilla on public-määre, tulee sille alkuun plus, jota seuraa metodin nimi. Metodille ei ole määritelty parametreja, joten ei myöskään piirretä metodin sulkujen sisälle. Metodille merkitään myös tieto siitä, että se ei palauta arvoa, tässä "void".

Metodin tulostaHenkilo sisällä on yksi koodirivi joka käyttää hyvakseen oliomuuttujia nimi ja ika -- luokkakaavio ei kerro sisäisestä toteutuksesta. Olion sisäisiin muuttujiin viitataan 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 ada = new Henkilo("Ada");
        Henkilo antti = new Henkilo("Antti");
        Henkilo martin = new Henkilo("Martin");

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

Tulostuu:

Ada, ikä 0 vuotta
Antti, ikä 0 vuotta
Martin, ikä 0 vuotta

Sama screencastina:

 

Luo luokka nimeltä Pilli. Pillillä on oliomuuttujina private String aani. Luo tämän jälkeen konstruktori public Pilli(String pillinAani), minkä avulla luodaan uusi pilli, jolle annetaan ääni.

Lisää pillille vielä metodi public void soi(), joka tulostaa pillin äänen.

Pillin tulee toimia seuraavasti.

Pilli sorsapilli = new Pilli("Kvaak");
Pilli kukkopilli = new Pilli("Peef");

sorsapilli.soi();
kukkopilli.soi();
sorsapilli.soi();
Kvaak
Peef
Kvaak

Luo luokka nimeltä Ovi. Ovella ei ole oliomuuttujia. Luo sille parametriton konstruktori (tai käytä oletuskonstruktoria). Luo tämän jälkeen ovelle metodi public void koputa(), jota kutsuttaessa tulostuu viesti "Who's there?".

Oven tulee toimia seuraavasti.

Ovi alexander = new Ovi();

alexander.koputa();
alexander.koputa();
Who's there?
Who's there?

Luo luokka Tuote joka esittää kaupan tuotetta jolla on hinta, lukumäärä ja nimi.

Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) Tuote.

Luokalla tulee olla:

  • Konstruktori public Tuote(String nimiAlussa, double hintaAlussa, int maaraAlussa)
  • Metodi public void tulostaTuote() joka tulostaa tuotteen tiedot tässä muodossa:
    Banaani, hinta 1.1, 13 kpl
    

Piirrä myös luokkaan liittyvä luokkakaavio itsellesi!

Oliomuuttujan arvon muuttaminen metodissa

Lisätään aiemmin rakentamallemme Henkilo-luokalle metodi, joka kasvattaa henkilön ikää vuodella:

public class Henkilo {
    // ...

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

Metodi kirjoitetaan tulostaHenkilo-metodin tapaan luokan Henkilo sisälle. Metodissa kasvatetaan oliomuuttujan ika arvoa yhdellä.

Myös luokkakaavio päivittyy.

[Henkilo|-nimi:String;-ika:int|+Henkilo(String);+tulostaHenkilo():void;+vanhene():void]

Kutsutaan metodia ja katsotaan mitä tapahtuu:

public class Main {

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

        ada.tulostaHenkilo();
        antti.tulostaHenkilo();

        System.out.println("");

        ada.vanhene();
        ada.vanhene();

        ada.tulostaHenkilo();
        antti.tulostaHenkilo();
    }
}

Ohjelman tulostus on seuraava:

Ada, ikä 0 vuotta
Antti, ikä 0 vuotta

Ada, ikä 2 vuotta
Antti, ikä 0 vuotta

Eli "syntyessään" molemmat oliot ovat nollavuotiaita (konstruktorissa suoritetaan mm. rivi this.ika = 0;). Olion ada metodia vanhene kutsutaan kaksi kertaa. Kuten tulostus näyttää, tämä saa aikaan sen että Adan ikä on vanhenemisen jälkeen 2 vuotta. Kutsumalla metodia Adaa vastaavalle oliolle, toisen henkilöolion ikä ei muutu, sillä jokaiselle luokasta luotavalle oliolle luodaan myös omat oliomuuttujat.

Metodin sisään voi lisätä myös ehto- ja toistolauseita. Alla olevaa vanhene-metodia käytettäessä kenestäkään ei tulisi yli 30-vuotiasta.

public class Henkilo {
    // ...

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

Tässä tehtävässä on useampi osa. Jokainen osa vastaa yhtä tehtäväpistettä.

Tehtäväpohjan mukana tulee osittain valmiiksi toteutettu luokka VahenevaLaskuri:

public class VahenevaLaskuri {
    private int arvo;   // oliomuuttuja joka muistaa laskurin arvon

    public VahenevaLaskuri(int arvoAlussa) {
        this.arvo = arvoAlussa;
    }

    public void tulostaArvo() {
        System.out.println("arvo: " + this.arvo);
    }

    public void vahene() {
        // kirjoita tänne metodin toteutus
        // laskurin arvon on siis tarkoitus vähentyä yhdellä
    }

    // ja tänne muut metodit
}

Seuraavassa esimerkki miten pääohjelma käyttää vähenevää laskuria:

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(10);

        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();
    }
}

Pitäisi tulostua:

arvo: 10
arvo: 9
arvo: 8

VahenevaLaskuri-luokan konstruktorille annetaan parametrina alkuarvo. Esimerkin oliota laskuri luodessa laskurille välitetään parametrina arvo 10. Esimerkin laskuri-olioon liittyvään oliomuuttujaan arvo asetetaan siis aluksi arvo 10. Laskurin arvon voi tulostaa metodilla tulostaArvo(). Laskurilla tulee myös olla metodi vahene() joka vähentää laskurin arvoa yhdellä.

Metodin vahene() toteutus

Täydennä luokan runkoon metodin vahene() toteutus sellaiseksi, että se vähentää kutsuttavan olion oliomuuttujan arvo arvoa yhdellä. Kun olet toteuttanut metodin vahene(), edellisen esimerkin pääohjelman tulee toimia esimerkkitulosteen mukaan.

Laskurin arvo ei saa olla negatiivinen

Täydennä metodin vahene() toteutus sellaiseksi, ettei laskurin arvo mene koskaan negatiiviseksi. Eli jos laskurin arvo on jo 0, ei vähennys sitä enää vähennä. Ehtolause auttaa tässä.

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(2);

        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();
    }
}

Tulostuu:

arvo: 2
arvo: 1
arvo: 0
arvo: 0

Laskurin arvon nollaus

Tee laskurille metodi public void nollaa() joka nollaa laskurin arvon, esim:

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(100);

        laskuri.tulostaArvo();
 
        laskuri.nollaa();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();
    }
}

Tulostuu:

arvo: 100
arvo: 0
arvo: 0

Laskurin arvon palautus

Tee laskurille metodi public void palautaAlkuarvo(), joka palauttaa laskurille arvon joka sillä oli alussa:

public class Paaohjelma {
    public static void main(String[] args) {
        VahenevaLaskuri laskuri = new VahenevaLaskuri(100);

        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.vahene();
        laskuri.tulostaArvo();

        laskuri.nollaa();
        laskuri.tulostaArvo();

        laskuri.palautaAlkuarvo();
        laskuri.tulostaArvo();
    }
}

Tulostuu:

arvo: 100
arvo: 99
arvo: 98
arvo: 0
arvo: 100

Vihje jotta alkuarvon voi palauttaa, se täytyy "muistaa" toisen oliomuuttujan avulla! Joudut siis lisäämään ohjelmaan toisen oliomuuttujan johon talletetaan laskurin alussa saama arvo.

Luo luokka Velka, jolla on double-tyyppiset oliomuuttujat saldo ja korkokerroin. Saldo ja korkokerroin annetaan konstruktorin parametrina public Velka(double saldoAlussa, double korkokerroinAlussa).

Luo luokalle myös metodit public void tulostaSaldo() sekä public void odotaVuosi(). Metodi tulostaSaldo tulostaa tämän hetkisen saldon, ja metodi odotaVuosi kasvattaa velan määrää.

Velan määrän kasvattaminen tapahtuu kertomalla saldo korkokertoimella.

Luokan tulee toimia seuraavasti:

public class Paaohjelma {
    public static void main(String[] args) {

        Velka asuntolaina = new Velka(120000.0, 1.01);
        asuntolaina.tulostaSaldo();

        asuntolaina.odotaVuosi();
        asuntolaina.tulostaSaldo();

        int vuosia = 0;

        while (vuosia < 20) {
            asuntolaina.odotaVuosi();
            vuosia++;
        }

        asuntolaina.tulostaSaldo();
    }
}

Ylläolevassa esimerkissä havainnollistetaan asuntolainan kehitystä prosentin korolla.

Tulostus:

120000.0
121200.0
147887.0328416936

Kun saat ohjelman toimimaan, tarkastele edelläolevaa esimerkkiä myös 1990-luvun alkupuolen laman korkokertoimilla. Tällöin korko oli jopa 15-20 prosenttia -- muuta yllä olevan esimerkin korkokertoimeksi 1.20 ja katso miten käy.

Arvon palauttaminen metodista

Metodi voi palauttaa arvon. Tähän mennessä olioihin luomamme metodit eivät palauttaneet mitään. Tämä on merkitty kirjoittamalla metodin määrittelyyn avainsana void.

public class Ovi {
    public void koputa() {
        // ...
    }
}

Avainsana void tarkoittaa että metodi ei palauta arvoa.

Jos haluamme, että metodi palauttaa arvon, tulee avainsanan void paikalle asettaa palautettavan muuttujan tyyppi. Seuraavassa esimerkissä näkyvälle luokalle Opettaja on määritelty metodi arvostele, joka palauttaa aina kokonaislukutyyppisen (int) muuttujan (tässä arvo 10). Arvon palauttaminen tapahtuu aina komennolla return:

public class Opettaja {
    public int arvostele() {
        return 10;
    }
}

Ylläoleva metodi siis palauttaa sitä kutsuttaessa int-tyyppisen arvon 10. Jotta metodin palauttamaa arvoa voisi käyttää, tulee se ottaa talteen muuttujaan. Tämä tapahtuu samalla tavalla kuin normaali muuttujan arvon asetus, eli yhtäsuuruusmerkillä:

public static void main(String[] args) {
    Opettaja opettaja = new Opettaja();

    int arvostelu = opettaja.arvostele();

    System.out.println("Arvosanaksi tuli " + arvostelu);
}
Arvosanaksi tuli 10

Metodin paluuarvo sijoitetaan int-tyyppiseen muuttujaan aivan kuin mikä tahansa muukin int-arvo. Paluuarvo voi toimia myös osana mitä tahansa lauseketta:

public static void main(String[] args) {
    Opettaja opettaja = new Opettaja();

    double keskiarvo = (opettaja.arvostele() + opettaja.arvostele()) / 2.0;

    System.out.println("Arvostelujen keskiarvo " + keskiarvo);
}
Arvostelujen keskiarvo 10.0

Kaikki tähän mennessä näkemämme muuttujatyypit voidaan myös palauttaa metodista. Yhteenveto:

  • Metodilla, joka ei palauta mitään, on void-määre palautettavan muuttujan tyyppinä.
    public void metodiJokaEiPalautaMitaan() {
        // metodin runko
    }
    
  • Metodilla, joka palauttaa kokonaislukutyyppisen muuttujan, on int-määre palautettavan muuttujan tyyppinä.
    public int metodiJokaPalauttaaKokonaisLuvun() {
        // metodin runko, tarvitsee return-komennon
    }
    
  • Metodilla, joka palauttaa merkkijonotyyppisen muuttujan, on String-määre palautettavan muuttujan tyyppinä.
    public String metodiJokaPalauttaaTekstin() {
        // metodin runko, tarvitsee return-komennon
    }
    
  • Metodilla, joka palauttaa liukulukutyyppisen muuttujan, on double-määre palautettavan muuttujan tyyppinä.
    public double metodiJokaPalauttaaLiukuluvun() {
        // metodin runko, tarvitsee return-komennon
    }
    

Jatketaan nyt henkilön parissa ja lisätään henkilölle iän palauttava metodi.

public class Henkilo {
    // ...

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

Luokka kokonaisuudessaan:

[Henkilo|-nimi:String;-ika:int|+Henkilo(String);+tulostaHenkilo():void;+vanhene():void;+palautaIka():int]

Havainnollistetaan metodin toimintaa:

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");
    }
}
Pekan ikä 2
Antin ikä 1

Pekka ja Antti yhteensä 3 vuotta

Seuraa materiaalin tähänastista esimerkkiä ja luo luokka Henkilo. Henkilön tulee sisältää seuraavan luokkakaavion määrittelemät ominaisuudet edellä mainittujen esimerkkien mukaisesti.

Henkilo pekka = new Henkilo("Pekka");
Henkilo antti = new Henkilo("Antti");

antti.tulostaHenkilo();

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");
Antti, ikä 0 vuotta
Pekan ikä 2
Antin ikä 1

Pekka ja Antti yhteensä 3 vuotta

Luo luokka nimeltä Musiikkikappale. Musiikkikappaleella on oliomuuttujat nimi (merkkijono) ja pituus sekunteina (kokonaisluku). Molemmat asetetaan konstruktorissa public Musiikkikappale(String kappaleenNimi, int kappaleenPituus). Lisää oliolle myös metodit public String nimi(), joka palauttaa kappaleen nimen, ja public int pituus(), joka palauttaa kappaleen pituuden.

Luokan tulee toimia seuraavasti.

Musiikkikappale garden = new Musiikkikappale("In The Garden", 10910);
System.out.println("Kappaleen " + garden.nimi() + " pituus on " + garden.pituus() + " sekuntia.");
Kappaleen In The Garden pituus on 10910 sekuntia.

Metodien sisäinen toiminnallisuus

Kuten aiemmin huomasimme, metodit sisältävät lähdekoodia aivan samalla tavalla kuin muutkin ohjelmamme osat. Metodeissa voi olla ehtolauseita tai toistolauseita, ja metodeista voi kutsua myös muita metodeja.

Tehdään seuraavaksi 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".

Luokka kokonaisuudessaan:

[Henkilo|-nimi:String;-ika:int|+Henkilo(String);+tulostaHenkilo():void;+vanhene():void;+palautaIka():int;+taysiIkainen():boolean;+getNimi():String]

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

Luo luokka Elokuva, jolla on oliomuuttujat nimi (String) ja ikaraja (int). Tee luokalle konstruktori public Elokuva(String elokuvanNimi, int elokuvanIkaraja) sekä metodit public String nimi() ja public int ikaraja(). Ensimmäinen palauttaa elokuvan nimen ja toinen elokuvan ikärajan.

Esimerkki luokan toiminnasta.

Elokuva chipmunks = new Elokuva("Alvin and the Chipmunks: The Squeakquel", 0);

Scanner lukija = new Scanner(System.in);

System.out.println("Minkä ikäinen olet?");
int ika = Integer.parseInt(lukija.nextLine());

System.out.println();
if (ika >= chipmunks.ikaraja()) {
    System.out.println("Saat katsoa elokuvan " + chipmunks.nimi());
} else {
    System.out.println("Et saa katsoa elokuvaa " + chipmunks.nimi());
}
Minkä ikäinen olet?
7

Saat katsoa elokuvan Alvin and the Chipmunks: The Squeakquel

Olion merkkijonoesitys ja toString-metodi

Olemme syyllistyneet 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, jota 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 tulostaHenkilo-metodin.

Olioscreencastin toinen osa:

 

Tehtäväpohjassa on määriteltynä luokka Agentti, jolla on etunimi ja sukunimi. Luokalle on määritelty metodi tulosta, joka luo seuraavanlaisen merkkijonoesityksen.

Agentti bond = new Agentti("James", "Bond");
bond.tulosta();
My name is Bond, James Bond

Poista luokan metodi tulosta ja luo luokalle metodi public String toString(), joka palauttaa edellämainitun merkkijonoesityksen.

Luokan tulee toimia jatkossa seuraavasti.

Agentti bond = new Agentti("James", "Bond");

bond.toString(); // ei tulosta mitään
System.out.println(bond);

Agentti ionic = new Agentti("Ionic", "Bond");
System.out.println(ionic);
My name is Bond, James Bond
My name is Bond, Ionic Bond

Helsingin Yliopiston opiskelijaruokaloissa eli Unicafeissa opiskelijat maksavat lounaansa käyttäen maksukorttia. Lopullinen Maksukortti tulee näyttämään luokkakaaviona seuraavalta:

[Maksukortti|-saldo:double|+Maksukortti(double);+syoEdullisesti():void;+syoMaukkaasti():void;+lataaRahaa(double):void;+toString():String]

Tässä tehtäväsäsarjassa tehdään luokka Maksukortti, jonka tarkoituksena on jäljitellä Unicafeissa tapahtuvaa maksutoimintaa.

Luokan runko

Projektiin tulee kuulumaan kaksi kooditiedostoa:

Tehtäväpohjan mukana tulee kooditiedosto Paaohjelma jonka sisällä on main-metodi.

Lisää projektiin uusi luokka nimeltä Maksukortti. Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) Maksukortti.

Tee ensin Maksukortti-olion konstruktori, jolle annetaan kortin alkusaldo ja joka tallentaa sen olion sisäiseen muuttujaan. Tee sitten toString-metodi, joka palauttaa kortin saldon muodossa "Kortilla on rahaa X euroa".

Seuraavassa on luokan Maksukortti runko:

public class Maksukortti {
    private double saldo;

    public Maksukortti(double alkusaldo) {
        // kirjoita koodia tähän
    }

    public String toString() {
        // kirjoita koodia tähän
    }
}

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        Maksukortti kortti = new Maksukortti(50);
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 50.0 euroa

Kortilla maksaminen

Täydennä Maksukortti-luokkaa seuraavilla metodeilla:

public void syoEdullisesti() {
    // kirjoita koodia tähän
}

public void syoMaukkaasti() {
    // kirjoita koodia tähän
}

Metodin syoEdullisesti tulisi vähentää kortin saldoa 2.60 eurolla ja metodin syoMaukkaasti tulisi vähentää kortin saldoa 4.60 eurolla.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        Maksukortti kortti = new Maksukortti(50);
        System.out.println(kortti);

        kortti.syoEdullisesti();
        System.out.println(kortti);

        kortti.syoMaukkaasti();
        kortti.syoEdullisesti();
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa kutakuinkin seuraava tulostus:

Kortilla on rahaa 50.0 euroa
Kortilla on rahaa 47.4 euroa
Kortilla on rahaa 40.199999999999996 euroa

Ei-negatiivinen saldo

Mitä tapahtuu, jos kortilta loppuu raha kesken? Ei ole järkevää, että saldo muuttuu negatiiviseksi. Muuta metodeita syoEdullisesti ja syoMaukkaasti niin, että ne eivät vähennä saldoa, jos saldo menisi negatiiviseksi.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        Maksukortti kortti = new Maksukortti(5);
        System.out.println(kortti);

        kortti.syoMaukkaasti();
        System.out.println(kortti);

        kortti.syoMaukkaasti();
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 5.0 euroa
Kortilla on rahaa 0.40000000000000036
Kortilla on rahaa 0.40000000000000036

Yllä toinen metodin syoMaukkaasti kutsu ei vaikuttanut saldoon, koska saldo olisi mennyt negatiiviseksi.

Kortin lataaminen

Lisää Maksukortti-luokkaan seuraava metodi:

public void lataaRahaa(double rahamaara) {
    // kirjoita koodia tähän
}

Metodin tarkoituksena on kasvattaa kortin saldoa parametrina annetulla rahamäärällä. Kuitenkin kortin saldo saa olla korkeintaan 150 euroa, joten jos ladattava rahamäärä ylittäisi sen, saldoksi tulisi tulla silti tasan 150 euroa.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        Maksukortti kortti = new Maksukortti(10);
        System.out.println(kortti);

        kortti.lataaRahaa(15);
        System.out.println(kortti);

        kortti.lataaRahaa(10);
        System.out.println(kortti);

        kortti.lataaRahaa(200);
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 10.0 euroa
Kortilla on rahaa 25.0 euroa
Kortilla on rahaa 35.0 euroa
Kortilla on rahaa 150.0 euroa

Kortin lataus negatiivisella arvolla

Muuta metodia lataaRahaa vielä siten, että jos yritetään ladata negatiivinen rahamäärä, ei kortilla oleva arvo muutu.

Seuraava pääohjelma testaa luokkaa:

public class Paaohjelma {
    public static void main(String[] args) {
        Maksukortti kortti = new Maksukortti(10);
        System.out.println("Pekka: " + kortti);
        kortti.lataaRahaa(-15);
        System.out.println("Pekka: " + kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Pekka: Kortilla on rahaa 10.0 euroa
Pekka: Kortilla on rahaa 10.0 euroa

Monta korttia

Tee pääohjelma, joka sisältää seuraavan tapahtumasarjan:

  • Luo Pekan kortti. Kortin alkusaldo on 20 euroa
  • Luo Matin kortti. Kortin alkusaldo on 30 euroa
  • Pekka syö maukkaasti
  • Matti syö edullisesti
  • Korttien arvot tulostetaan (molemmat omalle rivilleen, rivin alkuun kortin omistajan nimi)
  • Pekka lataa rahaa 20 euroa
  • Matti syö maukkaasti
  • Korttien arvot tulostetaan (molemmat omalle rivilleen, rivin alkuun kortin omistajan nimi)
  • Pekka syö edullisesti
  • Pekka syö edullisesti
  • Matti lataa rahaa 50 euroa
  • Korttien arvot tulostetaan (molemmat omalle rivilleen, rivin alkuun kortin omistajan nimi)

Pääohjelman runko on seuraava:

public class Main {
    public static void main(String[] args) {
        Maksukortti pekanKortti = new Maksukortti(20);
        Maksukortti matinKortti = new Maksukortti(30);

        // kirjoita koodia tähän
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Pekka: Kortilla on rahaa 15.4 euroa
Matti: Kortilla on rahaa 27.4 euroa
Pekka: Kortilla on rahaa 35.4 euroa
Matti: Kortilla on rahaa 22.799999999999997 euroa
Pekka: Kortilla on rahaa 30.199999999999996 euroa
Matti: Kortilla on rahaa 72.8 euroa
Pyöristysvirheet

Huomasit todennäköisesti, että osassa luvuista ilmenee pyöristysvirheitä. Esimerkiksi edellisessä tehtävässä Pekan saldo 30.7 saattaa tulostua muodossa 30.700000000000003. Tämä liittyy siihen, että liukuluvut kuten double tallennetaan oikeasti binäärimuodossa, eli nollina ja ykkösinä vain rajattua määrää lukuja käyttäen.

Koska liukulukuja on ääretön määrä (keksitkö miksi? kuinka monta liuku- tai desimaalilukua mahtuu vaikkapa lukujen 5 ja 6 väliin?), ei kaikkia liukulukuja yksinkertaisesti voi esittää rajatulla määrällä nollia ja ykkösiä. Tietokone joutuu siis rajoittamaan tallennustarkkuutta.

Normaalisti esimerkiksi tilien saldot tallennetaan kokonaislukuina siten, että arvo 1 vastaa esimerkiksi yhtä senttiä.

Metodin parametrit

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

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 -- koodi käytännössä asettaa parametrina saadulle pituus-muuttujalle siinä jo olevan arvon:

public void setPituus(int pituus) {
    // ÄLÄ TEE NÄIN!!!
    pituus = pituus;
}
public void setPituus(int pituus) {
    // VAAN NÄIN!!!
    this.pituus = pituus;
}

Luo luokka Kertoja jolla on:

  • Konstruktori public Kertoja(int luku).
  • Metodi public int kerro(int luku) joka palauttaa sille annetun luvun luku kerrottuna konstruktorille annetulla luvulla luku.

Tarvinnet tässä myös oliomuuttujan...

Esimerkki luokan käytöstä:

Kertoja kolmellaKertoja = new Kertoja(3);

System.out.println("kolmellaKertoja.kerro(2): " + kolmellaKertoja.kerro(2));

Kertoja neljallaKertoja = new Kertoja(4);

System.out.println("neljallaKertoja.kerro(2): " + neljallaKertoja.kerro(2));
System.out.println("kolmellaKertoja.kerro(1): " + kolmellaKertoja.kerro(1));
System.out.println("neljallaKertoja.kerro(1): " + neljallaKertoja.kerro(1));

Tulostus

kolmellaKertoja.kerro(2): 6
neljallaKertoja.kerro(2): 8
kolmellaKertoja.kerro(1): 3
neljallaKertoja.kerro(1): 4

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:

 

Lukujen määrä

Tee luokka Lukutilasto (tiedosto luomaasi luokkaa varten on tehtäväpohjassa valmiina), joka tuntee seuraavat toiminnot:

  • metodi lisaaLuku lisää uuden luvun tilastoon
  • metodi haeLukujenMaara kertoo lisättyjen lukujen määrän

Luokan ei tarvitse tallentaa mihinkään lisättyjä lukuja, vaan riittää muistaa niiden määrä. Metodin lisaaLuku ei tässä vaiheessa tarvitse edes ottaa huomioon, mikä luku lisätään tilastoon, koska ainoa tallennettava asia on lukujen määrä.

Luokan runko on seuraava:

public class Lukutilasto {
    private int lukujenMaara;

    public Lukutilasto() {
        // alusta tässä muuttuja lukujenMaara
    }

    public void lisaaLuku(int luku) {
        // kirjoita koodia tähän
    }

    public int haeLukujenMaara() {
        // kirjoita koodia tähän
    }
}

Seuraava ohjelma esittelee luokan käyttöä:

public class Paaohjelma {
    public static void main(String[] args) {
        Lukutilasto tilasto = new Lukutilasto();
        tilasto.lisaaLuku(3);
        tilasto.lisaaLuku(5);
        tilasto.lisaaLuku(1);
        tilasto.lisaaLuku(2);
        System.out.println("Määrä: " + tilasto.haeLukujenMaara());
    }
}

Ohjelman tulostus on seuraava:

Määrä: 4

Summa ja keskiarvo

Laajenna luokkaa seuraavilla toiminnoilla:

  • metodi summa kertoo lisättyjen lukujen summan (tyhjän lukutilaston summa on 0)
  • metodi keskiarvo kertoo lisättyjen lukujen keskiarvon (tyhjän lukutilaston keskiarvo on 0)

Luokan runko on seuraava:

public class Lukutilasto {
    private int lukujenMaara;
    private int summa;

    public Lukutilasto() {
        // alusta tässä muuttujat maara ja summa
    }

    public void lisaaLuku(int luku) {
        // kirjoita koodia tähän
    }

    public int haeLukujenMaara() {
        // kirjoita koodia tähän
    }

    public int summa() {
        // kirjoita koodia tähän
    }

    public double keskiarvo() {
        // kirjoita koodia tähän
    }
}

Seuraava ohjelma esittelee luokan käyttöä:

public class Main {
    public static void main(String[] args) {
        Lukutilasto tilasto = new Lukutilasto();
        tilasto.lisaaLuku(3);
        tilasto.lisaaLuku(5);
        tilasto.lisaaLuku(1);
        tilasto.lisaaLuku(2);
        System.out.println("Määrä: " + tilasto.haeLukujenMaara());
        System.out.println("Summa: " + tilasto.summa());
        System.out.println("Keskiarvo: " + tilasto.keskiarvo());
    }
}

Ohjelman tulostus on seuraava:

Määrä: 4
Summa: 11
Keskiarvo: 2.75

Summa käyttäjältä

Tee ohjelma, joka kysyy lukuja käyttäjältä, kunnes käyttäjä antaa luvun -1. Sitten ohjelma ilmoittaa lukujen summan.

Ohjelmassa tulee käyttää Lukutilasto-olioa summan laskemiseen.

HUOM: älä muuta Lukutilasto-luokkaa millään tavalla!

Anna lukuja:
4
2
5
4
-1
Summa: 15

Monta summaa

Muuta edellistä ohjelmaa niin, että ohjelma laskee myös parillisten ja parittomien lukujen summaa.

HUOM: Määrittele ohjelmassa kolme Lukutilasto-olioa ja laske ensimmäisen avulla kaikkien lukujen summa, toisen avulla parillisten lukujen summa ja kolmannen avulla parittomien lukujen summa.

Jotta testi toimisi, on oliot luotava pääohjelmassa edellä mainitussa järjestyksessä (eli ensin kaikkien summan laskeva olio, toisena parillisten summan laskeva ja viimeisenä parittomien summan laskeva olio)!

HUOM: älä muuta Lukutilasto-luokaa millään tavalla!

Ohjelman tulee toimia seuraavasti:

Anna lukuja:
4
2
5
2
-1
Summa: 13
Parillisten summa: 8
Parittomien summa: 5

Mistä olio-ohjelmoinnissa oikein on kyse: katsaus taaksepäin

Olio-ohjelmoinnissa on kyse pitkälti käsitteiden eristämisestä omiksi kokonaisuuksikseen tai toisin ajatellen abstraktioiden muodostamisesta. Voisi ajatella, että on turhaa luoda oliota jonka sisällä on ainoastaan luku, sillä saman voisi tehdä suoraan int-muuttujilla. Asia ei kuitenkaan ole aina 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. Eräs kuuluisa ohjelmoija on sanonut "Any fool can write code that a computer can understand. Good programmers write code that humans can understand". Koska viisari on oma selkeä käsitteensä, kannattaa ohjelman ymmärrettävyyden parantamiseksi siitä tehdä oma luokka.

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. Seuraavassa osiossa teemme myös 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.

Lausahdus ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista tulee toistumaan kurssilla.

Nyt termistökin saattaa tuntua jo tutummalta..

Olio

Olio on itsenäinen kokonaisuus, johon liittyy tietoa (oliomuuttujat) sekä käyttäytymistä (metodit). Oliot voivat olla hyvin erilaisia rakenteeltaan ja toiminnaltaan: jotkut voivat kuvata ongelma-alueen käsitteitä, ja jotkut voivat koordinoida olioiden välistä toimintaa. Olioiden kommunikointi tapahtuu metodikutsujen avulla -- metodikutsuilla sekä kysytään tietoa olioita että annetaan olioille käskyjä.

Yleisesti ottaen jokaisella oliolla on selkeästi määritellyt rajat ja toiminnallisuudet, jonka lisäksi jokainen olio tietää vain niistä muista olioista, joita se tarvitsee tehtävänsä tekemiseen. Toisin sanoen, olio piilottaa oman sisäisen toimintansa ja tarjoaa pääsyn toiminnallisuuksiin selkeästi määriteltyjen metodien kautta. Tämän lisäksi olio on riippumaton niistä olioista, joita se ei tehtäväänsä tarvitse.

Edellä käsiteltiin Henkilö-oliota, jota varten luotiin Henkilö-luokka. Kertauksen vuoksi on hyvä muistella luokan tehtävää: luokka sisältää olioiden tekemiseen tarvittavat rakennuspiirrustukset sekä määrittelee olioiden muuttujat ja metodit. Olio luodaan luokassa olevan konstruktorin perusteella.

Henkilö-olioomme liittyi nimi, ikä, paino ja pituus sekä muutamia metodeja. Jos mietimme henkilö-oliomme rakennetta tarkemmin, keksisimme varmaankin lisää henkilöihin liittyviä muuttujia kuten henkilöturvatunnus, puhelinnumero, osoite ja silmien väri. Pitäydytään toistaiseksi kuitenkin edellä mainituissa muuttujissa.

Olion käyttäytyminen määräytyy metodien avulla. Todellisuudessa henkilöt voivat tehdä hyvin monia erilaisia asioita, mutta henkilöitä käsittelevää sovellusta rakennettaessa henkilöön liittyvät toiminnallisuudet rakennetaan ongelma-alueen perusteella. Esimerkiksi elämänhallintaan tarkoitettu sovellus voisi pitää kirjaa edellä mainituista iästä, painosta ja pituudesta, sekä tarjota mahdollisuuden painoindeksin ja maksimisykkeen laskemiseen.

Oliot tarjoavat tyypillisesti pääsyn myös niiden tilaan. Olion tila on sen oliomuuttujien arvo kullakin ajanhetkellä.

Java-ohjelmointikielellä Henkilö-olion, joka pitää kirjaa nimestä, iästä, painosta ja pituudesta, sekä tarjoaa mahdollisuuden painoindeksi ja maksimisykkeen laskemiseen näyttäisi esimerkiksi seuraavalta. Huomaa, että alla oleva esimerkki poikkeaa hieman edellä rakennetusta esimerkistä. Alla pituus ja paino ilmaistaan doubleina -- pituuden yksikkö on metri.

public class Henkilo {
    private String nimi;
    private int ika;
    private double paino;
    private double pituus;

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

    public double painoindeksi() {
        return this.paino / (this.pituus * this.pituus);
    }

    public double maksimisyke() {
        return 206.3 - (0.711 * this.ika);
    }

    public String toString() {
        return this.nimi + ", BMI: " + this.painoindeksi()
            + ", maksimisyke: " + this.maksimisyke();
    }
}

Annetun henkilön maksimisykkeen ja painoindeksin selvittäminen on suoraviivaista edellä kuvatun Henkilo-luokan avulla.

Scanner lukija = new Scanner(System.in);
System.out.println("Mikä on nimesi?");
String nimi = lukija.nextLine();
System.out.println("Mikä on ikäsi?");
int ika = Integer.parseInt(lukija.nextLine());
System.out.println("Mikä on painosi?");
double paino = Double.parseDouble(lukija.nextLine());
System.out.println("Mikä on pituutesi?");
double pituus = Double.parseDouble(lukija.nextLine());

Henkilo henkilo = new Henkilo(nimi, ika, paino, pituus);
System.out.println(henkilo);
Mikä on nimesi?
Napoleone Buonaparte
Mikä on ikäsi?
51
Mikä on painosi?
80
Mikä on pituutesi?
1.70
Napoleone Buonaparte, BMI: 27.68166089965398, maksimisyke: 170.03900000000002

Luokka

Luokka määrittelee minkälaisia olioita siitä voidaan luoda. Se sisältää olion tietoa kuvaavat oliomuuttujat, olion luomiseen käytettävän konstruktorin tai konstruktorit, sekä olion käyttäytymisen määrittelevät metodit. Alla on kuvattuna luokka Suorakulmio, joka määrittelee eräänlaisen suorakulmion toiminnallisuuden-

// luokka
public class Suorakulmio {

    // oliomuuttujat
    private int leveys;
    private int korkeus;

    // konstruktori
    public Suorakulmio(int leveys, int korkeus) {
        this.leveys = leveys;
        this.korkeus = korkeus;
    }

    // metodit
    public void levenna() {
        this.leveys++;
    }

    public void kavenna() {
        if (this.leveys > 0) {
            this.leveys--;
        }
    }

    public int pintaAla() {
        return this.leveys * this.korkeus;
    }

    public String toString() {
        return "(" + this.leveys + ", " + this.korkeus + ")";
    }
}

Osa edellä määritellyistä metodeista ei palauta arvoa (metodit, joiden määrittelyssä käytetään avainsanaa void), ja osa metodeista palauttaa arvon (metodit, joiden määrittelyssä kerrotaan palautettavan muuttujan tyyppi). Yllä olevassa luokassa on määriteltynä myös metodi toString, jota käytetään olion sisäisen tilan tulostamiseen.

Luokasta luodaan olioita konstruktorin avulla new-komennolla. Alla luodaan kaksi suorakulmiota ja tulostaan niihin liittyvää tietoa.

Suorakulmio eka = new Suorakulmio(40, 80);
Suorakulmio nelio = new Suorakulmio(10, 10);
System.out.println(eka);
System.out.println(nelio);

eka.kavenna();
System.out.println(eka);
System.out.println(eka.pintaAla());
(40, 80)
(10, 10)
(39, 80)
3920

Luo kirjaa esittävä luokka Kirja. Jokaisella kirjalla on kirjailija, nimi ja sivujen lukumäärä.

Tee luokalle:

  • Konstruktori public Kirja(String kirjailija, String nimi, int sivuja)
  • Metodi public String getKirjailija() joka palauttaa kirjan kirjailijan nimen.
  • Metodi public String getNimi() joka palauttaa kirjan nimen.
  • Metodi public int getSivuja() joka palauttaa kirjan sivujen lukumäärän.
  • Tee kirjalle lisäksi public String toString()-metodi, jota käytetään kirja-olion tulostamiseen. Metodin kutsun tulee tuottaa esimerkiksi seuraavanlainen tulostus:
    J. K. Rowling, Harry Potter ja viisasten kivi, 223 sivua
    

Karvosen kaavan avulla voidaan laskea tavoitesyke fyysistä harjoittelua varten. Tavoitesykkeen laskeminen perustuu kaavaan (maksimisyke - leposyke) * (tavoitesykeprosentti) + leposyke, missä tavoitesyke annetaan prosenttina maksimisykkeestä.

Esimerkiksi, jos henkilön maksimisyke on 200, leposyke 50, ja tavoitesyke 75% maksimisykkeestä, on tavoiteltava sydämen syke noin ((200-50) * (0.75) + 50) eli 162.5 lyöntiä minuutissa.

Luo luokka Harjoitusapuri, jolle annetaan konstruktorin parametrina ikä ja leposyke. Harjoitusapurin tulee tarjota metodi tavoitesyke, jolle annetaan parametrina prosentuaalista maksimisykkeen osuutta kuvaava double-tyyppinen luku. Osuus annetaan lukuna nollan ja yhden välillä. Luokalla tulee olla:

  • Konstruktori public Harjoitusapuri(int ika, int leposyke)
  • Metodi public double tavoitesyke(double prosenttiaMaksimista), joka laskee ja palauttaa tavoiteltavan sykkeen.

Käytä maksimisykkeen laskemiseen kaavaa 206.3 - (0.711 * ikä).

Käyttöesimerkki:

Harjoitusapuri apuri = new Harjoitusapuri(30, 60);

double prosenttiosuus = 0.5;

while (prosenttiosuus < 1.0) {
    double tavoite = apuri.tavoitesyke(prosenttiosuus);
    System.out.println("Tavoite " + (prosenttiosuus * 100) + "% maksimista: " + tavoite);
    prosenttiosuus += 0.1;
}
Tavoite 50.0% maksimista: 122.48500000000001
Tavoite 60.0% maksimista: 134.98200000000003
Tavoite 70.0% maksimista: 147.479
Tavoite 80.0% maksimista: 159.976
Tavoite 89.99999999999999% maksimista: 172.473
Tavoite 99.99999999999999% maksimista: 184.97000000000003

Tässä tehtävässä tehdään luokka YlhaaltaRajoitettuLaskuri ja sovelletaan sitä kellon tekemiseen.

Rajoitettu laskuri

Tehdään luokka YlhaaltaRajoitettuLaskuri. Luokan olioilla on seuraava toiminnallisuus:

  • Laskurilla on oliomuuttuja, joka muistaa laskurin arvon. Laskurin arvo on luku väliltä 0...yläraja.
  • Aluksi laskurin arvo on 0.
  • Olion konstruktori määrittää laskurin ylärajan.
  • Metodi seuraava kasvattaa laskurin arvoa. Mutta jos laskurin arvo ylittää ylärajan, sen arvoksi tulee 0.
  • Metodi toString palauttaa laskurin arvon merkkijonona.

Tehtäväpohjassa on valmiina pääohjelmaa varten tiedosto Paaohjelma. Aloita tekemällä luokka YlhaaltaRajoitettuLaskuri vastaavasti kuin Maksukortti-tehtävässä. Näin tehdään myös tulevissa tehtäväsarjoissa.

Luokan rungoksi tulee seuraava:

public class YlhaaltaRajoitettuLaskuri {
    private int arvo;
    private int ylaraja;

    public YlhaaltaRajoitettuLaskuri(int ylarajanAlkuarvo) {
        // kirjoita koodia tähän
    }

    public void seuraava() {
        // kirjoita koodia tähän
    }

    public String toString() {
        // kirjoita koodia tähän
    }
}

Vihje: et voi palauttaa toStringissä suoraan kokonaislukutyyppisen oliomuuttujan laskuri arvoa. Kokonaislukumuuttujasta arvo saa merkkijonomuodon esim. lisäämällä sen eteen tyhjän merkkijonon eli kirjoittamalla "" + arvo.

Seuraavassa on pääohjelma, joka käyttää laskuria:

public class Paaohjelma {
    public static void main(String[] args) {
        YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(4);
        System.out.println("arvo alussa: " + laskuri);

        int i = 0;
        while (i < 10) {
            laskuri.seuraava();
            System.out.println("arvo: " + laskuri);
            i++;
        }
    }
}

Laskurille asetetaan konstruktorissa ylärajaksi 4, joten laskurin arvo on luku 0:n ja 4:n väliltä. Huomaa, miten metodi seuraava vie laskurin arvoa eteenpäin, kunnes se pyörähtää 4:n jälkeen 0:aan:

Ohjelman tulostuksen tulisi olla seuraava:

arvo alussa: 0
arvo: 1
arvo: 2
arvo: 3
arvo: 4
arvo: 0
arvo: 1
arvo: 2
arvo: 3
arvo: 4
arvo: 0

Etunolla tulostukseen

Tee toString-metodista sellainen, että se lisää arvon merkkijonoesitykseen etunollan, jos laskurin arvo on vähemmän kuin 10. Eli jos laskurin arvo on esim. 3, palautetaan merkkijono "03", jos arvo taas on esim. 12, palautetaan normaaliin tapaan merkkijono "12".

Muuta pääohjelma seuraavaan muotoon ja varmista, että tulos on haluttu.

public class Paaohjelma {
    public static void main(String[] args) {
        YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(14);
        System.out.println("arvo alussa: " + laskuri);
 
        int i = 0;
        while (i < 16) {
            laskuri.seuraava();
            System.out.println("arvo: " + laskuri);
            i++;
        }
    }
}
arvo alussa: 00
arvo: 01
arvo: 02
arvo: 03
arvo: 04
arvo: 05
arvo: 06
arvo: 07
arvo: 08
arvo: 09
arvo: 10
arvo: 11
arvo: 12
arvo: 13
arvo: 14
arvo: 00
arvo: 01

Kello, ensimmäinen versio

Käyttämällä kahta laskuria voimme muodostaa kellon. Tuntimäärä on laskuri, jonka yläraja on 23, ja minuuttimäärä on laskuri jonka yläraja on 59. Kuten kaikki tietävät, kello toimii siten, että aina kun minuuttimäärä pyörähtää nollaan, tuntimäärä kasvaa yhdellä.

Tee ensin laskurille metodi arvo, joka palauttaa laskurin arvon:

public int arvo() {
    // kirjoita koodia tähän
}

Tee sitten kello täydentämällä seuraava pääohjelmarunko (kopioi tämä pääohjelmaksesi sekä täydennä tarvittavilta osin kommenttien ohjaamalla tavalla):

public class Paaohjelma {
    public static void main(String[] args) {
        YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23);

        int i = 0;
        while (i < 121) {
            System.out.println(tunnit + ":" + minuutit);   // tulostetaan nykyinen aika
            // minuuttimäärä kasvaa
            // jos minuuttimäärä menee nollaan, tuntimäärä kasvaa

            i++;
        }
    }
}

Jos kellosi toimii oikein, sen tulostus näyttää suunnilleen seuraavalta:

00:00
00:01
...
00:59
01:00
01:01
01:02
...
01:59
02:00

Kello, toinen versio

Laajenna kelloasi myös sekuntiviisarilla. Tee lisäksi luokalle YlhaaltaRajoitettuLaskuri metodi asetaArvo, jolla laskurille pystyy asettamaan halutun arvon -- jos et ole ihan varma mitä tässä pitäisi tehdä, kertaa materiaalista kohta missä puhutaan "settereistä".

Jos laskurille yritetään asettaa kelvoton arvo eli negatiivinen luku tai ylärajaa suurempi luku, ei laskurin arvo muutu.

Tämän metodin avulla voit muuttaa kellon ajan heti ohjelman alussa haluamaksesi.

Voit testata kellon toimintaa seuraavalla ohjelmalla

import java.util.Scanner;

public class Paaohjelma {
    public static void main(String[] args)  {
        Scanner lukija = new Scanner(System.in);
        YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23);

        System.out.print("sekunnit: ");
        int sek = // kysy sekuntien alkuarvo käyttäjältä
        System.out.print("minuutit: ");
        int min = // kysy minuuttien alkuarvo käyttäjältä
        System.out.print("tunnit: ");
        int tun = // kysy tuntien alkuarvo käyttäjältä

        sekunnit.asetaArvo(sek);
        minuutit.asetaArvo(min);
        tunnit.asetaArvo(tun);

        int i = 0;
        while (i < 121) {
            // lisää edelliseen myös sekuntiviisari
            i++;
        }

    }
}

Kokeile laittaa kellosi alkamaan ajasta 23:59:50 ja varmista, että vuorokauden vaihteessa kello toimii odotetusti!

Bonus-tehtävä: ikuisesti käyvä kello (tehtävää ei palauteta!)

Ennen kuin alat tekemään tätä tehtävää, palauta jo tekemäsi kello!

Muuta pääohjelmasi seuraavaan muotoon:

public class Paaohjelma {
    public static void main(String[] args) throws Exception {
        YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59);
        YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23);

        sekunnit.asetaArvo(50);
        minuutit.asetaArvo(59);
        tunnit.asetaArvo(23);

        while (true) {
            System.out.println(tunnit + ":" + minuutit + ":" + sekunnit);
            Thread.sleep(1000);
            // lisää kellon aikaa sekunnilla eteenpäin
        }
    }
}

Nyt kello käy ikuisesti ja kasvattaa arvoaan sekunnin välein. Sekunnin odotus tapahtuu komennolla Thread.sleep(1000);, komennon parametri kertoo nukuttavan ajan millisekunteina. Jotta komento toimisi, pitää main:in esittelyriville tehdä pieni lisäys: public static void main(String[] args) throws Exception {, eli tummennettuna oleva throws Exception.

Saat ohjelman lopetettua painamalla NetBeans-konsolin (eli sen osan johon kello tulostaa arvonsa) vasemmalla laidalla olevasta punaisesta laatikosta.

Olio-ohjelmoinnin periaatteet

Olio-ohjelmointiin kuuluu oleellisesti kolme periaatetta: abstrahointi, kapselointi ja perintä. Käsittelemme periaatteet tässä lyhyesti; perintään tutustutaan tarkemmin ohjelmoinnin jatkokurssille.

Abstrahointi

Abstrahoinnin tavoitteena on ongelma-alueen käsitteellistäminen. Ohjelmoija pyrkii nimeämään ongelma-alueen käsitteitä kuvaavat luokat, oliot, metodit ja muuttujat mahdollisimman hyvin, jolloin muut näitä käyttävät ohjelmoijat ymmärtävät mistä kussakin on kyse.

Käytännössä kyse on siis prosessista, missä ongelma-alueesta tunnistetaan ja eristetään oleellisimmat piirteet, joiden perusteella niistä luodaan ohjelmaan toiminnallisuutta. Samalla pyritään tilanteeseen, missä ongelma-alueesta on poimittu vain ne käsitteet, jotka ovat oleellisia käsiteltävän ongelman ratkaisun kannalta.

Otetaan analogia tosielämästä ja puhutaan käsitteestä auto. Jokainen tietää mistä autossa on kyse ja mihin sitä käytetään. Moni pystyisi myös piirtämään auton pyydettäessä. Käsite auto kuitenkin piilottaa paljon pienempiä osia: autossa on esimerkiksi renkaat, runko, moottori, istuimia, ... Kukin näistä käsitteistä on myös oma abstraktionsa. Esimerkiksi rengas piilottaa myös pienempiä osia -- renkaalla on vanne, ulkokumi, sisäkumi, jnejne.

Abstrahoinnista on ohjelmoijalle useita etuja. Se helpottaa asioista puhumista ja sitä kautta niiden käsittelyä. Se helpottaa ohjelmointia -- esimerkiksi käyttämämme apukirjastot kuten Scanner ovat toisten tekemiä valmiita abstraktioita ohjelmointiin liittyvistä tyypillisistä ongelmista. Se myös helpottaa omaa ohjelmointiamme: aivan kuten auto koostuu useammasta pienemmästä osasta, voimme koostaa ohjelmamme useammasta abstraktiosta ja luoda tätä kautta uusia abstraktioita.

Kapselointi

Kapseloinissa on kyse toteutuksen piilottamisesta julkisen rajapinnan taakse. Käytännössä olio-ohjelmoinissa kyse on muuttujien ja metodien konkreettisen toiminnallisuuden piilottamisesta olion "sisään". Olion käyttöön tarvittavat metodit (ml. konstruktorit) -- eli rajapinta -- ovat käyttäjän nähtävissä, mutta käyttäjä ei pääse käsiksi olion sisäiseen toteutukseen.

Tällöin toiset oliot voivat kutsua olion metodeja ilman, että niiden tarvitsee tietää kutsuttavan olion sisäisestä tilasta tai metodien konkreettisesta toteutuksesta. Tällöin kukin olio on myös itsenäinen, eikä niiden sisäinen tila ole riippuvainen toisten olioiden sisäisestä tilasta.

Perintä

Olio-ohjelmoinnissa on mahdollista luoda luokkia, jotka perivät toisen luokan ominaisuuksia (eli oliomuuttujat ja metodit). Tällöin luokasta tehdyt oliot ovat samalla myös perityn luokan ilmentymiä, jolloin oliot voivat esiintyä useampina erilaisina olioina käyttötarpeesta riippuen.

Palaamme perintään ohjelmoinnin jatkokurssilla...

Yhteenveto

Tässä osassa otettiin ensiaskeleet olio-ohjelmoinnin saloihin. Tutustuimme käsitteisiin luokka ja olio, ja loimme useita erilaisia luokkia sekä oliota näiden luokkien perusteella. Jatkossa tulemme näkemään laajempia ohjelmia, missä ohjelmien toiminta rakentuu useista olioista.

Tässä tehtävässä saat käyttöösi valmiit luokat Puhelinmuistio ja Henkilo. Tehtävänäsi on toteuttaa ohjelma, joka tarjoaa mahdollisuuden henkilöiden lisäämiseen, poistamiseen, etsimiseen (puhelinnumeron perusteella) ja listaamiseen.

Tee toteutuksesi luokkaan PuhelinmuistioSovellus.

Voit oikeastaan halutessasi ohjelmoida tehtävän vaatiman toteutuksen katsomatta luokkien Puhelinmuistio ja Henkilo sisäistä toteutusta lainkaan. Tässä on kyse abstrahoinnista ja kapseloinnista -- tiedät toki Puhelinmuistion tarjoamat toiminnallisuudet, jotka ovat seuraavat:

  • public void listaa() listaa kaikki puhelinmuistiossa olevat henkilöt.
  • public void lisaa(String etunimi, String sukunimi, String puhelinnumero) lisää puhelinmuistioon henkilön mikäli henkilö ei ole vielä puhelinmuistiossa.
  • public void poista(String etunimi, String sukunimi) poistaa puhelinmuistiosta henkilön etunimen ja sukunimen perusteella.
  • public void etsiEtunimella(String etunimi) etsii ja listaa henkilöt, joiden etunimessä on parametrina annettu merkkijono.
  • public void etsiSukunimella(String sukunimi) etsii ja listaa henkilöt, joiden sukunimessä on parametrina annettu merkkijono.
  • public void etsiPuhelinnumerolla(String puhelinnumero) etsii ja listaa henkilöt, joiden puhelinnumerossa esiintyy parametrina annettu merkkijono.
  • public void lataa(String tiedostosta) lataa parametrina annetun nimisestä tiedostosta henkilöt puhelinmuistioon. Tiedoston formaatin tulee olla etunimi\tsukunimi\tpuhelinnumero, missä \t viittaa sarkainmerkkeihin.
  • public void tallenna(String tiedostoon) tallentaa puhelinmuistiossa olevat henkilöt parametrina annettuun listaan.

Sovelluksen tulee toimia seuraavalla tavalla. Tehtäväpohjan mukana tulee valmiina esimerkissä käytetty tiedosto nimet.tsv.

Minkä niminen tiedosto ladataan? nimet.tsv

Puhelinmuistio
Komennot:
lopeta - lopettaa puhelinmuistion
lisaa - lisää uuden henkilön puhelinmuistioon
listaa - listaa puhelinmuistion sisällön
etsi - etsii puhelinmuistiosta annettua numeroa
poista - poistaa henkilön puhelinmuistiosta

Komento: listaa
William	Goddard	1968-3503060-1
Reynold	Johnson	1956-350
John	Lynott	1968-3503060-2

Komento: lisaa
Kenet lisätään? 
Etunimi: Jurassic
Sukunimi: Park
Puhelinnumero: 1993-63000000

Komento: listaa
William	Goddard	1968-3503060-1
Reynold	Johnson	1956-350
John	Lynott	1968-3503060-2
Jurassic	Park	1993-63000000

Komento: poista
Kuka poistetaan? 
Etunimi: Jurassic
Sukunimi: Park

Komento: listaa
William	Goddard	1968-3503060-1
Reynold	Johnson	1956-350
John	Lynott	1968-3503060-2

Komento: etsi
Mitä numeroa etsitään? 123-1234
Ei tuloksia.

Komento: etsi
Mitä numeroa etsitään? 1956-350
Tuloksia yhteensä: 1
Reynold	Johnson	1956-350

Komento: lopeta
Minkä nimiseen tiedostoon muistio tallennetaan? nimet-2.tsv

Ohjelman suorituksen jälkeen puhelinmuistion käsittelemät henkilöt löytyvät tiedostosta nimet-2.tsv.

Sisällysluettelo