Tämä on suoraa jatkoa Ohjelmoinnin perusteet -kurssin materiaaliin.

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

Lue materiaalia siten, että teet samalla itse kaikki lukemasi esimerkit. Esimerkkeihin kannattaa tehdä pieniä muutoksia ja tarkkailla, miten muutokset vaikuttavat ohjelman toimintaan. Äkkiseltään voisi luulla, että esimerkkien tekeminen myös itse ja niiden muokkaaminen hidastaa opiskelua. Tämä ei kuitenkaan pidä ollenkaan paikkansa. Ohjelmoimaan ei ole vielä tietääksemme kukaan ihminen oppinut lukemalla (tai esim. luentoa kuuntelemalla). Oppiminen perustuu oleellisesti aktiiviseen tekemiseen ja rutiinin kasvattamiseen. Esimerkkien ja erityisesti erilaisten omien kokeilujen tekeminen on parhaita tapoja "sisäistää" luettua tekstiä.

Pyri tekemään tai ainakin yrittämään tehtäviä sitä mukaa kuin luet tekstiä. Jos et osaa heti tehdä jotain tehtävää, älä masennu, sillä saat ohjausta tehtävän tekemiseen pajassa.

Tekstiä ei ole tarkoitettu vain kertaalleen luettavaksi. Joudut varmasti myöhemmin palaamaan jo aiemmin lukemiisi kohtiin tai aiemmin tekemiisi tehtäviin. Tämä teksti ei sisällä kaikkea oleellista ohjelmointiin liittyvää. Itse asiassa ei ole olemassa mitään kirjaa josta löytyisi kaikki oleellinen. Eli joudut joka tapauksessa ohjelmoijan urallasi etsimään tietoa myös omatoimisesti. Kurssin harjoitukset sisältävät jo jonkun verran ohjeita, mistä suunnista ja miten hyödyllistä tietoa on mahdollista löytää.

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

Kurssi alkaa siitä mihin ohjelmoinnin perusteet loppui ja oikeastaan kaikki ohjelmoinnin perusteet -kurssilla opitut asiat oletetaan nyt osattavan. Kannattaa käydä aina silloin tällöin kertaamassa Ohjelmoinnin perusteet -kurssin materiaalia.

Huom! Käytämme tällä kurssilla NetBeans-nimistä ohjelmointiympäristöä. Ohjeet NetBeansin ja kurssilla käytettävän tehtäväautomaatin käyttöön löydät täältätäältä.

Viikko6

56 Muutamia hyödyllisiä tekniikoita

Kurssin lähestyessä loppua katsomme vielä muutamaa hyödyllistä Javan ominaisuutta.

56.1 Säännölliset lausekkeet

Säännöllinen lauseke määrittelee tiiviissä muodossa joukon merkkijonoja. Säännöllisiä lausekkeita käytetään muunmuassa merkkijonojen oikeellisuuden tarkistamiseen. Tarkastellaan tehtävää, jossa täytyy tarkistaa, onko käyttäjän antama opiskelijanumero oikeanmuotoinen. Opiskelijanumero alkaa merkkijonolla "01", jota seuraa 7 numeroa väliltä 0–9.

Opiskelijanumeron oikeellisuuden voisi tarkistaa esimerkiksi käymällä opiskelijanumeroa esittävän merkkijonon läpi merkki merkiltä charAt-metodin avulla. Toinen tapa olisi tarkistaa että ensimmäinen merkki on "0", ja käyttää Integer.parseInt metodikutsua merkkijonon muuntamiseen numeroksi. Tämän jälkeen voisi tarkistaa että Integer.parseInt-metodin palauttama luku on pienempi kuin 20000000.

Oikeellisuuden tarkistus säännöllisten lausekkeiden avulla vaatii ensin sopivan säännöllisen lausekkeen määrittelyn. Tämän jälkeen voimme käyttää String-luokan metodia matches, joka tarkistaa vastaako merkkijono parametrina annettua säännöllistä lauseketta. Opiskelijanumeron tapauksessa sopiva säännöllinen lauseke on "01[0-9]{7}", ja käyttäjän syöttämän opiskelijanumeron tarkistaminen käy seuraavasti:

System.out.print("Anna opiskelijanumero: ");
String numero = lukija.nextLine();

if (numero.matches("01[0-9]{7}")) {
    System.out.println("Muoto on oikea.");
} else {
    System.out.println("Muoto ei ole oikea.");
}

Käydään seuraavaksi läpi eniten käytettyjä säännöllisten lausekkeiden merkintöjä.

Pystyviiva eli vaihtoehtoisuus

Pystyviiva tarkoittaa, että säännöllisen lausekkeen osat ovat vaihtoehtoisia. Esimerkiksi lauseke 00|111|0000 määrittelee merkkijonot 00, 111 ja 0000. Metodi matches palauttaa arvon true jos merkkijono vastaa jotain määritellyistä vaihtoehdoista.

String merkkijono = "00";

if(merkkijono.matches("00|111|0000")) {
    System.out.println("Merkkijonosta löytyi joku kolmesta vaihtoehdosta");
} else {
    System.out.println("Merkkijonosta ei löytynyt yhtäkään vaihtoehdoista");
}
Merkkijonosta löytyi joku kolmesta vaihtoehdosta

Säännöllinen lauseke 00|111|0000 vaatii että merkkijono on täsmälleen määritellyn muotoinen: se ei määrittele "contains"-toiminnallisuutta.

String merkkijono = "1111";

if(merkkijono.matches("00|111|0000")) {
    System.out.println("Merkkijonosta löytyi joku kolmesta vaihtoehdosta");
} else {
    System.out.println("Merkkijonosta ei löytynyt yhtäkään vaihtoehdoista");
}
Merkkijonosta ei löytynyt yhtäkään vaihtoehdoista

Sulut, eli merkkijonon osaan rajattu vaikutusalue

Sulkujen avulla voi määrittää, mihin säännöllisen lausekkeen osaan sulkujen sisällä olevat merkinnät vaikuttavat. Jos haluamme sallia merkkijonot 00000 ja 00001, voimme määritellä ne pystyviivan avulla muodossa 00000|00001. Sulkujen avulla voimme rajoittaa vaihtoehtoisuuden vain osaan merkkijonoa. Lauseke 0000(0|1) määrittelee merkkijonot 00000 ja 00001.

Vastaavasti säännöllinen lauseke auto(|n|a) määrittelee sanan auto yksikön nominatiivin (auto), genetiivin (auton), partitiivin (autoa) ja akkusatiivin (auto tai auton).

System.out.print("Kirjoita joku sanan auto yksikön taivutusmuoto: ");
String sana = lukija.nextLine();

if (sana.matches("auto(|n|a|ssa|sta|on|lla|lta|lle|na|ksi|tta)")) {
    System.out.println("Oikein meni!");
} else {
    System.out.println("Taivutusmuoto ei ole oikea.");
}

Toistomerkinnät

Usein halutaan, että merkkijonossa toistuu jokin tietty alimerkkijono. Säännöllisissä lausekkeissa on käytössä seuraavat toistomerkinnät:

  • Merkintä * toisto 0... kertaa, esim
    String merkkijono = "trolololololo";
    
    if(merkkijono.matches("trolo(lo)*")) {
        System.out.println("Muoto on oikea.");
    } else {
        System.out.println("Muoto ei ole oikea.");
    }
    Muoto on oikea.
  • Merkintä + toisto 1... kertaa, esim
    String merkkijono = "trolololololo";
    
    if(merkkijono.matches("tro(lo)+")) {
        System.out.println("Muoto on oikea.");
    } else {
        System.out.println("Muoto ei ole oikea.");
    }
    Muoto on oikea.
    String merkkijono = "nänänänänänänänä Bätmään!";
    
    if(merkkijono.matches("(nä)+ Bätmään!")) {
        System.out.println("Muoto on oikea.");
    } else {
        System.out.println("Muoto ei ole oikea.");
    }
    Muoto on oikea.
  • Merkintä ? toisto 0 tai 1 kertaa, esim
    String merkkijono = "You have to accidentally the whole meme";
    
    if(merkkijono.matches("You have to accidentally (delete )?the whole meme")) {
        System.out.println("Muoto on oikea.");
    } else {
        System.out.println("Muoto ei ole oikea.");
    }
    Muoto on oikea.
  • Merkintä {a} toisto a kertaa, esim
    String merkkijono = "1010";
    
    if(merkkijono.matches("(10){2}")) {
        System.out.println("Muoto on oikea.");
    } else {
        System.out.println("Muoto ei ole oikea.");
    }
    Muoto on oikea.
  • Merkintä {a,b} toisto a ... b kertaa, esim
    String merkkijono = "1";
    
    if(merkkijono.matches("1{2,4}")) {
        System.out.println("Muoto on oikea.");
    } else {
        System.out.println("Muoto ei ole oikea.");
    }
    Muoto ei ole oikea.
  • Merkintä {a,} toisto a ... kertaa, esim
    String merkkijono = "11111";
    
    if(merkkijono.matches("1{2,}")) {
        System.out.println("Muoto on oikea.");
    } else {
        System.out.println("Muoto ei ole oikea.");
    }
    Muoto on oikea.

Samassa säännöllisessä lausekkeessa voi käyttää myös useampia toistomerkintöjä. Esimerkiksi säännöllinen lauseke 5{3}(1|0)*5{3} määrittelee merkkijonot, jotka alkavat ja loppuvat kolmella vitosella. Välissä saa tulla rajaton määrä ykkösiä ja nollia.

Hakasulut, eli merkkiryhmät

Merkkiryhmän avulla voi määritellä lyhyesti joukon merkkejä. Merkit kirjoitetaan hakasulkujen sisään, ja merkkivälin voi määrittää viivan avulla. Esimerkiksi merkintä [145] tarkoittaa samaa kuin (1|4|5) ja merkintä [2-36-9] tarkoittaa samaa kuin (2|3|6|7|8|9). Vastaavasti merkintä [a-c]* määrittelee säännöllisen lausekkeen, joka vaatii että merkkijono sisältää vain merkkejä a, b ja c.

Harjoitellaan hieman säännöllisten lausekkeiden käyttöä. Tehtävät tehdään oletuspakkauksessa olevaan luokkaan Paaohjelma.

151.1 Viikonpäivä

Tee säännöllisen lausekkeen avulla luokalle Paaohjelma metodi public static boolean onViikonpaiva(String merkkijono), joka palauttaa true jos sen parametrina saama merkkijono on viikonpäivän lyhenne (ma, ti, ke, to, pe, la tai su).

Esimerkkitulostuksia metodia käyttävästä ohjelmasta:

Anna merkkijono: ti
Muoto on oikea.
Anna merkkijono: abc
Muoto ei ole oikea.

151.2 Vokaalitarkistus

Tee luokalle Paaohjelma metodi public static boolean kaikkiVokaaleja(String merkkijono) joka tarkistaa säännöllisen lausekkeen avulla ovatko parametrina olevan merkkijonon kaikki merkit vokaaleja.

Esimerkkitulostuksia metodia käyttävästä ohjelmasta:

Anna merkkijono: aie
Muoto on oikea.
Anna merkkijono: ane
Muoto ei ole oikea.

151.3 Kellonaika

Säännölliset lausekkeet sopivat tietynlaisiin tilanteisiin. Joissain tapaukseesa lausekkeista tulee liian monimutkaisia, ja merkkijonon "sopivuus" kannattaa tarkastaa muulla tyylillä tai voi olla tarkoituksenmukaista käyttää säännöllisiä lausekkeita vain osaan tarkastuksesta.

Tee luokalle Paaohjelma metodi public static boolean kellonaika(String merkkijono) ohjelma, joka tarkistaa säännöllisen lausekkeen avulla onko parametrina oleva merkkijono muotoa tt:mm:ss oleva kellonaika (tunnit, minuutit ja sekunnit kaksinumeroisina). Tässä metodissa saat käyttää säännöllisten lausekkeiden lisäksi mitä tahansa muutakin tekniikkaa.

Esimerkkitulostuksia metodia käyttävästä ohjelmasta:

Anna merkkijono: 17:23:05
Muoto on oikea.
Anna merkkijono: abc
Muoto ei ole oikea.
Anna merkkijono: 33:33:33
Muoto ei ole oikea.

Nykyään lähes kaikista ohjelmointikielistä löytyy tuki säännöllisille lausekkeille. Säännöllisten lausekkeiden teoriaa tarkastellaan toisen vuoden kurssilla Laskennan mallit. Lisää säännöllisistä lausekkeista löydät esim. googlaamalla hakusanalla regular expressions java.

56.2 Enum eli lueteltu tyyppi

Toteutimme aiemmin pelikorttia mallintavan luokan Kortti suunilleen seuraavasti:

public class Kortti {

    public static final int RUUTU = 0;
    public static final int PATA = 1;
    public static final int RISTI = 2;
    public static final int HERTTA = 3;

    private int arvo;
    private int maa;

    public Kortti(int arvo, int maa) {
        this.arvo = arvo;
        this.maa = maa;
    }

    @Override
    public String toString() {
        return maanNimi() + " "+arvo;
    }

    private String maanNimi() {
        if (maa == 0) {
            return "RUUTU";
        } else if (maa == 1) {
            return  "PATA";
        } else if (maa == 2) {
            return "RISTI";
        }
        return "HERTTA";
    }

    public int getMaa() {
        return maa;
    }
}

Kortin maa tallennetaan kortissa olevaan oliomuuttujaan kokonaislukuna. Maan ilmaisemiseen on määritelty luettavuutta helpottavat vakiot. Kortteja ja maita ilmaisevia vakioita käytetään seuraavasti:

public static void main(String[] args) {
        Kortti kortti = new Kortti(10, Kortti.HERTTA);

        System.out.println(kortti);

        if (kortti.getMaa() == Kortti.PATA) {
            System.out.println("on pata");
        } else {
            System.out.println("ei ole pata");
        }

}

Maan esittäminen numerona on huono ratkaisu, sillä esimerkiksi seuraavat järjenvastaiset tavat käyttää korttia ovat mahdollisia:

Kortti jarjetonKortti = new Kortti(10, 55);

System.out.println(jarjetonKortti);

if (jarjetonKortti.getMaa() == 34) {
    System.out.println("kortin maa on 34");
} else {
    System.out.println("kortin maa on jotain muuta kun 34");
}

int maaPotenssiinKaksi = jarjetonKortti.getMaa() * jarjetonKortti.getMaa();

System.out.println("kortin maa potenssiin kaksi on " + maaPotenssiinKaksi);

Jos tiedämme muuttujien mahdolliset arvot ennalta, voimme käyttää niiden esittämiseen enum-tyyppistä luokkaa eli lueteltua tyyppiä. Luetellut tyypit ovat oma luokkatyyppinsä rajapinnan ja normaalin luokan lisäksi. Lueteltu tyyppi määritellään avainsanalla enum. Esimerkiksi seuraava Maa-enumluokka määrittelee neljä vakioarvoa: RUUTU, PATA, RISTI ja HERTTA.

public enum Maa {
    RUUTU, PATA, RISTI, HERTTA
}

Yksinkertaisimmassa muodossaan enum luettelee pilkulla erotettuina määrittelemänsä vakioarvot. Enumien vakiot on yleensä tapana kirjoittaa kokonaan isoin kirjaimin.

Enum luodaan (yleensä) omaan tiedostoon, samaan tapaan kuin luokka tai rajapinta. NetBeansissa Enumin saa luotua valitsemalla projektin kohdalla new/other/java/java enum.

Seuraavassa luokka Kortti jossa maa esitetään enumin avulla:

public class Kortti {

    private int arvo;
    private Maa maa;

    public Kortti(int arvo, Maa maa) {
        this.arvo = arvo;
        this.maa = maa;
    }

    @Override
    public String toString() {
        return maa + " "+arvo;
    }

    public Maa getMaa() {
        return maa;
    }

    public int getArvo() {
        return arvo;
    }
}

Kortin uutta versiota käytetään seuraavasti:

public class Paaohjelma {

    public static void main(String[] args) {
        Kortti eka = new Kortti(10, Maa.HERTTA);

        System.out.println(eka);

        if (eka.getMaa() == Maa.PATA) {
            System.out.println("on pata");
        } else {
            System.out.println("ei ole pata");
        }

    }
}

Tulostuu:

HERTTA 10
ei ole pata

Huomaamme, että enumin tunnukset tulostuvat mukavasti! Koska kortin maat ovat nyt tyyppiä Maa ei ylemmän esimerkin "järjenvastaiset" kummallisuudet, esim. "maan korottaminen toiseen potenssiin" onnistu. Oraclella on enum-tyyppiin liittyvä sivusto osoitteessa http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html.

56.3 Iteraattori

Tarkastellaan seuraavaa luokkaa Kasi, joka mallintaa tietyssä korttipelissä pelaajan kädessä olevien korttien joukkoa:

public class Kasi {
    private ArrayList<Kortti> kortit;

    public Kasi() {
        kortit = new ArrayList<Kortti>();
    }

    public void lisaa(Kortti kortti){
        kortit.add(kortti);
    }

    public void tulosta(){
        for (Kortti kortti : kortit) {
            System.out.println( kortti );
        }
    }
}

Luokan metodi tulosta tulostaa jokaisen kädessä olevan kortin tutuksi tullutta "for each"-lausetta käyttämällä. ArrayList ja muut Collection-rajapinnan toteuttavat "oliosäiliöt" toteuttavat rajapinnan Iterable. Rajapinnan Iterable toteuttavat oliot on mahdollista käydä läpi eli "iteroida" esimerkiksi. for each -tyyppisellä komennolla.

Oliosäiliö voidaan käydä läpi myös käyttäen ns. iteraattoria, eli olioa, joka on varta vasten tarkoitettu tietyn oliokokoelman läpikäyntiin. Seuraavassa on iteraattoria käyttävä versio korttien tulostamisesta:

public void tulosta() {
    Iterator<Kortti> iteraattori = kortit.iterator();

    while ( iteraattori.hasNext() ){
        System.out.println( iteraattori.next() );
    }
}

Iteraattori pyydetään kortteja sisältävältä arraylistiltä kortit. Iteraattori on ikäänkuin "sormi", joka osoittaa aina tiettyä listan sisällä olevaa olioa, ensin ensimmäistä ja sitten seuraavaa jne... kunnes "sormen" avulla on käyty jokainen olio läpi.

Iteraattori tarjoaa muutaman metodin. Metodilla hasNext() kysytään onko läpikäytäviä olioita vielä jäljellä. Jos on, voidaan iteraattorilta pyytää seuraavana vuorossa oleva olio metodilla next(). Metodi siis palauttaa seuraavana läpikäyntivuorossa olevan olion ja laittaa iteraattorin eli "sormen" osoittamaan seuraavana vuorossa olevaa läpikäytävää olioa.

Iteraattorin next-metodin palauttama olioviite voidaan ottaa toki talteen myös muuttujaan, eli metodi tulosta voitaisiin muotoilla myös seuraavasti:

public void tulosta(){
    Iterator<Kortti> iteraattori = kortit.iterator();

    while ( iteraattori.hasNext() ){
        Kortti seuraavanaVuorossa = iteraattori.next();
        System.out.println( seuraavanaVuorossa );
    }
}

Teemme metodin jonka avulla kädestä voi poistaa tiettyä arvoa pienemmät kortit:

public class Kasi {
    // ...

    public void poistaHuonommat(int arvo) {
        for (Kortti kortti : kortit) {
            if ( kortti.getArvo() < arvo ) {
                kortit.remove(kortti);
            }
        }
    }
}

Huomaamme että metodin suoritus aiheuttaa kummallisen virheen:

Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
        at java.util.AbstractList$Itr.next(AbstractList.java:343)
        at Kasi.poistaHuonommat(Kasi.java:26)
        at Paaohjelma.main(Paaohjelma.java:20)
Java Result: 1

Virheen syynä on se, että for-each:illa listaa läpikäydessä ei ole sallittua poistaa listalta olioita: komento for-each menee tästä "sekaisin".

Jos listalta halutaan poistaa osa olioista läpikäynnin aikana osa, tulee tämä tehdä iteraattoria käyttäen. Iteraattori-olion metodia remove kutsuttaessa listalta poistetaan siististi se alkio jonka iteraattori palautti edellisellä metodin next kutsulla. Toimiva versio metodista seuraavassa:

public class Kasi {
    // ...

    public void poistaHuonommat(int arvo) {
        Iterator<Kortti> iteraattori = kortit.iterator();

        while (iteraattori.hasNext()) {
            if (iteraattori.next().getArvo() < arvo) {
                // poistetaan listalta olio jonka edellinen next-metodin kutsu palautti
                iteraattori.remove();
            }
        }
    }
}

Tehdään ohjelma pienen yrityksen henkilöstön hallintaan.

152.1 Koulutus

Tee pakkaukseen henkilosto lueteltu tyyppi eli enum Koulutus jolla on tunnukset FT (tohtori), FM (maisteri), LuK (kandidaatti), FilYO (ylioppilas).

152.2 Henkilo

Tee pakkaukseen henkilosto luokka Luokka Henkilo. Henkilölle annetaan konstruktorin parametrina annettava nimi ja koulutus. Henkilöllä on myös koulutuksen kertova metodi public Koulutus getKoulutus() sekä alla olevan esimerkin mukaista jälkeä tekevä toString-metodi.

Henkilo arto = new Henkilo("Arto", Koulutus.FT);
System.out.println(arto);
Arto, FT

152.3 Tyontekijat

Tee pakkaukseen henkilosto luokka Luokka Tyontekijat. Työntekijät-olio sisältää listan Henkilo-olioita. Luokalla on parametriton konstruktori ja seuraavat metodit:

  • public void lisaa(Henkilo lisattava) lisää parametrina olevan henkilön työntekijäksi
  • public void lisaa(List<Henkilo> lisattavat) lisää parametrina olevan listan henkilöitä työntekijöiksi
  • public void tulosta() tulostaa kaikki työntekijät
  • public void tulosta(Koulutus koulutus) tulostaa työntekijät joiden koulutus on sama kuin parametrissa määritelty koulutus

HUOM: Luokan Tyontekijat tulosta-metodit on toteutettava iteraattoria käyttäen!

152.4 Irtisanominen

Tee luokalle Tyontekijat metodi public void irtisano(Koulutus koulutus) joka poistaa Työntekijöiden joukosta kaikki henkilöt joiden koulutus on sama kuin metodin parametrina annettu.

HUOM: toteuta metodi iteraattoria käyttäen!

Seuraavassa esimerkki luokan käytöstä:

Public class Paaohjelma {

    public static void main(String[] args) {
        Tyontekijat yliopisto = new Tyontekijat();
        yliopisto.lisaa(new Henkilo("Matti", Koulutus.FT));
        yliopisto.lisaa(new Henkilo("Pekka", Koulutus.FilYO));
        yliopisto.lisaa(new Henkilo("Arto", Koulutus.FT));

        yliopisto.tulosta();

        yliopisto.irtisano(Koulutus.FilYO);

        System.out.println("==");

        yliopisto.tulosta();
}

Tulostuu:

Matti, FT
Pekka, FilYO
Arto, FT
==
Matti, FT
Arto, FT

56.4 Toistolauseet ja continue

Toistolauseissa on komennon break lisäksi käytössä komento continue, joka mahdollistaa seuraavaan toistokierrokseen hyppäämisen.

List<String> nimet = Arrays.asList("Matti", "Pekka", "Arto");

for(String nimi: nimet) {
    if (nimi.equals("Arto")) {
        continue;
    }

    System.out.println(nimi);
}
Matti
Pekka

Komentoa continue käytetään esimerkiksi silloin, kun tiedetään että toistolauseessa iteroitavilla muuttujilla on arvoja, joita ei haluta käsitellä lainkaan. Klassinen lähestymistapa olisi if-lauseen käyttö, mutta komento continue mahdollistaa sisennyksiä välttävän, ja samalla ehkä luettavamman lähestymistavan käsiteltävien arvojen välttämiseen. Alla on kaksi esimerkkiä, jossa käydään listalla olevia lukuja läpi. Jos luku on alle 5, se on jaollinen sadalla, tai se on jaollinen neljälläkymmenellä, niin sitä ei tulosteta, muulloin se tulostetaan.

List<Integer> luvut = Arrays.asList(1, 3, 11, 6, 120);

for(int luku: luvut) {
    if (luku > 4 && luku % 100 != 0 && luku % 40 != 0) {
        System.out.println(luku);
    }
}

for(int luku: luvut) {
    if (luku < 5) {
        continue;
    }

    if (luku % 100 == 0) {
        continue;
    }

    if (luku % 40 == 0) {
        continue;
    }

    System.out.println(luku);
}
11
6
11
6

56.5 Enumien oliomuuttujat

Luetellut tyypit voivat sisältää oliomuuttujia. Oliomuuttujien arvot tulee asettaa luetellun tyypin määrittelevän luokan sisäisessä eli näkyvyysmääreen private omaavassa konstruktorissa. Enum-tyyppisillä luokilla ei saa olla public-konstruktoria.

Seuraavassa lueteltu tyyppi Vari, joka sisältää vakioarvot PUNAINEN, VIHREA ja SININEN. Vakioille on määritelty värikoodin kertova oliomuuttuja:

public enum Vari {
    PUNAINEN("#FF0000"),        // konstruktorin parametrit määritellään vakioarvoja lueteltaessa
    VIHREA("#00FF00"),
    SININEN("#0000FF");

    private String koodi;        // oliomuuttuja

    private Vari(String koodi) { // konstruktori
        this.koodi = koodi;
    }

    public String getKoodi() {
        return this.koodi;
    }
}

Lueteltua tyyppiä Vari voidaan käyttää esimerkiksi seuraavasti:

System.out.println(Vari.VIHREA.getKoodi());
#00FF00

Hiljattain Suomeen rantautunut Netflix lupasi lokakuussa 2006 miljoona dollaria henkilölle tai ryhmälle, joka kehittäisi ohjelman, joka on 10% parempi elokuvien suosittelussa kuin heidän oma ohjelmansa. Kilpailu ratkesi syyskuussa 2009 (http://www.netflixprize.com/).

Rakennetaan tässä tehtävässä ohjelma elokuvien suositteluun. Alla on sen toimintaesimerkki:

ArvioRekisteri arviot = new ArvioRekisteri();

Elokuva tuulenViemaa = new Elokuva("Tuulen viemää");
Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat");
Elokuva eraserhead = new Elokuva("Eraserhead");

Henkilo matti = new Henkilo("Matti");
Henkilo pekka = new Henkilo("Pekka");
Henkilo mikke = new Henkilo("Mikke");
Henkilo thomas = new Henkilo("Thomas");

arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO);
arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA);
arviot.lisaaArvio(matti, eraserhead, Arvio.OK);

arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK);
arviot.lisaaArvio(pekka, hiljaisetSillat, Arvio.HUONO);
arviot.lisaaArvio(pekka, eraserhead, Arvio.VALTTAVA);

arviot.lisaaArvio(mikke, eraserhead, Arvio.HUONO);


Suosittelija suosittelija = new Suosittelija(arviot);
System.out.println(thomas + " suositus: " +
        suosittelija.suositteleElokuva(thomas));
System.out.println(mikke + " suositus: " +
        suosittelija.suositteleElokuva(mikke));
Thomas suositus: Hiljaiset sillat
Mikke suositus: Tuulen viemää

Ohjelma osaa suositella elokuvia niiden yleisen arvion perusteella, sekä henkilökohtaisten henkilön antaminen arvioiden perusteella. Lähdetään rakentamaan ohjelmaa.

153.1 Henkilo ja Elokuva

Luo pakkaus suosittelija.domain ja lisää sinne luokat Henkilo ja Elokuva. Kummallakin luokalla on julkinen konstruktori public Luokka(String nimi), sekä metodi public String getNimi(), joka palauttaa konstruktorissa saadun nimen.

Henkilo henkilo = new Henkilo("Pekka");
Elokuva elokuva = new Elokuva("Eraserhead");

System.out.println(henkilo.getNimi() + " ja " + elokuva.getNimi());
Pekka ja Eraserhead

Lisää luokille myös public String toString()-metodi, joka palauttaa konstruktorissa parametrina annetun nimen, sekä korvaa metodit equals ja hashCode.

Korvaa equals siten että samuusvertailu tapahtuu oliomuuttujan nimi perusteella. Katso mallia luvusta 45.1. Luvussa 45.2. on ohje metodin hashCode korvaamiselle. Ainakin HashCode kannattaa generoida automaattisesti luvun lopussa olevan ohjeen mukaan:

NetBeans tarjoaa metodien equals ja hashCode automaattisen luonnin. Voit valita valikosta Source -> Insert Code, ja valita aukeavasta listasta equals() and hashCode(). Tämän jälkeen NetBeans kysyy oliomuuttujat joita metodeissa käytetään.

153.2 Arvio

Luo pakkaukseen suosittelija.domain lueteltu tyyppi Arvio. Enum-luokalla Arvio on julkinen metodi public int getArvo(), joka palauttaa arvioon liittyvän arvon. Arviotunnusten ja niihin liittyvien arvosanojen tulee olla seuraavat:

Tunnus Arvo
HUONO -5
VALTTAVA -3
EI_NAHNYT 0
NEUTRAALI 1
OK 3
HYVA 5

Luokkaa voi käyttää seuraavasti:

Arvio annettu = Arvio.HYVA;
System.out.println("Arvio " + annettu + ", arvo " + annettu.getArvo());
annettu = Arvio.NEUTRAALI;
System.out.println("Arvio " + annettu + ", arvo " + annettu.getArvo());
Arvio HYVA, arvo 5
Arvio NEUTRAALI, arvo 1

153.3 ArvioRekisteri, osa 1

Aloitetaan arvioiden varastointiin liittyvän palvelun toteutus.

Luo pakkaukseen suosittelija luokka ArvioRekisteri, jolla on konstruktori public ArvioRekisteri() sekä seuraavat metodit:

  • public void lisaaArvio(Elokuva elokuva, Arvio arvio) lisää arviorekisteriin parametrina annetulle elokuvalle uuden arvion. Samalla elokuvalla voi olla useita samanlaisiakin arvioita.
  • public List<Arvio> annaArviot(Elokuva elokuva) palauttaa elokuvalle lisätyt arviot listana.
  • public Map<Elokuva, List<Arvio>> elokuvienArviot() palauttaa mapin, joka sisältää arvioidut elokuvat avaimina. Jokaiseen elokuvaan liittyy lista, joka sisältää elokuvaan lisatyt arviot.

Testaa metodien toimintaa seuraavalla lähdekoodilla:

Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat");
Elokuva eraserhead = new Elokuva("Eraserhead");

ArvioRekisteri rekisteri = new ArvioRekisteri();
rekisteri.lisaaArvio(eraserhead, Arvio.HUONO);
rekisteri.lisaaArvio(eraserhead, Arvio.HUONO);
rekisteri.lisaaArvio(eraserhead, Arvio.HYVA);

rekisteri.lisaaArvio(hiljaisetSillat, Arvio.HYVA);
rekisteri.lisaaArvio(hiljaisetSillat, Arvio.OK);

System.out.println("Kaikki arviot: " + rekisteri.elokuvienArviot());
System.out.println("Arviot Eraserheadille: " + rekisteri.annaArviot(eraserhead));
Kaikki arviot: {Hiljaiset sillat=[HYVA, OK], Eraserhead=[HUONO, HUONO, HYVA]}
Arviot Eraserheadille: [HUONO, HUONO, HYVA]

153.4 ArvioRekisteri, osa 2

Lisätään seuraavaksi mahdollisuus henkilökohtaisten arvioiden lisäämiseen.

Lisää luokkaan ArvioRekisteri seuraavat metodit:

  • public void lisaaArvio(Henkilo henkilo, Elokuva elokuva, Arvio arvio) lisää parametrina annetulle elokuvalle tietyn henkilön tekemän arvion. Sama henkilö voi arvioida tietyn elokuvan vain kertaalleen. Henkilön tekemä arvio tulee myös lisätä kaikkiin elokuviin liittyviin arvioihin.
  • public Arvio haeArvio(Henkilo henkilo, Elokuva elokuva) palauttaa parametrina annetun henkilön tekemän arvion parametrina annetulle elokuvalle. Jos henkilö ei ole arvioinut kyseistä elokuvaa, palauta arvio Arvio.EI_NAHNYT.
  • public Map<Elokuva, Arvio> annaHenkilonArviot(Henkilo henkilo) palauttaa hajautustaulun, joka sisältää henkilön tekemät arviot. Hajautustaulun avaimena on arvioidut elokuvat, arvoina arvioituihin elokuviin liittyvät arviot. Jos henkilö ei ole arvioinut yhtään elokuvaa, palautetaan tyhjä hajautustaulu.
  • public List<Henkilo> arvioijat() palauttaa listan henkilöistä jotka ovat arvioineet elokuvia.

Henkilöiden tekemät arviot kannattanee tallentaa hajautustauluun, jossa avaimena on henkilö. Arvona hajautustaulussa on toinen hajautustaulu, jossa avaimena on elokuva ja arvona arvio.

Testaa paranneltua ArvioRekisteri-luokkaa seuraavalla lähdekoodipätkällä:

ArvioRekisteri arviot = new ArvioRekisteri();

Elokuva tuulenViemaa = new Elokuva("Tuulen viemää");
Elokuva eraserhead = new Elokuva("Eraserhead");

Henkilo matti = new Henkilo("Matti");
Henkilo pekka = new Henkilo("Pekka");

arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO);
arviot.lisaaArvio(matti, eraserhead, Arvio.OK);

arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK);
arviot.lisaaArvio(pekka, eraserhead, Arvio.OK);

System.out.println("Arviot Eraserheadille: " + arviot.annaArviot(eraserhead));
System.out.println("Matin arviot: " + arviot.annaHenkilonArviot(matti));
System.out.println("Arvioijat: " + arviot.arvioijat());
Arviot Eraserheadille: [OK, OK]
Matin arviot: {Tuulen viemää=HUONO, Eraserhead=OK}
Arvioijat: [Pekka, Matti]

Luodaan seuraavaksi muutama apuluokka arviointien helpottamiseksi.

153.5 HenkiloComparator

Luo pakkaukseen suosittelija.comparator luokka HenkiloComparator. Luokan HenkiloComparator tulee toteuttaa rajapinta Comparator<Henkilo>, ja sillä pitää olla konstruktori public HenkiloComparator(Map<Henkilo, Integer> henkiloidenSamuudet). Luokkaa HenkiloComparator käytetään myöhemmin henkilöiden järjestämiseen henkilöön liittyvän luvun perusteella.

HenkiloComparator-luokan tulee mahdollistaa henkilöiden järjestäminen henkilöön liittyvän luvun perusteella.

Testaa luokan toimintaa seuraavalla lähdekoodilla:

Henkilo matti = new Henkilo("Matti");
Henkilo pekka = new Henkilo("Pekka");
Henkilo mikke = new Henkilo("Mikke");
Henkilo thomas = new Henkilo("Thomas");

Map<Henkilo, Integer> henkiloidenSamuudet = new HashMap<Henkilo, Integer>();
henkiloidenSamuudet.put(matti, 42);
henkiloidenSamuudet.put(pekka, 134);
henkiloidenSamuudet.put(mikke, 8);
henkiloidenSamuudet.put(thomas, 82);

List<Henkilo> henkilot = Arrays.asList(matti, pekka, mikke, thomas);
System.out.println("Henkilöt ennen järjestämistä: " + henkilot);

Collections.sort(henkilot, new HenkiloComparator(henkiloidenSamuudet));
System.out.println("Henkilöt järjestämisen jälkeen: " + henkilot);
Henkilöt ennen järjestämistä: [Matti, Pekka, Mikke, Thomas]
Henkilöt järjestämisen jälkeen: [Pekka, Thomas, Matti, Mikke]

153.6 ElokuvaComparator

Luo pakkaukseen suosittelija.comparator luokka ElokuvaComparator. Luokan ElokuvaComparator tulee toteuttaa rajapinta Comparator<Elokuva>, ja sillä pitää olla konstruktori public ElokuvaComparator(Map<Elokuva, List<Arvio>> arviot). Luokkaa ElokuvaComparator käytetään myöhemmin elokuvien järjestämiseen niiden arvioiden perusteella.

ElokuvaComparator-luokan tulee tarjota mahdollisuus elokuvien järjestäminen niiden saamien arvosanojen keskiarvon perusteella. Korkeimman keskiarvon saanut elokuva tulee ensimmäisenä, matalimman keskiarvon saanut viimeisenä.

Testaa luokan toimintaa seuraavalla lähdekoodilla:

ArvioRekisteri arviot = new ArvioRekisteri();

Elokuva tuulenViemaa = new Elokuva("Tuulen viemää");
Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat");
Elokuva eraserhead = new Elokuva("Eraserhead");

Henkilo matti = new Henkilo("Matti");
Henkilo pekka = new Henkilo("Pekka");
Henkilo mikke = new Henkilo("Mikke");

arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO);
arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA);
arviot.lisaaArvio(matti, eraserhead, Arvio.OK);

arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK);
arviot.lisaaArvio(pekka, hiljaisetSillat, Arvio.HUONO);
arviot.lisaaArvio(pekka, eraserhead, Arvio.VALTTAVA);

arviot.lisaaArvio(mikke, eraserhead, Arvio.HUONO);

Map<Elokuva, List<Arvio>> elokuvienArviot = arviot.elokuvienArviot();

List<Elokuva> elokuvat = Arrays.asList(tuulenViemaa, hiljaisetSillat, eraserhead);
System.out.println("Elokuvat ennen järjestämistä: " + elokuvat);

Collections.sort(elokuvat, new ElokuvaComparator(elokuvienArviot));
System.out.println("Elokuvat järjestämisen jälkeen: " + elokuvat);
Elokuvat ennen järjestämistä: [Tuulen viemää, Hiljaiset sillat, Eraserhead]
Elokuvat järjestämisen jälkeen: [Hiljaiset sillat, Tuulen viemää, Eraserhead]

153.7 Suosittelija, osa 1

Toteuta pakkaukseen suosittelija luokka Suosittelija. Luokan Suosittelija konstruktori saa parametrinaan ArvioRekisteri-tyyppisen olion. Suosittelija käyttää arviorekisterissä olevia arvioita suositusten tekemiseen.

Toteuta luokalle metodi public Elokuva suositteleElokuva(Henkilo henkilo), joka suosittelee henkilölle elokuvia. Toteuta metodi ensin siten, että se suosittelee aina elokuvaa, jonka arvioiden arvosanojen keskiarvo on suurin. Vinkki: Tarvitset parhaan elokuvan selvittämiseen ainakin aiemmin luotua ElokuvaComparator-luokkaa, luokan ArvioRekisteri metodia public Map<Elokuva, List<Arvio>> elokuvienArviot(), sekä listaa olemassaolevista elokuvista.

Testaa ohjelman toimimista seuraavalla lähdekoodilla:

ArvioRekisteri arviot = new ArvioRekisteri();

Elokuva tuulenViemaa = new Elokuva("Tuulen viemää");
Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat");
Elokuva eraserhead = new Elokuva("Eraserhead");

Henkilo matti = new Henkilo("Matti");
Henkilo pekka = new Henkilo("Pekka");
Henkilo mikke = new Henkilo("Mikael");

arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO);
arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA);
arviot.lisaaArvio(matti, eraserhead, Arvio.OK);

arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK);
arviot.lisaaArvio(pekka, hiljaisetSillat, Arvio.VALTTAVA);
arviot.lisaaArvio(pekka, eraserhead, Arvio.VALTTAVA);

Suosittelija suosittelija = new Suosittelija(arviot);
Elokuva suositeltu = suosittelija.suositteleElokuva(mikke);
System.out.println("Mikaelille suositeltu elokuva oli: " + suositeltu);
Mikaelille suositeltu elokuva oli: Hiljaiset sillat

Nyt tekemämme ensimmäinen vaihe toimii oikein ainoastaan henkilöille, jotka eivät ole vielä arvostelleet yhtään elokuvaa. Heidän elokuvamaustaanhan on mahdoton sanoa mitään ja paras arvaus on suositella heille keskimäärin parhaan arvosanan saanutta elokuvaa.

153.8 Suosittelija, osa 2

Huom! Tehtävä on haastava. Kannattaa tehdä ensin muut tehtävät ja palata tähän myöhemmin. Voit palauttaa tehtäväsarjan TMC:hen vaikket saakaan tätä tehtävää tehdyksi, aivan kuten muidenkin tehtävien kohdalla.

Valitettavasti tämän osan virhediagnostiikkakaan ei ole samaa luokkaa kuin edellisissä kohdissa.

Jos henkilöt ovat lisänneet omia suosituksia suosituspalveluun, tiedämme jotain heidän elokuvamaustaan. Laajennetaan suosittelijan toiminnallisuutta siten, että se luo henkilökohtaisen suosituksen jos henkilö on jo arvioinut elokuvia. Edellisessä osassa toteutettu toiminnallisuus tulee säilyttää: Jos henkilö ei ole arvioinut yhtäkään elokuvaa, hänelle suositellaan elokuva arvosanojen perusteella.

Henkilökohtaiset suositukset perustuvat henkilön tekemien arvioiden samuuteen muiden henkilöiden tekemien arvioiden kanssa. Pohditaan seuraavaa taulukkoa, missä ylärivillä on elokuvat, ja vasemmalla on arvioita tehneet henkilöt. Taulukon solut kuvaavat annettuja arvioita.

Henkilo \ Elokuva Tuulen viemää Hiljaiset sillat Eraserhead Blues Brothers
Matti HUONO (-5) HYVA (5) OK (3) -
Pekka OK (3) - HUONO (-5) VALTTAVA (-3)
Mikael - - HUONO (-5) -
Thomas - HYVA (5) - HYVA (5)

Kun haluamme hakea Mikaelille sopivaa elokuvaa, tutkimme Mikaelin samuutta kaikkien muiden arvioijien kesken. Samuus lasketaan arvioiden perusteella: samuus on kummankin katsomien elokuvien arvioiden tulojen summa. Esimerkiksi Mikaelin ja Thomasin samuus on 0, koska Mikael ja Thomas eivät ole katsoneet yhtäkään samaa elokuvaa.

Mikaelin ja Pekan samuutta laskettaessa yhteisten elokuvien tulojen summa olisi 25. Mikael ja Pekka ovat katsoneet vain yhden yhteisen elokuvan, ja kumpikin antaneet sille arvosanan huono (-5).

-5 * -5 = 25

Mikaelin ja Matin samuus on -15. Mikael ja Matti ovat myös katsoneet vain yhden yhteisen elokuvan. Mikael antoi elokuvalle arvosanan huono (-5), Matti antoi sille arvosanan ok (3).

-5 * 3 = -15

Näiden perusteella Mikaelille suositellaan elokuvia Pekan elokuvamaun mukaan: suosituksena on elokuva Tuulen viemää.

Kun taas haluamme hakea Matille sopivaa elokuvaa, tutkimme Matin samuutta kaikkien muiden arvioijien kesken. Matti ja Pekka ovat katsoneet kaksi yhteistä elokuvaa. Matti antoi Tuulen viemälle arvosanan huono (-5), Pekka arvosanan OK (3). Elokuvalle Eraserhead Matti antoi arvosanan OK (3), Pekka arvosanan huono (-5). Matin ja Pekan samuus on siis -30.

-5 * 3 + 3 * -5 = -30

Matin ja Mikaelin samuus on edellisestä laskusta tiedetty -15. Samuudet ovat symmetrisia.

Matti ja Thomas ovat katsoneet Tuulen viemää, ja kumpikin antoi sille arvosanan hyvä (5). Matin ja Thomaksen samuus on siis 25.

5 * 5 = 25

Matille tulee siis suositella elokuvia Thomaksen elokuvamaun mukaan: suosituksena olisi Blues Brothers.

Toteuta yllä kuvattu suosittelumekanismi. Jos henkilölle ei löydy yhtään suositeltavaa elokuvaa, tai henkilö, kenen elokuvamaun mukaan elokuvia suositellaan on arvioinut elokuvat joita henkilö ei ole vielä katsonut huonoiksi, välttäviksi tai neutraaleiksi, palauta metodista suositteleElokuva arvo null. Edellisessä tehtävässä määritellyn lähestymistavan tulee toimia jos henkilö ei ole lisännyt yhtäkään arviota.

Älä suosittele elokuvia, jonka henkilö on jo nähnyt.

Voit testata ohjelmasi toimintaa seuraavalla lähdekoodilla:

ArvioRekisteri arviot = new ArvioRekisteri();

Elokuva tuulenViemaa = new Elokuva("Tuulen viemää");
Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat");
Elokuva eraserhead = new Elokuva("Eraserhead");
Elokuva bluesBrothers = new Elokuva("Blues Brothers");

Henkilo matti = new Henkilo("Matti");
Henkilo pekka = new Henkilo("Pekka");
Henkilo mikke = new Henkilo("Mikael");
Henkilo thomas = new Henkilo("Thomas");
Henkilo arto = new Henkilo("Arto");

arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO);
arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA);
arviot.lisaaArvio(matti, eraserhead, Arvio.OK);

arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK);
arviot.lisaaArvio(pekka, eraserhead, Arvio.HUONO);
arviot.lisaaArvio(pekka, bluesBrothers, Arvio.VALTTAVA);

arviot.lisaaArvio(mikke, eraserhead, Arvio.HUONO);

arviot.lisaaArvio(thomas, bluesBrothers, Arvio.HYVA);
arviot.lisaaArvio(thomas, hiljaisetSillat, Arvio.HYVA);

Suosittelija suosittelija = new Suosittelija(arviot);
System.out.println(thomas + " suositus: " + suosittelija.suositteleElokuva(thomas));
System.out.println(mikke + " suositus: " + suosittelija.suositteleElokuva(mikke));
System.out.println(matti + " suositus: " + suosittelija.suositteleElokuva(matti));
System.out.println(arto + " suositus: " + suosittelija.suositteleElokuva(arto));
Thomas suositus: Eraserhead
Mikael suositus: Tuulen viemää
Matti suositus: Blues Brothers
Arto suositus: Hiljaiset sillat

Miljoona käsissä? Ei ehkä vielä. Kursseilla Johdatus tekoälyyn ja Johdatus koneoppimiseen opitaan lisää tekniikoita oppivien järjestelmien rakentamiseen.

56.6 Vaihteleva määrä parametreja metodille

Olemme tähän mennessä luoneet metodimme siten, että niiden parametrien määrät ovat olleet selkeästi määritelty. Java tarjoaa tavan antaa metodille rajoittamattoman määrän määrätyntyyppisiä parametreja asettamalla metodimäärittelyssä parametrin tyypille kolme pistettä perään. Esimerkiksi metodille public int summa(int... luvut) voi antaa summattavaksi niin monta int-tyyppistä kokonaislukua kuin käyttäjä haluaa. Metodin sisällä parametrin arvoja voi käsitellä taulukkona.

public int summa(int... luvut) {
    int summa = 0;
    for (int i = 0; i < luvut.length; i++) {
        summa += luvut[i];
    }
    return summa;
}
System.out.println(summa(3, 5, 7, 9));  // luvut = {3, 5, 7, 9}
System.out.println(summa(1, 2));        // luvut = {1, 2}
24
3

Huomaa yllä miten parametrimäärittely int... luvut johtaa siihen, että metodin sisällä näkyy taulukkotyyppinen muuttuja luvut.

Metodille voi määritellä vain yhden parametrin joka saa rajattoman määrän arvoja, ja sen tulee olla metodimäärittelyn viimeinen parametri. Esimerkiksi:

public void tulosta(String... merkkijonot, int kertaa) // ei sallittu!
public void tulosta(int kertaa, String... merkkijonot) // sallittu!

Ennalta määrittelemätöntä parametrien arvojen määrää käytetään esimerkiksi silloin, kun halutaan tarjota rajapinta, joka ei rajoita sen käyttäjää tiettyyn parametrien määrään. Vaihtoehtoinen lähestymistapa on metodimäärittely, jolla on parametrina tietyn tyyppinen lista. Tällöin oliot voidaan asettaa listaan ennen metodikutsua, ja kutsua metodia antamalla lista sille parametrina.

Muutamassa tehtävässä (mm. ohpen kirjasto ja ohjan sanatutkimus) törmäsimme tilanteeseen, jossa jouduimme filtteröimään listalta jotain hakuehtoa vastaavat oliot. Esim. sanatutkimuksessa metodit zSisaltava, lLoppuiset, palindromit, kaikkiVoksSis tekivät oleellisesti saman asian: ne kävivät läpi tiedoston sisällön sana kerrallaan ja tarkastivat jokaisen sanan kohdalla päteekö sille tietty ehto, ja jos pätee, ottivat sanan talteen. Koska kaikkien metodien ehto oli erilainen, ei toisteisuutta tehtävissä osattu poistaa vaan kaikkien koodi oli ehtoa vaille "copypastea".

Tässä tehtävässä teemme ohjelman, jonka avulla on mahdollista filtteröidä rivejä Project Gutenbergin sivuilta löytyvistä kirjoista. Seuraavassa esimerkkinä Dostojevskin Rikos ja rangaistus. Haluamme, että erilaisia filtteröintiehtoja on monelaisia ja että filtteröinti voi tapahtua myös eri ehtojen kombinaationa. Ohjelman rakenteen pitää myös mahdollistaa uusien ehtojen lisääminen myöhemmin.

Sopiva ratkaisu tilanteeseen on jokaisen filtteröintiehdon määritteleminen omana rajapinnan Ehto toteuttavana oliona. Seuraavassa rajapinnan määritelmä:

public interface Ehto {
    boolean toteutuu(String rivi);
}

Seuraavassa eräs rajapinnan toteuttava filtteriluokka:

public class SisaltaaSanan implements Ehto {

    String sana;

    public SisaltaaSanan(String sana) {
        this.sana = sana;
    }

    @Override
    public boolean toteutuu(String rivi) {
        return rivi.contains(sana);
    }
}

Luokan oliot ovat siis hyvin yksinkertaisia, ne muistavat konstruktorin parametrina annetun sanan. Olion ainoalta metodilta voi kysyä toteutuuko ehto parametrina olevalle merkkijonolle, ja ehdon toteutuminen tarkoittaa olion tapauksessa sisältääkö merkkijono olion muistaman sanan.

Tehtäväpohjan mukana saat valmiina luokan GutenbergLukija, jonka avulla voit tutkia kirjojen rivejä filtteröitynä parametrina annetun hakuehdon perusteella:

public class GutenbergLukija {

    private List<String> sanat;

    public GutenbergLukija(String osoite) throws IllegalArgumentException {
        // kirjan verkosta hakeva koodi
    }

    public List&lg;String> rivitJoilleVoimassa(Ehto ehto){
        List&lg;String> ehdonTayttavat = new ArrayList&lg;String>();

        for (String rivi : sanat) {
            if ( ehto.toteutuu(rivi)) {
                ehdonTayttavat.add(rivi);
            }
        }

        return ehdonTayttavat;
    }
}

Seuraavassa tulostetaan Rikoksesta ja rangaistuksesta kaikki rivit, joilla esiintyy sana "beer":

public static void main(String[] args) {
    String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergLukija kirja = new GutenbergLukija(osoite);

    Ehto ehto = new SisaltaaSanan("beer");

    for (String rivi : kirja.rivitJoilleVoimassa(ehto)) {
        System.out.println(rivi);
    }
}

Huom: Project Gutenbergin sivuilla olevat tekstit ovat tarkoitettu vain ihmisten luettaviksi. Jos tekstejä haluaa lukea koneellisesti eli esim. GutenbergLukijalla, on kirjan sivulta, esim. http://www.gutenberg.org/ebooks/2554 mentävä alakulmassa olevaan linkin mirror sites takaa löytyvälle sivulle, esim. bulgarialaiselle e-book Ltd:n sivulle ja luettava kirja sieltä löytyvästä osoitteesta.

154.1 Kaikki sanat

Tee rajapinnan ehto toteuttava luokka KaikkiRivit, joka kelpuuttaa jokaisen rivin. Tämä ja muutkin tämän tehtävän luokat tulee toteuttaa pakkaukseen lukija.ehdot.

public static void main(String[] args) {
    String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergLukija kirja = new GutenbergLukija(osoite);

    Ehto ehto = new KaikkiRivit();

    for (String rivi : kirja.rivitJoilleVoimassa(ehto)) {
        System.out.println(rivi);
    }
}

154.2 Loppuu huuto- tai kysymysmerkkiin

Tee rajapinnan ehto toteuttava luokka LoppuuHuutoTaiKysymysmerkkiin, joka kelpuuttaa ne rivit, joiden viimeinen merkki on huuto- tai kysymysmerkki.

public static void main(String[] args) {
    String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergLukija kirja = new GutenbergLukija(osoite);

    Ehto ehto = new LoppuuHuutoTaiKysymysmerkkiin();

    for (String rivi : kirja.rivitJoilleVoimassa(ehto)) {
        System.out.println(rivi);
    }
}

Muistutus: merkkien vertailu Javassa tapahtuu == operaattorilla:

String nimi = "pekka";

// HUOM: 'p' on merkki eli char p, "p" taas merkkojono, jonka ainoa merkki on p
if ( nimi.charAt(0) == 'p' ) {
    System.out.println("alussa p");
} else {
    System.out.println("alussa jokin muu kuin p");
}

154.3 Pituus vähintään

Tee rajapinnan ehto toteuttava luokka PituusVahintaan, jonka oliot kelpuuttavat ne rivit, joiden pituus on vähintään olion konstruktorin parametrina annettu luku.

public static void main(String[] args) {
    String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergLukija kirja = new GutenbergLukija(osoite);

    Ehto ehto = new PituusVahintaan(40);

    for (String rivi : kirja.rivitJoilleVoimassa(ehto)) {
        System.out.println(rivi);
    }
}

154.4 molemmat

Tee rajapinnan ehto toteuttava luokka Molemmat. Luokan oliot saavat konstruktorin parametrina kaksi rajapinnan Ehto toteuttavaa olioa. Molemmat-olio kelpuuttavaa ne rivit, jotka sen kummatkin konstruktorissa saamansa ehdot kelpuuttavat. Seuraavassa tulostetaan kaikki huuto- tai kysymysmerkkiin loppuvat rivit, jotka sisältävät sanan "beer".

public static void main(String[] args) {
    String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergLukija kirja = new GutenbergLukija(osoite);

    Ehto ehto = new Molemmat(
                    new LoppuuHuutoTaiKysymysmerkkiin(),
                    new SisaltaaSanan("beer")
                );

    for (String rivi : kirja.rivitJoilleVoimassa(ehto)) {
        System.out.println(rivi);
    }
}

154.5 negaatio

Tee rajapinnan ehto toteuttava luokka Ei. Luokan oliot saavat parametrina rajapinnan Ehto toteuttavaavan olion. Ei-olio kelpuuttaa ne rivit, joita sen parametrina saama ehto ei kelpuuta. Seuraavassa tulostetaan rivit, joiden pituus vähemmän kuin 10.

public static void main(String[] args) {
    String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergLukija kirja = new GutenbergLukija(osoite);

    Ehto ehto = new Ei( new PituusVahintaan(10) );

    for (String rivi : kirja.rivitJoilleVoimassa(ehto)) {
        System.out.println(rivi);
    }
}

154.6 vähintään yksi

Tee rajapinnan ehto toteuttava luokka VahintaanYksi. Luokan oliot saavat konstruktorin parametrina mielivaltaisen määrän rajapinnan Ehto toteuttavia olioita, konstruktorissa siis käytettävä vaihtuvanmittaista parametrilistaa. VahintaanYksi-oliot kelpuuttavat ne rivit, jotka ainakin yksi sen konstruktoriparametrina saamista ehdoista kelpuuttaa. Seuraavassa tulostetaan rivit, jotka sisältävät jonkun sanoista "beer", "milk" tai "oil".

public static void main(String[] args) {
    String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt";
    GutenbergLukija kirja = new GutenbergLukija(osoite);

    Ehto ehto = new VahintaanYksi(
                    new SisaltaaSanan("beer"),
                    new SisaltaaSanan("milk"),
                    new SisaltaaSanan("oil")
                );

    for (String rivi : kirja.rivitJoilleVoimassa(ehto)) {
        System.out.println(rivi);
    }
}

Huomaa, että ehtoja voi kombinoida mielivaltaisesti. Seuraavassa ehto, joka hyväksyy rivit, joilla on vähintään yksi sanoista "beer", "milk" tai "oil" ja jotka ovat pituudeltaan 20-30 merkkiä.

Ehto sanat = new VahintaanYksi(
                new SisaltaaSanan("beer"),
                new SisaltaaSanan("milk"),
                new SisaltaaSanan("oil")
            );

Ehto oikeaPituus = new Molemmat(
                     new PituusVahintaan(20),
                     new Ei( new PituusVahintaan(31))
                   );

Ehto halutut = new Molemmat(sanat, oikeaPituus);

56.7 StringBuilder

Olemme tottuneet rakentamaan merkkijonoja seuraavaan tapaan:

public static void main(String[] args) {
    int[] t = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    System.out.println(muotoile(t));
}

public static String muotoile(int[] t) {
    String mj = "{";

    for (int i = 0; i < t.length; i++) {
        mj += t[i];
        if (i != t.length - 1) {
            mj += ", ";

        }
    }

    return mj + "}";
}

Tulostus:

{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Tapa on toimiva mutta ei kovin tehokas. Kuten muistamme, merkkijonot ovat immutaabeleita eli olioita joita ei voi muuttaa. Merkkijono-operaatioiden tuloksena onkin aina uusi merkkijono-olio. Eli edellisessäkin esimerkissä syntyi välivaiheena 10 merkkijono-olioa. Jos syötteen koko olisi isompi, alkaisi välivaiheena olevien olioiden luominen vaikuttaa ohjelman suoritusaikaan ikävällä tavalla.

Edellisen kaltaisissa tilanteissa onkin parempi käyttää merkkijonon muodostamisessa StringBuilder-olioita. Toisin kuin Stringit, StringBuilderit eivät ole immutaabeleita, ja yhtä StringBuilderolioa voi muokata. Tutustu StringBuilderin API-kuvaukseen (löydät sen esim googlaamalla stringbuilder java api 6) ja muuta tehtäväpohjassa oleva metodi public static String muotoile(int[] t) toimimaan StringBuilderia käyttäen seuraavaan tapaan:

{
 1, 2, 3, 4,
 5, 6, 7, 8,
 9, 10
}

Eli aaltosulkeet tulevat omalle rivilleen. Taulukon alkioita tulostetaan 4 per rivi ja rivin ensimmäistä edeltää välilyönti. Pilkun jälkeen ennen seuraavaa numeroa tulee olla tasan yksi välilyönti.

57 Loppuhuipennus

Kurssi alkaa olla ohi ja on loppuhuipennuksen aika!

Tässä tehtävässä luodaan rakenteet ja osa toiminnallisuudesta seuraavannäköiseen matopeliin. Tehtävän palautettavassa versiossa tosin pelin väritys on erilainen, mato on musta, omena punainen ja pohja harmaa.

156.1 Pala ja Omena

Luo pakkaukseen matopeli.domain luokka Pala. Luokalla Pala on konstruktori public Pala(int x, int y), joka saa palan sijainnin parametrina. Lisäksi luokalla Pala on seuraavat metodit.

  • public int getX() palauttaa Palan konstruktorissa saadun x-koordinaatin.
  • public int getY() palauttaa Palan konstruktorissa saadun y-koordinaatin.
  • public boolean osuu(Pala pala) palauttaa true jos oliolla on sama x- ja y-koordinaatti kuin parametrina saadulla Pala-luokan ilmentymällä.
  • public String toString() palauttaa palan sijainnin muodossa (x,y). Esim. (5,2) kun x-koordinaatin arvo on 5 ja y-koordinaatin arvo on 2.

Toteuta pakkaukseen matopeli.domain myös luokka Omena. Peri luokalla Omena luokka Pala.

156.2 Mato

Toteuta pakkaukseen matopeli.domain luokka Mato. Luokalla Mato on konstruktori public Mato(int alkuX, int alkuY, Suunta alkusuunta), joka luo uuden madon jonka suunta on parametrina annettu alkusuunta. Mato koostuu listasta Pala-luokan ilmentymiä. Huom: enum Suunta löytyy valmiina pakkauksesta Matopeli.

Mato luodaan yhden palan pituisena, mutta madon "aikuispituus" on kolme. Madon tulee kasvaa yhdellä aina kun se liikkuu. Kun madon pituus on kolme, se kasvaa isommaksi vain syödessään.

Toteuta madolle seuraavat metodit

  • public Suunta getSuunta() palauttaa madon suunnan.
  • public void setSuunta(Suunta suunta) asettaa madolle uuden suunnan. Mato liikkuu uuteen suuntaan kun metodia liiku kutsutaan seuraavan kerran.
  • public int getPituus() palauttaa madon pituuden. Madon pituuden tulee olla sama kuin getPalat()-metodikutsun palauttaman listan alkioiden määrä.
  • public List<Pala> getPalat() palauttaa listan pala-olioita, joista mato koostuu. Palat ovat listalla järjestyksessä, siten että pää sijaitsee listan lopussa.
  • public void liiku() liikuttaa matoa yhden palan verran eteenpäin.
  • public void kasva() kasvattaa madon kokoa yhdellä. Madon kasvaminen tapahtuu seuraavan liiku-metodikutsun yhteydessä. Sitä seuraaviin liiku-kutsuihin kasvaminen ei enää vaikuta. Jos madon pituus on 1 tai 2 kun metodia kutsutaan, ei kutsulla saa olla mitään vaikutusta matoon.
  • public boolean osuu(Pala pala) tarkistaa osuuko mato parametrina annettuun palaan. Jos mato osuu palaan, eli joku madon pala osuu metodille parametrina annettuun palaan, tulee metodin palauttaa arvo true. Muuten metodi palauttaa arvon false.
  • public boolean osuuItseensa() tarkistaa osuuko mato itseensä. Jos mato osuu itseensä, eli joku sen pala osuu johonkin toiseen sen palaan, metodi palauttaa arvon true. Muuten metodi palauttaa arvon false.

Metodien public void kasva() ja public void liiku() toiminnallisuus tulee toteuttaa siten, että mato kasvaa vasta seuraavalla liikkumiskerralla.

Liikkuminen kannattaa toteuttaa siten, että madolle luodaan liikkuessa aina uusi pala. Uuden palan sijainti riippuu madon kulkusuunnasta: vasemmalle mennessä uuden palan sijainti on edellisen pääpalan sijainnista yksi vasemmalle, eli sen x-koordinaatti on yhtä pienempi. Jos uuden palan sijainti on edellisen pääpalan alapuolella, eli madon suunta on alas, tulee uuden palan y-koordinaatin olla yhtä isompi kuin pääpalan y-koordinaatti (käytämme siis piirtämisestä tuttua koordinaattijärjestelmää, jossa y-akseli on kääntynyt).

Liikkuessa uusi pala lisätään listan loppuun, ja poistetaan listan alussa oleva alkio. Uudesta palasta siis tulee madon "uusi pää" ja jokaisen palan koordinaatteja ei tarvitse päivittää erikseen. Toteuta kasvaminen siten, että listan alussa olevaa palaa, eli "madon häntää" ei poisteta jos metodia kasva on juuri kutsuttu.

Huom! Kasvata matoa aina sen liikkuessa jos sen pituus on pienempi kuin 3.

Mato mato = new Mato(5, 5, Suunta.OIKEA);
System.out.println(mato.getPalat());
mato.liiku();
System.out.println(mato.getPalat());
mato.liiku();
System.out.println(mato.getPalat());
mato.liiku();
System.out.println(mato.getPalat());

mato.kasva();
System.out.println(mato.getPalat());
mato.liiku();
System.out.println(mato.getPalat());

mato.setSuunta(Suunta.VASEN);
System.out.println(mato.osuuItseensa());
mato.liiku();
System.out.println(mato.osuuItseensa());
[(5,5)]
[(5,5), (6,5)]
[(5,5), (6,5), (7,5)]
[(6,5), (7,5), (8,5)]
[(6,5), (7,5), (8,5)]
[(6,5), (7,5), (8,5), (9,5)]
false
true

156.3 Matopeli, osa 1

Muokataan seuraavaksi pakkauksessa matopeli.peli olevaa matopelin toiminnallisuutta kapseloivaa luokka Matopeli. Matopeli-luokka perii luokan Timer, joka tarjoaa ajastustoiminnallisuuden pelin päivittämiseen. Luokka Timer vaatii toimiakseen ActionListener-rajapinnan toteuttavan luokan. Olemme toteuttaneet luokalla Matopeli rajapinnan ActionListener.

Muokkaa matopelin konstruktorin toiminnallisuutta siten, että konstruktorissa luodaan peliin liittyvä Mato. Luo mato siten, että sijainti riippuu Matopeli-luokan konstruktorissa saaduista parametreista. Madon x-koordinaatin tulee olla leveys / 2, y-koordinaatin korkeus / 2 ja suunnan Suunta.ALAS.

Luo konstruktorissa myös omena. Konstruktorissa luotavan omenan sijainnin tulee olla satunnainen, kuitenkin niin että omenan x-koordinaatti on aina välillä [0, leveys[, ja y-koordinaatti välillä [0, korkeus[.

Lisää matopeliin lisäksi seuraavat metodit

  • public Mato getMato() palauttaa matopelin madon.
  • public void setMato(Mato mato) asettaa matopeliin metodin parametrina olevan madon. Jos metodia getMato kutsutaan madon asetuksen jälkeen, tulee metodin getMato palauttaa viite samaan matoon.
  • public Omena getOmena palauttaa matopelin omenan.
  • public void setOmena(Omena omena) asettaa matopeliin metodin parametrina olevan omenan. Jos metodia getOmena kutsutaan omenan asetuksen jälkeen, tulee metodin getOmena palauttaa viite samaan omenaan.

156.4 Matopeli, osa 2

Muokkaa metodin actionPerformed-toiminnallisuutta siten, että metodissa toteutetaan seuraavat askeleet annetussa järjestyksessä.

  1. Liikuta matoa
  2. Jos mato osuu omenaan, syö omena ja kutsu madon kasva-metodia. Arvo peliin uusi omena.
  3. Jos mato törmää itseensä, aseta muuttujan jatkuu arvoksi false
  4. Jos mato törmää seinään, aseta muuttujan jatkuu arvoksi false
  5. Kutsu rajapinnan Paivitettava toteuttavan muuttujan paivitettava metodia paivita.
  6. Kutsu Timer-luokalta perittyä setDelay-metodia siten, että pelin nopeus kasvaa suhteessa madon pituuteen. Kutsu setDelay(1000 / mato.getPituus()); käy hyvin: kutsussa oletetaan että olet määritellyt oliomuuttujan nimeltä mato.

Aletaan seuraavaksi rakentamaan käyttöliittymäkomponentteja.

156.5 Näppäimistön kuuntelija

Toteuta pakkaukseen matopeli.gui luokka Nappaimistonkuuntelija. Luokalla on konstruktori public Nappaimistonkuuntelija(Mato mato), ja se toteuttaa rajapinnan KeyListener. Korvaa metodi keyPressed siten, että nuolinäppäintä ylös painettaessa madolle asetetaan suunta ylös. Nuolinäppäintä alas painettaessa madolle asetetaan suunta alas, vasemmalle painettaessa suunta vasen, ja oikealle painettaessa suunta oikea.

156.6 Piirtoalusta

Toteuta pakkaukseen matopeli.gui luokka Piirtoalusta, joka perii luokan JPanel. Piirtoalusta saa konstruktorin parametrina luokan Matopeli ilmentymän sekä int-tyyppisen muuttujan palanSivunPituus. Muuttuja palanSivunPituus kertoo minkä levyinen ja korkuinen yksittäinen pala on.

Korvaa luokalta JPanel peritty metodi paintComponent(Graphics g) siten, että piirrät metodissa paintComponent madon ja omenan. Käytä madon piirtämiseen Graphics-olion tarjoamaa fill3DRect-metodia. Madon värin tulee olla musta (Color.BLACK). Omenan piirtämisessä tulee käyttää Graphics-olion tarjoamaa fillOval-metodia. Omenan värin tulee olla punainen (Color.RED).

Huom: metodin paintComponent alussa tulee olla kutsu korvattuun metodiin, eli ensimmäisen rivin tulee olla super.paintComponent(g);

Toteuta luokalla Piirtoalusta myös rajapinta Paivitettava. Paivitettava-rajapinnan määrittelemän metodin paivita tulee kutsua JPanel-luokalta perittyä repaint-metodia.

156.7 Kayttoliittyma

Muuta luokkaa Kayttoliittyma siten, että käyttöliittymä sisältää piirtoalustan. Metodissa luoKomponentit tulee luoda piirtoalustan ilmentymä ja lisätä se container-olioon. Luo metodin luoKomponentit lopussa luokan Nappaimistokuuntelija ilmentymä, ja lisää se frame-olioon.

Lisää luokalle Kayttoliittyma myös metodi public Paivitettava getPaivitettava(), joka palauttaa metodissa luoKomponentit luotavan piirtoalustan.

Voit käynnistää käyttöliittymän Main-luokassa seuraavasti. Ennen pelin käynnistystä odotamme että käyttöliittymä luodaan. Kun käyttöliittymä on luotu, se kytketään matopeliin ja matopeli käynnistetään.

Matopeli matopeli = new Matopeli(20, 20);

Kayttoliittyma kali = new Kayttoliittyma(matopeli, 20);
SwingUtilities.invokeLater(kali);

while (kali.getPaivitettava() == null) {
    try {
        Thread.sleep(100);
    } catch (InterruptedException ex) {
        System.out.println("Piirtoalustaa ei ole vielä luotu.");
    }
}

matopeli.setPaivitettava(kali.getPaivitettava());
matopeli.start();

Peliin jää vielä paljon paranneltavaa. Palauta nyt tehtävä ja voit sen jälkeen laajentaa ja parannella peliä haluamallasi tavalla!

58 Kurssipalaute

Olemme saaneet paljon arvokasta palautetta TMC:n kautta. Näin kurssin viimeisenä kysymyksenä haluaisimme koko kurssin sisältöä koskevan palautteen. Anna palaute täyttämällä täältä löytyvä lomake. Palaute on anonyymi.

Jotta saat merkatuksi tämän tehtävän, aja tehtävän TMC-testit ja lähetä tehtävä palvelimelle.

Vaikka olemme saaneet paljon arvokasta palautetta TMC:n kautta yksittäisistä tehtävistä, palauttaessasi tätä, kirjoitathan kommenttikenttään palautteen koko MOOC-kurssista. Mainitsethan siinä, mikäli et halua palautettasi julkaistavan anonyymisti sivustoillamme. Vain osa palautteesta tullaan julkaisemaan.

Retired

Alla materiaalia, joka on ajan mittaa karsiutunut kurssilta. Jätämme sen kuitenkin tarjolle, jotta voit halutessasi opiskella asioita kurssin nykyisen muodon ulkopuolelta.

58.1 Tiedostojen valitseminen käyttöliittymästä

Silloin tällöin eteen tulee tilanne, jossa käyttäjän pitää pystyä valitsemaan tiedosto tiedostojärjestelmästä. Java tarjoaa tiedostojen valintaan valmiin käyttöliittymäkomponentin JFileChooser.

JFileChooser poikkeaa tähän mennessä käyttämistämme käyttöliittymäkomponenteista siinä, että se avaa uuden ikkunan. Avautuvan ikkunan ulkonäkö riippuu hieman käyttöjärjestelmästä, esimerkiksi hieman vanhemmassa Fedora-käyttöjärjestelmässä ikkuna on seuraavannäköinen.

JFileChooser-olio voidaan luoda missä tahansa. Olion metodille showOpenDialog annetaan parametrina käyttöliittymäkomponentti, johon se liittyy, esimerkiksi JFrame-luokan ilmentymä. Metodi showOpenDialog avaa tiedostonvalintaikkunan, ja palauttaa int-tyyppisen statuskoodin riippuen käyttäjän valinnasta. Luokassa JFileChooser on määritelty int-tyyppiset luokkamuuttujat, jotka kuvaavat statuskoodeja. Esimerkiksi onnistuneella valinnalla on arvo JFileChooser.APPROVE_OPTION.

Valittuun tiedostoon pääsee JFileChooser-oliosta käsiksi metodilla getSelectedFile.

JFileChooser chooser = new JFileChooser();

int valinta = chooser.showOpenDialog(frame);

if (valinta == JFileChooser.APPROVE_OPTION) {
    File valittu = chooser.getSelectedFile();
    System.out.println("Valitsit tiedoston: " + valittu.getName());
} else if (valinta == JFileChooser.CANCEL_OPTION) {
    System.out.println("Et valinnut tiedostoa!");
}

Yllä oleva esimerkki avaa valintaikkunan, ja tulostaa valitun tiedoston nimen jos valinta onnistuu. Jos valinta epäonnistuu, ohjelma tulostaa "Et valinnut tiedostoa!".

Tiedostojen filtteröinti

Tiedostojen filtteröinnillä tarkoitetaan vain tietynlaisten tiedostojen näyttämistä tiedostoikkunassa. JFileChooser-oliolle voi asettaa filtterin metodilla setFileFilter. Metodi setFileFilter saa parametrina abstraktin luokan FileFilter-ilmentymän, esimerkiksi luokasta FileNameExtensionFilter tehdyn olion.

Luokka FileNameExtensionFilter mahdollistaa tiedostojen filtteröinnin niiden päätteiden perusteella. Esimerkiksi pelkät txt-päätteiset tekstitiedostot saa näkyviin seuraavasti.

JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new FileNameExtensionFilter("Tekstitiedostot", "txt"));

int valinta = chooser.showOpenDialog(frame);

if (valinta == JFileChooser.APPROVE_OPTION) {
    File valittu = chooser.getSelectedFile();
    System.out.println("Valitsit tiedoston: " + valittu.getName());
} else if (valinta == JFileChooser.CANCEL_OPTION) {
    System.out.println("Et valinnut tiedostoa!");
}

Nopeustesti

Luodaan ohjelma, joka mittaa kliksutteluvauhtia. Käyttöliittymä tulee näyttämään esimerkiksi seuraavalta.

Oma luokka JButtonille

Toteuta pakkaukseen nopeustesti luokka Nappi, joka perii JButtonin. Luokalla Nappi tulee olla konstruktori public Nappi(String text, Color aktiivinen, Color passiivinen). Konstruktorin parametrina saama merkkijono text tulee antaa parametrina yläluokan konstruktorille (kutsu super(text)).

Korvaa luokasta JButton peritty metodi protected void paintComponent(Graphics g) siten, että piirrät metodissa napin kokoisen värillisen ympyrän. Saat napin leveyden ja korkeuden JButton-luokalta perityistä metodeista getWidth() ja getHeight(). Kutsu korvatun metodin alussa yläluokan paintComponent-metodia.

Ympyrän värin tulee riippua Napin tilasta: jos nappi on aktiivinen (metodi isEnabled palauttaa true tulee ympyrän väri olla konstruktorin parametrina saatu aktiivinenVari. Muulloin käytetään väriä passiivinenVari.

Perustoiminta

Toteuta luokkaan Nopeustesti käyttöliittymä, jossa on neljä nappulaa ja teksti. Käytä asettelussa napeille omaa JPanel-alustaa, joka asetetaan BorderLayout-asettelijan keskelle. Teksti tulee BorderLayout-asettelijan alaosaan.

Käytä edellisessä osassa luomaasi Nappi-luokkaa. Napeille tulee antaa konstruktorissa tekstit 1, 2, 3 ja 4.

Nappuloiden aktiivisuus

Vain yhden nappulan kerrallaan tulee olla painettavissa (eli aktiivisena). Voit tehdä nappulasta ei-aktiivisen metodikutsulla nappi.setEnabled(false). Vastaavasti nappi muutetaan aktiiviseksi kutsulla nappi.setEnabled(true).

Kun aktiivisena olevaa nappulaa painetaan, tulee käyttöliittymän arpoa uusi aktiivinen nappi.

Pisteytys

Tehdään peliin pisteytys: mitataan 20 painallukseen kuluva aika. Helpoin tapa ajan mittaamiseen on metodin System.currentTimeMillis() kutsuminen. Metodi palauttaa kokonaisluvunu, joka laskee millisekunteja (tuhannesosasekunteja) jostain tietysti ajanhetkestä lähtien. Siispä voit mitata kulunutta aikaa kutsumalla currentTimeMillis pelin alussa ja lopussa ja laskemalla erotuksen.

Toteuta siis seuraava: peli laskee napinpainallusten määrän, ja 20. painalluksen jälkeen asettaa kaikki nappulat epäaktiivisiksi ja näyttää JLabel-komponentissa viestin "Pisteesi: XXXX", jossa XXXX on painalluksiin kulunut aika (millisekunteina) jaettuna 20:lla. Pienempi pistemäärä on siis parempi.

Tiedostonnäytin

Tässä tehtävässä toteutetaan ohjelma, joka lukee käyttäjän valitseman tiedoston ja näyttää sen sisällön käyttöliittymässä.

Ohjelmassa on eroteltu käyttöliittymään ja sovelluslogiikka. Tehtäväpohjassa on valmiina sovelluslogiikan rajapinta TiedostonLukija sekä käyttöliittymäluokan runko Kayttoliittyma.

Käyttöliittymän rakentaminen

Täydennä käyttöliittymäluokan metodi luoKomponentit. Ohjelma tarvitsee toimiakseen kolme käyttöliittymäkomponenttia:

  • JButton-nappi, jossa on teksti "Valitse tiedosto..."
  • JTextArea-tekstieditorin avulla näytetään tiedoston sisältö, komponentin editointimahdollisuus tulee kytkeä pois päältä metodilla setEditable. JTextArea eroaa JTextField-komponentista siten, että JTextArea-komponentissa voi olla tekstiä useammalla rivillä.
  • JLabel-tietokenttä, joka sisältää näytettävän tiedoston nimen (ilman polkua!)

Koska tässä tehtävässä on vain kolme aseteltavaa komponenttia, riittävät asetteluun BorderLayout-asettelijan vaihtoehdot: BorderLayout.NORTH, BorderLayout.CENTER ja BorderLayout.SOUTH. Käyttöliittymäkomponentti JTextArea kannattaa sijoittaa keskelle, jotta se saa mahdollisimman paljon tilaa tekstin näyttämiselle.

Käyttöliittymän pitäisi näyttää suunnilleen seuraavalta. Alla olevassa esimerkissä JLabel-oliossa ei ole mitään tekstiä.

Tiedoston lukeminen

Luo pakkaukseen tiedostonnaytin.sovelluslogiikka luokka OmaTiedostonLukija, joka toteuttaa rajapinnan TiedostonLukija. Rajapinnassa on yksi metodi, lueTiedosto, joka lukee sille annetun tiedoston kokonaisuudessaan merkkijonoon ja palauttaa tämän merkkijonon.

Rajapinnan koodi:

package tiedostonnaytin.sovelluslogiikka;

import java.io.File;

public interface TiedostonLukija {
    String lueTiedosto(File tiedosto);
}

Huom: Palautettavassa merkkijonossa tulee säilyttää myös rivinvaihdot "\n". Esimerkiksi Scanner-lukijan metodi nextLine() poistaa palauttamistaan merkkijonoista rivinvaihdot, joten joudut joko lisäämään ne takaisin tai lukemaan tiedostoa eri tavalla.

Käyttöliittymän kytkeminen sovelluslogiikkaan

Viimeisessä tehtävän osassa toteutetaan käyttöliittymän JButton-napille tapahtumankuuntelija. Saat itse päättää luokalle sopivan nimen.

Tapahtumankuuntelijan tehtävänä on näyttää JFileChooser-tiedostonvalintaikkuna kun JButton-nappia painetaan. Kun käyttäjä valitsee tiedoston, tulee tapahtumankuuntelijan lukea tiedoston sisältö ja näyttää se JTextArea-kentässä. Tämän jälkeen tapahtumankuuntelijan tulee vielä päivittää JLabel-kenttään näytetyn tiedoston nimi (ilman tiedostopolkua).

JFileChooser-olion metodille showOpenDialog tulee antaa parametrina Kayttoliittyma-luokassa oleva JFrame-ikkunaolio. Jos käyttäjä valitsee tiedoston, tulee tiedosto lukea tapahtumankuuntelijassa Kayttoliittyma-luokassa määriteltyä TiedostonLukija-oliota apuna käyttäen. Kannattaa luoda tapahtumankuuntelija siten, että sille annetaan konstruktorissa kaikki tarvitut oliot.

Huomaa, että valintaikkunan voi myös sulkea valitsematta tiedostoa!

Kun tiedosto on avattu, tulee käyttöliittymän näyttää esimerkiksi seuraavalta.

Tekstiseikkailu

Tehtäväsarjassa tehdään laajennettava tekstiseikkailupelin runko. Seikkailu koostuu kohdista, joissa jokaisessa ruudulle tulee tekstiä. Kohdat voivat olla joko välivaiheita, kysymyksiä, tai monivalintakohtia. Monivalinta-tyyppisen kohdan näyttämä teksti voi olla esimerkiksi seuraavanlainen:

Huoneessa on kaksi ovea. Kumman avaat?

1. Vasemmanpuoleisen.
2. Oikeanpuoleisen.
3. Juoksen pakoon.

Käyttäjä vastaa kohdassa esitettävään tekstiin. Yllä olevaan tekstiin voi vastata 1, 2 tai 3, ja vastauksesta riippuu, minne käyttäjä siirtyy seuraavaksi.

Peliin tullaan toteuttamaan kohtia kuvaava rajapinta ja tekstikäyttöliittymä, jonka kautta peliä pelataan.

Huom! Toteuta kaikki tehtävän vaiheet pakkaukseen "seikkailu"

Kohta ja Välivaihe

Pelissä voi olla hyvinkin erilaisia kohtia, ja edellä olleessa esimerkissä ollut monivalinta on vain eräs vaihtoehto.

Toteuta kohdan käyttäytymistä kuvaava rajapinta Kohta. Rajapinnalla Kohta tulee olla metodi String teksti(), joka palauttaa kohdassa tulostettavan tekstin. Metodin teksti lisäksi kohdalla tulee olla metodi Kohta seuraavaKohta(String vastaus), jonka toteuttavat luokat palauttavat seuraavan kohdan vastauksen perusteella.

Toteuta tämän jälkeen yksinkertaisin tekstiseikkailun kohta, eli ei-interaktiivinen tekstiruutu, josta pääsee etenemään millä tahansa syötteellä. Toteuta ei-interaktiivista tekstiruutua varten luokka Valivaihe, jolla on seuraavanlainen API.

  • Luokka toteuttaa rajapinnan Kohta.
  • Konstruktori ottaa parametrikseen käyttäjälle näytettävän tekstin.
  • public String teksti()-metodi palauttaa konstruktorissa annetun tekstin sekä rivin "(jatka painamalla enteriä)". (Rivinvaihto saadaan aikaan merkillä "\n".)
  • public void asetaSeuraava(Kohta seuraava)-metodilla voidaan asettaa Kohta-olio, jonka seuraavaKohta(String vastaus) aina palauttaa (vastauksesta riippumatta).

Testaa ohjelmaasi seuraavalla esimerkillä:

Scanner lukija = new Scanner(System.in);

Valivaihe alkuteksti = new Valivaihe("Olipa kerran ohjelmoija.");
Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla.");
alkuteksti.asetaSeuraava(johdanto);

Kohta nykyinen = alkuteksti;
System.out.println(nykyinen.teksti());

nykyinen = nykyinen.seuraavaKohta(lukija.nextLine());
if (nykyinen == null) {
    System.out.println("Virhe ohjelmassa!");
}

System.out.println(nykyinen.teksti());

nykyinen = nykyinen.seuraavaKohta(lukija.nextLine());
if (nykyinen != null) {
    System.out.println("Virhe ohjelmassa!");
}
Olipa kerran ohjelmoija.
(jatka painamalla enteriä)

Joka alkoi ohjelmoimaan Javalla.
(jatka painamalla enteriä)

Käyttöliittymä

Pelin käyttöliittymä (luokka Kayttoliittyma) saa konstruktorin parametrina Scanner-olion ja Kohta-rajapinnan toteuttavan pelin aloittavan olion. Luokka tarjoaa metodin public void kaynnista(), joka käynnistää pelin suorituksen.

Käyttöliittymä käsittelee kaikkia kohtia Kohta-rajapinnan kautta. Käyttöliittymän tulee jokaisessa kohdassa kysyä kohtaan liittyvältä metodilta teksti tekstiä, joka käyttäjälle näytetään. Tämän jälkeen käyttöliittymä kysyy käyttäjältä vastauksen, ja antaa sen parametrina kohta-olion metodille seuraavaKohta. Metodi seuraavaKohta palauttaa vastauksen perusteella seuraavan kohdan, johon pelin on määrä siirtyä. Peli loppuu, kun metodi seuraavaKohta palauttaa arvon null.

Koska pääohjelma tulee käyttämään kohtia vain Kohta-rajapinnan kautta, voidaan peliin lisätä vaikka minkälaisia kohtia pääohjelmaa muuttamatta. Riittää tehdä uusia Kohta-rajapinnan toteuttavia luokkia.

Toteuta luokka Kayttoliittyma, ja testaa sen toimintaa seuraavalla esimerkillä

Scanner lukija = new Scanner(System.in);
Valivaihe alku = new Valivaihe("Olipa kerran ohjelmoija.");
Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla.");
Valivaihe loppu = new Valivaihe("Ja päätti muuttaa Helsinkiin.");

alku.asetaSeuraava(johdanto);
johdanto.asetaSeuraava(loppu);

new Kayttoliittyma(lukija, alku).kaynnista();
Olipa kerran ohjelmoija.
(jatka painamalla enteriä)
>

Joka alkoi ohjelmoimaan Javalla.
(jatka painamalla enteriä)
>

Ja päätti muuttaa Helsinkiin.
(jatka painamalla enteriä)
>

Käytä seuraavaa metodia käyttöliittymän kaynnista-metodina. Yritä piirtää paperille mitä käy kun käyttöliittymä käynnistetään.

public void kaynnista() {
    Kohta nykyinen = alkukohta;

    while (nykyinen != null) {
        System.out.println(nykyinen.teksti());
        System.out.print("> ");
        String vastaus = lukija.nextLine();

        nykyinen = nykyinen.seuraavaKohta(vastaus);
        System.out.println("");
    }
}

Käyttöliittymän kaynnista-metodi sisältää siis toistolauseen, jossa ensin tulostetaan käsiteltävän kohdan teksti. Tämän jälkeen kysytään käyttäjältä syötettä. Käyttäjän syöte annetaan vastauksena käsiteltävän kohdan seuraavaKohta-metodille. Metodi seuraavaKohta palauttaa kohdan, jota käsitellään seuraavalla toiston kierroksella. Jos palautettu kohta oli null, lopetetaan toisto.

Kysymyksiä

Tekstiseikkailussa voi olla kysymyksiä, joihin on annettava oikea vastaus ennen kuin pelaaja pääsee eteenpäin. Tee luokka Kysymys seuraavasti:

  • Luokka toteuttaa Kohta-rajapinnan.
  • Konstruktori saa parametreina kysymystekstin ja oikean vastauksen.
  • Seuraavan kohdan voi asettaa asetaSeuraava-metodilla.
  • Jos seuraavaKohta-metodia kutsutaan oikealla vastauksella, metodi palauttaa seuraavan kohdan, muuten metodi ei päästä etenemään ja palauttaa arvon this, eli viitteen tähän olioon.

Luokkaa voi testata seuraavalla pääohjelmalla:

Scanner lukija = new Scanner(System.in);

Kysymys alku = new Kysymys("Minä vuonna Javan ensimmäinen versio julkaistiin?", "1995");
Valivaihe hyva = new Valivaihe("Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling.");

alku.asetaSeuraava(hyva);

new Kayttoliittyma(lukija, alku).kaynnista();
Minä vuonna Javan ensimmäinen versio julkaistiin?
> 2000

Minä vuonna Javan ensimmäinen versio julkaistiin?
> 1995

Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling.
(jatka painamalla enteriä)
>

Monivalintakysymykset

Tällä hetkellä tekstiseikkailu tukee välivaiheita ja yksinkertaisia kysymyksiä. Tekstiseikkailu on siis lineaarinen, eli lopputulokseen ei voi käytännössä vaikuttaa. Lisätään seikkailuun monivalintakysymyksiä, joiden avulla pelin kehittäjä voi luoda vaihtoehtoista toimintaa.

Esimerkki vaihtoehtoisesta toiminnasta:

Kello on 13:37 ja päätät mennä syömään. Minne menet?
1. Exactumiin
2. Chemicumiin
> 1

Ruoka on loppu :(
(jatka painamalla enteriä)
>
Kello on 13:37 ja päätät mennä syömään. Minne menet?
1. Exactumiin
2. Chemicumiin
> 2

Mainio valinta!
(jatka painamalla enteriä)
>

Toteuta luokka Monivalinta, jonka API on seuraavanlainen

  • Toteuttaa rajapinnan Kohta.

  • public Monivalinta(String teksti)
  • Luokan konstruktori saa parametrina näytettävän tekstin.
  • public void lisaaVaihtoehto(String valinta, Kohta seuraava)
  • Lisää vaihtoehdon ja siihen liittyvän seuraavan kohdan. Yhdessä monivalintakysymyksessä voi olla rajaton määrä vaihtoehtoja.
  • public String teksti()
  • Palauttaa merkkijonona sekä konstruktorissa annetun perustekstin että kaikki valintavaihtoehdot. Valintavaihtoehdot tulee olla eritelty numeroilla.
  • public Kohta seuraavaKohta(String valinta)
  • Palauttaa käyttäjän valitsemaa vaihtoehtoa vastaavan kohdan. Käyttäjä valitsee aina kirjoittamalla numeron. Huom! Voit muuttaa merkkijonon numeroksi luokan Integer luokkametodilla parseInt.

Testaa ohjelmasi toimintaa seuraavalla pääohjelmalla:

Scanner lukija = new Scanner(System.in);

Monivalinta lounas = new Monivalinta("Kello on 13:37 ja päätät mennä syömään. Minne menet?");
Monivalinta chemicum = new Monivalinta("Lounasvaihtoehtosi ovat seuraavat:");

Valivaihe exactum = new Valivaihe("Exactumista on kaikki loppu, joten menet Chemicumiin.");

exactum.asetaSeuraava(chemicum);

lounas.lisaaVaihtoehto("Exactumiin", exactum);
lounas.lisaaVaihtoehto("Chemicumiin", chemicum);

Valivaihe nom = new Valivaihe("Olipas hyvää");

chemicum.lisaaVaihtoehto("Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta", nom);
chemicum.lisaaVaihtoehto("Jauhelihakebakot, paprikakastiketta", nom);
chemicum.lisaaVaihtoehto("Mausteista kalapataa", nom);

new Kayttoliittyma(lukija, lounas).kaynnista();
Kello on 13:37 ja päätät mennä syömään. Minne menet?
1. Exactumiin
2. Chemicumiin
> 1

Exactumista on kaikki loppu, joten menet Chemicumiin.
(jatka painamalla enteriä)
>

Lounasvaihtoehtosi ovat seuraavat:
1. Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta
2. Jauhelihakebakot, paprikakastiketta
3. Mausteista kalapataa
> 2

Olipas hyvää
(jatka painamalla enteriä)
>

Luokan Monivalinta sisäinen toteutus saattaa olla haastava. Kannattaa esimerkiksi käyttää listaa vastausvaihtoehtojen (merkkijonojen) tallentamiseen, ja hajautustaulua kohtien tallentamiseen valintavaihtoehdon indeksillä.

Tilastot kuntoon

NHL:ssä pidetään pelaajista yllä monenlaisia tilastotietoja. Teemme nyt oman ohjelman NHL-pelaajien tilastojen hallintaan.

Pelaajalistan tulostus

Tee luokka Pelaaja, johon voidaan tallettaa pelaajan nimi, joukkue, pelatut ottelut, maalimäärä, ja syöttömäärä. Luokalla tulee olla konstruktori, joka saa edellämainitut tiedot edellä annetussa järjestyksessä.

Tee kaikille edelläminituille arvoille myös ns. getterimetodit, jotka palauttavat arvot:

  • String getNimi
  • String getJoukkue
  • int getOttelut
  • int getMaalit
  • int getSyotot
  • int getPisteet - laskee kokonaispistemäärän eli maalien ja syöttöjen summan

Talleta seuraavat pelaajat ArrayList:iin ja tulosta listan sisältö:

public static void main(String[] args) {
    ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>();
    pelaajat.add(new Pelaaja("Alex Ovechkin", "WSH", 71, 28, 46));
    pelaajat.add(new Pelaaja("Dustin Byfuglien", "ATL", 69, 19, 31));
    pelaajat.add(new Pelaaja("Phil Kessel", "TOR", 70, 28, 24));
    pelaajat.add(new Pelaaja("Brendan Mikkelson", "ANA, CGY", 23, 0, 2));
    pelaajat.add(new Pelaaja("Matti Luukkainen", "SaPKo", 1, 0, 0 ));

    for (Pelaaja pelaaja : pelaajat) {
        System.out.println(pelaaja);
    }
}

Pelaajan toString()-metodin muodostaman tulostuksen tulee olla seuraavassa muodossa:

Alex Ovechkin WSH 71 28 + 46 = 74
Dustin Byfuglien ATL 69 19 + 31 = 50
Phil Kessel TOR 70 28 + 24 = 52
Brendan Mikkelson ANA, CGY 23 0 + 2 = 2
Matti Luukkainen SaPKo 1 0 + 0 = 0

Ensin siis nimi, sitten joukkue, jonka jälkeen ottelut, maalit, plusmerkki, syötöt, yhtäsuuruusmerkki ja kokonaispisteet eli maalien ja syöttöjen summa.

Tulostuksen siistiminen

Tee Pelaaja-luokkaan metodi toSiistiMerkkijono(), joka palauttaa samat tiedot siististi aseteltuna siten, että jokaiselle muuttujalle on varattu tietty määrä tilaa tulostuksessa.

Tulostuksen tulee näyttää seuraavalta:

Alex Ovechkin             WSH             71  28 + 46 = 74
Dustin Byfuglien          ATL             69  19 + 31 = 50
Phil Kessel               TOR             70  28 + 24 = 52
Brendan Mikkelson         ANA, CGY        23   0 +  2 =  2
Matti Luukkainen          SaPKo            1   0 +  0 =  0

Nimen jälkeen joukkueen nimien täytyy alkaa samasta kohdasta. Saat tämän aikaan esim. muotoilemalla nimen tulostuksen yhteydessä seuraavasti:

String nimiJaTyhjaa = String.format("%-25s", nimi);

Komento tekee merkkijonon nimiJaTyhjaa joka alkaa merkkijonon nimi sisällöllä ja se jälkeen tulee välilyöntejä niin paljon että merkkijonon pituudeksi tulee 25. Joukkueen nimi tulee vastaavalla tavalla tulostaa 14 merkin pituisena merkkijonona. Tämän jälkeen on otteluiden määrä (2 merkkiä), jota seuraa 2 välilyöntiä. Tämän jälkeen on maalien määrä (2 merkkiä), jota seuraa merkkijono " + ". Tätä seuraa syöttöjen määrä (2 merkkiä), merkkijono " = ", ja lopuksi yhteispisteet (2 merkkiä).

Lukuarvot eli ottelu-, maali-, syöttö- ja pistemäärä muotoillaan kahden merkin mittaisena, eli lukeman 0 sijaan tulee tulostua välilyönti ja nolla. Seuraava komento auttaa tässä:

String maalitMerkkeina = String.format("%2d", maalit);

Pistepörssin tulostus

Lisää luokalle Pelaaja rajapinta Comparable<Pelaaja>, jonka avulla pelaajat voidaan järjestää kokonaispistemäärän mukaiseen laskevaan järjestykseen. Järjestä pelaajat Collections-luokan avulla ja tulosta pistepörssi:

Collections.sort(pelaajat);

System.out.println("NHL pistepörssi:\n");
for (Pelaaja pelaaja : pelaajat) {
    System.out.println(pelaaja);
}

Tulostuu:

NHL pistepörssi:

Alex Ovechkin             WSH           71  28 + 46 = 74
Phil Kessel               TOR           70  28 + 24 = 52
Dustin Byfuglien          ATL           69  19 + 31 = 50

Ohjeita tähän tehtävään materiaalissa.

Kaikkien pelaajien tiedot

Tilastomme on vielä hieman vajavainen, siinä on vaan muutaman pelaajan tiedot (ja nekin vastaavat 16.3. tilannetta). Kaikkien tietojen syöttäminen käsin olisi kovin vaivalloista. Onneksemme internetistä osoitteesta http://nhlstatistics.herokuapp.com/players.txt löytyy päivittyvä, koneen luettavaksi tarkoitettu lista pelaajatiedoista.

Huom: kun menet osoitteeseen ensimmäistä kertaa, sivun latautuminen kestää muutaman sekunnin (sivu pyörii virtuaalipalvelimella joka sammutetaan jos sivua ei ole hetkeen käytetty). Sen jälkeen sivu toimii nopeasti.

Datan lukeminen internetistä on helppoa. Projektissasi on valmiina luokka Tilasto, joka lataa annetun verkkosivun.

import java.io.InputStream;
import java.net.URL;
import java.util.Scanner;

public class Tilasto {
    private static final String OSOITE =
            "http://nhlstatistics.herokuapp.com/players.txt";

    private Scanner lukija;

    public Tilasto() {
        this(OSOITE);
    }

    public Tilasto(String osoite) {
        try {
            URL url = new URL(osoite);
            lukija = new Scanner(url.openStream());
        } catch (Exception ex) {
        }
    }

    public Tilasto(InputStream in) {
        try {
            lukija = new Scanner(in);
        } catch (Exception ex) {
        }
    }

    public boolean onkoRivejaJaljella() {
        return lukija.hasNextLine();
    }

    public String annaSeuraavaRivi() {
        String rivi = lukija.nextLine();
        return rivi.trim();
    }
}

Tilasto-luokka lukee pelaajien tilastotiedot internetistä. Metodilla annaSeuraavaRivi() saadaan selville yhden pelaajan tiedot. Tietoja on tarkoitus lukea niin kauan kuin pelaajia riittää, tämä voidaan tarkastaa metodilla onkoRivejaJaljella()

Kokeile että ohjelmasi onnistuu tulostamaan Tilasto-luokan hakemat tiedot:

public static void main(String[] args) {
    Tilasto tilasto = new Tilasto();

    while (tilasto.onkoRivejaJaljella()) {
        String pelaajaRivina = tilasto.annaSeuraavaRivi();
        System.out.println(pelaajaRivina);
    }
}

Tulostus on seuraavan muodoinen:

Evgeni Malkin;PIT;62;39;46;54 
Steven Stamkos;TBL;70;50;34;64
Claude Giroux;PHI;66;26;56;27
Jason Spezza;OTT;72;29;46;30
// ... ja yli 800:n muun pelaajan tiedot

Huom: tulostuksen alussa ja lopussa ja jokaisen pelaajan välissä on html-tägejä, esim. <br/> joka aiheuttaa www-sivulle rivin vaihtumisen.

Tulostuksessa pelaajan tiedot on erotettu toisistaan puolipisteellä. Ensin nimi, sitten joukkue, ottelut, maalit, syötöt ja laukaukset.

Pelaajaa vastaava merkkijono on siis yksittäinen merkkijono. Saat pilkottua sen osiin split-komennolla seuraavasti:

        while (tilasto.onkoRivejaJaljella()) {
            String pelaajaRivina = tilasto.annaSeuraavaRivi();
            String[] pelaajaOsina = pelaajaRivina.split(";");
            for (int j = 0; j 

  

Kokeile että tämä toimii. Saat tästä tehtävästä pisteet seuraavan tehtävän yhteydessä.

Kaikkien pelaajien pistepörssi

Tee kaikista Tilasto-luokan hakemien pelaajien tiedoista Pelaaja-olioita ja lisää ne ArrayListiin. Lisää tehtävään luokka PelaajatTilastosta. Käytä alla olevaa koodia luokan runkona.

import java.util.ArrayList;

public class PelaajatTilastosta {
    public ArrayList<Pelaaja> haePelaajat(Tilasto tilasto) {
        ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>();
        while (tilasto.onkoRivejaJaljella()) {
            String pelaajaRivina = tilasto.annaSeuraavaRivi();
            String[] pelaajaOsina = pelaajaRivina.split(";");

            // Lisätään uusi pelaaja vain, jos syötteessä on kenttiä riittävästi
            if (pelaajaOsina.length > 4) {
                int ottelut = Integer.parseInt(pelaajaOsina[2].trim());
                // Täydennä koodia lukemalla kaikki pelaajaOsina-taulukon kentät uuteen Pelaaja-olioon
                // ...
                // pelaajat.add(new Pelaaja( ... ));
            }
        }

        return pelaajat;
    }
}

Tehtävänäsi on täydentää runkoa siten, että jokaisesta luetusta rivistä luodaan pelaaja, joka lisätään pelaajat-listaan. Huom! Tilasto-luokka palauttaa merkkijonoja, joten joudut muuntamaan merkkijonoja myös numeroiksi. Esimerkiksi numeromuotoinen ottelut on muutettava int:iksi Integer.parseInt-metodilla.

Jos merkkijonon metodi split ei ole tuttu, se jakaa merkkijonon useampaan osaan annetun merkin kohdalta. Esimerkiksi komento merkkijono.split(";"); palauttaa merkkijonosta taulukon, jossa alkuperäisen merkkijonon puolipisteellä erotetut osat ovat kukin omassa taulukon indeksissä.

Voit käyttää testauksen apuna seuraavaa pääohjelmaa:

        Tilasto tilasto = new Tilasto();

        PelaajatTilastosta pelaajienHakija = new PelaajatTilastosta();
        ArrayList<Pelaaja> pelaajat = pelaajienHakija.haePelaajat(tilasto);

        for (Pelaaja pelaaja : pelaajat) {
            System.out.println( pelaaja );
        }

Maali ja syöttöpörssi

Haluamme tulostaa myös maalintekijäpörssin eli pelaajien tiedot maalimäärän mukaan järjestettynä sekä syöttöpörssin. NHL:n kotisivu tarjoaa tämänkaltaisen toiminnallisuuden, eli selaimessa näytettävä lista on mahdollista saada järjestettyä halutun kriteerin mukaan.

Edellinen tehtävä määritteli pelaajien suuruusjärjestyksen perustuvan kokonaispistemäärään. Luokalla voi olla vain yksi compareTo-metodi, joten joudumme muunlaisia järjestyksiä saadaksemme turvautumaan muihin keinoihin.

Vaihtoehtoiset järjestämistavat toteutetaan erillisten luokkien avulla. Pelaajien vaihtoehtoisten järjestyksen määräävän luokkien tulee toteuttaa Comparator<Pelaaja>-rajapinta. Järjestyksen määräävän luokan olio vertailee kahta parametrina saamaansa pelaajaa. Metodeja on ainoastaan yksi compare(Pelaaja p1, Pelaaja p2), jonka tulee palauttaa negatiivinen arvo, jos pelaaja p1 on järjestyksessä ennen pelaajaa p2, positiivinen arvo jos p2 on järjestyksessä ennen p1:stä ja 0 muuten.

Periaatteena on luoda jokaista järjestämistapaa varten oma vertailuluokka, esim. maalipörssin järjestyksen määrittelevä luokka:

import java.util.Comparator;

public class Maali implements Comparator<Pelaaja> {
    public int compare(Pelaaja p1, Pelaaja p2) {
        // maalien perusteella tapahtuvan vertailun koodi tänne
    }
}

Tee Comparator-rajapinnan toteuttavat luokat Maali ja Syotto, ja niille vastaavat maali- ja syöttöpörssien generoimiseen sopivat sopivat vertailufunktiot.

Järjestäminen tapahtuu edelleen luokan Collections metodin sort avulla. Metodi saa nyt toiseksi parametrikseen järjestyksen määräävän luokan olion:

Maali maalintekijat = new Maali();
Collections.sort(pelaajat, maalintekijat);
System.out.println("NHL parhaat maalintekijät\n");

// tulostetaan maalipörssi

Järjestyksen määrittelevä olio voidaan myös luoda suoraan sort-kutsun yhteydessä:

Collections.sort(pelaajat, new Maali());
System.out.println("NHL parhaat maalintekijät\n");

// tulostetaan maalipörssi

Kun sort-metodi saa järjestyksen määrittelevän olion parametrina, se käyttää olion compareTo()-metodia pelaajia järjestäessään.

Olioiden samuus

pois?

Kaikki oliot ovat tyyppiä Object, joten minkä tahansa tyyppisen olion voi antaa parametrina Object-tyyppisiä parametreja vastaanottavalle metodille.

Tehtävän mukana tulee rajapinta Vertaaja. Toteuta pakkaukseen samuus luokka OlioidenVertaaja, joka toteuttaa rajapinnan Vertaaja. Metodien tulee toimia seuraavasti:

  • public boolean samaOlio(Object o1, Object o2) metodi palauttaa true, mikäli parametrina saatujen olioiden viitteet ovat samat, muutoin metodi palauttaa false. Olioiden viitteiden samuutta vertaillaan "=="-ilmaisulla.
  • public boolean vastaavat(Object o1, Object o2) metodi palauttaa true, mikäli parametrina saadut oliot ovat samanlaiset. Muutoin metodi palauttaa false. Käytä tässä vertailussa equals-metodia. Tarkemmin equals-metodin toiminnasta Javan Object luokan APIsta. Huomaa, että equals-metodin toiminta riippuu siitä, onko verrattavan olion luokka korvannut Object-luokassa määritellyn equals-metodin.
  • public boolean samaMerkkijonoEsitys(Object o1, Object o2) metodi palauttaa true, mikäli parametrina saatujen olioiden merkkijonoesitykset (metodin toString-palauttamat merkkijonot) ovat samat. Muutoin metodi palauttaa false.

Tehtävän mukana tulee luokka Henkilo, jossa equals- ja compareTo-metodit on korvattu. Kokeile toteuttamiesi metodien toimintaa seuraavalla esimerkkikoodilla.

OlioidenVertaaja vertaaja = new OlioidenVertaaja();
Henkilo henkilo1 = new Henkilo("221078-123X", "Pekka", "Helsinki");
Henkilo henkilo2 = new Henkilo("221078-123X", "Pekka", "Helsinki");  // täysin samansisältöinen kuin eka
Henkilo henkilo3 = new Henkilo("110934-123X", "Pekka", "Helsinki");  // eri pekka vaikka asuukin helsingissä

System.out.println(vertaaja.samaOlio(henkilo1, henkilo1));
System.out.println(vertaaja.samaOlio(henkilo1, henkilo2));
System.out.println(vertaaja.vastaavat(henkilo1, henkilo2));
System.out.println(vertaaja.vastaavat(henkilo1, henkilo3));
System.out.println(vertaaja.samaMerkkijonoEsitys(henkilo1, henkilo2));

Henkilo henkilo4 = new Henkilo("221078-123X", "Pekka", "Savonlinna"); // henkilo1:n pekka mutta asuinpaikka muuttuu

System.out.println(vertaaja.samaOlio(henkilo1, henkilo4));
System.out.println(vertaaja.vastaavat(henkilo1, henkilo4));
System.out.println(vertaaja.samaMerkkijonoEsitys(henkilo1, henkilo4));

Ylläolevan koodin tulostuksen pitäisi olla seuraava:

true
false
true
false
true
false
true
false

Kuviot

Tehtäväpohjan mukana tulee luokat Ympyra, Suorakulmio ja TasasivuinenKolmio. Luokat liittyvät samaan aihepiiriin, ja niillä on hyvin paljon yhteistä toiminnallisuutta. Tutustu luokkiin ennenkuin lähdet tekemään, jolloin hahmotat tarkemmin syyt muutoksille. Jos huomaat että luokissa on alustavasti sisennys hieman pielessä, kannattaa sisennys hoitaa kuntoon luettavuuden helpottamiseksi.

Kuvio

Toteuta pakkaukseen kuviot abstrakti luokka Kuvio, jossa on kuvioihin liittyvää toiminnallisuutta. Luokan kuvio tulee sisältää konstruktori public Kuvio(int x, int y), metodit public int getX(), public int getY(), sekä abstraktit metodit public abstract double pintaAla() ja public abstract double piiri().

Ympyra perii kuvion

Muuta luokan Ympyra toteutusta siten, että se perii luokan Kuvio. Luokan Ympyra ulkoinen toiminnallisuus ei saa muuttua, eli sen tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio avulla tai itse. Muistathan että konstruktorikutsun super avulla voit käyttää yliluokan konstruktoria. Kun metodi public int getX() on toteutettu jo yliluokassa se ei tarvitse erillistä toteutusta luokassa Ympyra.

Kuvio kuvio = new Ympyra(10, 10, 15);
System.out.println("X " + kuvio.getX());
System.out.println("Y " + kuvio.getY());
System.out.println("Pinta-ala " + kuvio.pintaAla());
System.out.println("Piiri " + kuvio.piiri());
X 10
Y 10
Pinta-ala 706.85834...
Piiri 94.24777...

Suorakulmio ja Tasakylkinen kolmio perii kuvion

Muuta luokkien Suorakulmio ja TasakylkinenKolmio toteutusta siten, että ne perivät luokan Kuvio. Luokkien ulkoinen toiminnallisuus ei saa muuttua, eli niiden tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio avulla tai itse.

Kuvio kuvio = new Suorakulmio(10, 10, 15, 15);
System.out.println("X " + kuvio.getX());
System.out.println("Y " + kuvio.getY());
System.out.println("Pinta-ala " + kuvio.pintaAla());
System.out.println("Piiri " + kuvio.piiri());
System.out.println("");

kuvio = new TasakylkinenKolmio(10, 10, 15);
System.out.println("X " + kuvio.getX());
System.out.println("Y " + kuvio.getY());
System.out.println("Pinta-ala " + kuvio.pintaAla());
System.out.println("Piiri " + kuvio.piiri());
X 10
Y 10
Pinta-ala 225.0
Piiri 60.0

X 10
Y 10
Pinta-ala 97.42785...
Piiri 45.0

58.2 TreeMap

Joukkojen järjestyksessä pitäminen onnistuu Set rajapinnan toteuttavan TreeSet-olion avulla. Aiemmassa Tehtavakirjanpito-esimerkissä henkilökohtaiset tehtäväpisteet tallennettiin Map-rajapinnan toteuttavaan HashMap-olioon. Kuten HashSet, HashMap ei pidä alkioita järjestyksessä. Rajapinnasta Map on olemassa toteutus TreeMap, jossa hajautustaulun avaimia pidetään järjestyksessä. Muutetaan Tehtavakirjanpito-luokkaa siten, että henkilökohtaiset pisteet tallennetaan TreeMap-tyyppiseen hajautustauluun.

public class Tehtavakirjanpito {
    private Map<String, Set<Integer>> tehdytTehtavat;

    public Tehtavakirjanpito() {
        this.tehdytTehtavat = new TreeMap<String, Set<Integer>>();
    }

    public void lisaa(String kayttaja, int tehtava) {
        if (!this.tehdytTehtavat.containsKey(kayttaja)) {
            this.tehdytTehtavat.put(kayttaja, new TreeSet<Integer>());
        }

        Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja);
        tehdyt.add(tehtava);
    }

    public void tulosta() {
        for (String kayttaja: this.tehdytTehtavat.keySet()) {
            System.out.println(kayttaja + ": " + this.tehdytTehtavat.get(kayttaja));
        }
    }
}

Muunsimme samalla Set-rajapinnan toteutukseksi TreeSet-luokan. Huomaa että koska olimme käyttäneet rajapintoja, muutoksia tuli hyvin pieneen osaan koodista. Etsi kohdat jotka muuttuivat!

Käyttäjäkohtaiset tehtävät voidaan nyt tulostaa järjestyksessä.

Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito();
kirjanpito.lisaa("Mikael", 3);
kirjanpito.lisaa("Mikael", 4);
kirjanpito.lisaa("Mikael", 3);
kirjanpito.lisaa("Mikael", 3);

kirjanpito.lisaa("Pekka", 4);
kirjanpito.lisaa("Pekka", 4);

kirjanpito.lisaa("Matti", 1);
kirjanpito.lisaa("Matti", 2);

kirjanpito.tulosta();
Matti: [1, 2]
Mikael: [3, 4]
Pekka: [4]

Luokka TreeMap vaatii että avaimena käytetyn luokan tulee toteuttaa Comparable-rajapinta. Jos luokka ei toteuta rajapintaa Comparable, voidaan luokalle TreeMap antaa konstruktorin parametrina Comparator-luokan toteuttama olio aivan kuten TreeSet-luokalle.

Sähköposteja

Tehtävänäsi on toteuttaa sähköpostiohjelmaan komponentti, joka säilöö viestejä. Tehtäväpohjan mukana tulee luokka Sahkoposti, joka esittää sähköpostiviestiä. Luokalla Sahkoposti on oliomuuttujat:

  • saapumisaika (yksinkertaisesti kokonaisluku)
  • lähettäjä
  • otsikko
  • sisältö

Toteutetaan tässä luokka Viestivarasto, joka tarjoaa sähköpostien hallintaan liittyviä toimintoja.

Viestivarasto, lisääminen ja hakeminen

Luo pakkaukseen posti luokka Viestivarasto, ja lisää sille seuraavat metodit:

  • public void lisaa(Sahkoposti s) lisää viestin
  • public Sahkoposti hae(String otsikko) palauttaa viestin jolla on annettu otsikko tai null jos sellaista ei ole.

Voit olettaa että millään kahdella viestillä ei ole samaa otsikkoa.

Ajan perusteella hakeminen

Lisää luokkaan Viestivarasto seuraavat metodit

  • public Sahkoposti hae(int aika) palauttaa viestin joka saapui annettuun aikaan tai null jos sellaista ei ole. Voit olettaa että millään kahdella viestillä ei ole samaa saapumisaikaa.
  • public Sahkoposti haeUusinViesti() hakee uusimman viestin (eli sen jonka saapumisaika on isoin) ai null jos sellaista ei ole.
  • public Sahkoposti haeUusinViesti(int ylaraja) hakee uusimman viestin joka ei ole saapunut annetun ajan ylaraja jälkeen. Metodi palauttaa null jos tällaista viestiä ei ole.

Huom! Kannattaa käyttää kahta erillistä rakennetta viestien tallentamiseen. Otsikon perusteella tallentamiseen voit käyttää HashMappia, ja viestien tallentamiseen ajan mukaan TreeMappia. Näin saat toteutettua hae-operaatiot tehokkaasti. Tutustu myös TreeMapin metodeihin lastKey() ja floorKey().

59 Ohjelmien automaattinen testaaminen

Errare humanum est

Ihminen on erehtyväinen ja paraskin ohjelmoija tekee virheitä. Ohjelman kehitysvaiheessa tapahtuvien virheiden lisäksi huomattava osa virheistä syntyy olemassa olevaa ohjelmaa muokattaessa. Ohjelman muokkauksen aikana tehdyt virheet eivät välttämättä näy muokattavassa osassa, vaan voivat ilmaantua välillisesti erillisessä osassa ohjelmaa: osassa, joka käyttää muutettua osaa.

Ohjelmien automaattinen testaaminen tarkoittaa toistettavien testien luomista. Testeillä varmistetaan että ohjelma toimii halutusti, ja että ohjelma säilyttää toiminnallisuutensa myös muutosten jälkeen. Sanalla automaattinen painotetaan sitä, että luodut testit ovat toistettavia ja että ne voidaan suorittaa aina haluttaessa -- ohjelmoijan ei tarvitse olla läsnä testejä suoritettaessa.

Otimme aiemmin askeleita kohti testauksen automatisointia antamalla Scanner-oliolle parametrina merkkijonon, jonka se tulkitsee käyttäjän näppäimistöltä antamaksi syötteeksi. Automaattisessa testaamisessa testaaminen viedään viedä pidemmälle: koneen tehtävänä on myös tarkistaa että ohjelman tuottama vastaus on odotettu.

Automaattisen testauksen tällä kurssilla painotettu osa-alue on yksikkötestaus, jossa testataan ohjelman pienten osakokonaisuuksien -- metodien ja luokkien -- toimintaa. Yksikkötestaamiseen käytetään Javalla yleensä JUnit-testauskirjastoa.

59.1 Pino ja automaattiset testit

Pino on kaikille ihmisille tuttu asia. Esimerkiksi ravintola Unicafessa lautaset ovat yleensä pinossa. Pinon päältä voi ottaa lautasen ja pinon päälle voi lisätä lautasia. On myös helppo selvittää onko pinossa vielä lautasia jäljellä.

Pino on myös ohjelmoinnissa usein käytetty aputietorakenne. Rajapintana lukuja sisältävä pino näyttää seuraavalta.

public interface Pino {
    boolean tyhja();
    boolean taynna();
    void pinoon(int luku);
    int pinosta();
    int huipulla();
    int lukuja();
}

Rajapinnan määrittelemien metodien on tarkoitus toimia seuraavasti:

  • public boolean tyhja() palauttaa true jos pino on tyhjä
  • public boolean taynna() palauttaa true jos pino on täynnä
  • public void pinoon(int luku) laittaa parametrina olevan luvun pinon päälle
  • public int huipulla() kertoo pinon huipulla olevan alkion
  • public int pinosta() poistaa ja palauttaa pinon päällä olevan alkion
  • public int lukuja() kertoo pinossa olevien lukujen määrän
  • public int tilaa() kertoo pinon vapaan tilan määrän

Toteutetaan rajapinnan Pino toteuttava luokka OmaPino, johon talletetaan lukuja. Pinoon mahtuvien lukujen määrä annetaan pinon konstruktorissa. Toteutamme pinon hieman aiemmasta poikkeavasti -- emme testaa ohjelmaa pääohjelman avulla, vaan käytämme pääohjelman sijasta automatisoituja JUnit-testejä ohjelman testaamiseen.

Tutustuminen JUnitiin

NetBeansissa olevat ohjelmamme ovat tähän asti sijainneet aina Source Packagesissa tai sen sisällä olevissa pakkauksissa. Ohjelman lähdekoodit tulevat aina kansioon Source Packages. Automaattisia testejä luodessa testit luodaan valikon Test Packages alle. Uusia JUnit-testejä voi luoda valitsemalla projektin oikealla hiirennapilla ja valitsemalla avautuvasta valikosta New -> JUnit Test.... Jos vaihtoehto JUnit test ei näy listassa, löydät sen valitsemalla Other.

JUnit-testit sijaitsevat luokassa. Uutta testitiedostoa luodessa ohjelma pyytää testitiedoston nimen. Tyypillisesti nimeksi annetaan testattavan luokan tai toiminnallisuuden nimi. Luokan nimen tulee aina päättyä sanaan Test. Esimerkiksi alla luodaan testiluokka PinoTest, joka sijaitsee pakkauksessa pino. NetBeans haluaa luoda käyttöömme myös valmista runkoa testiluokalle -- joka käy hyvin.

Jos NetBeans kysyy minkä JUnit-version haluat käyttöösi, valitse JUnit 4.x.

Kun testiluokka PinoTest on luotu, näkyy se projektin valikon Test Packages alla.

Luokka PinoTest näyttää aluksi seuraavalta

package pino;

import org.junit.*;
import static org.junit.Assert.*;

public class PinoTest {

    public PinoTest() {
    }

    @BeforeClass
    public static void setUpClass() throws Exception {
    }

    @AfterClass
    public static void tearDownClass() throws Exception {
    }

    @Before
    public void setUp() {
    }

    @After
    public void tearDown() {
    }
    // TODO add test methods here.
    // The methods must be annotated with annotation @Test. For example:
    //
    // @Test
    // public void hello() {}
}

Meille oleellisia osia luokassa PinoTest ovat metodit public void setUp, jonka yläpuolella on merkintä @Before, ja kommentoitu metodipohja public void hello(), jonka yläpuolella on merkintä @Test. Metodit, joiden yläpuolella on merkintä @Test ovat ohjelman toiminnallisuutta testaavia testimetodeja. Metodi setUp taas suoritetaan ennen jokaista testiä.

Muokataan luokkaa PinoTest siten, että sillä testataan rajapinnan Pino toteuttamaa luokkaa OmaPino. Älä välitä vaikkei luokkaa OmaPino ole vielä luotu. Pino on testiluokan oliomuuttuja, joka alustetaan ennen jokaista testiä metodissa setUp.

package pino;

import org.junit.*;
import static org.junit.Assert.*;

public class PinoTest {

    Pino pino;

    @Before
    public void setUp() {
        pino = new OmaPino(3);
    }

    @Test
    public void alussaTyhja() {
        assertTrue(pino.tyhja());
    }

    @Test
    public void lisayksenJalkeenEiTyhja() {
        pino.pinoon(5);
        assertFalse(pino.tyhja());
    }

    @Test
    public void lisattyAlkioTuleePinosta() {
        pino.pinoon(3);
        assertEquals(3, pino.pinosta());
    }

    @Test
    public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() {
        pino.pinoon(3);
        pino.pinosta();
        assertTrue(pino.tyhja());
    }

    @Test
    public void lisatytAlkiotTulevatPinostaOikeassaJarjestyksessa() {
        pino.pinoon(1);
        pino.pinoon(2);
        pino.pinoon(3);

        assertEquals(3, pino.pinosta());
        assertEquals(2, pino.pinosta());
        assertEquals(1, pino.pinosta());
    }

    @Test
    public void tyhjennyksenJalkeenPinoonLaitettuAlkioTuleeUlosPinosta() {
        pino.pinoon(1);
        pino.pinosta();

        pino.pinoon(5);

        assertEquals(5, pino.pinosta());
    }

    // ...
}

Jokainen testi, eli merkinnällä @Test varustettu metodi, alkaa tilanteesta, jossa on luotu uusi tyhjä pino. Jokainen yksittäinen @Test-merkitty metodi on oma testinsä. Yksittäisellä testimetodilla testataan aina yhtä pientä osaa pinon toiminnallisuudesta. Testit suoritetaan toisistaan täysin riippumattomina, eli jokainen testi alkaa "puhtaaltä pöydältä", setUp-metodin alustamasta tilanteesta.

Yksittäiset testit noudattavat aina samaa kaavaa. Ensin luodaan tilanne jossa tapahtuvaa toimintoa halutaan testata, sitten tehdään testattava toimenpide, ja lopuksi tarkastetaan onko tilanne odotetun kaltainen. Esimerkiksi seuraava testi testaa että lisäyksen ja poiston jälkeen pino on taas tyhjä -- huomaa myös kuvaava testimetodin nimeäminen:

@Test
public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() {
    pino.pinoon(3);
    pino.pinosta();
    assertTrue(pino.tyhja());
}

Ylläoleva testi testaa toimiiko metodi tyhja() jos pino on tyhjennetty. Ensin laitetaan pinoon luku metodilla pinoon, jonka jälkeen pino tyhjennetään kutsumalla metodia pinosta(). Tällöin on saatu aikaan tilanne jossa pinon pitäisi olla tyhjennetty. Viimeisellä rivillä testataan, että pinon metodi tyhja() palauttaa arvon true testausmetodilla assertTrue(). Jos metodi tyhja() ei palauta arvoa true näemme testejä suorittaessa virheen.

Jokainen testi päättyy jonkun assert-metodin kutsuun. Esimerkiksi metodilla assertEquals() voidaan varmistaa onko metodin palauttama luku tai merkkijono haluttu, ja metodilla assertTrue() varmistetaan että metodin palauttama arvo on true. Erilaiset assert-metodit saadaan käyttöön luokan alussa olevalla määrittelyllä import static org.junit.Assert.*;.

Testit suoritetaan joko painamalla alt ja F6 tai valitsemalla Run -> Test project. (Macintosh-koneissa tulee painaa ctrl ja F6). Punainen väri ilmaisee että testin suoritus epäonnistui -- testattava toiminnallisuus ei toiminut kuten toivottiin. Vihreä väri kertoo että testin testaama toiminnallisuus toimi kuten haluttiin.

Luokan OmaPino toteutus

Pinon toteuttaminen testien avulla tapahtuisi askel kerrallaan siten, että lopulta kaikki testit toimivat. Ohjelman rakentaminen aloitetaan yleensä hyvin varovasti. Rakennetaan ensin luokka OmaPino siten, että ensimmäinen testi alussaTyhja alkaa toimimaan. Älä tee mitään kovin monimutkaista, "quick and dirty"-ratkaisu kelpaa näin alkuun. Kun testi menee läpi (eli näyttää vihreää), siirry ratkaisemaan seuraavaa kohtaa.

Testi alussaTyhja menee läpi aina kun palautamme arvon true metodista tyhja.

package pino;

import java.util.ArrayList;
import java.util.List;

public class OmaPino implements Pino {

    public OmaPino(int maksimikoko) {
    }

    @Override
    public boolean tyhja() {
        return true;
    }

    // tyhjät metodirungot

Siirrytään ratkaisemaan kohtaa lisayksenJalkeenEiTyhja. Tarvitsemme toteutuksen metodille pinoon. Yksi lähestymistapa on muokata luokkaa OmaPino siten, että se sisältää taulukon. Taulukkoa käytetään, että pinottavat luvut talletetaan pinon taulukkoon yksi kerrallaan. Seuraava kuvasarja selkeyttää taulukossa olevien alkioiden pinoon laittamista ja pinosta ottamista.

pino = new OmaPino(4);

  0   1   2   3
-----------------
|   |   |   |   |
-----------------
alkioita: 0

pino.pinoon(5);

  0   1   2   3
-----------------
| 5 |   |   |   |
-----------------
alkiota: 1

pino.pinoon(3);

  0   1   2   3
-----------------
| 5 | 3 |   |   |
-----------------
alkiota: 2

pino.pinoon(7);

  0   1   2   3
-----------------
| 5 | 3 | 7 |   |
-----------------
alkiota: 3

pino.pinosta();

  0   1   2   3
-----------------
| 5 | 3 |   |   |
-----------------
alkiota: 2

Ohjelman tulee siis muistaa kuinka monta alkiota pinossa on. Uusi alkio laitetaan jo pinossa olevien perään. Alkion poisto aiheuttaa sen, että taulukon viimeinen käytössä ollut paikka vapautuu ja alkiomäärän muistavan muuttujan arvo pienenee.

Luokan OmaPino toteutusta jatketaan askel kerrallaan kunnes kaikki testit menevät läpi. Jossain vaiheessa ohjelmoija todennäköisesti huomaisi, että taulukko kannattaa vaihtaa ArrayList-rakenteeksi.

Huomaat todennäköisesti ylläolevan esimerkin luettuasi että olet jo tehnyt hyvin monta testejä käyttävää ohjelmaa. Osa TMC:n toiminnallisuudesta rakentuu JUnit-testien varaan, ongelmat ovat varsinkin kurssin alkupuolella pilkottu pieniin testeihin, joiden avulla ohjelmoijaa on ohjattu eteenpäin. TMC:n mukana tulevat testit ovat kuitenkin usein monimutkaisempia kuin ohjelmien normaalissa automaattisessa testauksessa niiden tarvitsee olla. TMC:ssä ja kurssilla käytettävien testien kirjoittajien tulee muunmuassa varmistaa luokkien olemassaolo, jota normaalissa automaattisessa testauksessa harvemmin tarvitsee tehdä.

Harjoitellaan seuraavaksi ensin testien lukemista, jonka jälkeen kirjoitetaan muutama testi.

Tehtävälista

Tehtäväpohjassa on rajapinnan Tehtavalista toteuttava luokka MuistiTehtavalist. Ohjelmaa varten on koodattu valmiiksi testit, joita ohjelma ei kuitenkaan läpäise. Tehtävänäsi on tutustua testiluokkaan TehtavalistaTest, ja korjata luokka MuistiTehtavalista siten, että ohjelman testit menevät läpi.

Huom! Tässä tehtävässä sinun ei tarvitse koskea testiluokkaan TehtavalistaTest.

Lukutilasto

Huom! Tässä tehtävässä on jo mukana testiluokka, johon sinun tulee kirjoittaa lisää testejä. Vastauksen oikeellisuus testataan vasta TMC-palvelimella: tehtävästä saa pisteet vasta kun molemmat tehtävät on suoritettu palvelimella hyväksytysti. Ole tarkka metodien nimennän ja lisättyjen lukujen kanssa.

Tehtävässä tulee pakkauksessa tilasto sijaitseva luokka Lukutilasto.

  • public void lisaaLuku(int luku)
    Lisää annetun luvun lukutilastoon.
  • public int summa()
    Palauttaa tilastossa olevien lukujen summan.
  • public int lukujenMaara()
    Palauttaa tilastossa olevien lukujen maaran.
  • public boolean sisaltaa(int luku)
    Palauttaa totuusarvon, joka kertoo onko parametrina annettu luku tilastossa.

Testikansiossa olevassa pakkauksessa tilasto on luokka LukutilastoTest, johon sinun tulee lisätä uusia testimetodeja.

Lukujen määrän kasvamisen tarkistus

Lisää testiluokkaan testimetodi public void lukujenMaaraKasvaaKahdellaKunLisataanKaksiLukua(), jossa lukutilastoon lisätään luvut 3 ja 5. Tämän jälkeen metodissa tarkistetaan että lukutilastossa on kaksi lukua käyttäen lukutilaston metodia lukujenMaara. Käytä Assert-luokan assertEquals-metodia palautettujen arvojen tarkastamiseen.

Summan tarkistus yhdellä luvulla

Lisää testiluokkaan testimetodi public void summaOikeinYhdellaLuvulla(), jossa lukutilastoon lisätään luku 3. Tämän jälkeen metodissa tarkistetaan lukutilaston summa-metodin avulla että tilastossa olevien lukujen summa on 3. Käytä Assert-luokan assertEquals-metodia palautettujen arvojen tarkastamiseen.

59.2 Jätkänshakin sovelluslogiikka (pakollinen)

Huom! tämä tehtävä on pakollinen yliopistoon hakeville.

Tämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen. Tehtävässä toteutetaan sovelluslogiikka jätkänshakille ja harjoitellaan ohjelmarakenteen osittaista omatoimista suunnittelua.

Tehtäväpohjassa tulee mukana käyttöliittymä jätkänshakille, jossa pelilaudan koko on aina 3x3 ruutua. Käyttöliittymä huolehtii ainoastaan pelilaudalla tehtyihin tapahtumiin reagoimisesta, sekä pelilaudan ja pelitilanteen tietojen päivittämisestä. Pelin logiikka on erotettu JatkanshakinSovelluslogiikka-rajapinnan avulla omaksi luokakseen.

package jatkanshakki.sovelluslogiikka;

public interface JatkanshakinSovelluslogiikka {
    char getNykyinenVuoro();
    int getMerkkienMaara();

    void asetaMerkki(int sarake, int rivi);
    char getMerkki(int sarake, int rivi);

    boolean isPeliLoppu();
    char getVoittaja();
}

Rajapinnan JatkanshakinSovelluslogiikka lisäksi tehtäväpohjassa on apuluokka, joka määrittelee pelilaudan ruutujen mahdolliset tilat char-tyyppisinä kirjaimina. Ruutu voi olla joko tyhjä, tai siinä voi olla risti tai nolla. Apuluokassa Jatkanshakki on näille määrittelyt:

package jatkanshakki.sovelluslogiikka;

public class Jatkanshakki {
    public static final char RISTI = 'X';
    public static final char NOLLA = 'O';
    public static final char TYHJA = ' ';
}

Tehtävänäsi on täydentää pakkauksessa jatkanshakki.sovelluslogiikka olevaa rajapinnan JatkanshakinSovelluslogiikka toteuttavaa luokkaa OmaJatkanshakinSovelluslogiikka. Luokka OmaJatkanshakinSovelluslogiikka mahdollistaa jätkänshakin pelaamisen.

Rajapinta JatkanshakinSovelluslogiikka määrittelee seuraavat toiminnot, jotka luokan OmaJatkanshakinSovelluslogiikka tulee toteuttaa:

  • char getNykyinenVuoro() palauttaa pelaajan merkkiä vastaavan arvon: RISTI, NOLLA tai pelin päätyttyä TYHJA
  • int getMerkkienMaara() palauttaa pelilaudalle tähän mennessä asetettujen merkkien määrän (välillä 0-9)
  • void asetaMerkki(int sarake, int rivi) asettaa pelaajan vuoron mukaisen merkin annettuun ruutuun sarakkeen (0-2) ja rivin (0-2) perusteella ja antaa vuoron toiselle pelaajalle. Metodi heittää poikkeuksen IllegalArgumentException, jos sarake tai rivi on pelilaudan ulkopuolella tai ruudussa on jo merkki, ja poikkeuksen IllegalStateException, jos peli on jo loppu.
  • char getMerkki(int sarake, int rivi) palauttaa sarakkeen ja rivin määrittelemän ruudun tilan, joka voi olla TYHJA, RISTI tai NOLLA. Metodi heittää poikkeuksen IllegalArgumentException, jos sarake tai rivi on pelilaudan ulkopuolella.
  • boolean isPeliLoppu() palauttaa arvon true, jos toinen pelaajista voitti pelin tai peli päättyi tasapeliin, muutoin metodi palauttaa false
  • char getVoittaja() palauttaa arvon TYHJA, jos peli on kesken tai peli päättyi tasapeliin, muutoin metodi palauttaa voittajan merkin: RISTI tai NOLLA

Ensimmäinen pelivuoro on aina merkillä RISTI. Pelin voittaa se pelaaja, joka saa ensimmäisenä kolme merkkiä vaakasuoraan, pystysuoraan tai vinottain. Tasapeli todetaan vasta, kun pelilauta on täynnä merkkejä eli tyhjiä ruutuja ei enää ole.

Vinkki: Pelilaudan tilanteen voi esittää esimerkiksi yhdeksän alkion char-taulukolla, jonne talletetaan peliruutujen tilat. Sarakkeen ja rivin perusteella voidaan laskea taulukon indeksi: rivi * 3 + sarake.

Materiaali