Sisällysluettelo

Tehtävät

Kysely / tutkimus: Tehtävänvaihtotesti

Tässä osallistut taas kokeellisessa psykologiassa käytettyyn tehtävänvaihtotestiin, missä tutkitaan aivojen mukautumista kahden tai useamman samanaikaisen tehtävän suorittamiseen. Tavoitteenamme on selvittää vaikuttaako ohjelmointi aivojen kykyyn hoitaa useampia samanaikaisia tehtäviä. Tämä testi tehdään useamman kerran kurssin aikana.

Toisin kuin edellisissä tehtävänvaihtotehtävissä, käytät tässä peliä, missä on tavoitteena ohjata pelihahmoa porttien läpi sekä tunnistaa kuvioita.

Käytössä olevassa pelissä on seitsemän osaa. Ensimmäisessä osassa harjoitellaan porttien läpäisemistä, toisessa osassa porttien läpäisemisen lisäksi pidetään hahmoa tietyllä tasolla, ja kolmannessa osassa reagoidaan lisäksi kuvioihin. Neljännessä osassa, edellisten lisäksi, tunnistetaan värejä; viidennessä osassa tunnistetaan kuvioita, kuudennessa osassa tunnistetaan molempia ja harjoitellaan oikeaa testausta varten. Seitsemännessä osassa pelataan peliä.

Testin tulokset tulevat vain tutkimuskäyttöön, eikä mahdollisesti raportoitavista tuloksista voida yksilöidä yksittäisiä vastaajia. Pelin pelaaminen kestää noin 10 minuuttia.

Kun olet vastannut allaolevaan lomakkeeseen, testi aukeaa uudessa ikkunassa. Varmistathan, että selaimesi sallii uusien ikkunoiden avaamisen tästä ohjelmointimateriaalista!

Tehtävänvaihtotestin taustakysely

55 Muutamia hyödyllisiä tekniikoita

55.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
    class="sh_java"> 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.

Tehtävä 169: Säännölliset lausekkeet

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

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

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

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

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.

55.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:

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

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

System.out.println("kortin maa potenssiin kaksi on " + maaPotenssiinKaksi);
kortin maa on jotain muuta kuin 34
kortin maa potenssiin kaksi on 3025

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:

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.

55.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() {
        this.kortit = new ArrayList<>();
    }

    public void lisaa(Kortti kortti){
        this.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();
            }
        }
    }
}

Tehtävä 170: Enum ja Iteraattori

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

170.1 Koulutus

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

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

170.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!

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

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

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

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

Tehtävä 171: Elokuvien suosittelija

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.

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

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

171.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]

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

171.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<>();
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]

171.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]

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

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

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

Tehtävä 172: JoustavatHakuehdot

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<String> rivitJoilleVoimassa(Ehto ehto){
        List<String> ehdonTayttavat = new ArrayList<>();

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

        return ehdonTayttavat;
    }
}

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

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.

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

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

172.2 Loppuu huuto- tai kysymysmerkkiin

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

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

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

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

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

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

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

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

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

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

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

Tehtävä 173: String builder

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.

56 Hieman isompi ohjelma

Tehtävä 174: Matopeli

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.

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

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

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

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

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

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

174.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!