Nämä harjoitukset liittyvät lähinnä oppimateriaalin lukuun 8. Vähän flirttaillaan myös jo luvun 9 kanssa.
Kaikki harjoitustehtävät on syytä tehdä. Jotkin tehtävät on merkitty keltaisella värillä. Ne ovat ehkä hieman muita haastavampia. Ilman niitäkin harjoituksista voi saada maksimipisteet, mutta ne lasketaan silti mukaan harjoituspisteitä määrättäessä – ne voivat siis korvata joitakin haasteettomampia tehtäviä tms. Mutta ennen kaikkea noista keltaisista tehtävistä sitä vasta oppiikin!
Huom:
// 3. harjoitukset, tehtävä 2.2, Oili Opiskelija
Luvussa
2 Oliot ja kapselointi, luokka olion mallina
keskeinen esimerkki oli luokka Varasto:
Tälle luokalle aletaan nyt tehdä täydennyksiä aliluokkana!
Luokka Varasto osaa jo hoidella tuotteen määrän käsittelyn. Nyt tuotteelle halutaan lisäksi tuotenimi ja nimen käsittelyvälineet. Luokan voisi tietysti ohjelmoida alusta alkaen uudelleen, mutta miksi ihmeessä? Ohjelmoidaan Tuotevarasto Varaston aliluokaksi!
Toteutetaan ensin pelkkä yksityinen kenttä tuotenimelle, konstruktori ja getteri nimikentälle:
Muista millä tavoin konstruktori voi ensi toimenaan suorittaa yliluokan konstruktorin!
Käyttöesimerkki:
... Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice System.out.println(mehu); // saldo = 988.7, vielä tilaa 11.3 ...
Tulostus siis:
Juice saldo = 988.7, vielä tilaa 11.3
Kuten edellisestä esimerkistä näkee, Tuotevarasto-olion perimä toString() ei tiedä (tietenkään!) mitään tuotteen nimestä. Asialle on tehtävä jotain! Lisätään samalla myös setteri tuotenimelle:
Uuden toString()-metodin voisi toki ohjelmoida käyttäen yliluokalta perittyjä gettereitä, joilla perittyjen, mutta piilossa pidettyjen kenttien arvoja saa käyttöönsä. Koska yliluokkaan on kuitenkin jo ohjelmoitu tarvittava taito varastotilanteen merkkiesityksen tuottamiseen, miksi nähdä vaivaa sen uudelleen ohjelmointiin. Käytä siis hyväksesi perittyä toStringiä. Muista miten korvattua metodia voi kutsua aliluokassa!
Käyttöesimerkki:
... Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, vielä tilaa 10.299999999999955 ...
Tulostus siis:
Juice Juice: saldo = 989.7, vielä tilaa 10.299999999999955
Toisen viikon tehtävissä 3.1-3.4 IntJoukko-olioille toteutettiin joukko-operaatiot kirjastometodeina. Tehtävissä 4.1-4.3 asia hoidettiin toisin: IntJoukko-luokkaa täydennettiiin joukko-operaatioaksessorein.
Nyt samat operaatiot toteutetaan vielä kolmannella tavalla: laaditaan IntJoukko-luokalle aliluokka IntJoukkoOp, joka täydentää perittyjä ominaisuuksia joukko-operaatioilla.
Ota erikoistamisen lähtökohdaksi toisen viikon tehtävän 2.4 "tavallinen" IntJoukko-luokka, älä tietenkään sitä, jonne joukko-operaatiot jo lisättiin.
Vihje 1: Paljonkaan uutta ohjelmointia ei taida tulla, koska vastaavat algoritmit olet jo ohjelmoinut...
Vihjei 2: Voit jättää konstruktorin ohjelmoimatta aliluokkaan, koska tässä tapauksessa aliluokkaan syntyy automaattisesti parametriton oletuskonstruktori public IntJoukkoOp(), joka myös ihan automaattisesti osaa käydä suorittamassa yliluokan parametrittoman konstruktorin.
public IntJoukkoOp yhdiste(IntJoukkoOp toinen) palauttaa arvonaan joukon, joka sisältää kaikki this-joukon ja joukon toinen alkiot.
Käyttöesimerkki:
IntJoukkoOp a, b, c; a = new IntJoukkoOp(); b = new IntJoukkoOp(); // .... a:han ja b:hen viedään kaikenlaisia lukuja c = a.yhdiste(b); System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("c = " + c);
public IntJoukkoOp leikkaus(IntJoukkoOp toinen) palauttaa arvonaan joukon, joka sisältää täsmälleen kaikki alkiot, jotka kuuluvat sekä this-joukkoon että joukkoon toinen.
Käyttöesimerkki:
IntJoukkoOp a, b, c; a = new IntJoukkoOp(); b = new IntJoukkoOp(); // .... a:han ja b:hen viedään kaikenlaisia lukuja c = a.leikkaus(b); System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("c = " + c);
public IntJoukkoOp erotus(IntJoukkoOp toinen) palauttaa arvonaan joukon, joka sisältää kaikki this-joukon alkiot, jotka eivät kuulu joukkoon toinen.
Käyttöesimerkki:
IntJoukkoOp a, b, c; a = new IntJoukkoOp(); b = new IntJoukkoOp(); // .... a:han ja b:hen viedään kaikenlaisia lukuja c = a.erotus(b); System.out.println("a = " + a); System.out.println("b = " + b); System.out.println("c = " + c);
Muokkaa toisen viikon tehtävän 3.4 komentotulkista sellainen, jossa käytetään IntJoukkoOp-olioita.
Toisinaan saattaa olla kiinostavaa tietää, millä tavoin jonkin tuotteen varastotilanne muuttuu: onko varasto usein hyvin vajaa, ollaanko usein ylärajalla, onko vaihelu suurta vai pientä, jne. Varustetaanpa siksi Tuotevarasto-luokka taidolla muistaa tuotteen määrän muutoshistoriaa.
Aloitetaan apuvälineen laadinnalla.
Muutoshistorian muistamisen voisi toki toteuttaa suoraankin ArrayList<Double>-oliona luokassa Tuotevarasto, mutta nyt laaditaan kuitenkin oma erikoistettu väline tähän tarkoitukseen. Väline toteutetaan kapseloimalla ArrayList<Double>-olio.
Muutoshistoria-luokan API:
Havainnollista olioiden luontia ja käyttöä pienellä ohjelmalla.
Täydennä Muutoshistoria-luokkaa analyysimetodein:
Pajamestarien vihje: API:n Collections tekee laiskan ohjelmoijan elämän tässä kohtaa helpommaksi...
Luennoijan vihje: Kannattaa muistaa for-each! Se osaa kivasti askeltaa kokoelman kuin kokoelman alkioiden arvot läpi yksi kerrallaan. Tätä kannattaa kokeilla, vaikka laiskottelisinkin pajamestarien vihjeen vinkkaamalla tavalla.
Havainnollista uusien metodien käyttöä ja toimivuutta pienellä ohjelmalla.
Täydennä Muutoshistoria-luokkaa analyysimetodein:
Esimerkki laskennasta: Tarkastellaan esimerkiksi muutoshistoriaa: 0, 5, 9, 2, 6. Muutosten itseisarvot ovat 5, 4, 7 ja 4, joten suurin muutoksen itseisarvo on 7. Varianssi on 12.3 seuraavan perusteella: (1 / 4) * ((0 - 4,4)2 + (5 - 4,4)2 + (9 - 4,4)2 + (2 - 4,4)2 + (6 - 4,4)2) = 12.3
Havainnollista uusien metodien käyttöä ja toimivuutta pienellä ohjelmalla.
Toteuta luokan Tuotevarasto aliluokkana MuistavaTuotevarasto. Uusi versio tarjoaa vanhojen lisäksi varastotilanteen muutoshistoriaan liittyviä palveluita. Historiaa hallitaan Muutoshistoria-oliolla.
API-raakile:
Huomaa että tässä esiversiossa historia ei vielä toimi kunnolla; nyt vasta vain aloitussaldo muistetaan.
Käyttöesimerkki:
// tuttuun tapaan: MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, vielä tilaa 10.3 ... // mutta vielä historia() ei toimi kunnolla: System.out.println(mehu.historia()); // [1000.0] // saadaan siis vasta konstruktorin asettama historian alkupiste... ...
Tulostus siis:
Juice Juice: saldo = 989.7, vielä tilaa 10.299999999999955 [1000.0]
On aika aloittaa historia! Ensimmäinen versio ei historiasta tiennyt kuin alkupisteen. Täydennä luokkaa metodein
Käyttöesimerkki:
// tuttuun tapaan: MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, vielä tilaa 10.3 ... // mutta nyt on historiaakin: System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7] ...
Tulostus siis:
Juice Juice: saldo = 989.7, vielä tilaa 10.299999999999955 [1000.0, 988.7, 989.7]
Muista miten korvaava metodi voi käyttää hyväkseen korvattua metodia!
Täydennä luokkaa metodilla
Käyttöesimerkki:
MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); mehu.lisaaVarastoon(1.0); //System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7] mehu.tulostaAnalyysi();
Metodi tulostaAnalyysi kirjoittaa ilmoituksen tyyliin:
Tuote: Juice Historia: [1000.0, 988.7, 989.7] Suurin tuotemäärä: 1000.0 Pienin tuotemäärä: 988.7 Keskiarvo: 992.8
Täydennä analyysin tulostus sellaiseksi, että mukana ovat myös muutoshistorian suurin muutos ja historian varianssi.
Havainnollista kehiteltyä analyysiraporttia pienellä esimerkkiohjelmalla.
[Tämä mainio tehtäväsarja on Arto Vihavaisen kehittelemä.]
Sovelluskehys on ohjelma, joka tarjoaa lähtökohdan ja joukon palveluita jonkin erityisen sovelluksen toteuttamiseen. Yksi hyvin tavallinen tapa käyttää sovelluskehyksen ideaa on laatia luokka, josta aliluokkana erikoistetaan yliluokan tarjoamista palveluista jokin erityinen sovellus.
Game of Life on matemaatikko John Conway'n kehittelemä yksinkertainen "populaatiosimulaattori", kts. http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life.
Olet kaverisi kanssa suunnitellut Game of Life-pelin tekemistä jo muutaman hetken. Suunnitelmananne on se, että hän toteuttaa graafisen ulkoilmeen, sinulle on jäänyt iso osa pelin taustalogiikan toteutuksesta.
Game of Lifen säännöt ovat seuraavat:
Kaverisi on käynyt jo ohjelmoinnin jatkokurssin, ja tuntee periytymisen periaatteet. Hän löpisee myös jotakin käsitteistä "abstrakti luokka" ja "rajapintaluokka": "Abstrakteista luokista ei voi tehdä ilmentymiä, mutta niiden avulla voi määritellä toiminnallisuuksia, jotka perivän luokan tulee toteuttaa. Abstrakti luokka on vähän samanlainen kuin rajapintaluokka, mutta rajapintaluokassa on vain metodimäärittelyt – eikä ollenkaan toteutusta". Et ymmärtänyt hänen löpinöistään juurikaan mitään, mutta ei se haittaa!
Kaverisi tekemä graafinen ulkoasu on jo lähes valmis, ja se odottaa vain logiikkamoottoria käyttöönsä. Sovitte aiemmin yhdessä APIn pelille, jonka pohjalta kaverisi on jo toteuttanut abstraktin luokan GameOfLifeAlusta.
GameOfLifeAlusta tarjoaa seuraavat toiminnot:
Luokka GameOfLifeAlusta on ns. abstrakti luokka, jonne kaverisi on määritellyt joukon abstrakteja metodeita, jotka sinun täytyy toteuttaa saadaksesi pelin toimimaan. Toiminnallisuudet toteuttavat metodit ovat seuraavat:
Alla on runko luokalle OmaAlusta, joka perii pakkauksessa gameoflife olevan GameOfLifeAlusta-luokan. GameOfLifeAlusta on kaverisi tarjoamassa gameoflife.jar-sovelluskirjastossa jonka voit ladata tästä linkistä . NetBeans käyttäjät: sovelluskirjasto ja luokan OmaAlusta pohja tulee automaattisesti testien mukana. (Älä häkelly tuosta @Override-ilmauksesta! Se tahtoo vain muistuttaa ohjelmoijaa, että peritty metodi syrjäytetään eli korvataan (override) aliluokassa. Pakkauksistakin puhutaan myöhemmin. Nyt tee vain niin kuin käsketään...)
import gameoflife.GameOfLifeAlusta; public class OmaAlusta extends GameOfLifeAlusta { public OmaAlusta(int leveys, int korkeus) { super(leveys, korkeus); } @Override public boolean onElossa(int x, int y) { return false; } @Override public void muutaElavaksi(int x, int y) { return; } @Override public void muutaKuolleeksi(int x, int y) { return; } @Override public void alustaSatunnaisetPisteet(double todennakoisyysPisteelle) { return; } @Override public int getElossaOlevienNaapurienLukumaara(int x, int y) { return 0; } @Override public void hoidaSolu(int x, int y, int elossaOleviaNaapureita) { return; } }
Tässä ohjelmarungossa kaikki perityt abstraktit metodit on korvattu ei-abstrakteilla metodeilla, jotka eivät kuitenkaan vielä tee oikeastaan mitään. Mutta koska ne eivät ole abstrakteja, tästä luokasta voi luoda ilmentymiä – toisin kuin abstraktista luokasta GameOfLifeAlusta.
Toteuta todellisen toiminnallisuuden tarjoavina ensin metodit:
Testaa toteutustasi seuraavalla testiohjelmalla (kaverisi on tehnyt myös tekstipohjaisen simulaattorin!)
import gameoflife.komentorivi.KomentoriviGameOfLife; public class Main { public static void main(String[] args) { OmaAlusta alusta = new OmaAlusta(7, 5); alusta.muutaElavaksi(2, 0); alusta.muutaElavaksi(4, 0); alusta.muutaElavaksi(3, 3); alusta.muutaKuolleeksi(3, 3); alusta.muutaElavaksi(0, 2); alusta.muutaElavaksi(1, 3); alusta.muutaElavaksi(2, 3); alusta.muutaElavaksi(3, 3); alusta.muutaElavaksi(4, 3); alusta.muutaElavaksi(5, 3); alusta.muutaElavaksi(6, 2); KomentoriviGameOfLife gom = new KomentoriviGameOfLife(alusta); gom.pelaa(); } }
Tulostuksen pitäisi olla seuraavanlainen:
Paina enter jatkaaksesi, muut lopettaa: <enter> X X X X XXXXX Paina enter jatkaaksesi, muut lopettaa: stop Kiitos!
Huom: Jos käytät komentoriviä, voit kääntää ohjelman siten, että gameoflife.jar-kirjasto tulee mukaan komennolla:
javac *.java -cp gameoflife.jar
Ohjelma suoritetaan seuraavalla komennolla:
java -cp gameoflife.jar:. Main
Tässä oletetaan että gameoflife.jar-tiedosto on samassa kansiossa lähdekoodiesi kanssa. Vipu -cp kertoo polun käytettäviin luokkiin ja luokkakirjastoihin.
Toteuta metodi alustaSatunnaisetPisteet(double todennakoisyysPisteelle), joka alustaa kaikki alkiot siten, että kukin alkio on elävä todennäköisyydellä todennakoisyysPisteelle. Todennäköisyys annetaan double-arvona suljetulla välillä [0, 1].
Testaa metodia. Arvolla 0.0 ei pitäisi olla yhtään elossa olevaa solua, arvolla 1.0 kaikkien solujen tulisi olla elossa (eli näkyä X-merkkisinä). Arvolla 0.5 noin puolet soluista on eläviä.
Toteuta metodi getElossaOlevienNaapurienLukumaara(int x, int y), joka laskee elossa olevien naapurien lukumäärän. Keskellä taulukkoa olevalla solulla on kahdeksan naapuria, reunassa olevalla solulla 5, kulmassa olevalla 3. Solua pisteessä (x, y) lasketa naapuriksi eli solu ei ole oma naapurinsa.
Testaa metodia seuraavilla lauseilla (voit keksiä myös muita testitapauksia!):
OmaAlusta alusta = new OmaAlusta(7, 5); alusta.muutaElavaksi(0, 1); alusta.muutaElavaksi(1, 0); alusta.muutaElavaksi(1, 2); alusta.muutaElavaksi(2, 2); alusta.muutaElavaksi(2, 1); System.out.println("Elossa naapureita (0,0): " + alusta.getElossaOlevienNaapurienLukumaara(0, 0)); System.out.println("Elossa naapureita (1,1): " + alusta.getElossaOlevienNaapurienLukumaara(1, 1));
Tulostuksen pitäisi olla seuraavanlainen:
Elossa naapureita (0,0): 2 Elossa naapureita (1,1): 5
Huh! Jäljellä on vielä metodin hoidaSolu(int x, int y, int elossaOleviaNaapureita) toteuttaminen. GameOfLife-pelin säännöthän olivat seuraavat:
Toteuta metodi ylläolevien sääntöjen mukaan. Huom: Kannattaa ohjelmoida ja testata yksi sääntö kerrallaan!
Mainiota, huudahtaa kaverisi. Hän sai valmiiksi myös graafisen simulaattorin pelille. Simulaattorin konstruktori on määritelty siten, että sen muodollisen parametrin tyyppi on GameOfLifeAlusta. Kokeile simulaattoria seuraavan esimerkkikoodin avulla (olet jo aiemmin toteuttanut luokan OmaAlusta).
import gameoflife.Simulaattori; public class Main { public static void main(String[] args) { OmaAlusta alusta = new OmaAlusta(100, 100); alusta.alustaSatunnaisetPisteet(0.7); Simulaattori simulaattori = new Simulaattori(alusta); simulaattori.simuloi(); } }
Kaverisi kertoo että simulaattorissa on myös mahdollista herättää soluja henkiin hiirtä painamalla.
Näytä ohjelmaasi ohjaajalle ja kerro miksi Simulaattori toimii, vaikka esimerkkikoodissa Simulaattorin konstruktorille annetaan parametrina OmaAlusta-tyyppinen olio. Anna myös esimerkki luokasta, jonka ilmentymää ei voisi antaa parametrina Simulaattori-luokan konstruktorille! Tässä kannattaa ehkä muistella luentojen ja materiaalin toteamusta siitä, että jokainen kissa on eläin, mutta jokainen eläin ei ole kissa. Sitäkin voi miettiä, että mikään kahvikuppi ei ole eläin eikä mikään eläin kahvikuppi...