Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.
Tekijät: Arto Vihavainen ja Matti Luukkainen
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.
Käytössä olevassa testiohjelmassa on neljä osaa. Ensimmäisessä osassa mitataan reaktioaikaa, toisessa osassa parittoman (odd) ja parillisen (even) luvun tunnistamista, kolmannessa osassa konsonantin (consonant) ja vokaalin (vowel) tunnistamista, ja viimeisessä osassa yhdistetään osat kaksi ja kolme. Näet jokaisen osion jälkeen edellisen osion tulokset.
Testin tulokset tulevat vain tutkimuskäyttöön, eikä mahdollisesti raportoitavista tuloksista voida yksilöidä yksittäisiä vastaajia. Tehtävän suorittamiseen menee noin 10-15 minuuttia.
Kun olet vastannut allaolevaan lomakkeeseen, testi aukeaa uudessa ikkunassa. Varmistathan, että selaimesi sallii uusien ikkunoiden avaamisen tästä ohjelmointimateriaalista!
Aloitetaan tehtäväsarja taas muutamalla kertaustehtävällä.
Vuosi on karkausvuosi, jos se on jaollinen 4:llä. Kuitenkin jos vuosi on jaollinen 100:lla, se on karkausvuosi vain silloin, kun se on jaollinen myös 400:lla.
Tee ohjelma, joka lukee käyttäjältä vuosiluvun, ja tarkistaa, onko vuosi karkausvuosi.
Anna vuosi: 2011 Vuosi ei ole karkausvuosi.
Anna vuosi: 2012 Vuosi on karkausvuosi.
Anna vuosi: 1800 Vuosi ei ole karkausvuosi.
Anna vuosi: 2000 Vuosi on karkausvuosi.
Tee metodi varianssi
, joka laskee ja palauttaa saamansa kokonaislukuja sisältävän listan otosvarianssin. Ohjeen varianssin laskemiseksi voit katsoa esimerkiksi tehtävän lopusta tai Wikipediasta kohdasta populaatio- ja otosvarianssi.
Tee metodi käyttäen apuna aiemmissa tehtävissä tehtyä metodia keskiarvo
. Huom! Kutsu metodia kuitenkin vain kertaalleen yhden varianssin laskemisen aikana.
public static double varianssi(ArrayList<Integer> lista) { // kirjoita koodia tähän } public static void main(String[] args) { ArrayList<Integer> lista = new ArrayList<>(); lista.add(3); lista.add(2); lista.add(7); lista.add(2); System.out.println("Varianssi: " + varianssi(lista)); }
Ohjelman tulostus:
Varianssi: 5.666667
(Lukujen keskiarvo on 3.5, joten otosvarianssi on ((3 - 3.5)² + (2 - 3.5)² + (7 - 3.5)² + (2 - 3.5)²)/(4 - 1) ˜ 5,666667.)
Kun listassa on N kappaletta lukuja voidaan otosvarianssi lasketaan seuraavasti:
((luku1 - keskiarvo)² + (luku2 - keskiarvo)² + ... + (lukuN - keskiarvo)²) / (N - 1)
Huom! Muistathan kokeillessasi ohjelmaa, että yhden alkion kokoisen listan (otos)varianssia ei ole määritelty! Kaavassa tapahtuu tällöin nollalla jakaminen. Java esittää nollalla jakamisen tuloksen epänumerona NaN
Tässä tehtävässä tehdään luokka YlhaaltaRajoitettuLaskuri
ja sovelletaan sitä kellon tekemiseen.
Tehdään luokka YlhaaltaRajoitettuLaskuri
. Luokan olioilla on seuraava toiminnallisuus:
seuraava
kasvattaa laskurin arvoa. Mutta jos laskurin arvo ylittää ylärajan, sen arvoksi tulee 0.toString
palauttaa laskurin arvon merkkijonona.Tehtäväpohjassa on valmiina pääohjelmaa varten tiedosto Paaohjelma
. Aloita tekemällä luokka YlhaaltaRajoitettuLaskuri
vastaavasti kuin Maksukortti-tehtävässä. Näin tehdään myös tulevissa tehtäväsarjoissa.
Luokan rungoksi tulee seuraava:
public class YlhaaltaRajoitettuLaskuri { private int arvo; private int ylaraja; public YlhaaltaRajoitettuLaskuri(int ylarajanAlkuarvo) { // kirjoita koodia tähän } public void seuraava() { // kirjoita koodia tähän } public String toString() { // kirjoita koodia tähän } }
Vihje: et voi palauttaa toStringissä suoraan kokonaislukutyyppisen oliomuuttujan laskuri
arvoa. Kokonaislukumuuttujasta arvo
saa merkkijonomuodon esim. lisäämällä sen eteen tyhjän merkkijonon eli kirjoittamalla "" + arvo
.
Seuraavassa on pääohjelma, joka käyttää laskuria:
public class Paaohjelma { public static void main(String[] args) { YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(4); System.out.println("arvo alussa: " + laskuri); int i = 0; while (i < 10) { laskuri.seuraava(); System.out.println("arvo: " + laskuri); i++; } } }
Laskurille asetetaan konstruktorissa ylärajaksi 4, joten laskurin arvo on luku 0:n ja 4:n väliltä. Huomaa, miten metodi seuraava
vie laskurin arvoa eteenpäin, kunnes se pyörähtää 4:n jälkeen 0:aan:
Ohjelman tulostuksen tulisi olla seuraava:
arvo alussa: 0 arvo: 1 arvo: 2 arvo: 3 arvo: 4 arvo: 0 arvo: 1 arvo: 2 arvo: 3 arvo: 4 arvo: 0
Tee toString
-metodista sellainen, että se lisää arvon merkkijonoesitykseen etunollan, jos laskurin arvo on vähemmän kuin 10. Eli jos laskurin arvo on esim. 3, palautetaan merkkijono "03", jos arvo taas on esim. 12, palautetaan normaaliin tapaan merkkijono "12".
Muuta pääohjelma seuraavaan muotoon ja varmista, että tulos on haluttu.
public class Paaohjelma { public static void main(String[] args) { YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(14); System.out.println("arvo alussa: " + laskuri); int i = 0; while (i < 16) { laskuri.seuraava(); System.out.println("arvo: " + laskuri); i++; } } }
arvo alussa: 00 arvo: 01 arvo: 02 arvo: 03 arvo: 04 arvo: 05 arvo: 06 arvo: 07 arvo: 08 arvo: 09 arvo: 10 arvo: 11 arvo: 12 arvo: 13 arvo: 14 arvo: 00 arvo: 01
Käyttämällä kahta laskuria voimme muodostaa kellon. Tuntimäärä on laskuri, jonka yläraja on 23, ja minuuttimäärä on laskuri jonka yläraja on 59. Kuten kaikki tietävät, kello toimii siten, että aina kun minuuttimäärä pyörähtää nollaan, tuntimäärä kasvaa yhdellä.
Tee ensin laskurille metodi arvo
, joka palauttaa laskurin arvon:
public int arvo() { // kirjoita koodia tähän }
Tee sitten kello täydentämällä seuraava pääohjelmarunko (kopioi tämä pääohjelmaksesi sekä täydennä tarvittavilta osin kommenttien ohjaamalla tavalla):
public class Paaohjelma { public static void main(String[] args) { YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23); int i = 0; while (i < 121) { System.out.println(tunnit + ":" + minuutit); // tulostetaan nykyinen aika // minuuttimäärä kasvaa // jos minuuttimäärä menee nollaan, tuntimäärä kasvaa i++; } } }
Jos kellosi toimii oikein, sen tulostus näyttää suunnilleen seuraavalta:
00:00 00:01 ... 00:59 01:00 01:01 01:02 ... 01:59 02:00
Laajenna kelloasi myös sekuntiviisarilla. Tee lisäksi luokalle YlhaaltaRajoitettuLaskuri
metodi asetaArvo
, jolla laskurille pystyy asettamaan halutun arvon -- jos et ole ihan varma mitä tässä pitäisi tehdä, kertaa materiaalista kohta missä puhutaan "settereistä".
Jos laskurille yritetään asettaa kelvoton arvo eli negatiivinen luku tai ylärajaa suurempi luku, ei laskurin arvo muutu.
Tämän metodin avulla voit muuttaa kellon ajan heti ohjelman alussa haluamaksesi.
Voit testata kellon toimintaa seuraavalla ohjelmalla
import java.util.Scanner; public class Paaohjelma { public static void main(String[] args) { Scanner lukija = new Scanner(System.in); YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23); System.out.print("sekunnit: "); int sek = // kysy sekuntien alkuarvo käyttäjältä System.out.print("minuutit: "); int min = // kysy minuuttien alkuarvo käyttäjältä System.out.print("tunnit: "); int tun = // kysy tuntien alkuarvo käyttäjältä sekunnit.asetaArvo(sek); minuutit.asetaArvo(min); tunnit.asetaArvo(tun); int i = 0; while (i < 121) { // lisää edelliseen myös sekuntiviisari i++; } } }
Kokeile laittaa kellosi alkamaan ajasta 23:59:50 ja varmista, että vuorokauden vaihteessa kello toimii odotetusti!
Bonus-tehtävä: ikuisesti käyvä kello (tehtävää ei palauteta!)
Ennen kuin alat tekemään tätä tehtävää, palauta jo tekemäsi kello!
Muuta pääohjelmasi seuraavaan muotoon:
public class Paaohjelma { public static void main(String[] args) throws Exception { YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23); sekunnit.asetaArvo(50); minuutit.asetaArvo(59); tunnit.asetaArvo(23); while (true) { System.out.println(tunnit + ":" + minuutit + ":" + sekunnit); Thread.sleep(1000); // lisää kellon aikaa sekunnilla eteenpäin } } }
Nyt kello käy ikuisesti ja kasvattaa arvoaan sekunnin välein. Sekunnin odotus tapahtuu komennolla Thread.sleep(1000);
, komennon parametri kertoo nukuttavan ajan millisekunteina. Jotta komento toimisi, pitää main:in esittelyriville tehdä pieni lisäys: public static void main(String[] args) throws Exception {
, eli tummennettuna oleva throws Exception
.
Saat ohjelman lopetettua painamalla NetBeans-konsolin (eli sen osan johon kello tulostaa arvonsa) vasemmalla laidalla olevasta punaisesta laatikosta.
Edellisellä tehtävässä rakennettiin luokka YlhaaltaRajoitettuLaskuri
ja rakennettiin laskurien avulla pääohjelmaan kello. Tehdään nyt myös itse kellosta olio. Luokan kello runko näyttää seuraavalta:
public class Kello { private YlhaaltaRajoitettuLaskuri tunnit; private YlhaaltaRajoitettuLaskuri minuutit; private YlhaaltaRajoitettuLaskuri sekunnit; public Kello(int tunnitAlussa, int minuutitAlussa, int sekunnitAlussa) { // laskurit tunneille, minuuteille ja sekunneille; // laskurien arvot tulee asettaa parametreina saatuun aikaan } public void etene() { // kello etenee sekunnilla } public String toString() { // palauttaa kellon merkkijonoesityksen } }
Luokkaan YlhaaltaRajoitettuLaskuri on kopioitu eräs ratkaisu edelliseen tehtävään. Toteuta luokan Kello
konstruktori ja puuttuvat metodit kolmea ylhäältä rajoitettua laskuria hyödyntäen.
Voit testata kelloasi seuraavalla pääohjelmalla:
public class Main { public static void main(String[] args) { Kello kello = new Kello(23, 59, 50); int i = 0; while (i < 20) { System.out.println(kello); kello.etene(); i++; } } }
Tulostuksen tulisi edetä seuraavasti:
23:59:50 23:59:51 23:59:52 23:59:53 23:59:54 23:59:55 23:59:56 23:59:57 23:59:58 23:59:59 00:00:00 00:00:01 ...
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 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
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."); }
Usein halutaan, että merkkijonossa toistuu jokin tietty alimerkkijono. Säännöllisissä lausekkeissa on käytössä seuraavat toistomerkinnät:
*
toisto 0... kertaa, esimString merkkijono = "trolololololo"; if(merkkijono.matches("trolo(lo)*")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
+
toisto 1... kertaa, esimMuoto 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.
?
toisto 0 tai 1 kertaa, esimString 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.
{a}
toisto a
kertaa, esimString merkkijono = "1010"; if(merkkijono.matches("(10){2}")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
{a,b}
toisto a
... b
kertaa, esimString 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.
{a,}
toisto a
... kertaa, esimString 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.
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
.
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.
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.
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.
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.
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(); } } } }
Tehdään ohjelma pienen yrityksen henkilöstön hallintaan.
Tee pakkaukseen henkilosto
lueteltu tyyppi eli enum Koulutus
jolla on tunnukset FT
(tohtori), FM
(maisteri), LuK
(kandidaatti), FilYO
(ylioppilas).
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
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äksipublic void lisaa(List<Henkilo> lisattavat)
lisää parametrina olevan listan henkilöitä työntekijöiksipublic void tulosta()
tulostaa kaikki työntekijätpublic void tulosta(Koulutus koulutus)
tulostaa työntekijät joiden koulutus on sama kuin parametrissa määritelty koulutusHUOM: Luokan Tyontekijat
tulosta
-metodit on toteutettava iteraattoria käyttäen!
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
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
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.
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.
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
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]
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.
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]
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]
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.
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.
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. kirjasto ja 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.
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); }
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"); }
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); }
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); }
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); }
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);
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.
Hirsipuu on peli, jossa käyttäjä yrittää arvata piilossa olevan sanan. Normaalissa hirsipuussa tietokone valitsee sanan, ja pitää sitä piilossa kun käyttäjä yrittää arvata sanaan liittyviä kirjaimia. Arvauskertoja on rajattu määrä: jos pelaaja arvaa kaikki sanaan liittyvän kirjaimet, hän voittaa pelin. Jos taas pelaaja ei arvaa sanoja, tietokone voittaa pelin.
Toteutetaan tässä palasia hieman ärsyttävämpään versioon hirsipuusta, missä tietokone pyrkii voittamaan pelin huijaamalla.
Huijauksen ideana on se, että tietokone voi vaihtaa valitsemansa sanan tarvittaessa lennosta. Pelin lopullinen toiminnallisuus on seuraava:
... Sinulla on 3 arvausta jäljellä. Olet käyttänyt merkit: [a, b, c, d, e, f, g, h, i, j] Sana: -a--a Arvaus: r Ei r-kirjaimia. Sinulla on 2 arvausta jäljellä. Olet käyttänyt merkit: [a, b, c, d, e, f, g, h, i, j, r] Sana: -a--a Arvaus: s Löytyi ainakin yksi s-kirjain. Sinulla on 2 arvausta jäljellä. Olet käyttänyt merkit: [a, b, c, d, e, f, g, h, i, j, r, s] Sana: -as-a Arvaus: p Ei p-kirjaimia. Sinulla on 1 arvaus jäljellä. Olet käyttänyt merkit: [a, b, c, d, e, f, g, h, i, j, p, r, s] Sana: -as-a Arvaus: t Löytyi ainakin yksi t-kirjain. Sinulla on 1 arvaus jäljellä. Olet käyttänyt merkit: [a, b, c, d, e, f, g, h, i, j, p, r, s, t] Sana: -asta Arvaus: v Ei v-kirjaimia. Parempaa onnea ensi kerralla! Sana oli: rasta
Ohjelman tekstikäyttöliittymä on toteutettu valmiiksi Main-luokkaan.
Tässä toteutettavaa luokkaa Sanalista
käytetään käytettävissä olevien sanojen rajaamiseen. Luokkaan Sanalista
on määritelty merkkijonolistan parametrina ottavan konstruktorin sekä seuraavat metodit.
public List<String> sanat()
- palauttaa sanalistalla olevat sanat.public Sanalista sanatJoidenPituusOn(int pituus)
- palauttaa uuden sanalista-olion, jossa on vain ne sanat, joiden pituus on parametrina annetun muuttujan arvo.public Sanalista sanatJoissaEiEsiinnyKirjainta(char kirjain)
- palauttaa uuden sanalista-olion, jossa on vain ne sanat, joissa ei esiinny parametrina annettua kirjainmerkkiä.public Sanalista sanatJoissaMerkit(String merkkijono)
- palauttaa uuden sanalista-olion, jossa on vain ne sanat, joissa on merkit parametrina annetun merkkijonon määräämissä kohdissa. Annettu merkkijono on muotoa --d-
, missä viivat kuvaavat mitä tahansa merkkiä ja kirjaimet merkkejä, joiden täytyy olla sanassa juuri annetulla paikalla.public int koko()
- palauttaa sanalistan sisältämien sanojen määrän.Toteuta edelliset metodit luokassa Sanalista
oleviin metodirunkoihin.
Luokka Hirsipuu
pitää kirjaa hirsipuu-pelin tilanteesta. Hirsipuulla on konstruktori, joka saa parametrinaan sanalistan sekä arvausten määrän. Hirsipuu valitsee konstruktorissa myös arvattavan sanan annetulta sanalistalta.
Hirsipuu tarjoaa lisäksi ainakin seuraavat metodit.
public boolean arvaa(Character merkki)
- arvaa parametrina annettua merkkiä. Lisää arvauksen arvauslistalle. Jos merkki löytyy arvattavasta sanasta, palauttaa true. Jos merkkiä taas ei löydy, vähentää arvausten määrää yhdellä, ja palauttaa false.public List<Character> arvaukset()
- palauttaa tehdyt arvaukset listaoliona.
public int arvauksiaJaljella()
- kertoo jäljellä olevien arvausten määrän.public String sana()
- kertoo arvattavan sanan siten, että kirjaimet, joita ei ole vielä arvattu, peitetään merkillä -
.public String oikeaSana()
- kertoo arvattavan sanan ilman peittelyä.public boolean onLoppu()
- kertoo onko peli loppu. Peli on loppu jos kaikki arvattavan sanan merkit on arvattu.Toteuta edelliset metodit. Kun edelliset metodit on toteutettu, voit jo pelata hirsipuuta.
Tarkastele toteutuksen avuksi Test Packages
-kansiossa sijaitsevaa luokkaa BHirsipuuTest
. Voitko päätellä mitä luokassa olevat metodit tekevät?
Jatka hirsipuun kehitystä siten, että hyödynnät sanalistaa ja pyrit tekemään hirsipuu-pelistä sellaisen, että se välttelee pelaajan arvauksia mahdollisimman hyvin. Kannattaa aloittaa arvaa-metodin parantamisesta.
Tähän osioon ei ole testejä -- palauta peli kun hirsipuu välttelee arvauksia mielestäsi tarpeeksi hyvin.
Kerro myös palautuksen yhteydessä hirsipuutekoälysi oleellisimmat tausta-ajatukset.
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.
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
.
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
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.Muokkaa metodin actionPerformed
-toiminnallisuutta siten, että metodissa toteutetaan seuraavat askeleet annetussa järjestyksessä.
jatkuu
arvoksi false
jatkuu
arvoksi false
Paivitettava
toteuttavan muuttujan paivitettava
metodia paivita
.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.
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.
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.
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!