Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

Ohjelmoinnin jatkokurssi: harjoitukset s2011: 3/6 (14.-18.11.)

(Muutettu viimeksi 15.11.2011, sivu perustettu 2.11.2011.)

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:

Tuotevarasto

Luvussa 2 Oliot ja kapselointi, luokka olion mallina keskeinen esimerkki oli luokka Varasto:

Tälle luokalle aletaan nyt tehdä täydennyksiä aliluokkana!

Tuotevarasto, vaihe 1

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

Tuotevarasto, vaihe 2

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

Joukko-operaatioiden toteutus aliluokassa

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.

Yhdiste

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

Leikkaus

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

Erotus

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

Komentotulkki

Muokkaa toisen viikon tehtävän 3.4 komentotulkista sellainen, jossa käytetään IntJoukkoOp-olioita.

Tuotevarasto ja muutoshistoria

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.

Muutoshistoria.java, vaihe 1

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.

Muutoshistoria.java, vaihe 2

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.

Muutoshistoria.java, vaihe 3

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.

MuistavaTuotevarasto, vaihe 1

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]

MuistavaTuotevarasto, vaihe 2

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!

MuistavaTuotevarasto, vaihe 3

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

MuistavaTuotevarasto, vaihe 4

Täydennä analyysin tulostus sellaiseksi, että mukana ovat myös muutoshistorian suurin muutos ja historian varianssi.

Havainnollista kehiteltyä analyysiraporttia pienellä esimerkkiohjelmalla.

Sovelluskehyksen käyttöä: Game of Life

[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:

GameOfLife-toteutus, vaihe 1

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.

GameOfLife-toteutus, vaihe 2

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

GameOfLife-toteutus, vaihe 3

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

GameOfLife-toteutus, vaihe 4

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!

GameOfLife-toteutus, vaihe 5

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