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
Kurssin lähestyessä loppua katsomme vielä muutamaa hyödyllistä Javan ominaisuutta.
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, esimString 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.
?
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). Tässä metodissa saat käyttää säännöllisten lausekkeiden lisäksi mitä tahansa muutakin tekniikkaa.
Esimerkkitulostuksia metodia käyttävästä ohjelmasta:
Anna merkkijono: 17:23:05 Muoto on oikea.
Anna merkkijono: abc Muoto ei ole oikea.
Anna merkkijono: 33:33:33 Muoto ei ole oikea.
Nykyään lähes kaikista ohjelmointikielistä löytyy tuki säännöllisille lausekkeille. Säännöllisten lausekkeiden teoriaa tarkastellaan toisen vuoden kurssilla Laskennan mallit. Lisää säännöllisistä lausekkeista löydät esim. googlaamalla hakusanalla regular expressions java.
Toteutimme aiemmin pelikorttia mallintavan luokan Kortti
suunilleen seuraavasti:
public class Kortti { public static final int RUUTU = 0; public static final int PATA = 1; public static final int RISTI = 2; public static final int HERTTA = 3; private int arvo; private int maa; public Kortti(int arvo, int maa) { this.arvo = arvo; this.maa = maa; } @Override public String toString() { return maanNimi() + " "+arvo; } private String maanNimi() { if (maa == 0) { return "RUUTU"; } else if (maa == 1) { return "PATA"; } else if (maa == 2) { return "RISTI"; } return "HERTTA"; } public int getMaa() { return maa; } }
Kortin maa tallennetaan kortissa olevaan oliomuuttujaan kokonaislukuna. Maan ilmaisemiseen on määritelty luettavuutta helpottavat vakiot. Kortteja ja maita ilmaisevia vakioita käytetään seuraavasti:
public static void main(String[] args) { Kortti kortti = new Kortti(10, Kortti.HERTTA); System.out.println(kortti); if (kortti.getMaa() == Kortti.PATA) { System.out.println("on pata"); } else { System.out.println("ei ole pata"); } }
Maan esittäminen numerona on huono ratkaisu, sillä esimerkiksi seuraavat järjenvastaiset tavat käyttää korttia ovat mahdollisia:
Kortti jarjetonKortti = new Kortti(10, 55); System.out.println(jarjetonKortti); if (jarjetonKortti.getMaa() == 34) { System.out.println("kortin maa on 34"); } else { System.out.println("kortin maa on jotain muuta kun 34"); } int maaPotenssiinKaksi = jarjetonKortti.getMaa() * jarjetonKortti.getMaa(); System.out.println("kortin maa potenssiin kaksi on " + maaPotenssiinKaksi);
Jos tiedämme muuttujien mahdolliset arvot ennalta, voimme käyttää niiden esittämiseen enum
-tyyppistä luokkaa eli lueteltua tyyppiä. Luetellut tyypit ovat oma luokkatyyppinsä rajapinnan ja normaalin luokan lisäksi. Lueteltu tyyppi määritellään avainsanalla enum
. Esimerkiksi seuraava Maa
-enumluokka määrittelee neljä vakioarvoa: RUUTU
, PATA
, RISTI
ja HERTTA
.
public enum Maa { RUUTU, PATA, RISTI, HERTTA }
Yksinkertaisimmassa muodossaan enum
luettelee pilkulla erotettuina määrittelemänsä vakioarvot. Enumien vakiot on yleensä tapana kirjoittaa kokonaan isoin kirjaimin.
Enum luodaan (yleensä) omaan tiedostoon, samaan tapaan kuin luokka tai rajapinta. NetBeansissa Enumin saa luotua valitsemalla projektin kohdalla new/other/java/java enum.
Seuraavassa luokka Kortti
jossa maa esitetään enumin avulla:
public class Kortti { private int arvo; private Maa maa; public Kortti(int arvo, Maa maa) { this.arvo = arvo; this.maa = maa; } @Override public String toString() { return maa + " "+arvo; } public Maa getMaa() { return maa; } public int getArvo() { return arvo; } }
Kortin uutta versiota käytetään seuraavasti:
public class Paaohjelma { public static void main(String[] args) { Kortti eka = new Kortti(10, Maa.HERTTA); System.out.println(eka); if (eka.getMaa() == Maa.PATA) { System.out.println("on pata"); } else { System.out.println("ei ole pata"); } } }
Tulostuu:
HERTTA 10 ei ole pata
Huomaamme, että enumin tunnukset tulostuvat mukavasti! Koska kortin maat ovat nyt tyyppiä Maa
ei ylemmän esimerkin "järjenvastaiset" kummallisuudet, esim. "maan korottaminen toiseen potenssiin" onnistu. Oraclella on enum
-tyyppiin liittyvä sivusto osoitteessa http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html.
Tarkastellaan seuraavaa luokkaa Kasi
, joka mallintaa tietyssä korttipelissä pelaajan kädessä olevien korttien joukkoa:
public class Kasi { private ArrayList<Kortti> kortit; public Kasi() { kortit = new ArrayList<Kortti>(); } public void lisaa(Kortti kortti){ kortit.add(kortti); } public void tulosta(){ for (Kortti kortti : kortit) { System.out.println( kortti ); } } }
Luokan metodi tulosta
tulostaa jokaisen kädessä olevan kortin tutuksi tullutta "for each"-lausetta käyttämällä. ArrayList ja muut Collection-rajapinnan toteuttavat "oliosäiliöt" toteuttavat rajapinnan Iterable. Rajapinnan Iterable toteuttavat oliot on mahdollista käydä läpi eli "iteroida" esimerkiksi. for each -tyyppisellä komennolla.
Oliosäiliö voidaan käydä läpi myös käyttäen ns. iteraattoria, eli olioa, joka on varta vasten tarkoitettu tietyn oliokokoelman läpikäyntiin. Seuraavassa on iteraattoria käyttävä versio korttien tulostamisesta:
public void tulosta() { Iterator<Kortti> iteraattori = kortit.iterator(); while ( iteraattori.hasNext() ){ System.out.println( iteraattori.next() ); } }
Iteraattori pyydetään kortteja sisältävältä arraylistiltä kortit
. Iteraattori on ikäänkuin "sormi", joka osoittaa aina tiettyä listan sisällä olevaa olioa, ensin ensimmäistä ja sitten seuraavaa jne... kunnes "sormen" avulla on käyty jokainen olio läpi.
Iteraattori tarjoaa muutaman metodin. Metodilla hasNext()
kysytään onko läpikäytäviä olioita vielä jäljellä. Jos on, voidaan iteraattorilta pyytää seuraavana vuorossa oleva olio metodilla next()
. Metodi siis palauttaa seuraavana läpikäyntivuorossa olevan olion ja laittaa iteraattorin eli "sormen" osoittamaan seuraavana vuorossa olevaa läpikäytävää olioa.
Iteraattorin next-metodin palauttama olioviite voidaan ottaa toki talteen myös muuttujaan, eli metodi tulosta
voitaisiin muotoilla myös seuraavasti:
public void tulosta(){ Iterator<Kortti> iteraattori = kortit.iterator(); while ( iteraattori.hasNext() ){ Kortti seuraavanaVuorossa = iteraattori.next(); System.out.println( seuraavanaVuorossa ); } }
Teemme metodin jonka avulla kädestä voi poistaa tiettyä arvoa pienemmät kortit:
public class Kasi { // ... public void poistaHuonommat(int arvo) { for (Kortti kortti : kortit) { if ( kortti.getArvo() < arvo ) { kortit.remove(kortti); } } } }
Huomaamme että metodin suoritus aiheuttaa kummallisen virheen:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) at java.util.AbstractList$Itr.next(AbstractList.java:343) at Kasi.poistaHuonommat(Kasi.java:26) at Paaohjelma.main(Paaohjelma.java:20) Java Result: 1
Virheen syynä on se, että for-each:illa listaa läpikäydessä ei ole sallittua poistaa listalta olioita: komento for-each menee tästä "sekaisin".
Jos listalta halutaan poistaa osa olioista läpikäynnin aikana osa, tulee tämä tehdä iteraattoria käyttäen. Iteraattori-olion metodia remove
kutsuttaessa listalta poistetaan siististi se alkio jonka iteraattori palautti edellisellä metodin next
kutsulla. Toimiva versio metodista seuraavassa:
public class Kasi { // ... public void poistaHuonommat(int arvo) { Iterator<Kortti> iteraattori = kortit.iterator(); while (iteraattori.hasNext()) { if (iteraattori.next().getArvo() < arvo) { // poistetaan listalta olio jonka edellinen next-metodin kutsu palautti iteraattori.remove(); } } } }
Tehdään ohjelma pienen yrityksen henkilöstön hallintaan.
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ä:
Public class Paaohjelma { public static void main(String[] args) { Tyontekijat yliopisto = new Tyontekijat(); yliopisto.lisaa(new Henkilo("Matti", Koulutus.FT)); yliopisto.lisaa(new Henkilo("Pekka", Koulutus.FilYO)); yliopisto.lisaa(new Henkilo("Arto", Koulutus.FT)); yliopisto.tulosta(); yliopisto.irtisano(Koulutus.FilYO); System.out.println("=="); yliopisto.tulosta(); }
Tulostuu:
Matti, FT Pekka, FilYO Arto, FT == Matti, FT Arto, FT
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<Henkilo, Integer>(); henkiloidenSamuudet.put(matti, 42); henkiloidenSamuudet.put(pekka, 134); henkiloidenSamuudet.put(mikke, 8); henkiloidenSamuudet.put(thomas, 82); List<Henkilo> henkilot = Arrays.asList(matti, pekka, mikke, thomas); System.out.println("Henkilöt ennen järjestämistä: " + henkilot); Collections.sort(henkilot, new HenkiloComparator(henkiloidenSamuudet)); System.out.println("Henkilöt järjestämisen jälkeen: " + henkilot);
Henkilöt ennen järjestämistä: [Matti, Pekka, Mikke, Thomas] Henkilöt järjestämisen jälkeen: [Pekka, Thomas, Matti, Mikke]
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. 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≶String>(); for (String rivi : sanat) { if ( ehto.toteutuu(rivi)) { ehdonTayttavat.add(rivi); } } return ehdonTayttavat; } }
Seuraavassa tulostetaan Rikoksesta ja rangaistuksesta kaikki rivit, joilla esiintyy sana "beer":
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new SisaltaaSanan("beer"); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Huom: Project Gutenbergin sivuilla olevat tekstit ovat tarkoitettu vain ihmisten luettaviksi. Jos tekstejä haluaa lukea koneellisesti eli esim. GutenbergLukijalla, on kirjan sivulta, esim. http://www.gutenberg.org/ebooks/2554 mentävä alakulmassa olevaan linkin mirror sites takaa löytyvälle sivulle, esim. bulgarialaiselle e-book Ltd:n sivulle ja luettava kirja sieltä löytyvästä osoitteesta.
Tee rajapinnan ehto
toteuttava luokka KaikkiRivit
, joka kelpuuttaa jokaisen rivin. Tämä ja muutkin tämän tehtävän luokat tulee toteuttaa pakkaukseen lukija.ehdot
.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new KaikkiRivit(); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka LoppuuHuutoTaiKysymysmerkkiin
, joka kelpuuttaa ne rivit, joiden viimeinen merkki on huuto- tai kysymysmerkki.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new LoppuuHuutoTaiKysymysmerkkiin(); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Muistutus: merkkien vertailu Javassa tapahtuu == operaattorilla:
String nimi = "pekka"; // HUOM: 'p' on merkki eli char p, "p" taas merkkojono, jonka ainoa merkki on p if ( nimi.charAt(0) == 'p' ) { System.out.println("alussa p"); } else { System.out.println("alussa jokin muu kuin p"); }
Tee rajapinnan ehto
toteuttava luokka PituusVahintaan
, jonka oliot kelpuuttavat ne rivit, joiden pituus on vähintään olion konstruktorin parametrina annettu luku.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new PituusVahintaan(40); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka Molemmat
. Luokan oliot saavat konstruktorin parametrina kaksi rajapinnan Ehto
toteuttavaa olioa. Molemmat
-olio kelpuuttavaa ne rivit, jotka sen kummatkin konstruktorissa saamansa ehdot kelpuuttavat. Seuraavassa tulostetaan kaikki huuto- tai kysymysmerkkiin loppuvat rivit, jotka sisältävät sanan "beer".
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new Molemmat( new LoppuuHuutoTaiKysymysmerkkiin(), new SisaltaaSanan("beer") ); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka Ei
. Luokan oliot saavat parametrina rajapinnan Ehto
toteuttavaavan olion. Ei
-olio kelpuuttaa ne rivit, joita sen parametrina saama ehto ei kelpuuta.
Seuraavassa tulostetaan rivit, joiden pituus vähemmän kuin 10.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new Ei( new PituusVahintaan(10) ); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka VahintaanYksi
. Luokan oliot saavat konstruktorin parametrina mielivaltaisen määrän rajapinnan Ehto
toteuttavia olioita, konstruktorissa siis käytettävä vaihtuvanmittaista parametrilistaa. VahintaanYksi
-oliot kelpuuttavat ne rivit, jotka ainakin yksi sen konstruktoriparametrina saamista ehdoista kelpuuttaa. Seuraavassa tulostetaan rivit, jotka sisältävät jonkun sanoista "beer", "milk" tai "oil".
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new VahintaanYksi( new SisaltaaSanan("beer"), new SisaltaaSanan("milk"), new SisaltaaSanan("oil") ); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Huomaa, että ehtoja voi kombinoida mielivaltaisesti. Seuraavassa ehto, joka hyväksyy rivit, joilla on vähintään yksi sanoista "beer", "milk" tai "oil" ja jotka ovat pituudeltaan 20-30 merkkiä.
Ehto sanat = new VahintaanYksi( new SisaltaaSanan("beer"), new SisaltaaSanan("milk"), new SisaltaaSanan("oil") ); Ehto oikeaPituus = new Molemmat( new PituusVahintaan(20), new Ei( new PituusVahintaan(31)) ); Ehto halutut = new Molemmat(sanat, oikeaPituus);
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.
Kurssi alkaa olla ohi ja on loppuhuipennuksen aika!
Tässä tehtävässä luodaan rakenteet ja osa toiminnallisuudesta seuraavannäköiseen matopeliin. Tehtävän palautettavassa versiossa tosin pelin väritys on erilainen, mato on musta, omena punainen ja pohja harmaa.
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!
Olemme saaneet paljon arvokasta palautetta TMC:n kautta. Näin kurssin viimeisenä kysymyksenä haluaisimme koko kurssin sisältöä koskevan palautteen. Anna palaute täyttämällä täältä löytyvä lomake. Palaute on anonyymi.
Jotta saat merkatuksi tämän tehtävän, aja tehtävän TMC-testit ja lähetä tehtävä palvelimelle.
Vaikka olemme saaneet paljon arvokasta palautetta TMC:n kautta yksittäisistä tehtävistä, palauttaessasi tätä, kirjoitathan kommenttikenttään palautteen koko MOOC-kurssista. Mainitsethan siinä, mikäli et halua palautettasi julkaistavan anonyymisti sivustoillamme. Vain osa palautteesta tullaan julkaisemaan.
Silloin tällöin eteen tulee tilanne, jossa käyttäjän pitää pystyä valitsemaan tiedosto tiedostojärjestelmästä. Java tarjoaa tiedostojen valintaan valmiin käyttöliittymäkomponentin JFileChooser
.
JFileChooser poikkeaa tähän mennessä käyttämistämme käyttöliittymäkomponenteista siinä, että se avaa uuden ikkunan. Avautuvan ikkunan ulkonäkö riippuu hieman käyttöjärjestelmästä, esimerkiksi hieman vanhemmassa Fedora-käyttöjärjestelmässä ikkuna on seuraavannäköinen.
JFileChooser-olio voidaan luoda missä tahansa. Olion metodille showOpenDialog
annetaan parametrina käyttöliittymäkomponentti, johon se liittyy, esimerkiksi JFrame
-luokan ilmentymä. Metodi showOpenDialog
avaa tiedostonvalintaikkunan, ja palauttaa int
-tyyppisen statuskoodin riippuen käyttäjän valinnasta. Luokassa JFileChooser
on määritelty int
-tyyppiset luokkamuuttujat, jotka kuvaavat statuskoodeja. Esimerkiksi onnistuneella valinnalla on arvo JFileChooser.APPROVE_OPTION
.
Valittuun tiedostoon pääsee JFileChooser
-oliosta käsiksi metodilla getSelectedFile
.
JFileChooser chooser = new JFileChooser(); int valinta = chooser.showOpenDialog(frame); if (valinta == JFileChooser.APPROVE_OPTION) { File valittu = chooser.getSelectedFile(); System.out.println("Valitsit tiedoston: " + valittu.getName()); } else if (valinta == JFileChooser.CANCEL_OPTION) { System.out.println("Et valinnut tiedostoa!"); }
Yllä oleva esimerkki avaa valintaikkunan, ja tulostaa valitun tiedoston nimen jos valinta onnistuu. Jos valinta epäonnistuu, ohjelma tulostaa "Et valinnut tiedostoa!"
.
Tiedostojen filtteröinnillä tarkoitetaan vain tietynlaisten tiedostojen näyttämistä tiedostoikkunassa. JFileChooser-oliolle voi asettaa filtterin metodilla setFileFilter
. Metodi setFileFilter
saa parametrina abstraktin luokan FileFilter
-ilmentymän, esimerkiksi luokasta FileNameExtensionFilter
tehdyn olion.
Luokka FileNameExtensionFilter
mahdollistaa tiedostojen filtteröinnin niiden päätteiden perusteella. Esimerkiksi pelkät txt
-päätteiset tekstitiedostot saa näkyviin seuraavasti.
JFileChooser chooser = new JFileChooser(); chooser.setFileFilter(new FileNameExtensionFilter("Tekstitiedostot", "txt")); int valinta = chooser.showOpenDialog(frame); if (valinta == JFileChooser.APPROVE_OPTION) { File valittu = chooser.getSelectedFile(); System.out.println("Valitsit tiedoston: " + valittu.getName()); } else if (valinta == JFileChooser.CANCEL_OPTION) { System.out.println("Et valinnut tiedostoa!"); }
Luodaan ohjelma, joka mittaa kliksutteluvauhtia. Käyttöliittymä tulee näyttämään esimerkiksi seuraavalta.
Toteuta pakkaukseen nopeustesti
luokka Nappi
, joka perii JButtonin. Luokalla Nappi
tulee olla konstruktori public Nappi(String text, Color aktiivinen, Color passiivinen)
. Konstruktorin parametrina saama merkkijono text
tulee antaa parametrina yläluokan konstruktorille (kutsu super(text)
).
Korvaa luokasta JButton peritty metodi protected void paintComponent(Graphics g)
siten, että piirrät metodissa napin kokoisen värillisen ympyrän. Saat napin leveyden ja korkeuden JButton-luokalta perityistä metodeista getWidth()
ja getHeight()
. Kutsu korvatun metodin alussa yläluokan paintComponent
-metodia.
Ympyrän värin tulee riippua Napin tilasta: jos nappi on aktiivinen (metodi isEnabled
palauttaa true
tulee ympyrän väri olla konstruktorin parametrina saatu aktiivinenVari
. Muulloin käytetään väriä passiivinenVari
.
Toteuta luokkaan Nopeustesti
käyttöliittymä, jossa on neljä nappulaa ja teksti. Käytä asettelussa napeille omaa JPanel-alustaa, joka asetetaan BorderLayout-asettelijan keskelle. Teksti tulee BorderLayout-asettelijan alaosaan.
Käytä edellisessä osassa luomaasi Nappi
-luokkaa. Napeille tulee antaa konstruktorissa tekstit 1, 2, 3 ja 4.
Vain yhden nappulan kerrallaan tulee olla painettavissa (eli aktiivisena). Voit tehdä nappulasta ei-aktiivisen metodikutsulla nappi.setEnabled(false)
. Vastaavasti nappi muutetaan aktiiviseksi kutsulla nappi.setEnabled(true)
.
Kun aktiivisena olevaa nappulaa painetaan, tulee käyttöliittymän arpoa uusi aktiivinen nappi.
Tehdään peliin pisteytys: mitataan 20 painallukseen kuluva aika. Helpoin tapa ajan mittaamiseen on metodin System.currentTimeMillis()
kutsuminen. Metodi palauttaa kokonaisluvunu, joka laskee millisekunteja (tuhannesosasekunteja) jostain tietysti ajanhetkestä lähtien. Siispä voit mitata kulunutta aikaa kutsumalla currentTimeMillis
pelin alussa ja lopussa ja laskemalla erotuksen.
Toteuta siis seuraava: peli laskee napinpainallusten määrän, ja 20. painalluksen jälkeen asettaa kaikki nappulat epäaktiivisiksi ja näyttää JLabel
-komponentissa viestin "Pisteesi: XXXX"
, jossa XXXX
on painalluksiin kulunut aika (millisekunteina) jaettuna 20:lla. Pienempi pistemäärä on siis parempi.
Tässä tehtävässä toteutetaan ohjelma, joka lukee käyttäjän valitseman tiedoston ja näyttää sen sisällön käyttöliittymässä.
Ohjelmassa on eroteltu käyttöliittymään ja sovelluslogiikka. Tehtäväpohjassa on valmiina sovelluslogiikan rajapinta TiedostonLukija
sekä käyttöliittymäluokan runko Kayttoliittyma
.
Täydennä käyttöliittymäluokan metodi luoKomponentit
. Ohjelma tarvitsee toimiakseen kolme käyttöliittymäkomponenttia:
setEditable
. JTextArea
eroaa JTextField
-komponentista siten, että JTextArea
-komponentissa voi olla tekstiä useammalla rivillä.Koska tässä tehtävässä on vain kolme aseteltavaa komponenttia, riittävät asetteluun BorderLayout
-asettelijan vaihtoehdot: BorderLayout.NORTH
, BorderLayout.CENTER
ja BorderLayout.SOUTH
. Käyttöliittymäkomponentti JTextArea
kannattaa sijoittaa keskelle, jotta se saa mahdollisimman paljon tilaa tekstin näyttämiselle.
Käyttöliittymän pitäisi näyttää suunnilleen seuraavalta. Alla olevassa esimerkissä JLabel
-oliossa ei ole mitään tekstiä.
Luo pakkaukseen tiedostonnaytin.sovelluslogiikka
luokka OmaTiedostonLukija
, joka toteuttaa rajapinnan TiedostonLukija
. Rajapinnassa on yksi metodi, lueTiedosto
, joka lukee sille annetun tiedoston kokonaisuudessaan merkkijonoon ja palauttaa tämän merkkijonon.
Rajapinnan koodi:
package tiedostonnaytin.sovelluslogiikka; import java.io.File; public interface TiedostonLukija { String lueTiedosto(File tiedosto); }
Huom: Palautettavassa merkkijonossa tulee säilyttää myös rivinvaihdot "\n"
. Esimerkiksi Scanner
-lukijan metodi nextLine()
poistaa palauttamistaan merkkijonoista rivinvaihdot, joten joudut joko lisäämään ne takaisin tai lukemaan tiedostoa eri tavalla.
Viimeisessä tehtävän osassa toteutetaan käyttöliittymän JButton
-napille tapahtumankuuntelija. Saat itse päättää luokalle sopivan nimen.
Tapahtumankuuntelijan tehtävänä on näyttää JFileChooser
-tiedostonvalintaikkuna kun JButton
-nappia painetaan. Kun käyttäjä valitsee tiedoston, tulee tapahtumankuuntelijan lukea tiedoston sisältö ja näyttää se JTextArea
-kentässä. Tämän jälkeen tapahtumankuuntelijan tulee vielä päivittää JLabel
-kenttään näytetyn tiedoston nimi (ilman tiedostopolkua).
JFileChooser-olion metodille showOpenDialog
tulee antaa parametrina Kayttoliittyma
-luokassa oleva JFrame
-ikkunaolio. Jos käyttäjä valitsee tiedoston, tulee tiedosto lukea tapahtumankuuntelijassa Kayttoliittyma
-luokassa määriteltyä TiedostonLukija
-oliota apuna käyttäen. Kannattaa luoda tapahtumankuuntelija siten, että sille annetaan konstruktorissa kaikki tarvitut oliot.
Huomaa, että valintaikkunan voi myös sulkea valitsematta tiedostoa!
Kun tiedosto on avattu, tulee käyttöliittymän näyttää esimerkiksi seuraavalta.
Tehtäväsarjassa tehdään laajennettava tekstiseikkailupelin runko. Seikkailu koostuu kohdista, joissa jokaisessa ruudulle tulee tekstiä. Kohdat voivat olla joko välivaiheita, kysymyksiä, tai monivalintakohtia. Monivalinta-tyyppisen kohdan näyttämä teksti voi olla esimerkiksi seuraavanlainen:
Huoneessa on kaksi ovea. Kumman avaat? 1. Vasemmanpuoleisen. 2. Oikeanpuoleisen. 3. Juoksen pakoon.
Käyttäjä vastaa kohdassa esitettävään tekstiin. Yllä olevaan tekstiin voi vastata 1
, 2
tai 3
, ja vastauksesta riippuu, minne käyttäjä siirtyy seuraavaksi.
Peliin tullaan toteuttamaan kohtia kuvaava rajapinta ja tekstikäyttöliittymä, jonka kautta peliä pelataan.
Huom! Toteuta kaikki tehtävän vaiheet pakkaukseen "seikkailu"
Pelissä voi olla hyvinkin erilaisia kohtia, ja edellä olleessa esimerkissä ollut monivalinta on vain eräs vaihtoehto.
Toteuta kohdan käyttäytymistä kuvaava rajapinta Kohta
. Rajapinnalla Kohta
tulee olla metodi String teksti()
, joka palauttaa kohdassa tulostettavan tekstin. Metodin teksti
lisäksi kohdalla tulee olla metodi Kohta seuraavaKohta(String vastaus)
, jonka toteuttavat luokat palauttavat seuraavan kohdan vastauksen perusteella.
Toteuta tämän jälkeen yksinkertaisin tekstiseikkailun kohta, eli ei-interaktiivinen tekstiruutu, josta pääsee etenemään millä tahansa syötteellä. Toteuta ei-interaktiivista tekstiruutua varten luokka Valivaihe
, jolla on seuraavanlainen API.
Kohta
. public String teksti()
-metodi palauttaa konstruktorissa annetun tekstin sekä rivin "(jatka painamalla enteriä)"
. (Rivinvaihto saadaan aikaan merkillä "\n".)public void asetaSeuraava(Kohta seuraava)
-metodilla voidaan asettaa Kohta
-olio, jonka seuraavaKohta(String vastaus)
aina palauttaa (vastauksesta riippumatta). Testaa ohjelmaasi seuraavalla esimerkillä:
Scanner lukija = new Scanner(System.in); Valivaihe alkuteksti = new Valivaihe("Olipa kerran ohjelmoija."); Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla."); alkuteksti.asetaSeuraava(johdanto); Kohta nykyinen = alkuteksti; System.out.println(nykyinen.teksti()); nykyinen = nykyinen.seuraavaKohta(lukija.nextLine()); if (nykyinen == null) { System.out.println("Virhe ohjelmassa!"); } System.out.println(nykyinen.teksti()); nykyinen = nykyinen.seuraavaKohta(lukija.nextLine()); if (nykyinen != null) { System.out.println("Virhe ohjelmassa!"); }
Olipa kerran ohjelmoija. (jatka painamalla enteriä) Joka alkoi ohjelmoimaan Javalla. (jatka painamalla enteriä)
Pelin käyttöliittymä (luokka Kayttoliittyma
) saa konstruktorin parametrina Scanner
-olion ja Kohta
-rajapinnan toteuttavan pelin aloittavan olion. Luokka tarjoaa metodin public void kaynnista()
, joka käynnistää pelin suorituksen.
Käyttöliittymä käsittelee kaikkia kohtia Kohta
-rajapinnan kautta. Käyttöliittymän tulee jokaisessa kohdassa kysyä kohtaan liittyvältä metodilta teksti
tekstiä, joka käyttäjälle näytetään. Tämän jälkeen käyttöliittymä kysyy käyttäjältä vastauksen, ja antaa sen parametrina kohta-olion metodille seuraavaKohta
. Metodi seuraavaKohta
palauttaa vastauksen perusteella seuraavan kohdan, johon pelin on määrä siirtyä. Peli loppuu, kun metodi seuraavaKohta
palauttaa arvon null
.
Koska pääohjelma tulee käyttämään kohtia vain Kohta
-rajapinnan kautta, voidaan peliin lisätä vaikka minkälaisia kohtia pääohjelmaa muuttamatta. Riittää tehdä uusia Kohta
-rajapinnan toteuttavia luokkia.
Toteuta luokka Kayttoliittyma
, ja testaa sen toimintaa seuraavalla esimerkillä
Scanner lukija = new Scanner(System.in); Valivaihe alku = new Valivaihe("Olipa kerran ohjelmoija."); Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla."); Valivaihe loppu = new Valivaihe("Ja päätti muuttaa Helsinkiin."); alku.asetaSeuraava(johdanto); johdanto.asetaSeuraava(loppu); new Kayttoliittyma(lukija, alku).kaynnista();
Olipa kerran ohjelmoija. (jatka painamalla enteriä) > Joka alkoi ohjelmoimaan Javalla. (jatka painamalla enteriä) > Ja päätti muuttaa Helsinkiin. (jatka painamalla enteriä) >
Käytä seuraavaa metodia käyttöliittymän kaynnista
-metodina. Yritä piirtää paperille mitä käy kun käyttöliittymä käynnistetään.
public void kaynnista() { Kohta nykyinen = alkukohta; while (nykyinen != null) { System.out.println(nykyinen.teksti()); System.out.print("> "); String vastaus = lukija.nextLine(); nykyinen = nykyinen.seuraavaKohta(vastaus); System.out.println(""); } }
Käyttöliittymän kaynnista
-metodi sisältää siis toistolauseen, jossa ensin tulostetaan käsiteltävän kohdan teksti. Tämän jälkeen kysytään käyttäjältä syötettä. Käyttäjän syöte annetaan vastauksena käsiteltävän kohdan seuraavaKohta
-metodille. Metodi seuraavaKohta
palauttaa kohdan, jota käsitellään seuraavalla toiston kierroksella. Jos palautettu kohta oli null
, lopetetaan toisto.
Tekstiseikkailussa voi olla kysymyksiä, joihin on annettava oikea vastaus ennen kuin pelaaja pääsee eteenpäin. Tee luokka Kysymys
seuraavasti:
Kohta
-rajapinnan. asetaSeuraava
-metodilla. seuraavaKohta
-metodia kutsutaan oikealla vastauksella, metodi palauttaa seuraavan kohdan, muuten metodi ei päästä etenemään ja palauttaa arvon this
, eli viitteen tähän olioon. Luokkaa voi testata seuraavalla pääohjelmalla:
Scanner lukija = new Scanner(System.in); Kysymys alku = new Kysymys("Minä vuonna Javan ensimmäinen versio julkaistiin?", "1995"); Valivaihe hyva = new Valivaihe("Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling."); alku.asetaSeuraava(hyva); new Kayttoliittyma(lukija, alku).kaynnista();
Minä vuonna Javan ensimmäinen versio julkaistiin? > 2000 Minä vuonna Javan ensimmäinen versio julkaistiin? > 1995 Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling. (jatka painamalla enteriä) >
Tällä hetkellä tekstiseikkailu tukee välivaiheita ja yksinkertaisia kysymyksiä. Tekstiseikkailu on siis lineaarinen, eli lopputulokseen ei voi käytännössä vaikuttaa. Lisätään seikkailuun monivalintakysymyksiä, joiden avulla pelin kehittäjä voi luoda vaihtoehtoista toimintaa.
Esimerkki vaihtoehtoisesta toiminnasta:
Kello on 13:37 ja päätät mennä syömään. Minne menet? 1. Exactumiin 2. Chemicumiin > 1 Ruoka on loppu :( (jatka painamalla enteriä) >
Kello on 13:37 ja päätät mennä syömään. Minne menet? 1. Exactumiin 2. Chemicumiin > 2 Mainio valinta! (jatka painamalla enteriä) >
Toteuta luokka Monivalinta
, jonka API on seuraavanlainen
Kohta
. public Monivalinta(String teksti)
public void lisaaVaihtoehto(String valinta, Kohta seuraava)
public String teksti()
public Kohta seuraavaKohta(String valinta)
Integer
luokkametodilla parseInt
. Testaa ohjelmasi toimintaa seuraavalla pääohjelmalla:
Scanner lukija = new Scanner(System.in); Monivalinta lounas = new Monivalinta("Kello on 13:37 ja päätät mennä syömään. Minne menet?"); Monivalinta chemicum = new Monivalinta("Lounasvaihtoehtosi ovat seuraavat:"); Valivaihe exactum = new Valivaihe("Exactumista on kaikki loppu, joten menet Chemicumiin."); exactum.asetaSeuraava(chemicum); lounas.lisaaVaihtoehto("Exactumiin", exactum); lounas.lisaaVaihtoehto("Chemicumiin", chemicum); Valivaihe nom = new Valivaihe("Olipas hyvää"); chemicum.lisaaVaihtoehto("Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta", nom); chemicum.lisaaVaihtoehto("Jauhelihakebakot, paprikakastiketta", nom); chemicum.lisaaVaihtoehto("Mausteista kalapataa", nom); new Kayttoliittyma(lukija, lounas).kaynnista();
Kello on 13:37 ja päätät mennä syömään. Minne menet? 1. Exactumiin 2. Chemicumiin > 1 Exactumista on kaikki loppu, joten menet Chemicumiin. (jatka painamalla enteriä) > Lounasvaihtoehtosi ovat seuraavat: 1. Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta 2. Jauhelihakebakot, paprikakastiketta 3. Mausteista kalapataa > 2 Olipas hyvää (jatka painamalla enteriä) >
Luokan Monivalinta
sisäinen toteutus saattaa olla haastava. Kannattaa esimerkiksi käyttää listaa vastausvaihtoehtojen (merkkijonojen) tallentamiseen, ja hajautustaulua kohtien tallentamiseen valintavaihtoehdon indeksillä.
NHL:ssä pidetään pelaajista yllä monenlaisia tilastotietoja. Teemme nyt oman ohjelman NHL-pelaajien tilastojen hallintaan.
Tee luokka Pelaaja
, johon voidaan tallettaa pelaajan nimi, joukkue, pelatut ottelut, maalimäärä, ja syöttömäärä. Luokalla tulee olla konstruktori, joka saa edellämainitut tiedot edellä annetussa järjestyksessä.
Tee kaikille edelläminituille arvoille myös ns. getterimetodit, jotka palauttavat arvot:
String getNimi
String getJoukkue
int getOttelut
int getMaalit
int getSyotot
int getPisteet
- laskee kokonaispistemäärän eli maalien ja syöttöjen summanTalleta seuraavat pelaajat ArrayList:iin ja tulosta listan sisältö:
public static void main(String[] args) { ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>(); pelaajat.add(new Pelaaja("Alex Ovechkin", "WSH", 71, 28, 46)); pelaajat.add(new Pelaaja("Dustin Byfuglien", "ATL", 69, 19, 31)); pelaajat.add(new Pelaaja("Phil Kessel", "TOR", 70, 28, 24)); pelaajat.add(new Pelaaja("Brendan Mikkelson", "ANA, CGY", 23, 0, 2)); pelaajat.add(new Pelaaja("Matti Luukkainen", "SaPKo", 1, 0, 0 )); for (Pelaaja pelaaja : pelaajat) { System.out.println(pelaaja); } }
Pelaajan toString()
-metodin muodostaman tulostuksen tulee olla seuraavassa muodossa:
Alex Ovechkin WSH 71 28 + 46 = 74 Dustin Byfuglien ATL 69 19 + 31 = 50 Phil Kessel TOR 70 28 + 24 = 52 Brendan Mikkelson ANA, CGY 23 0 + 2 = 2 Matti Luukkainen SaPKo 1 0 + 0 = 0
Ensin siis nimi, sitten joukkue, jonka jälkeen ottelut, maalit, plusmerkki, syötöt, yhtäsuuruusmerkki ja kokonaispisteet eli maalien ja syöttöjen summa.
Tee Pelaaja
-luokkaan metodi toSiistiMerkkijono()
, joka palauttaa samat tiedot siististi aseteltuna siten, että jokaiselle muuttujalle on varattu tietty määrä tilaa tulostuksessa.
Tulostuksen tulee näyttää seuraavalta:
Alex Ovechkin WSH 71 28 + 46 = 74 Dustin Byfuglien ATL 69 19 + 31 = 50 Phil Kessel TOR 70 28 + 24 = 52 Brendan Mikkelson ANA, CGY 23 0 + 2 = 2 Matti Luukkainen SaPKo 1 0 + 0 = 0
Nimen jälkeen joukkueen nimien täytyy alkaa samasta kohdasta. Saat tämän aikaan esim. muotoilemalla nimen tulostuksen yhteydessä seuraavasti:
String nimiJaTyhjaa = String.format("%-25s", nimi);
Komento tekee merkkijonon nimiJaTyhjaa
joka alkaa merkkijonon nimi
sisällöllä ja se jälkeen tulee välilyöntejä niin paljon että merkkijonon pituudeksi tulee 25. Joukkueen nimi tulee vastaavalla tavalla tulostaa 14 merkin pituisena merkkijonona. Tämän jälkeen on otteluiden määrä (2 merkkiä), jota seuraa 2 välilyöntiä. Tämän jälkeen on maalien määrä (2 merkkiä), jota seuraa merkkijono " + ". Tätä seuraa syöttöjen määrä (2 merkkiä), merkkijono " = ", ja lopuksi yhteispisteet (2 merkkiä).
Lukuarvot eli ottelu-, maali-, syöttö- ja pistemäärä muotoillaan kahden merkin mittaisena, eli lukeman 0 sijaan tulee tulostua välilyönti ja nolla. Seuraava komento auttaa tässä:
String maalitMerkkeina = String.format("%2d", maalit);
Lisää luokalle Pelaaja rajapinta Comparable<Pelaaja>
, jonka avulla pelaajat voidaan järjestää kokonaispistemäärän mukaiseen laskevaan järjestykseen. Järjestä pelaajat Collections-luokan avulla ja tulosta pistepörssi:
Collections.sort(pelaajat); System.out.println("NHL pistepörssi:\n"); for (Pelaaja pelaaja : pelaajat) { System.out.println(pelaaja); }
Tulostuu:
NHL pistepörssi: Alex Ovechkin WSH 71 28 + 46 = 74 Phil Kessel TOR 70 28 + 24 = 52 Dustin Byfuglien ATL 69 19 + 31 = 50
Ohjeita tähän tehtävään materiaalissa.
Tilastomme on vielä hieman vajavainen, siinä on vaan muutaman pelaajan tiedot (ja nekin vastaavat 16.3. tilannetta). Kaikkien tietojen syöttäminen käsin olisi kovin vaivalloista. Onneksemme internetistä osoitteesta http://nhlstatistics.herokuapp.com/players.txt
löytyy päivittyvä, koneen luettavaksi tarkoitettu lista pelaajatiedoista.
Huom: kun menet osoitteeseen ensimmäistä kertaa, sivun latautuminen kestää muutaman sekunnin (sivu pyörii virtuaalipalvelimella joka sammutetaan jos sivua ei ole hetkeen käytetty). Sen jälkeen sivu toimii nopeasti.
Datan lukeminen internetistä on helppoa. Projektissasi on valmiina luokka Tilasto
, joka lataa annetun verkkosivun.
import java.io.InputStream; import java.net.URL; import java.util.Scanner; public class Tilasto { private static final String OSOITE = "http://nhlstatistics.herokuapp.com/players.txt"; private Scanner lukija; public Tilasto() { this(OSOITE); } public Tilasto(String osoite) { try { URL url = new URL(osoite); lukija = new Scanner(url.openStream()); } catch (Exception ex) { } } public Tilasto(InputStream in) { try { lukija = new Scanner(in); } catch (Exception ex) { } } public boolean onkoRivejaJaljella() { return lukija.hasNextLine(); } public String annaSeuraavaRivi() { String rivi = lukija.nextLine(); return rivi.trim(); } }
Tilasto
-luokka lukee pelaajien tilastotiedot internetistä. Metodilla annaSeuraavaRivi()
saadaan selville yhden pelaajan tiedot. Tietoja on tarkoitus lukea niin kauan kuin pelaajia riittää, tämä voidaan tarkastaa metodilla onkoRivejaJaljella()
Kokeile että ohjelmasi onnistuu tulostamaan Tilasto
-luokan hakemat tiedot:
public static void main(String[] args) { Tilasto tilasto = new Tilasto(); while (tilasto.onkoRivejaJaljella()) { String pelaajaRivina = tilasto.annaSeuraavaRivi(); System.out.println(pelaajaRivina); } }
Tulostus on seuraavan muodoinen:
Evgeni Malkin;PIT;62;39;46;54
Steven Stamkos;TBL;70;50;34;64
Claude Giroux;PHI;66;26;56;27
Jason Spezza;OTT;72;29;46;30
// ... ja yli 800:n muun pelaajan tiedot
Huom: tulostuksen alussa ja lopussa ja jokaisen pelaajan välissä on html-tägejä, esim. <br/> joka aiheuttaa www-sivulle rivin vaihtumisen.
Tulostuksessa pelaajan tiedot on erotettu toisistaan puolipisteellä. Ensin nimi, sitten joukkue, ottelut, maalit, syötöt ja laukaukset.
Pelaajaa vastaava merkkijono on siis yksittäinen merkkijono. Saat pilkottua sen osiin split
-komennolla seuraavasti:
while (tilasto.onkoRivejaJaljella()) { String pelaajaRivina = tilasto.annaSeuraavaRivi(); String[] pelaajaOsina = pelaajaRivina.split(";"); for (int j = 0; jKokeile että tämä toimii. Saat tästä tehtävästä pisteet seuraavan tehtävän yhteydessä.
Kaikkien pelaajien pistepörssi
Tee kaikista
Tilasto
-luokan hakemien pelaajien tiedoista Pelaaja-olioita ja lisää ne ArrayListiin. Lisää tehtävään luokkaPelaajatTilastosta
. Käytä alla olevaa koodia luokan runkona.import java.util.ArrayList; public class PelaajatTilastosta { public ArrayList<Pelaaja> haePelaajat(Tilasto tilasto) { ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>(); while (tilasto.onkoRivejaJaljella()) { String pelaajaRivina = tilasto.annaSeuraavaRivi(); String[] pelaajaOsina = pelaajaRivina.split(";"); // Lisätään uusi pelaaja vain, jos syötteessä on kenttiä riittävästi if (pelaajaOsina.length > 4) { int ottelut = Integer.parseInt(pelaajaOsina[2].trim()); // Täydennä koodia lukemalla kaikki pelaajaOsina-taulukon kentät uuteen Pelaaja-olioon // ... // pelaajat.add(new Pelaaja( ... )); } } return pelaajat; } }Tehtävänäsi on täydentää runkoa siten, että jokaisesta luetusta rivistä luodaan pelaaja, joka lisätään pelaajat-listaan. Huom!
Tilasto
-luokka palauttaa merkkijonoja, joten joudut muuntamaan merkkijonoja myös numeroiksi. Esimerkiksi numeromuotoinenottelut
on muutettavaint
:iksiInteger.parseInt
-metodilla.Jos merkkijonon metodi
split
ei ole tuttu, se jakaa merkkijonon useampaan osaan annetun merkin kohdalta. Esimerkiksi komentomerkkijono.split(";");
palauttaa merkkijonosta taulukon, jossa alkuperäisen merkkijonon puolipisteellä erotetut osat ovat kukin omassa taulukon indeksissä.Voit käyttää testauksen apuna seuraavaa pääohjelmaa:
Tilasto tilasto = new Tilasto(); PelaajatTilastosta pelaajienHakija = new PelaajatTilastosta(); ArrayList<Pelaaja> pelaajat = pelaajienHakija.haePelaajat(tilasto); for (Pelaaja pelaaja : pelaajat) { System.out.println( pelaaja ); }Maali ja syöttöpörssi
Haluamme tulostaa myös maalintekijäpörssin eli pelaajien tiedot maalimäärän mukaan järjestettynä sekä syöttöpörssin. NHL:n kotisivu tarjoaa tämänkaltaisen toiminnallisuuden, eli selaimessa näytettävä lista on mahdollista saada järjestettyä halutun kriteerin mukaan.
Edellinen tehtävä määritteli pelaajien suuruusjärjestyksen perustuvan kokonaispistemäärään. Luokalla voi olla vain yksi
compareTo
-metodi, joten joudumme muunlaisia järjestyksiä saadaksemme turvautumaan muihin keinoihin.Vaihtoehtoiset järjestämistavat toteutetaan erillisten luokkien avulla. Pelaajien vaihtoehtoisten järjestyksen määräävän luokkien tulee toteuttaa
Comparator<Pelaaja>
-rajapinta. Järjestyksen määräävän luokan olio vertailee kahta parametrina saamaansa pelaajaa. Metodeja on ainoastaan yksicompare(Pelaaja p1, Pelaaja p2)
, jonka tulee palauttaa negatiivinen arvo, jos pelaajap1
on järjestyksessä ennen pelaajaap2
, positiivinen arvo josp2
on järjestyksessä ennenp1
:stä ja 0 muuten.Periaatteena on luoda jokaista järjestämistapaa varten oma vertailuluokka, esim. maalipörssin järjestyksen määrittelevä luokka:
import java.util.Comparator; public class Maali implements Comparator<Pelaaja> { public int compare(Pelaaja p1, Pelaaja p2) { // maalien perusteella tapahtuvan vertailun koodi tänne } }Tee
Comparator
-rajapinnan toteuttavat luokatMaali
jaSyotto
, ja niille vastaavat maali- ja syöttöpörssien generoimiseen sopivat sopivat vertailufunktiot.Järjestäminen tapahtuu edelleen luokan
Collections
metodinsort
avulla. Metodi saa nyt toiseksi parametrikseen järjestyksen määräävän luokan olion:Maali maalintekijat = new Maali(); Collections.sort(pelaajat, maalintekijat); System.out.println("NHL parhaat maalintekijät\n"); // tulostetaan maalipörssiJärjestyksen määrittelevä olio voidaan myös luoda suoraan sort-kutsun yhteydessä:
Collections.sort(pelaajat, new Maali()); System.out.println("NHL parhaat maalintekijät\n"); // tulostetaan maalipörssiKun sort-metodi saa järjestyksen määrittelevän olion parametrina, se käyttää olion
compareTo()
-metodia pelaajia järjestäessään.
pois?
Kaikki oliot ovat tyyppiä Object
, joten minkä tahansa tyyppisen olion voi antaa parametrina Object
-tyyppisiä parametreja vastaanottavalle metodille.
Tehtävän mukana tulee rajapinta Vertaaja
. Toteuta pakkaukseen samuus
luokka OlioidenVertaaja
, joka toteuttaa rajapinnan Vertaaja
. Metodien tulee toimia seuraavasti:
equals
-metodia. Tarkemmin equals
-metodin toiminnasta Javan Object luokan APIsta. Huomaa, että equals
-metodin toiminta riippuu siitä, onko verrattavan olion luokka korvannut Object
-luokassa määritellyn equals
-metodin. toString
-palauttamat merkkijonot) ovat samat. Muutoin metodi palauttaa false.Tehtävän mukana tulee luokka Henkilo
, jossa equals
- ja compareTo
-metodit on korvattu. Kokeile toteuttamiesi metodien toimintaa seuraavalla esimerkkikoodilla.
OlioidenVertaaja vertaaja = new OlioidenVertaaja(); Henkilo henkilo1 = new Henkilo("221078-123X", "Pekka", "Helsinki"); Henkilo henkilo2 = new Henkilo("221078-123X", "Pekka", "Helsinki"); // täysin samansisältöinen kuin eka Henkilo henkilo3 = new Henkilo("110934-123X", "Pekka", "Helsinki"); // eri pekka vaikka asuukin helsingissä System.out.println(vertaaja.samaOlio(henkilo1, henkilo1)); System.out.println(vertaaja.samaOlio(henkilo1, henkilo2)); System.out.println(vertaaja.vastaavat(henkilo1, henkilo2)); System.out.println(vertaaja.vastaavat(henkilo1, henkilo3)); System.out.println(vertaaja.samaMerkkijonoEsitys(henkilo1, henkilo2)); Henkilo henkilo4 = new Henkilo("221078-123X", "Pekka", "Savonlinna"); // henkilo1:n pekka mutta asuinpaikka muuttuu System.out.println(vertaaja.samaOlio(henkilo1, henkilo4)); System.out.println(vertaaja.vastaavat(henkilo1, henkilo4)); System.out.println(vertaaja.samaMerkkijonoEsitys(henkilo1, henkilo4));
Ylläolevan koodin tulostuksen pitäisi olla seuraava:
true false true false true false true false
Tehtäväpohjan mukana tulee luokat Ympyra
, Suorakulmio
ja TasasivuinenKolmio
. Luokat liittyvät samaan aihepiiriin, ja niillä on hyvin paljon yhteistä toiminnallisuutta. Tutustu luokkiin ennenkuin lähdet tekemään, jolloin hahmotat tarkemmin syyt muutoksille. Jos huomaat että luokissa on alustavasti sisennys hieman pielessä, kannattaa sisennys hoitaa kuntoon luettavuuden helpottamiseksi.
Toteuta pakkaukseen kuviot
abstrakti luokka Kuvio
, jossa on kuvioihin liittyvää toiminnallisuutta. Luokan kuvio tulee sisältää konstruktori public Kuvio(int x, int y)
, metodit public int getX()
, public int getY()
, sekä abstraktit metodit public abstract double pintaAla()
ja public abstract double piiri()
.
Muuta luokan Ympyra
toteutusta siten, että se perii luokan Kuvio
. Luokan Ympyra
ulkoinen toiminnallisuus ei saa muuttua, eli sen tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio
avulla tai itse. Muistathan että konstruktorikutsun super
avulla voit käyttää yliluokan konstruktoria. Kun metodi public int getX()
on toteutettu jo yliluokassa se ei tarvitse erillistä toteutusta luokassa Ympyra
.
Kuvio kuvio = new Ympyra(10, 10, 15); System.out.println("X " + kuvio.getX()); System.out.println("Y " + kuvio.getY()); System.out.println("Pinta-ala " + kuvio.pintaAla()); System.out.println("Piiri " + kuvio.piiri());
X 10 Y 10 Pinta-ala 706.85834... Piiri 94.24777...
Muuta luokkien Suorakulmio
ja TasakylkinenKolmio
toteutusta siten, että ne perivät luokan Kuvio
. Luokkien ulkoinen toiminnallisuus ei saa muuttua, eli niiden tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio
avulla tai itse.
Kuvio kuvio = new Suorakulmio(10, 10, 15, 15); System.out.println("X " + kuvio.getX()); System.out.println("Y " + kuvio.getY()); System.out.println("Pinta-ala " + kuvio.pintaAla()); System.out.println("Piiri " + kuvio.piiri()); System.out.println(""); kuvio = new TasakylkinenKolmio(10, 10, 15); System.out.println("X " + kuvio.getX()); System.out.println("Y " + kuvio.getY()); System.out.println("Pinta-ala " + kuvio.pintaAla()); System.out.println("Piiri " + kuvio.piiri());
X 10 Y 10 Pinta-ala 225.0 Piiri 60.0 X 10 Y 10 Pinta-ala 97.42785... Piiri 45.0
Joukkojen järjestyksessä pitäminen onnistuu Set
rajapinnan toteuttavan TreeSet
-olion avulla. Aiemmassa Tehtavakirjanpito
-esimerkissä henkilökohtaiset tehtäväpisteet tallennettiin Map
-rajapinnan toteuttavaan HashMap
-olioon. Kuten HashSet
, HashMap
ei pidä alkioita järjestyksessä. Rajapinnasta Map
on olemassa toteutus TreeMap
, jossa hajautustaulun avaimia pidetään järjestyksessä. Muutetaan Tehtavakirjanpito
-luokkaa siten, että henkilökohtaiset pisteet tallennetaan TreeMap
-tyyppiseen hajautustauluun.
public class Tehtavakirjanpito { private Map<String, Set<Integer>> tehdytTehtavat; public Tehtavakirjanpito() { this.tehdytTehtavat = new TreeMap<String, Set<Integer>>(); } public void lisaa(String kayttaja, int tehtava) { if (!this.tehdytTehtavat.containsKey(kayttaja)) { this.tehdytTehtavat.put(kayttaja, new TreeSet<Integer>()); } Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja); tehdyt.add(tehtava); } public void tulosta() { for (String kayttaja: this.tehdytTehtavat.keySet()) { System.out.println(kayttaja + ": " + this.tehdytTehtavat.get(kayttaja)); } } }
Muunsimme samalla Set
-rajapinnan toteutukseksi TreeSet
-luokan. Huomaa että koska olimme käyttäneet rajapintoja, muutoksia tuli hyvin pieneen osaan koodista. Etsi kohdat jotka muuttuivat!
Käyttäjäkohtaiset tehtävät voidaan nyt tulostaa järjestyksessä.
Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito(); kirjanpito.lisaa("Mikael", 3); kirjanpito.lisaa("Mikael", 4); kirjanpito.lisaa("Mikael", 3); kirjanpito.lisaa("Mikael", 3); kirjanpito.lisaa("Pekka", 4); kirjanpito.lisaa("Pekka", 4); kirjanpito.lisaa("Matti", 1); kirjanpito.lisaa("Matti", 2); kirjanpito.tulosta();
Matti: [1, 2] Mikael: [3, 4] Pekka: [4]
Luokka TreeMap
vaatii että avaimena käytetyn luokan tulee toteuttaa Comparable
-rajapinta. Jos luokka ei toteuta rajapintaa Comparable
, voidaan luokalle TreeMap
antaa konstruktorin parametrina Comparator
-luokan toteuttama olio aivan kuten TreeSet
-luokalle.
Tehtävänäsi on toteuttaa sähköpostiohjelmaan komponentti, joka säilöö viestejä. Tehtäväpohjan mukana tulee luokka Sahkoposti
, joka esittää sähköpostiviestiä. Luokalla Sahkoposti
on oliomuuttujat:
Toteutetaan tässä luokka Viestivarasto
, joka tarjoaa sähköpostien hallintaan liittyviä toimintoja.
Luo pakkaukseen posti
luokka Viestivarasto
, ja lisää sille seuraavat metodit:
public void lisaa(Sahkoposti s)
lisää viestinpublic Sahkoposti hae(String otsikko)
palauttaa viestin jolla on annettu otsikko tai null jos sellaista ei ole. Voit olettaa että millään kahdella viestillä ei ole samaa otsikkoa.
Lisää luokkaan Viestivarasto
seuraavat metodit
public Sahkoposti hae(int aika)
palauttaa viestin joka saapui annettuun aikaan tai null jos sellaista ei ole. Voit olettaa että millään kahdella viestillä ei ole samaa saapumisaikaa.public Sahkoposti haeUusinViesti()
hakee uusimman viestin (eli sen jonka saapumisaika on isoin) ai null jos sellaista ei ole.public Sahkoposti haeUusinViesti(int ylaraja)
hakee uusimman viestin joka ei ole saapunut annetun ajan ylaraja
jälkeen. Metodi palauttaa null jos tällaista viestiä ei ole.Huom! Kannattaa käyttää kahta erillistä rakennetta viestien tallentamiseen. Otsikon perusteella tallentamiseen voit käyttää HashMap
pia, ja viestien tallentamiseen ajan mukaan TreeMap
pia. Näin saat toteutettua hae-operaatiot tehokkaasti. Tutustu myös TreeMap
in metodeihin lastKey()
ja floorKey()
.
Errare humanum est
Ihminen on erehtyväinen ja paraskin ohjelmoija tekee virheitä. Ohjelman kehitysvaiheessa tapahtuvien virheiden lisäksi huomattava osa virheistä syntyy olemassa olevaa ohjelmaa muokattaessa. Ohjelman muokkauksen aikana tehdyt virheet eivät välttämättä näy muokattavassa osassa, vaan voivat ilmaantua välillisesti erillisessä osassa ohjelmaa: osassa, joka käyttää muutettua osaa.
Ohjelmien automaattinen testaaminen tarkoittaa toistettavien testien luomista. Testeillä varmistetaan että ohjelma toimii halutusti, ja että ohjelma säilyttää toiminnallisuutensa myös muutosten jälkeen. Sanalla automaattinen painotetaan sitä, että luodut testit ovat toistettavia ja että ne voidaan suorittaa aina haluttaessa -- ohjelmoijan ei tarvitse olla läsnä testejä suoritettaessa.
Otimme aiemmin askeleita kohti testauksen automatisointia antamalla Scanner-oliolle parametrina merkkijonon, jonka se tulkitsee käyttäjän näppäimistöltä antamaksi syötteeksi. Automaattisessa testaamisessa testaaminen viedään viedä pidemmälle: koneen tehtävänä on myös tarkistaa että ohjelman tuottama vastaus on odotettu.
Automaattisen testauksen tällä kurssilla painotettu osa-alue on yksikkötestaus, jossa testataan ohjelman pienten osakokonaisuuksien -- metodien ja luokkien -- toimintaa. Yksikkötestaamiseen käytetään Javalla yleensä JUnit-testauskirjastoa.
Pino on kaikille ihmisille tuttu asia. Esimerkiksi ravintola Unicafessa lautaset ovat yleensä pinossa. Pinon päältä voi ottaa lautasen ja pinon päälle voi lisätä lautasia. On myös helppo selvittää onko pinossa vielä lautasia jäljellä.
Pino on myös ohjelmoinnissa usein käytetty aputietorakenne. Rajapintana lukuja sisältävä pino näyttää seuraavalta.
public interface Pino { boolean tyhja(); boolean taynna(); void pinoon(int luku); int pinosta(); int huipulla(); int lukuja(); }
Rajapinnan määrittelemien metodien on tarkoitus toimia seuraavasti:
public boolean tyhja()
palauttaa true jos pino on tyhjäpublic boolean taynna()
palauttaa true jos pino on täynnäpublic void pinoon(int luku)
laittaa parametrina olevan luvun pinon päällepublic int huipulla()
kertoo pinon huipulla olevan alkionpublic int pinosta()
poistaa ja palauttaa pinon päällä olevan alkionpublic int lukuja()
kertoo pinossa olevien lukujen määränpublic int tilaa()
kertoo pinon vapaan tilan määränToteutetaan rajapinnan Pino
toteuttava luokka OmaPino
, johon talletetaan lukuja. Pinoon mahtuvien lukujen määrä annetaan pinon konstruktorissa. Toteutamme pinon hieman aiemmasta poikkeavasti -- emme testaa ohjelmaa pääohjelman avulla, vaan käytämme pääohjelman sijasta automatisoituja JUnit-testejä ohjelman testaamiseen.
NetBeansissa olevat ohjelmamme ovat tähän asti sijainneet aina Source Packagesissa tai sen sisällä olevissa pakkauksissa. Ohjelman lähdekoodit tulevat aina kansioon Source Packages. Automaattisia testejä luodessa testit luodaan valikon Test Packages alle. Uusia JUnit-testejä voi luoda valitsemalla projektin oikealla hiirennapilla ja valitsemalla avautuvasta valikosta New -> JUnit Test...
. Jos vaihtoehto JUnit test ei näy listassa, löydät sen valitsemalla Other.
JUnit-testit sijaitsevat luokassa. Uutta testitiedostoa luodessa ohjelma pyytää testitiedoston nimen. Tyypillisesti nimeksi annetaan testattavan luokan tai toiminnallisuuden nimi. Luokan nimen tulee aina päättyä sanaan Test
. Esimerkiksi alla luodaan testiluokka PinoTest
, joka sijaitsee pakkauksessa pino
. NetBeans haluaa luoda käyttöömme myös valmista runkoa testiluokalle -- joka käy hyvin.
Jos NetBeans kysyy minkä JUnit-version haluat käyttöösi, valitse JUnit 4.x
.
Kun testiluokka PinoTest
on luotu, näkyy se projektin valikon Test Packages alla.
Luokka PinoTest
näyttää aluksi seuraavalta
package pino; import org.junit.*; import static org.junit.Assert.*; public class PinoTest { public PinoTest() { } @BeforeClass public static void setUpClass() throws Exception { } @AfterClass public static void tearDownClass() throws Exception { } @Before public void setUp() { } @After public void tearDown() { } // TODO add test methods here. // The methods must be annotated with annotation @Test. For example: // // @Test // public void hello() {} }
Meille oleellisia osia luokassa PinoTest
ovat metodit public void setUp
, jonka yläpuolella on merkintä @Before
, ja kommentoitu metodipohja public void hello()
, jonka yläpuolella on merkintä @Test
. Metodit, joiden yläpuolella on merkintä @Test
ovat ohjelman toiminnallisuutta testaavia testimetodeja. Metodi setUp
taas suoritetaan ennen jokaista testiä.
Muokataan luokkaa PinoTest
siten, että sillä testataan rajapinnan Pino
toteuttamaa luokkaa OmaPino
. Älä välitä vaikkei luokkaa OmaPino
ole vielä luotu. Pino on testiluokan oliomuuttuja, joka alustetaan ennen jokaista testiä metodissa setUp
.
package pino; import org.junit.*; import static org.junit.Assert.*; public class PinoTest { Pino pino; @Before public void setUp() { pino = new OmaPino(3); } @Test public void alussaTyhja() { assertTrue(pino.tyhja()); } @Test public void lisayksenJalkeenEiTyhja() { pino.pinoon(5); assertFalse(pino.tyhja()); } @Test public void lisattyAlkioTuleePinosta() { pino.pinoon(3); assertEquals(3, pino.pinosta()); } @Test public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() { pino.pinoon(3); pino.pinosta(); assertTrue(pino.tyhja()); } @Test public void lisatytAlkiotTulevatPinostaOikeassaJarjestyksessa() { pino.pinoon(1); pino.pinoon(2); pino.pinoon(3); assertEquals(3, pino.pinosta()); assertEquals(2, pino.pinosta()); assertEquals(1, pino.pinosta()); } @Test public void tyhjennyksenJalkeenPinoonLaitettuAlkioTuleeUlosPinosta() { pino.pinoon(1); pino.pinosta(); pino.pinoon(5); assertEquals(5, pino.pinosta()); } // ... }
Jokainen testi, eli merkinnällä @Test
varustettu metodi, alkaa tilanteesta, jossa on luotu uusi tyhjä pino. Jokainen yksittäinen @Test-merkitty metodi on oma testinsä. Yksittäisellä testimetodilla testataan aina yhtä pientä osaa pinon toiminnallisuudesta. Testit suoritetaan toisistaan täysin riippumattomina, eli jokainen testi alkaa "puhtaaltä pöydältä", setUp
-metodin alustamasta tilanteesta.
Yksittäiset testit noudattavat aina samaa kaavaa. Ensin luodaan tilanne jossa tapahtuvaa toimintoa halutaan testata, sitten tehdään testattava toimenpide, ja lopuksi tarkastetaan onko tilanne odotetun kaltainen. Esimerkiksi seuraava testi testaa että lisäyksen ja poiston jälkeen pino on taas tyhjä -- huomaa myös kuvaava testimetodin nimeäminen:
@Test public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() { pino.pinoon(3); pino.pinosta(); assertTrue(pino.tyhja()); }
Ylläoleva testi testaa toimiiko metodi tyhja()
jos pino on tyhjennetty. Ensin laitetaan pinoon luku metodilla pinoon
, jonka jälkeen pino tyhjennetään kutsumalla metodia pinosta()
. Tällöin on saatu aikaan tilanne jossa pinon pitäisi olla tyhjennetty. Viimeisellä rivillä testataan, että pinon metodi tyhja()
palauttaa arvon true
testausmetodilla assertTrue()
. Jos metodi tyhja()
ei palauta arvoa true
näemme testejä suorittaessa virheen.
Jokainen testi päättyy jonkun assert
-metodin kutsuun. Esimerkiksi metodilla assertEquals()
voidaan varmistaa onko metodin palauttama luku tai merkkijono haluttu, ja metodilla assertTrue()
varmistetaan että metodin palauttama arvo on true
. Erilaiset assert
-metodit saadaan käyttöön luokan alussa olevalla määrittelyllä import static org.junit.Assert.*;
.
Testit suoritetaan joko painamalla alt ja F6 tai valitsemalla Run -> Test project. (Macintosh-koneissa tulee painaa ctrl ja F6). Punainen väri ilmaisee että testin suoritus epäonnistui -- testattava toiminnallisuus ei toiminut kuten toivottiin. Vihreä väri kertoo että testin testaama toiminnallisuus toimi kuten haluttiin.
Pinon toteuttaminen testien avulla tapahtuisi askel kerrallaan siten, että lopulta kaikki testit toimivat. Ohjelman rakentaminen aloitetaan yleensä hyvin varovasti. Rakennetaan ensin luokka OmaPino
siten, että ensimmäinen testi alussaTyhja alkaa toimimaan. Älä tee mitään kovin monimutkaista, "quick and dirty"-ratkaisu kelpaa näin alkuun. Kun testi menee läpi (eli näyttää vihreää), siirry ratkaisemaan seuraavaa kohtaa.
Testi alussaTyhja menee läpi aina kun palautamme arvon true
metodista tyhja
.
package pino; import java.util.ArrayList; import java.util.List; public class OmaPino implements Pino { public OmaPino(int maksimikoko) { } @Override public boolean tyhja() { return true; } // tyhjät metodirungot
Siirrytään ratkaisemaan kohtaa lisayksenJalkeenEiTyhja. Tarvitsemme toteutuksen metodille pinoon
. Yksi lähestymistapa on muokata luokkaa OmaPino
siten, että se sisältää taulukon. Taulukkoa käytetään, että pinottavat luvut talletetaan pinon taulukkoon yksi kerrallaan. Seuraava kuvasarja selkeyttää taulukossa olevien alkioiden pinoon laittamista ja pinosta ottamista.
pino = new OmaPino(4); 0 1 2 3 ----------------- | | | | | ----------------- alkioita: 0 pino.pinoon(5); 0 1 2 3 ----------------- | 5 | | | | ----------------- alkiota: 1 pino.pinoon(3); 0 1 2 3 ----------------- | 5 | 3 | | | ----------------- alkiota: 2 pino.pinoon(7); 0 1 2 3 ----------------- | 5 | 3 | 7 | | ----------------- alkiota: 3 pino.pinosta(); 0 1 2 3 ----------------- | 5 | 3 | | | ----------------- alkiota: 2
Ohjelman tulee siis muistaa kuinka monta alkiota pinossa on. Uusi alkio laitetaan jo pinossa olevien perään. Alkion poisto aiheuttaa sen, että taulukon viimeinen käytössä ollut paikka vapautuu ja alkiomäärän muistavan muuttujan arvo pienenee.
Luokan OmaPino
toteutusta jatketaan askel kerrallaan kunnes kaikki testit menevät läpi. Jossain vaiheessa ohjelmoija todennäköisesti huomaisi, että taulukko kannattaa vaihtaa ArrayList
-rakenteeksi.
Huomaat todennäköisesti ylläolevan esimerkin luettuasi että olet jo tehnyt hyvin monta testejä käyttävää ohjelmaa. Osa TMC:n toiminnallisuudesta rakentuu JUnit-testien varaan, ongelmat ovat varsinkin kurssin alkupuolella pilkottu pieniin testeihin, joiden avulla ohjelmoijaa on ohjattu eteenpäin. TMC:n mukana tulevat testit ovat kuitenkin usein monimutkaisempia kuin ohjelmien normaalissa automaattisessa testauksessa niiden tarvitsee olla. TMC:ssä ja kurssilla käytettävien testien kirjoittajien tulee muunmuassa varmistaa luokkien olemassaolo, jota normaalissa automaattisessa testauksessa harvemmin tarvitsee tehdä.
Harjoitellaan seuraavaksi ensin testien lukemista, jonka jälkeen kirjoitetaan muutama testi.
Tehtäväpohjassa on rajapinnan Tehtavalista
toteuttava luokka MuistiTehtavalist
. Ohjelmaa varten on koodattu valmiiksi testit, joita ohjelma ei kuitenkaan läpäise. Tehtävänäsi on tutustua testiluokkaan TehtavalistaTest
, ja korjata luokka MuistiTehtavalista
siten, että ohjelman testit menevät läpi.
Huom! Tässä tehtävässä sinun ei tarvitse koskea testiluokkaan TehtavalistaTest
.
Huom! Tässä tehtävässä on jo mukana testiluokka, johon sinun tulee kirjoittaa lisää testejä. Vastauksen oikeellisuus testataan vasta TMC-palvelimella: tehtävästä saa pisteet vasta kun molemmat tehtävät on suoritettu palvelimella hyväksytysti. Ole tarkka metodien nimennän ja lisättyjen lukujen kanssa.
Tehtävässä tulee pakkauksessa tilasto
sijaitseva luokka Lukutilasto
.
public void lisaaLuku(int luku)
public int summa()
public int lukujenMaara()
public boolean sisaltaa(int luku)
Testikansiossa olevassa pakkauksessa tilasto
on luokka LukutilastoTest
, johon sinun tulee lisätä uusia testimetodeja.
Lisää testiluokkaan testimetodi public void lukujenMaaraKasvaaKahdellaKunLisataanKaksiLukua()
, jossa lukutilastoon lisätään luvut 3 ja 5. Tämän jälkeen metodissa tarkistetaan että lukutilastossa on kaksi lukua käyttäen lukutilaston metodia lukujenMaara. Käytä Assert
-luokan assertEquals
-metodia palautettujen arvojen tarkastamiseen.
Lisää testiluokkaan testimetodi public void summaOikeinYhdellaLuvulla()
, jossa lukutilastoon lisätään luku 3. Tämän jälkeen metodissa tarkistetaan lukutilaston summa-metodin avulla että tilastossa olevien lukujen summa on 3. Käytä Assert
-luokan assertEquals
-metodia palautettujen arvojen tarkastamiseen.
Huom! tämä tehtävä on pakollinen yliopistoon hakeville.
Tämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen. Tehtävässä toteutetaan sovelluslogiikka jätkänshakille ja harjoitellaan ohjelmarakenteen osittaista omatoimista suunnittelua.
Tehtäväpohjassa tulee mukana käyttöliittymä jätkänshakille, jossa pelilaudan koko on aina 3x3 ruutua. Käyttöliittymä huolehtii ainoastaan pelilaudalla tehtyihin tapahtumiin reagoimisesta, sekä pelilaudan ja pelitilanteen tietojen päivittämisestä. Pelin logiikka on erotettu JatkanshakinSovelluslogiikka
-rajapinnan avulla omaksi luokakseen.
package jatkanshakki.sovelluslogiikka; public interface JatkanshakinSovelluslogiikka { char getNykyinenVuoro(); int getMerkkienMaara(); void asetaMerkki(int sarake, int rivi); char getMerkki(int sarake, int rivi); boolean isPeliLoppu(); char getVoittaja(); }
Rajapinnan JatkanshakinSovelluslogiikka
lisäksi tehtäväpohjassa on apuluokka, joka määrittelee pelilaudan ruutujen mahdolliset tilat char
-tyyppisinä kirjaimina. Ruutu voi olla joko tyhjä, tai siinä voi olla risti tai nolla. Apuluokassa Jatkanshakki
on näille määrittelyt:
package jatkanshakki.sovelluslogiikka; public class Jatkanshakki { public static final char RISTI = 'X'; public static final char NOLLA = 'O'; public static final char TYHJA = ' '; }
Tehtävänäsi on täydentää pakkauksessa jatkanshakki.sovelluslogiikka
olevaa rajapinnan JatkanshakinSovelluslogiikka
toteuttavaa luokkaa OmaJatkanshakinSovelluslogiikka
. Luokka OmaJatkanshakinSovelluslogiikka
mahdollistaa jätkänshakin pelaamisen.
Rajapinta JatkanshakinSovelluslogiikka
määrittelee seuraavat toiminnot, jotka luokan OmaJatkanshakinSovelluslogiikka
tulee toteuttaa:
RISTI
, NOLLA
tai pelin päätyttyä TYHJA
IllegalArgumentException
, jos sarake tai rivi on pelilaudan ulkopuolella tai ruudussa on jo merkki, ja poikkeuksen IllegalStateException
, jos peli on jo loppu.TYHJA
, RISTI
tai NOLLA
. Metodi heittää poikkeuksen IllegalArgumentException
, jos sarake tai rivi on pelilaudan ulkopuolella.true
, jos toinen pelaajista voitti pelin tai peli päättyi tasapeliin, muutoin metodi palauttaa false
TYHJA
, jos peli on kesken tai peli päättyi tasapeliin, muutoin metodi palauttaa voittajan merkin: RISTI
tai NOLLA
Ensimmäinen pelivuoro on aina merkillä RISTI
. Pelin voittaa se pelaaja, joka saa ensimmäisenä kolme merkkiä vaakasuoraan, pystysuoraan tai vinottain. Tasapeli todetaan vasta, kun pelilauta on täynnä merkkejä eli tyhjiä ruutuja ei enää ole.
Vinkki: Pelilaudan tilanteen voi esittää esimerkiksi yhdeksän alkion char
-taulukolla, jonne talletetaan peliruutujen tilat. Sarakkeen ja rivin perusteella voidaan laskea taulukon indeksi: rivi * 3 + sarake
.