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 ohjelmastan sisältä. 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 montako 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:

  • mitä metodeita olioilla on
  • minkälainen olioiden tila on tai toisinsanoen mitä ominaisuuksia olioilla on

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

72.1 Ensimmäinen tilisi

Huom: tämän tehtävän jokaista alikohtaa varten on oma tehtäväpohja, tee tämä tehtävä pohjaan 072.1

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

72.2 Ensimmäinen tilisiirtosi

Huom: tämän tehtävän jokaista alikohtaa varten on oma tehtäväpohja, tee tämä tehtävä pohjaan 072.2

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. Tulosta molemmat tilit

72.3 Tilisiirtoja

Huom: tämän tehtävän jokaista alikohtaa varten on oma tehtäväpohja, tee tämä tehtävä pohjaan 072.3

Yllä siirsit rahaa tililtä toiselle. Tehdään seuraavaksi metodi joka tekee saman!

Toteuta ohjelmapohjaan metodi public static void tilisiirto(Tili mista, Tili minne, double paljonko) joka siirtää rahaa tililtä toiselle. Sinun ei tarvitse tarkistaa että mista-tilin saldo riittää.

Tämän jälkeen tee main-metodissasi seuraavaa:

  1. Luo tili "A" saldolla 100.0
  2. Luo tili "B" saldolla 0.0
  3. Luo tili "C" saldolla 0.0
  4. Siirrä 50.0 tililtä A tilille B.
  5. Siirrä 25.0 tililtä B tilille C.

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

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 073.Tuote 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
          

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

Luo luokka Kertoja jolla on:

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

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

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

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

75.2 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ä:

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

75.3 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

75.4 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 oliomuuttujan johon talletetaan laskurin alussa saama arvo.

Kumpulan kampuksella Helsingissä toimivaan Unicafe-nimiseen gourmet-ravintolaan tarvitaan uusi ruokalista. Keittiömestari tietää ohjelmoinnista, ja haluaa listan hallinnointiin tietokonejärjestelmän. Toteutetaan tässä tehtävässä järjestelmän sydän, luokka Ruokalista.

Tehtäväpohjan mukana tulee Main-luokka, jossa voit testata ruokalistan toimintaa. Ruokalistan toteuttamista varten saat seuraavanlaisen tehtäväpohjan:

import java.util.ArrayList;

public class Ruokalista {

    private ArrayList<String> ateriat;

    public Ruokalista() {
        this.ateriat = new ArrayList<String>();
    }

    // toteuta tänne tarvittavat metodit
}

Ruokalistaoliolla on siis oliomuuttujana ArrayList, jonka on tarkoitus tallentaa ruokalistalla olevien ruokalajien nimet.

Ruokalistan tulee tarjota metodit public void lisaaAteria(String ateria), public void tulostaAteriat(), ja public void tyhjennaRuokalista().

76.1 Aterian lisääminen

Toteuta metodi public void lisaaAteria(String ateria), joka lisää uuden aterian ruokalistan ateriat-listaan. Jos lisättävä ateria on jo listassa, sitä ei lisätä uudelleen.

76.2 Aterioiden tulostaminen

Toteuta metodi public void tulostaAteriat(), joka tulostaa ateriat. Esimerkiksi kolmen aterian lisäyksen jälkeen tulostuksen tulee olla seuraavanlainen.

ensimmäisenä lisätty ateria
toisena lisätty ateria
kolmantena lisätty ateria

76.3 Ruokalistan tyhjentäminen

Toteuta metodi public void tyhjennaRuokalista() joka tyhjentää ruokalistan. ArrayList-luokalla on metodi josta on tässä hyötyä. NetBeans osaa vihjata käytettävissä olevista metodeista kun kirjoitat olion nimen ja pisteen. Yritä kirjoittaa ateriat. metodirungon sisällä ja katso mitä käy.

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:

Helsingin Yliopiston opiskelijaruokaloissa eli Unicafeissa opiskelijat maksavat lounaansa käyttäen Lyyra-korttia.

Tässä tehtäväsäsarjassa tehdään luokka LyyraKortti, jonka tarkoituksena on jäljitellä Lyyra-kortin käyttämistä Unicafessa.

77.1 Luokan runko

Projektiin tulee kuulumaan kaksi kooditiedostoa:

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

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

Tee ensin LyyraKortti-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 LyyraKortti runko:

public class LyyraKortti {
    private double saldo;

    public LyyraKortti(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) {
        LyyraKortti kortti = new LyyraKortti(50);
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 50.0 euroa

77.2 Kortilla maksaminen

Täydennä LyyraKortti-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.50 eurolla ja metodin syoMaukkaasti tulisi vähentää kortin saldoa 4.00 eurolla.

Seuraava pääohjelma testaa luokkaa:

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

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

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

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 50.0 euroa
Kortilla on rahaa 47.5 euroa
Kortilla on rahaa 41.0 euroa

77.3 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) {
        LyyraKortti kortti = new LyyraKortti(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 1.0 euroa
Kortilla on rahaa 1.0 euroa

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

77.4 Kortin lataaminen

Lisää LyyraKortti-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) {
        LyyraKortti kortti = new LyyraKortti(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

77.5 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) {
        LyyraKortti kortti = new LyyraKortti(10);
        System.out.println(kortti);
        kortti.lataaRahaa(-15);
        System.out.println(kortti);
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 10.0 euroa
Kortilla on rahaa 10.0 euroa

77.6 Monta korttia

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

  • Luo Pekan lyyrakortti. Kortin alkusaldo on 20 euroa
  • Luo Matin lyyrakortti. 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) {
        LyyraKortti pekanKortti = new LyyraKortti(20);
        LyyraKortti matinKortti = new LyyraKortti(30);

        // kirjoita koodia tähän
    }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Pekka: Kortilla on rahaa 16.0 euroa
Matti: Kortilla on rahaa 27.5 euroa
Pekka: Kortilla on rahaa 36.0 euroa
Matti: Kortilla on rahaa 23.5 euroa
Pekka: Kortilla on rahaa 31.0 euroa
Matti: Kortilla on rahaa 73.5 euroa

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.

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

78.1 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 LyyraKortti-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

78.2 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

78.3 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

78.4 Kello, toinen versio

Laajenna kelloasi myös sekuntiviisarilla. Tee lisäksi luokalle YlhaaltaRajoitettuLaskuri metodi asetaArvo, jolla laskurille pystyy asettamaan halutun arvon. 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.

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

79.1 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

79.2 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

79.3 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-luokaa millään tavalla!

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

79.4 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

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:

  • Sataa räntää todennäköisyydellä 0.1 (10%)
  • Sataa lunta todennäköisyydellä 0.3 (30%)
  • Aurinko paistaa todennäköisyydellä 0.6 (60%)

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äpohjassa on luokka Noppa, jolla on seuraavat toiminnot:

  • konstruktori Noppa(int tahkojenMaara) luo uuden noppa-olion annetulla nopan tahkojen (eri oman numeronsa sisältämien "puolien") määrällä
  • metodi heita kertoo nopanheiton tuloksen (tulos riippuu tahkojen määrästä)

Luokan runko on seuraava:

import java.util.Random;

public class Noppa {
    private Random random = new Random();
    private int tahkojenMaara;

    public Noppa(int tahkojenMaara) {
      // Alusta muuttuja tahkojenMaara tässä
    }

    public int heita() {
        // arvotaan luku väliltä 1-tahkojenMaara
    }
}

Täydennä luokkaa Noppa siten, että noppa palauttaa jokaisella heitolla arvotun luvun väliltä 1...tahkojen määrä. Seuraavassa noppaa testaava pääohjelma:

public class Ohjelma {
    public static void main(String[] args) {
        Noppa noppa = new Noppa(6);

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

Tulostus voisi olla esimerkiksi seuraava:

1
6
3
5
3
3
2
2
6
1

Tehtävänäsi on täydentää luokkaa SalasananArpoja, jossa on seuraavat toiminnot:

  • konstruktori SalasananArpoja luo uuden olion, joka käyttää annettua salasanan pituutta
  • metodi luoSalasana palauttaa uuden, merkeistä a-z muodostetun konstruktorin parametrin määräämän pituisen salasanan

Luokan runko on seuraava:

import java.util.Random;

public class SalasananArpoja {
    // Määrittele muuttuja tässä

    public SalasananArpoja(int pituus) {
        // Alusta muuttuja tässä
    }

    public String luoSalasana() {
        // Kirjoita tähän koodi, joka palauttaa uuden salasanan
    }
}

Seuraavassa ohjelma joka käyttää SalasananArpoja-olioa:

public class Ohjelma {
    public static void main(String[] args) {
        SalasananArpoja arpoja = new SalasananArpoja(13);
        System.out.println("Salasana: " + arpoja.luoSalasana());
        System.out.println("Salasana: " + arpoja.luoSalasana());
        System.out.println("Salasana: " + arpoja.luoSalasana());
        System.out.println("Salasana: " + arpoja.luoSalasana());
    }
}

Ohjelman tulostus voisi näyttää seuraavalta:

Salasana: mcllsoompezvs
Salasana: urcxboisknkme
Salasana: dzaccatonjcqu
Salasana: bpqmedlbqaopq

Vihje1: näin muutat kokonaisluvun luku kirjaimeksi:

int luku = 17;
char merkki = "abcdefghijklmnopqrstuvxyz".charAt(luku);

Vihje2: tehtävän 78 vihjeestä lienee tässäkin tehtävässä apua.

Tehtävänäsi on täydentää luokkaa LottoRivi, joka arpoo viikon lottonumerot. Lottonumerot ovat väliltä 1–39 ja niitä arvotaan 7. Lottorivi koostuu siis 7:stä eri numerosta väliltä 1–39. Luokassa on seuraavat toiminnot:

  • konstruktori LottoRivi luo uuden LottoRivi-olion joka sisältää uudet, arvotut numerot
  • metodi numerot palauttaa tämän lottorivin lottonumerot
  • metodi arvoNumerot arpoo riville uudet numerot
  • metodi sisaltaaNumeron kertoo onko arvotuissa numeroissa annettu numero

Luokan runko on seuraava:

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

    public class LottoRivi {
    private ArrayList<Integer> numerot;

    public LottoRivi() {
        // Alustetaan lista numeroille
        this.numerot = new ArrayList<Integer>();
        // Arvo numerot heti LottoRivin luomisen yhteydessä
        this.arvoNumerot();
    }

    public ArrayList<Integer> numerot() {
        return this.numerot;
    }

    public void arvoNumerot() {
        // Kirjoita numeroiden arvonta tänne käyttämällä metodia sisaltaaNumeron()
    }

    public boolean sisaltaaNumeron(int numero) {
        // Testaa tässä onko numero jo arvottujen numeroiden joukossa
    }
}

Tehtäväpohjan mukana tulee seuraava pääohjelma:

import java.util.ArrayList;

    public class Ohjelma {
        public static void main(String[] args) {
        LottoRivi lottoRivi = new LottoRivi();
        ArrayList<Integer> lottonumerot = lottoRivi.numerot();

        System.out.println("Lottonumerot:");
        for (int numero : lottonumerot) {
            System.out.print(numero + " ");
        }
        System.out.println("");
    }
}

Ohjelman mahdollisia tulostuksia ovat seuraavat:

Lottonumerot:
3 5 10 14 15 27 37
Lottonumerot:
2 9 11 18 23 32 34

Huom! Sama numero saa esiintyä lottorivissä vain kerran.

Hirsipuu-pelistä pitävä kaverisi ohjelmoi hirsipuupelin joka näyttää seuraavalta.

Hänen koneestaan hajosi kovalevy, eikä hän ollut tehnyt varmuuskopiota. Hän on kuitenkin luvannut pelin lahjaksi pikkuveljelleen, ja aikataulukiireidensä takia tarvitsee apuasi pelin toteutukseen. Tässä tehtäväsarjassa toteutat kaverisi hirsipuu-pelille arvauslogiikan ja annetun sanan salaamisen.

Kaverisi on luonut pelille käyttöliittymän sekä valmiin rungon sovelluksen logiikkaan. Kun lataat projektin TMC:ltä, näet käyttöliittymä- ja testiluokkien lisäksi allaolevan ohjelmarungon.

public class HirsipuuLogiikka {

    private String sana;
    private String arvatutKirjaimet;
    private int virheidenLukumaara;

    public HirsipuuLogiikka(String sana) {
        this.sana = sana.toUpperCase();
        this.arvatutKirjaimet = "";
        this.virheidenLukumaara = 0;
    }

    public int virheidenLukumaara() {
        return this.virheidenLukumaara;
    }

    public String arvatutKirjaimet() {
        return this.arvatutKirjaimet;
    }

    public int virheitaHavioon() {
        return 12;
    }

    public void arvaaKirjain(String kirjain) {
        // Ohjelmoi tänne toiminnallisuus kirjaimen arvaamiseksi.

        // Arvattua kirjainta ei saa arvata uudestaan. Jos arvattava sana 
        // ei sisällä kirjainta, virheiden lukumäärän tulee kasvaa.
        // Lisää arvattu kirjain arvattuihin kirjaimiin

    }

    public String salattuSana() {
        // Ohjelmoi tänne toiminnallisuus salatun sanan luomiseksi
        // ja antamiseksi. 

        // Luo uusi salattu sana this.sana-merkkijonoa kirjain kirjaimelta 
        // läpikäyden. Jos kirjain on arvatuissa kirjaimissa, voit lisätä sen 
        // sellaisenaan. Jos ei, lisää uuteen sanaan merkki "_".

        // Muista varmistaa ettet ohjelmoi päättymätöntä toistolauseketta!

        // palauta lopuksi salattua sanaa kuvaava merkkijono.

        return "";
    }
}

Tässä tehtävässä sinun tulee lisätä toiminnallisuutta vain luokan HirsipuuLogiikka metodeihin arvaaKirjain(String kirjain) ja salattuSana().

Logiikan testaaminen

Tehtäväpohja sisältää kaksi testiluokkaa. Luokka Main käynnistää kaverisi toteuttaman graafisen version pelistä. Luokkaa Testiohjelma taas voit käyttää HirsipuuLogiikka-luokan testaamiseen.

83.1 Kirjaimen arvaaminen

Muokkaa tässä tehtävässä vain metodin arvaaKirjain(String kirjain) toiminnallisuutta.

Kun käyttäjä arvaa jonkin kirjaimen, kutsuu hirsipuun käyttöliittymä hirsipuulogiikan metodia arvaaKirjain jonka tulee tarkistaa onko arvattava kirjain jo arvattujen kirjainten (joita hirsipuulogiikka säilyttää oliomuuttujassa this.arvatutKirjaimet) joukossa. Jos kirjain on jo arvattujen kirjainten joukossa, metodin ei tule tehdä mitään. Jos arvattavaa kirjainta ei ole arvattu aiemmin, on tarkastettava onko se arvattavassa sanassa (jota hirsipuulogiikka säilyttää oliomuuttujassa this.sana). Jos ei, virheiden lukumäärää (oliomuuttuja this.virheidenLukumaara) pitää kasvattaa yhdellä. Arvattu kirjain pitää lopuksi lisätä osaksi arvattuja kirjaimia, jos se ei ole jo siellä.

Esimerkki arvaaKirjain-metodin toiminnasta:

HirsipuuLogiikka l = new HirsipuuLogiikka("kissa");
System.out.println("Arvataan: A, D, S, F, D");
l.arvaaKirjain("A");   // oikea
l.arvaaKirjain("D");   // väärä
l.arvaaKirjain("S");   // oikea
l.arvaaKirjain("F");   // väärä
l.arvaaKirjain("D");   // Tämän ei pitäisi kasvattaa virheiden lukumäärää koska D oli jo arvattu
System.out.println("Arvatut kirjaimet: "+l.arvatutKirjaimet());
System.out.println("Virheiden lukumäärä: "+l.virheidenLukumaara());
Arvataan: A, D, S, F, D
Arvatut kirjaimet: ADSF
Virheiden lukumäärä: 2

83.2 Salatun sanan luominen

Hirsipuun käyttöliittymä näyttää käyttäjälle salatun muodon arvattavana olevasta sanasta. Yllä olevassa kuvassa salattu sana on METO_I, eli arvattavana olevasta sanasta näytetään vain arvatut kirjaimet, muut korvataan alaviivalla. Salatun sanan muodostaa hirsipuulogiikan metodi salattuSana() jonka sisältöä tässä tehtävässä on tarkoitus muokata.

Salattu sana luodaan luomalla arvattavasta sanasta (this.sana) uusi versio. Jokainen kirjain jota ei ole vielä arvattu tulee vaihtaa merkkiin "_". Jos kirjain on jo arvattu, eli se löytyy arvatuista kirjaimista, voidaan se lisätä sellaisenaan salattuun sanaan.

Avainsanoista while, charAt ja contains lienee tästä hyötyä. Saat muutettua yksittäisen merkin merkkijonoksi seuraavasti:

char merkki = 'a';
String merkkijono = "" + merkki;

Esimerkki metodin toiminnasta:

HirsipuuLogiikka l = new HirsipuuLogiikka("kissa");
System.out.println("Sana on: "+l.salattuSana());

System.out.println("Arvataan: A, D, S, F, D");
l.arvaaKirjain("A");
l.arvaaKirjain("D");
l.arvaaKirjain("S");
l.arvaaKirjain("F");
l.arvaaKirjain("D");
System.out.println("Arvatut kirjaimet: "+l.arvatutKirjaimet());
System.out.println("Virheiden lukumäärä: "+l.virheidenLukumaara());
System.out.println("Sana on: "+l.salattuSana());
Sana on: _____
Arvataan: A, D, S, F, D
Arvatut kirjaimet: ADSF
Virheiden lukumäärä: 2
Sana on: __SSA

Testaa lopuksi sinun ja kaverisi yhteistyössä tekemää ohjelmaa Main-luokan avulla! Voit muuttaa arvattavaa sanaa muuttamalla HirsipuuLogiikka-luokan konstruktorille parametrina annettavaa merkkijonoa:

HirsipuuLogiikka logiikka = new HirsipuuLogiikka("parametri");
HirsipuuIkkuna peliIkkuna = new HirsipuuIkkuna(logiikka);
peliIkkuna.pelaa();

Peliä pelataan näppäimistöltä, pelin voi lopettaa painamalla peli-ikkunan vasemmassa yläkulmassa olevaa x-merkkiä.