Sisällysluettelo

Tehtävät

Kysely: Oma arvio osaamisesta

Tutkimus: Oma arvio osaamisesta

Käytämme tietoja kurssiemme kehittämiseen ja opetuksen tutkimukseen.

Tässä kyselyssä tutkitaan suhtautumistasi omaan kurssin aikana karttuneeseen osaamiseesi.

 

Arvioi skaalalla yhdestä sataan tämän kurssin sisällön hallitsemistasi. Kirjoita arvo 1 jos olet sitä mieltä että et hallitse kurssin sisältöä lainkaan, arvo 50 jos olet sitä mieltä että hallitset noin puolet kurssin sisällöstä, ja arvo 100, jos olet sitä mieltä että hallitset tähänastisen kurssin sisällön täysin.

 

Kirjoita allaolevaan laatikkoon ne kurssin asiat, mitkä ovat olleet mielestäsi helpoimpia:

 

Kirjoita allaolevaan laatikkoon ne kurssin asiat, mitkä ovat olleet mielestäsi vaikeimpia:

Kysely: Ohjelmointirutiini ja muuttujien ymmärrys, osa 4

Tutkimus: Ohjelmointirutiini ja muuttujien ymmärrys

Käytämme tietoja kurssiemme kehittämiseen ja opetuksen tutkimukseen.

Tässä kyselyssä kartoitetaan tähän mennessä kertynyttä ohjelmointirutiiniasi sekä muuttujien ymmärrystä. Vastaathan kyselyyn nykyisen osaamisesi perusteella, älä siis esimerkiksi kokeile koodinpätkiä tietokoneella ennen vastauksen antamista.

 

Tässä sinun tulee kuvata yhdellä lauseella allaolevan koodin tehtävää. Älä kirjoita koodin kulkua rivi riviltä, vaan kerro mihin koodia voi käyttää. Oleta että muuttujat y1, y2, ja y3 on määritelty, ja jokainen on int-tyyppinen. Kun koodissa on kommentteja, oleta että kommenteissa kuvattu koodi on olemassa; älä toteuta koodia itse.

if (y1 < y2) {
    // koodi, millä vaihdetaan muuttujien y1 ja y2
    // arvot keskenään tulee tähän
}

if (y2 < y3) {
    // koodi, millä vaihdetaan muuttujien y2 ja y3
    // arvot keskenään tulee tähän
}

if (y1 < y2) {
    // koodi, millä vaihdetaan muuttujien y1 ja y2
    // arvot keskenään tulee tähän
}

Kirjoita allaolevaan laatikkoon ylläolevan koodin tarkoitus yhdellä lauseella:

Kysely: Ohjelmointiin liittyvät uskomukset ja tunteet

Tutkimus: Ohjelmointiin liittyvät uskomukset ja tunteet

Käytämme tietoja kurssiemme kehittämiseen ja opetuksen tutkimukseen.

Tässä kyselyssä tarkistellaan erilaisia ohjelmointiin liittyviä uskomuksia ja tunteita. Vastaathan kyselyyn nykyisen kokemuksesi perusteella.

 

30 Virhetilanteiden etsiminen ja korjaaminen

Ohjelmia luodessa niihin päätyy virheitä. Joskus virheet eivät ole niin vakavia, ja aiheuttavat päänvaivaa lähinnä ohjelman käyttäjälle, mutta joskus virheet voivat johtaa hyvinkin vakaviin seurauksiin.

Tehdään seuraavaksi muutamia ohjelmia, joissa on virhe, ja muistellaan samalla mistä nämä virheet tulevat.

Tehtävä 121: NullPointerException

Toteuta ohjelma, jonka suorittaminen tuottaa virheen NullPointerException. Ohjelman tulee olla sellainen, että käyttäjän ei tarvitse antaa konelle syötettä (esim. näppäimistöltä).

Tehtävä 122: IndexOutOfBoundsException

Toteuta ohjelma, jonka suorittaminen tuottaa virheen IndexOutOfBoundsException. Ohjelman tulee olla sellainen, että käyttäjän ei tarvitse antaa konelle syötettä (esim. näppäimistöltä).

Tehtävä 123: StringIndexOutOfBoundsException

Toteuta ohjelma, jonka suorittaminen tuottaa virheen StringIndexOutOfBoundsException. Ohjelman tulee olla sellainen, että käyttäjän ei tarvitse antaa konelle syötettä (esim. näppäimistöltä).

Tehtävä 124: ArrayIndexOutOfBoundsException

Toteuta ohjelma, jonka suorittaminen tuottaa virheen ArrayIndexOutOfBoundsException. Ohjelman tulee olla sellainen, että käyttäjän ei tarvitse antaa konelle syötettä (esim. näppäimistöltä).

Ohjelmien muuttuessa monimutkaisemmiksi, tulee virheiden löytämisestäkin koko ajan haastavampaa. NetBeansiin integroitu debuggeri voi olla avuksi virheiden löytämisessä. Seuraavalla screencastilla esitellään debuggerin käyttöä. Screencast esittelee myös miten projekteja voidaan luoda, avata ja sulkea sekä miten ohjelmia voidaan suorittaa NetBeansin ulkopuolella.

Tehtävä 125: Debuggailua

Ohjelmassa on yritetty luoda sovellus, joka kysyy käyttäjältä merkkijonoa ja lukua. Sovelluksen pitäisi toimia esimerkiksi seuraavasti:

Sana: testi
Luku: 3
t
 e
  s
t
 i

Esimerkki 2:

Sana: esim
Luku: 2
e
 s
i
 m

Tällä hetkellä se ei kuitenkaan toimi oikein. Ota selvää miksi, ja korjaa ohjelma.

30.1 Stack trace

Kun ohjelmassa tapahtuu virhe, se tyypillisesti tulostaa ns. stack tracen, eli niiden metodikutsujen listan, joiden seurauksena virhetilanne syntyi. Stack trace voi näyttää esimerkiksi seuraavalta:

Exception in thread "main" java.lang.NullPointerException
        at Kirja.haeOtsikko(Kirja.java:22)
        at Kirjoittaja.haeOtsikot(Kirjoittaja.java:62)
        at Ohjelma.main(Ohjelma.java:15)

Listan alussa kerrotaan minkälainen virhe tapahtui (tässä NullPointerException), ja seuraavalla rivillä kerrotaan missä virhe tapahtui. Rivi "at Kirja.haeOtsikko(Kirja.java:22)" sanoo, että virhe tapahtui luokan Kirja rivillä 22.

        at Kirja.haeOtsikko(Kirja.java:22)

Voimme tarkastella Kirja-luokan riviä, ja etsiä syitä virheelle. Kirjan kyseinen metodi voisi olla esimerkiksi seuraavanlainen:

public String haeOtsikko() {
    if(otsikko.length() > 0) { <-- rivi 22
        return otsikko;
    }

    return "Ei otsikkoa";
}

Tämä tarkoittaisi todennäköisesti, että muuttujan otsikko arvo on null kun otsikkoa haetaan.

30.2 Muistilista debuggaukseen

Jos koodisi ei toimi etkä tiedä missä on virhe, näillä askeleilla pääset alkuun.

  1. Sisennä koodisi oikein ja selvitä, puuttuuko sulkuja.
  2. Testaa ohjelman kulkua erilaisilla syötteillä, ja selvitä minkälaisella syötteellä ohjelma ei toimi halutusti. Jos sait virheen testeistä, testit saattavat myös kertoa käytetyn syötteen.
  3. Lisää ohjelmaan tulostuskomentoja, joissa tulostat käytettävien muuttujien arvoja ohjelman suorituksen eri vaiheissa.
  4. Tarkista, että kaikki käyttämäsi muuttujat on alustettu. Jos tätä ei ole tehty, seuraa virhe NullPointerException.
  5. Jos ohjelmasi aiheuttaa poikkeuksen, kannattaa ehdottomasti kiinnittää huomiota poikkeuksen yhteydessä olevaan stack traceen, eli niiden metodikutsujen listaan, minkä seurauksena poikkeuksen aiheuttanut tilanne syntyi.
  6. Opettele käyttämään debuggeria, aiemmin nähdyllä videolla pääsee alkuun.

31 Ollako static vaiko eikö olla?

Kurssin alussa kaikissa metodeissa esiintyi määre static, mutta aloittaessamme olioiden käytön, tuon määreen käyttö jopa kiellettiin. Mistä on kysymys?

Seuraavassa esimerkissä on metodi nollaaTaulukko joka toimii nimensä mukaisesti eli asettaa nollan parametrina saamansa taulukon kaikkien lokeroiden arvoksi.

public class Ohjelma {

    public static void nollaaTaulukko(int[] taulukko) {
        for (int i = 0; i < taulukko.length; i++) {
            taulukko[i] = 0;
        }
    }

    public static void main(String[] args) {
        int[] luvut = {1, 2, 3, 4, 5};

        for (int luku : luvut) {
            System.out.print(luku + " ");  // tulostuu 1 2 3 4 5
        }

        System.out.println();

        nollaaTaulukko(luvut);

        for (int luku : luvut) {
            System.out.print(luku + " ");  // tulostuu 0 0 0 0 0
        }
    }
}

Yllä olevassa esimerkissä metodilla nollaaTaulukko on määre static ja sen kutsuminen tapahtuu ilman alussa olevaa olioviitettä.

Staattiset metodit eivät liity olioon vaan luokkaan. Staattisia metodeja kutsutaan usein myös luokkametodeiksi. Toisin kuin olioiden metodit (joilla ei ole määrettä static), staattiseen metodiin ei liity olioa, eikä niillä voi muokata oliomuuttujia.

Staattiselle metodille voi toki antaa olion parametrina. Staattinen metodi ei kuitenkaan voi käsitellä mitään muita lukuja, merkkijonoja, taulukoita tai olioita kuin niitä, jotka annetaan sille parametrina, tai jotka se luo itse.

Toisin sanoen, staattista metodia käyttävän koodin tulee antaa staattiselle metodille ne arvot ja oliot, joita staattisessa metodissa käsitellään.

Koska staattinen metodi ei liity mihinkään olioon, ei sitä kutsuta oliometodien tapaan olionNimi.metodinNimi(), vaan ylläolevan esimerkin tapaan käytetään pelkkää staattisen metodin nimeä.

Jos staattisen metodin koodi on eri luokan sisällä kuin sitä kutsuva metodi, voi staattista metodia kutsua muodossa LuokanNimi.staattisenMetodinNimi(). Edellinen esimerkki alla muutettuna siten, että pääohjelma ja metodi ovat omissa luokissaan (eli eri tiedostoissa):

public class Ohjelma {
    public static void main(String[] args) {
        int[] luvut = {1, 2, 3, 4, 5};

        for (int luku : luvut) {
            System.out.print(luku + " ");  // tulostuu 1 2 3 4 5
        }

        System.out.println();

        TaulukonKasittely.nollaaTaulukko(luvut);

        for (int luku : luvut) {
            System.out.print(luku + " ");  // tulostuu 0 0 0 0 0
        }
    }
}
public class TaulukonKasittely {
    public static void nollaaTaulukko(int[] taulukko) {
        for (int i=0; i < taulukko.length; i++) {
            taulukko[i] = 0;
        }
    }
}

Toisen luokan sisällä -- tässä tämän toisen luokan nimi on TaulukonKasittely -- määriteltyä staattista metodia kutsutaan yllä muodossa TaulukonKasittely.nollaaTaulukko(parametri);.

31.1 Milloin staattisia metodeja tulisi käyttää

Kaikki olion tilaa käsittelevät metodit tulee määritellä oliometodeina, joilla ei ole static-määrettä. Esimerkiksi edellisillä viikoilla määrittelemiemme luokkien Henkilo, Paivays, Kello, Joukkue, ... kaikki metodit tulee määritellä ilman static-määrettä.

Palataan vielä luokkaan Henkilo. Seuraavassa on osa luokan määritelmästä. Kaikkiin oliomuuttujiin viitataan this-määreen avulla sillä korostamme, että metodeissa käsitellään olion "sisällä" olevia oliomuuttujia.

public class Henkilo {
    private String nimi;
    private int ika;

    public Henkilo(String nimi) {
        this.ika = 0;
        this.nimi = nimi;
    }

    public boolean taysiIkainen() {
        if (this.ika < 18) {
            return false;
        }

        return true;
    }

    public void vanhene() {
        this.ika++;
    }

    public String getNimi() {
        return this.nimi;
    }
}

Koska metodit käsittelevät oliota, ei niitä voi määrittää static:eiksi eli "olioista riippumattomiksi". Jos näin yritetään tehdä, ei metodi toimi. Esimerkiksi allaoleva Henkilo-olion iän muokkausta yrittävä metodi vanhene ei toimi:

public class Henkilo {
    //...

    public static void vanhene() {
        this.ika++;
    }
}

Seurauksena on virheilmoitus non static variable ika can not be referenced from static context, joka tarkoittaa että oliomuuttujaan ei voida viitata luokkametodista; staattinen metodi ei siis pysty käsittelemään oliomuuttujaa.

Eli milloin staattista metodia sitten kannattaa käyttää? Tarkastellaan aiemmin materiaalissa nähtyä henkilöolioita käsittelevää esimerkkiä:

public class Main {
    public static void main(String[] args) {
        Henkilo ada = new Henkilo("Ada");
        Henkilo antti = new Henkilo("Antti");
        Henkilo juhana = new Henkilo("Juhana");

        for (int i = 0; i < 30; i++) {
            ada.vanhene();
            juhana.vanhene();
        }

        antti.vanhene();

        if (ada.taysiIkainen()) {
            System.out.println(ada.getNimi() + " on täysi-ikäinen");
        } else {
            System.out.println(ada.getNimi() + " on alaikäinen ");
        }

        if (antti.taysiIkainen()) {
            System.out.println(antti.getNimi() + " on täysi-ikäinen");
        } else {
            System.out.println(antti.getNimi() + " on alaikäinen");
        }

        if (juhana.taysiIkainen()) {
            System.out.println(juhana.getNimi() + " on täysi-ikäinen");
        } else {
            System.out.println(juhana.getNimi() + " on alaikäinen ");
        }
    }
}

Huomaamme, että henkilöiden täysi-ikäisyyden ilmottamiseen liittyvä koodinpätkä on copy-pastettu kolme kertaa peräkkäin. Todella rumaa!

Henkilön täysi-ikäisyyden ilmoittaminen on mainio kohde staattiselle metodille. Kirjoitetaan ohjelma uudelleen metodia hyödyntäen:

public class Main {

    public static void main(String[] args) {
        Henkilo ada = new Henkilo("Ada");
        Henkilo antti = new Henkilo("Antti");
        Henkilo juhana = new Henkilo("Juhana");

        for (int i = 0; i < 30; i++) {
            ada.vanhene();
            juhana.vanhene();
        }

        antti.vanhene();

        ilmoitaTaysiIkaisyys(ada);

        ilmoitaTaysiIkaisyys(antti);

        ilmoitaTaysiIkaisyys(juhana);
    }

    private static void ilmoitaTaysiIkaisyys(Henkilo henkilo) {
        if (henkilo.taysiIkainen()) {
            System.out.println(henkilo.getNimi() + " on täysi-ikäinen");
        } else {
            System.out.println(henkilo.getNimi() + " on alaikäinen");
        }
    }
}

Metodi ilmoitaTaysiIkaisyys on määritelty staattiseksi, eli se ei liity mihinkään olioon, mutta metodi saa parametrikseen henkilöolion. Metodia ei ole määritelty Henkilö-luokan sisälle sillä vaikka se käsittelee parametrinaan saamaan henkilöolioa, se on juuri kirjoitetun pääohjelman apumetodi, jonka avulla pääohjelma on saatu kirjoitettua selkeämmin.

Tehtävä 126: Kirjaston tietojärjestelmä

Kumpulan tiedekirjasto tarvitsee uuden järjestelmän kirjojen hallintaan. Tässä tehtävässä toteutetaan prototyyppi, jossa toteutetaan kirjan haku nimen, julkaisijan tai julkaisuvuoden perusteella.

Rakennetaan järjestelmä osista, ensin toteutetaan oleelliset luokat eli Kirja ja Kirjasto. Luokka Kirja sisältää kirjaan liittyvät tiedot, luokka Kirjasto tarjoaa erilaisia hakutoiminnallisuuksia kirjoihin liittyen.

126.1 Kirja

Luodaan ensiksi luokka Kirja. Kirjalla on oliomuuttujina nimeke, eli kirjan nimi, julkaisija, eli kirjan julkaisija, ja julkaisuvuosi eli vuosi jolloin kirja on julkaistu. Kaksi ensimmäistä muuttujaa on merkkijonotyyppisiä, viimeisin on kokonaisluku. Oletamme tässä että kirjalla on aina vain yksi kirjoittaja.

Toteuta luokka Kirja. Kirjalla tulee olla myös konstruktori public Kirja(String niemeke, String julkaisija, int julkaisuvuosi) sekä metodit public String nimeke(), public String julkaisija(), public int julkaisuvuosi() ja public String toString(). Arvannet mitä metodien tulee tehdä, alla esimerkki.

Testaa luokan toimintaa:

Kirja cheese = new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007);
System.out.println(cheese.nimeke());
System.out.println(cheese.julkaisija());
System.out.println(cheese.julkaisuvuosi());

System.out.println(cheese);
Cheese Problems Solved
Woodhead Publishing
2007
Cheese Problems Solved, Woodhead Publishing, 2007

126.2 Kirjasto

Kirjaston tehtävä on antaa käyttäjälle mahdollisuus kirjojen lisäämiseen ja niiden hakemiseen. Luo luokka Kirjasto, jolla on konstruktori public Kirjasto() ja metodit public void lisaaKirja(Kirja uusiKirja) ja public void tulostaKirjat()

Kirjasto kirjasto = new Kirjasto();

Kirja cheese = new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007);
kirjasto.lisaaKirja(cheese);

Kirja nhl = new Kirja("NHL Hockey", "Stanley Kupp", 1952);
kirjasto.lisaaKirja(nhl);

kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

kirjasto.tulostaKirjat();
Cheese Problems Solved, Woodhead Publishing, 2007
NHL Hockey, Stanley Kupp, 1952
Battle Axes, Tom A. Hawk, 1851

126.3 Hakutoiminnallisuus

Kirjastosta tulee pystyä etsimään kirjoja nimekkeiden ja julkaisijoiden perusteella. Lisää kirjastolle metodit public ArrayList<Kirja> haeKirjaNimekkeella(String nimeke), public ArrayList<Kirja> haeKirjaJulkaisijalla(String julkaisija) ja public ArrayList<Kirja> haeKirjaJulkaisuvuodella(int julkaisuvuosi). Metodit palauttavat listan kirjoista, joissa on haluttu nimeke, julkaisija tai julkaisuvuosi.

Huom: joudut siis tehdä metodin jonka paluuarvona on ArrayList. Tämä onnustuu seuraavaa metodirunkoa hyödyntäen:

public class Kirjasto {
    // ...

    public ArrayList<Kirja> haeKirjaNimekkeella(String nimeke) {
        ArrayList<Kirja> loydetyt = new ArrayList<>();

        // käy läpi kaikki kirjat ja lisää ne joilla haetun kaltainen nimeke listalle loydetyt

        return loydetyt;
    }
}

Huom! Kun haet teet hakua merkkijonon avulla, älä tee tarkkaa hakua (metodi equals) vaan käytä String-luokan metodia contains. Huomaat todennäköisesti myös että sinulla on ns. copy-paste -koodia Kirjasto-luokan koodissa. Keksitkö tavan päästä siitä eroon?

Kirjasto kirjasto = new Kirjasto();

kirjasto.lisaaKirja(new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007));
kirjasto.lisaaKirja(new Kirja("The Stinky Cheese Man and Other Fairly Stupid Tales", "Penguin Group", 1992));
kirjasto.lisaaKirja(new Kirja("NHL Hockey", "Stanley Kupp", 1952));
kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

ArrayList<Kirja> hakutulos = kirjasto.haeKirjaNimekkeella("Cheese");
for (Kirja kirja: hakutulos) {
    System.out.println(kirja);
}

System.out.println("---");
for (Kirja kirja: kirjasto.haeKirjaJulkaisijalla("Penguin Group  ")) {
    System.out.println(kirja);
}

System.out.println("---");
for (Kirja kirja: kirjasto.haeKirjaJulkaisuvuodella(1851)) {
    System.out.println(kirja);
}
Cheese Problems Solved, Woodhead Publishing, 2007
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992
---
---
Battle Axes, Tom A. Hawk, 1851

126.4 Paranneltu hakutoiminnallisuus

Hakutoiminnallisuutemme on jo hyvä, mutta se ei ymmärrä isojen ja pienten kirjainten eroa. Yllä olleessa esimerkissä haku nimekkeellä "cheese" ei olisi tuottanut yhtäkään tulosta. Myös toinen esimerkki, jossa oli ylimääräisiä välilyöntejä, ei näyttänyt haluttua tulosta. Haluamme että nimekkeiden ja julkaisijoiden nimillä haettaessa ei välitetä merkkien koosta, ja että käyttäjä voi syöttää ylimääräisiä välilyöntejä kirjan nimen alkuun tai loppuun (meidän ei tarvitse välittää sanojen välillä olevista tyhjistä!). Toteutetaan pieni apukirjasto StringUtils merkkijonojen vertailuun.

Luo luokka StringUtils, ja lisää sille staattinen metodi public static boolean sisaltaa(String sana, String haettava), joka tarkistaa sisältääkö merkkijono sana merkkijonon haettava. Jos jommankumman merkkijonon arvo on null, metodin tulee palauttaa arvo false. Metodin tarjoaman vertailun tulee olla välittämättä merkin koosta.

Lisää metodille sisaltaa myös toiminnallisuus, joka poistaa merkkijonojen sana ja haettava alusta ja lopusta ylimääräiset välilyönnit. Käytä tähän String-luokan metodia trim, esim. trimmattu = trimmattava.trim()

Vinkki! String-luokan metodista toUpperCase() on hyötyä kun haluat verrata ovatko kaksi merkkijonoa samat -- riippumatta niiden alkuperäisestä merkkikoosta.

Kun olet saanut metodin valmiiksi, käytä sitä Kirjasto-luokassa. Alla esimerkki:

if (StringUtils.sisaltaa(kirja.nimeke(), nimeke)) {
    // kirja löytyi!
}
Kirjasto kirjasto = new Kirjasto();

kirjasto.lisaaKirja(new Kirja("Cheese Problems Solved", "Woodhead Publishing", 2007));
kirjasto.lisaaKirja(new Kirja("The Stinky Cheese Man and Other Fairly Stupid Tales", "Penguin Group", 1992));
kirjasto.lisaaKirja(new Kirja("NHL Hockey", "Stanley Kupp", 1952));
kirjasto.lisaaKirja(new Kirja("Battle Axes", "Tom A. Hawk", 1851));

for (Kirja kirja: kirjasto.haeKirjaNimekkeella("CHEESE")) {
    System.out.println(kirja);
}

System.out.println("---");
for (Kirja kirja: kirjasto.haeKirjaJulkaisijalla("PENGUIN  ")) {
    System.out.println(kirja);
}
Cheese Problems Solved, Woodhead Publishing, 2007
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992
---
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992

32 Ongelman ratkaiseminen paloittain

Muistellaan taas ongelmien ratkaisemista paloittain. Käydään läpi esimerkki, jossa tehdään ohjelma, joka kyselee käyttäjältä sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Tehdään oletus, että käyttäjä voi antaa korkeintaan 1000 sanaa.

Anna sana: porkkana
Anna sana: selleri
Anna sana: nauris
Anna sana: lanttu
Anna sana: selleri
Annoit saman sanan uudestaan!

Yksi ohjelmoinnin haasteista on se, että on vaikea päättää miten lähestyä tehtävää, eli miten jäsentää ongelmaa ja mistä aloittaa.

Ongelmat koostuvat oikeastaan aina useammasta aliongelmasta. Esimerkiksi ylläolevassa osassa on (ainakin) kaksi "aliongelmaa". Ensimmäinen on sanojen toistuva lukeminen käyttäjältä kunnes tietty ehto toteutuu. Tämä voitaisiin hahmotella seuraavaan tapaan ohjelmarungoksi:

32.1 Toisto ja lopetus

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);
    
    while (true) {
        String sana = lukija.nextLine();
            
        if (pitää lopettaa) {
            break;
        }
  
    }

    System.out.println("Piti lopettaa!");
}

Sanojen kysely pitää kun käyttäjä syöttää jo aiemmin syötetyn sanan. Tehdään metodi, joka tarkistaa onko sana jo syötetty. Vielä ei tiedetä miten metodi kannattaisi tehdä, joten tehdään siitä vasta runko:

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);
    
    while (true) {
        String sana = lukija.nextLine();
            
        if (onJoSyotetty(sana)) {
            break;
        }
    }

    System.out.println("Annoit saman sanan uudestaan");
}

public static boolean onJoSyotetty(String sana) {
    // tänne jotain

    return false;
}

Ohjelmaa on hyvä testata koko ajan, joten tehdään metodista kokeiluversio:

public static boolean onJoSyotetty(String sana) {
    if (sana.equals("loppu")) {
        return true;
    }

    return false;
}

Nyt toisto jatkuu niin kauan kunnes syötteenä on sana loppu:

  Anna sana: porkkana
  Anna sana: selleri
  Anna sana: nauris
  Anna sana: lanttu
  Anna sana: loppu
  Annoit saman sanan uudestaan!

Ohjelma ei toimi vielä kokonaisuudessaan, mutta ensimmäinen osaongelma eli ohjelman pysäyttäminen kunnes tietty ehto toteutuu on saatu toimimaan.

32.2 Oleellisten tietojen tallentaminen

Toinen osaongelma on aiemmin syötettyjen sanojen muistaminen. Taulukko sopii tietysti mainiosti tähän tarkoitukseen -- myös ArrayList kävisi mainiosti, mutta harjoitellaan tässä tarkoituksella taulukkojen käyttöä. Tarvitaan myös muuttuja joka muistaa kuinka monta sanaa on syötetty:

public static void main(String[] args) {
    String[] aiemmatSanat = new String[1000];
    int sanojaSyotetty = 0;

    // ...
}

Kun uusi sana syötetään, on se lisättävä syötettyjen sanojen joukkoon. Tämä tapahtuu lisäämällä main:in while-silmukkaan taulukkoa ja sanojen laskuria päivittävät rivit:

while (true) {
    String sana = lukija.nextLine();

    if (onJoSyotetty(sana, aiemmatSanat, sanojaSyotetty)) {
        break;
    }

    // lisätään uusi sana aiempien sanojen taulukkoon
    aiemmatSanat[sanojaSyotetty] = sana;
    sanojaSyotetty++;
}

Jälleen kannattaa testata, että ohjelma toimii edelleen. Voi olla hyödyksi esim. lisätä ohjelman loppuun testitulostus, joka varmistaa että syötetyt sanat todella menivät taulukkoon:

while (true) {
    String sana = lukija.nextLine();

    if (onJoSyotetty(sana, aiemmatSanat, sanojaSyotetty)) {
        break;
    }

    // lisätään uusi sana aiempien sanojen taulukkoon
    aiemmatSanat[sanojaSyotetty] = sana;
    sanojaSyotetty++;
}

// testitulostus joka varmistaa että kaikki toimii edelleen
for (int i=0; i < sanojaSyotetty; i++) {
    System.out.println(aiemmatSanat[i]);
}

32.3 Osaongelmien ratkaisujen yhdistäminen

Muokataan vielä äsken tekemämme metodi onJoSyotetty tutkimaan onko kysytty sana jo syötettyjen joukossa, eli taulukon käytössä olevassa alkuosassa:

public static boolean onJoSyotetty(String sana, String[] aiemmatSanat, int sanojaSyotetty) {
    for (int i = 0; i < sanojaSyotetty; i++) {
        if (sana.equals(aiemmatSanat[i])) {
            return true;
        }
    }

    return false;
}

public static void main(String[] args) {
    String[] aiemmatSanat = new String[1000];
    int sanojaSyotetty = 0;

    while (true) {
        String sana = lukija.nextLine();

        if (onJoSyotetty(sana, aiemmatSanat, sanojaSyotetty)) {
            break;
        }

        aiemmatSanat[sanojaSyotetty] = sana;
        sanojaSyotetty++;
    }

    System.out.println("Annoit saman sanan uudestaan");
}

Nyt koodi on valmis ja alkaa olla kohtuullisen luettava.

Eli ohjelmoidessasi, seuraa aina näitä neuvoja:
  • Etene pieni askel kerrallaan
    • Yritä pilkkoa ongelma osaongelmiin ja ratkaise vain yksi osaongelma kerrallaan
    • Testaa aina että ohjelma on etenemässä oikeaan suuntaan eli että osaongelman ratkaisu meni oikein
    • Tunnista ehdot, minkä tapauksessa ohjelman tulee toimia eri tavalla. Esimerkiksi yllä tarkistus, jolla katsotaan onko sana jo syötetty, johtaa erilaiseen toiminnallisuuden.
  • Kirjoita mahdollisimman "siistiä" koodia
    • sisennä koodi
    • käytä kuvaavia muuttujien ja metodien nimiä
    • älä tee liian pitkiä metodeja, edes mainia
    • tee yhdessä metodissa vaan yksi asia
    • poista koodistasi kaikki copy-paste
    • korvaa koodisi "huonot" ja epäsiistit osat siistillä koodilla

Hyvät ja kokeneet ohjelmoijat noudattavat näitä käytänteitä sen takia että ohjelmointi olisi helpompaa, ja että ohjelmien lukeminen, ylläpitäminen ja muokkaaminen olisi helpompaa myös muille.

32.4 Oliot luonnollisena osana ongelmanratkaisua

Rakensimme äsken askel askeleelta ratkaisun ongelmaan, missä luetaan käyttäjältä sanoja, kunnes käyttäjä antaa saman sanan uudestaan. Syöte ohjelmalle oli esimerkiksi seuraavanlainen:

Anna sana: porkkana
Anna sana: selleri
Anna sana: nauris
Anna sana: lanttu
Anna sana: selleri
Annoit saman sanan uudestaan!

Päädyimme ratkaisuun

public static boolean onJoSyotetty(String sana, String[] aiemmatSanat, int sanojaSyotetty) {
    for (int i = 0; i < sanojaSyotetty; i++) {
        if (sana.equals(aiemmatSanat[i])) {
            return true;
        }
    }

    return false;
}

public static void main(String[] args) {
    String[] aiemmatSanat = new String[1000];
    int sanojaSyotetty = 0;

    while (true) {
        String sana = lukija.nextLine();

        if (onJoSyotetty(sana, aiemmatSanat, sanojaSyotetty)) {
            break;
        }

        aiemmatSanat[sanojaSyotetty] = sana;
        sanojaSyotetty++;
    }

    System.out.println("Annoit saman sanan uudestaan");
}

32.5 Eristetään käsitteet luokiksi

Pääohjelman käyttämät apumuuttujat, eli taulukko aiemmatSanat ja kokonaisluku sanojaSyotetty ovat todella ikäviä "matalan tason" yksityiskohtia pääohjelman kannalta. Pääohjelman kannaltahan on oleellista, että muistetaan niiden sanojen joukko jotka on nähty jo aiemmin. Sanojen joukko on selkeä erillinen "käsite", tai abstraktio. Tälläiset selkeät käsitteet ovat potentiaalisia olioita; kun koodissa huomataan "käsite" voi sen eristämistä erilliseksi luokaksi harkita.

Sanajoukko

Tehdään luokka Sanajoukko, jonka käyttöönoton jälkeen pääohjelma näyttää seuraavalta:

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);
    Sanajoukko aiemmatSanat = new Sanajoukko();

    while (true) {
        String sana = lukija.nextLine();

        if (aiemmatSanat.sisaltaa(sana)) {
            break;
        }

        aiemmatSanat.lisaa(sana);
    }

    System.out.println("Annoit saman sanan uudestaan");
}

Pääohjelman, eli sanajoukon käyttäjän, kannalta Sanajoukko olisi hyvä jos sillä olisi metodit boolean sisaltaa(String sana) jolla tarkastetaan sisältyykö annettu sana jo sanajoukkoon ja void lisaa(String sana) jolla annettu sana lisätään joukkoon.

Huomaamme, että näin kirjoitettuna pääohjelman luettavuus on huomattavasti parempi kuin taulukkoa käyttäen. Pääohjelma on lähes suomen kieltä!

Luokan Sanajoukko runko näyttää seuraavanlaiselta:

public class Sanajoukko {
    // sopivia oliomuuttujia

    public Sanajoukko() {
        // konstruktori
    }

    public boolean sisaltaa(String sana) {
        // sisältää-metodin toteutus
        return false;
    }

    public void lisaa(String sana) {
        // lisaa-metodin toteutus
    }
}

Sanajoukon alustava toteutus kopioidaan aiemmasta ratkaisusta

Voimme toteuttaa sanajoukon siirtämällä aiemman ratkaisumme taulukon sanajoukon oliomuuttujaksi:

public class Sanajoukko {
    private String[] joukonSanat;
    private int sanojaSyotetty;

    public Sanajoukko() {
        this.joukonSanat = new String[1000];
        this.sanojaSyotetty = 0;
    }

    // ...
}

Oliomuuttujien alustus tapahtuu tuttuun tapaan konstruktorissa.

Uuden sanan lisääminen on helppoa. Koska sanojaSyotetty muistaa monta sanaa taulukossa jo on, ja taulukon indeksit alkavat nollasta, tulee uusi sana juuri tähän paikkaan. Muuttujan arvo pitää muistaa vielä kasvattaa:

public class Sanajoukko {
    private String[] joukonSanat;
    private int sanojaSyotetty;

    public Sanajoukko() {
        this.joukonSanat = new String[1000];
        this.sanojaSyotetty = 0;
    }

    public void lisaa(String sana) {
        this.joukonSanat[this.sanojaSyotetty] = sana;
        this.sanojaSyotetty++;
    }

    // ...
}

Ja vielä metodi, jolla tarkistetaan onko sana jo joukossa. Eli käydään sanat muistavan taulukon alkuosaa läpi syötettyjen sanojen verran ja palautetaan true jos etsitty sana löytyy:

public class Sanajoukko {
    private String[] joukonSanat;
    private int sanojaSyotetty;

    public Sanajoukko() {
        this.joukonSanat = new String[1000];
        this.sanojaSyotetty = 0;
    }

    public void lisaa(String sana) {
        this.joukonSanat[this.sanojaSyotetty] = sana;
        this.sanojaSyotetty++;
    }

    public boolean sisaltaa(String sana) {
        for (int i = 0; i < sanojaSyotetty; i++) {
            if (joukonSanat[i].equals(sana)) {
                return true;
            }
        }

        return false;
    }
}

Ratkaisu on nyt varsin elegantti. Erillinen käsite on saatu erotettua ja pääohjelma näyttää siistiltä. Kaikki "likaiset yksityiskohdat" (taulukko ja lukumäärä sanoista) on saatu siivottua eli kapseloitua olion sisälle.

Valmiiden välineiden käyttäminen

Sanojen muistaminenhan voidaan toteuttaa helposti myös ArrayListia käyttämällä. Korvataan luokan Sanajoukko sisällä oleva taulukko listalla:

import java.util.ArrayList;

public class Sanajoukko {
    private ArrayList<String> joukonSanat;

    public Sanajoukko() {
        this.joukonSanat = new ArrayList<>();
    }

    public boolean sisaltaa(String sana) {
        return this.joukonSanat.contains(sana);
    }

    public void lisaa(String sana) {
        this.joukonSanat.add(sana);
    }
}

Näin päädytään ratkaisuun jossa Sanajoukko on ainoastaan ArrayList:in "wräpperi". Onko tässä järkeä? Kenties. Voimme nimittäin halutessamme tehdä Sanajoukolle muitakin muutoksia. Ennen pitkään saatamme esim. huomata, että sanajoukko pitää tallentaa tiedostoon. Jos tekisimme nämä muutokset Sanajoukon koodiin, ei pääohjelmaa tarvitsisi muuttaa mitenkään.

Uusien toiminnallisuuksien toteuttaminen: palindromit

Voi olla, että jatkossa ohjelmaa halutaan laajentaa siten, että Sanajoukko-luokan olisi osattava uusia asiota. Jos esim. pääohjelmassa haluttaisiin tietää kuinka moni syötetyistä sanoista oli palindromi, voidaan sanajoukkoa laajentaa metodilla palindromeja:

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);
    Sanajoukko aiemmatSanat = new Sanajoukko();

    while (true) {
        String sana = lukija.nextLine();

        if (aiemmatSanat.sisaltaa(sana)) {
            break;
        }

        aiemmatSanat.lisaa(sana);
    }

    System.out.println("Annoit saman sanan uudestaan");
    System.out.println("Sanoistasi " + aiemmatSanat.palindromeja() + " oli palindromeja");
}

Pääohjelma säilyy siistinä, viimeisellä rivillä kutsutaan uutta hyvin nimettyä metodia, palindromien laskeminen jää Sanajoukko-olion huoleksi. Metodin toteutus voisi olla seuraavanlainen:

import java.util.ArrayList;

public class Sanajoukko {
    private ArrayList<String> joukonSanat;

    public Sanajoukko() {
        this.joukonSanat = new ArrayList<>();
    }

    public boolean sisaltaa(String sana) {
        return this.joukonSanat.contains(sana);
    }

    public void lisaa(String sana) {
        this.joukonSanat.add(sana);
    }

    public int palindromeja() {
        int palindromit = 0;

        for (String sana : this.joukonSanat) {
            if (onPalindromi(sana)) {
                palindromit++;
            }
        }
        
        return palindromit;
    }

    private boolean onPalindromi(String sana) {
        int loppu = sana.length() - 1;

        for (int i = 0; i < sana.length() / 2; i++) {
            if (sana.charAt(i) != sana.charAt(loppu - i)) {
                return false;
            }
        }
    
        return true;
    }
}

Metodi palindromeja käy läpi kaikki joukon sanat ja käyttää apumetodia onPalindromi avukseen palindromien määrän laskemisessa. Koska apumetodi onPalindromi on tarkoitettu ainoastaan olion sisäiseen käyttöön, on sen näkyvyysmääreeksi määritelty private. Olion käyttäjän siis ei ole mahdollista kutsua metodia, metodia voi kutsua ainoastaan olion sisältä. Palaamme näkyvyysmääreisiin tarkemmin myöhemmin.

Uusiokäyttö

Kun ohjelmakoodin käsitteet on eriytetty omiksi luokikseen, voi niitä uusiokäyttää helposti myös muissa projekteissa. Esimerkiksi luokkaa SanaJoukko voisi käyttää yhtä hyvin graafisesta käyttöliittymästä, ja se voisi myös olla osa kännykässä olevaa sovellusta. Tämän lisäksi ohjelman toiminnan testaaminen on huomattavasti helpompaa silloin kun ohjelma on jaettu erillisiin käsitteisiin, joita kutakin voi käyttää myös omana itsenäisenä yksikkönään.

32.6 Tehtäviä joissa saat itse keksiä ohjelmalle sopivan rakenteen

Tehtävä 127: Arvosanajakauma

Tehtävä vastaa kolmea yksiosaista tehtävää.

HUOM: Ohjelmassa saa käyttää vain yhtä Scanner-olioa, eli vain kertaalleen saa sanoa new Scanner. Jos tarvitset Scanneria monessa kohtaa, voit välittää sen muualle parametrina seuraavaan tyyliin:

public static void main(String[] args) {
    Scanner lukija = new Scanner(System.in);

    // ...

    teeJotain(lukija);
}

public static void teeJotain(Scanner lukija) {
    String rivi = lukija.nextLine();
    // ...
}

Tai jos toinen olio tarvitsee Scanneria, voi sen välittää konstruktoriparametrina ja tallettaa oliomuuttujaan jolloin Scanner on olion kaikkien metodien käytössä.

Ohjelman syöte on joukko kokonaislukuja, jotka kuvaavat opiskelijoiden kokeesta saamia pistemääriä. Käyttäjä syöttää yhden pistemäärän per rivi. Kun syöte on -1, lopettaa ohjelma pistemäärien kyselyn.

Pisteiden syöttö toimii seuraavasti:

Syötä koepisteet, -1 lopettaa:
34
41
53
36
55
27
43
40
-1

Pisteiden syöttämisen jälkeen ohjelma tulostaa kurssin arvosanajakauman ja hyväksymisprosentin seuraavassa muodossa:

Arvosanajakauma:
5: **
4:
3: ***
2: *
1: *
0: *
Hyväksymisprosentti: 87.5

Arvosananajakauma muodostetaan seuraavasti:

    pistemäärä arvosana
    0–29 hylätty
    30–34 1
    35–39 2
    40–44 3
    45–49 4
    50–60 5
  • Jokainen koepistemäärä muutetaan arvosanaksi ylläolevalla kaavalla. Jos syötetty pistemäärä ei ole välillä 0-60, ei sitä huomioida mitenkään.
  • Arvosanat tulostetaan tähtinä. Esim jos arvosanaan 5 oikeuttavia koepistemääriä on 2 kappaletta, tulostuu rivi 5: **. Jos johonkin arvosanaan oikeuttavia pistemääriä ei ole, ei yhtään tähteä tulostu, esimerkissämme näin oli nelosten kohdalla, tulostui rivi jossa on ainoastaan 4:

Hyväksyttyjä ovat muut paitsi arvosanan 0 saaneet. Hyväksyttyjä edellä on siis 7 osallistujaa 8:sta. Hyväksymisprosentti lasketaan kaavalla 100*hyväksytyt/osallistujat.

Tehtävä 128: Lintubongarin tietokanta

HUOM: Ohjelmassa saa käyttää vain yhtä Scanner-olioa, eli vain kertaalleen saa sanoa new Scanner. Jos tarvitset Scanneria monessa kohtaa, voit välittää sen muualle parametrina.

Tehtävä vastaa kolmea yksiosaista tehtävää.

Tässä tehtävässä suunnittelet ja toteutat tietokannan lintubongareille. Tietokanta sisältää lintuja, joista jokaisella on nimi (merkkijono) ja latinankielinen nimi (merkkijono). Tämän lisäksi tietokanta laskee kunkin linnun havaintokertoja.

Ohjelmasi täytyy toteuttaa seuraavat komennot:

  • Lisaa - lisää linnun (huom: komennon nimessä ei ä-kirjainta!)
  • Havainto - lisää havainnon
  • Tilasto - tulostaa kaikki linnut
  • Nayta - tulostaa yhden linnun (huom: komennon nimessä ei ä-kirjainta!)
  • Lopeta - lopettaa ohjelman

Lisäksi virheelliset syötteet pitää käsitellä. (Ks. Simo alla). Tässä vielä esimerkki ohjelman toiminnasta:

? Lisaa
Nimi: Korppi
Latinankielinen nimi: Corvus Corvus
? Lisaa
Nimi: Haukka
Latinankielinen nimi: Dorkus Dorkus
? Havainto
Mikä havaittu? Haukka
? Havainto
Mikä havaittu? Simo
Ei ole lintu!
? Havainto
Mikä havaittu? Haukka
? Tilasto
Haukka (Dorkus Dorkus): 2 havaintoa
Korppi (Corvus Corvus): 0 havaintoa
? Nayta
Mikä? Haukka
Haukka (Dorkus Dorkus): 2 havaintoa
? Lopeta

Huom! Ohjelmasi rakenne on täysin vapaa. Testaamme vain että Paaohjelma luokan main-metodi toimii kuten tässä on kuvailtu. Yritä miettiä millaisista luokista ja olioista olisi hyötyä ohjelman toteuttamisessa!

33 Taulukon järjestäminen ja etsintä

Palaamme jälleen taulukkojen pariin.

33.1 Taulukon järjestäminen Javan valmiilla työkaluilla.

Kuten olemme nähneet, Javassa on valmiina paljon kaikenlaista hyödyllistä. Esimerkiksi ArrayListin käsittelyyn löytyi useita hyödyllisiä apumetodeja luokasta Collections. Taulukoille löytyy vastaavia apuvälineitä luokasta Arrays. Taulukon saa järjestettyä komennolla Arrays.sort(taulukko).

Huom: Komennon käyttäminen vaatii, että ohjelmatiedoston yläosassa lukee seuraava määrittely:

import java.util.Arrays;

Jos unohdat import-rivin, NetBeans tarjoaa apua sen kirjoittamiseen. Kokeile klikata punaisella alleviivatun koodirivin vasempaan laitaan ilmestyvää lampun kuvaa.

Seuraava ohjelma luo taulukon ja järjestää taulukossa olevat luvut Arrays.sort -komennon avulla.

int[] luvut = {-3, -111, 7, 42};
Arrays.sort(luvut);

for(int luku: luvut) {
    System.out.println(luku);
}
-111
-3
7
42

33.2 Järjestämisalgoritmin toteuttaminen

Taulukon järjestäminen on helppoa Javan valmiin kaluston avulla. Ohjelmoijan yleissivistykseen kuuluu kuitenkin ainakin yhden järjestämisalgoritmin (eli tavan järjestää taulukko) tuntemus. Tutustutaan yhteen "klassiseen" järjestämisalgoritmiin, valintajärjestämiseen. Tutustuminen tapahtuu harjoitustehtävien avulla.

Tehtävä 129: Järjestäminen

Huom: tässä tehtävässä on tarkoitus järjestää taulukko itse. Et saa käyttää Arrays.sort()-metodia tai ArrayListejä apunasi!

129.1 Pienin

Tee metodi pienin, joka palauttaa taulukon pienimmän luvun.

Metodin runko on seuraava:

public static int pienin(int[] taulukko) {
    // kirjoita koodia tähän
}

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

int[] luvut = {6, 5, 8, 7, 11};
System.out.println("Pienin: " + pienin(luvut));
Pienin: 5

129.2 Pienimmän indeksi

Tee metodi pienimmanIndeksi, joka palauttaa taulukon pienimmän luvun indeksin (eli luvun kohdan taulukossa).

Metodin runko on seuraava:

public static int pienimmanIndeksi(int[] taulukko) {
    // kirjoita koodia tähän
}

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

// indeksit:   0  1  2  3  4
int[] luvut = {6, 5, 8, 7, 11};
System.out.println("Pienimmän indeksi: " + pienimmanIndeksi(luvut));
Pienimmän indeksi: 1

Taulukon pienin luku on 5, ja sen indeksi eli sijaintipaikka taulukossa on 1. Muistathan, että taulukon numerointi alkaa 0:sta.

129.3 Pienimmän indeksi taulukon loppuosassa

Tee metodi pienimmanIndeksiAlkaen, joka toimii samalla tavalla kuin edellisen tehtävän metodi mutta ottaa huomioon vain taulukon loppuosan jostain indeksistä alkaen. Metodille annetaan taulukon lisäksi aloitusindeksi, josta lähtien pienintä lukua etsitään.

Metodin runko on seuraava:

public static int pienimmanIndeksiAlkaen(int[] taulukko, int aloitusIndeksi) {
    // kirjoita koodia tähän
}

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

// indeksit:    0  1  2  3   4
int[] luvut = {-1, 6, 9, 8, 12};
System.out.println(pienimmanIndeksiAlkaen(luvut, 1));
System.out.println(pienimmanIndeksiAlkaen(luvut, 2));
System.out.println(pienimmanIndeksiAlkaen(luvut, 4));
1
3
4

Esimerkissä ensimmäinen metodikutsu etsii pienimmän luvun indeksin aloittaen indeksistä 1. Indeksistä 1 alkaen pienin luku on 6, ja sen indeksi on 1. Vastaavasti toinen metodikutsu etsii pienimmän luvun indeksiä indeksistä 2 aloittaen. Tällöin pienin luku on 8, ja sen indeksi on 3. Viimeinen kutsu etsii pienimmän luvun indeksiä taulukon viimeisestä indeksistä eli kohdasta 4 aloittaen. Tällöinen muita paikkoja ei ole, joten pienin on paikassa 4.

129.4 Lukujen vaihtaminen

Tee metodi vaihda, jolle annetaan taulukko ja kaksi sen indeksiä. Metodi vaihtaa indekseissä olevat luvut keskenään.

Metodin runko on seuraava:

public static void vaihda(int[] taulukko, int indeksi1, int indeksi2) {
    // kirjoita koodia tähän
}

Seuraavassa estellään metodin toimintaa. Taulukon tulostamisessa käytetään apuna taulukon merkkijonoksi muotoilevaa Arrays.toString-metodia:

int[] luvut = {3, 2, 5, 4, 8};

System.out.println(Arrays.toString(luvut));

vaihda(luvut, 1, 0);
System.out.println(Arrays.toString(luvut));

vaihda(luvut, 0, 3);
System.out.println(Arrays.toString(luvut));
[3, 2, 5, 4, 8]
[2, 3, 5, 4, 8]
[4, 3, 5, 2, 8]

129.5 Järjestäminen

Nyt koossa on joukko hyödyllisiä metodeja, joiden avulla voimme toteuttaa järjestämisalgoritmin nimeltä vaihtojärjestäminen.

Vaihtojärjestämisen idea on seuraava:

  • Siirretään taulukon pienin luku indeksiin 0.
  • Siirretään taulukon toiseksi pienin luku indeksiin 1.
  • Siirretään taulukon kolmanneksi pienin luku indeksiin 2.
  • Jne.

Toisin sanoen:

  • Tarkastellaan taulukkoa indeksistä 0 alkaen. Vaihdetaan keskenään indeksissä 0 oleva luku sekä taulukon pienin luku indeksistä 0 alkaen.
  • Tarkastellaan taulukkoa indeksistä 1 alkaen. Vaihdetaan keskenään indeksissä 1 oleva luku sekä taulukon pienin luku indeksistä 1 alkaen.
  • Tarkastellaan taulukkoa indeksistä 2 alkaen. Vaihdetaan keskenään indeksissä 2 oleva luku sekä taulukon pienin luku indeksistä 2 alkaen.
  • Jne.

Toteuta metodi jarjesta, joka perustuu yllä olevaan ideaan. Metodissa on syytä olla silmukka, joka käy läpi taulukon indeksejä. Metodeista pieninIndeksiAlkaen ja vaihda on varmasti hyötyä. Tulosta myös taulukon sisältö ennen järjestämistä ja jokaisen kierroksen jälkeen, jotta voit varmistaa algoritmin toimivan oikein.

Metodin runko on seuraava:

public static void jarjesta(int[] taulukko) {
}

Testaa metodin toimintaa ainakin seuraavalla esimerkillä:

int[] luvut = {8, 3, 7, 9, 1, 2, 4};
jarjesta(luvut);

Ohjelman tulosteen tulisi olla seuraavanlainen. Huomaa että sinun tulee tulostaa taulukon sisältö jokaisen vaihtamisen jälkeen!

[8, 3, 7, 9, 1, 2, 4]
[1, 3, 7, 9, 8, 2, 4]
[1, 2, 7, 9, 8, 3, 4]
[1, 2, 3, 9, 8, 7, 4]
[1, 2, 3, 4, 8, 7, 9]
[1, 2, 3, 4, 7, 8, 9]
[1, 2, 3, 4, 7, 8, 9]

Huomaat, miten taulukko tulee pikkuhiljaa järjestykseen alkaen alusta ja edeten loppua kohti.

33.3 Etsintä

Järjestämisen lisäksi toinen hyvin tyypillinen ongelma johon ohjelmoija törmää, on tietyn arvon etsiminen taulukosta. Olemme aiemmin jo toteuttaneet metodeja, jotka etsivät arvoja listoista ja taulukoista. Taulukkojen tapauksessa lukuja ja merkkijonoja voi etsiä seuraavasti:

public static boolean onkoTaulukossa(int[] taulukko, int etsittava) {
    for (int luku : taulukko) {
        if (luku == etsittava)  {
            return true;
        }
    }

    return false;
}

public static boolean onkoSanaTaulukossa(String[] taulukko, String etsittava) {
    for (String sana: taulukko) {
        if (sana.equals(etsittava))  {
            return true;
        }
    }

    return false;
}

Tämänkaltainen toteutus on paras mihin olemme tähän asti pystyneet. Metodin huono puoli on se, että jos taulukossa on hyvin suuri määrä lukuja, kuluu etsintään paljon aikaa. Metodi käy läpi pahimmassa tapauksessa jokaisen taulukossa olevan alkion. Tämä tarkoittaa sitä, että taulukon, jossa on esimerkiksi 16777216 alkiota, läpikäynti vaatii 16777216 alkion tutkiskelua.

Toisaalta, jos taulukossa olevat luvut ovat suuruusjärjestyksessä, voidaan etsiminen tehdä huomattavasti tehokkaammin soveltamalla tekniikkaa nimeltään binäärihaku. Tutkitaan binäärihaun ideaa seuraavan taulukon kautta:

// indeksit   0   1   2   3    4   5    6   7   8   9  10
// luvut     -7  -3   3   7   11  15   17  21  24  28  30

Oletetaan että haluamme löytää luvun 17. Hyödynnetään tietoa siitä että taulukon arvot ovat järjestyksessä sen sijaan, että kävisimme taulukon lukuja läpi taulukon alusta lähtien. Tutkitaan taulukon keskimmäistä alkiota. Taulukon puolessa välissä olevan alkion indeksi on isoin indeksi 10 jaettuna kahdella eli 5. Keskimmäinen alkio on merkattu seuraavaan tähdellä:

                                   *
// indeksit   0   1   2   3    4   5    6   7   8   9  10
// luvut     -7  -3   3   7   11  15   17  21  24  28  30

Puolessa välissä on luku 15, joka ei ollut hakemamme luku. Etsimme lukua 17, joten koska taulukon alkiot ovat suuruusjärjestyksessä, ei etsitty luku voi missään tapauksessa olla luvun 15 vasemmalla puolella. Voimme siis päätellä että kaikki indeksit, jotka ovat pienempiä tai yhtäsuuria kuin 5, eivät missään nimessä sisällä hakemaamme arvoa.

Alue, jolta etsimme haettavaa lukua voidaan nyt rajata lukuihin, jotka sijaitsevat indeksin 5 oikealla puolella, eli indekseihin välillä [6, 10] (6, 7, 8, 9, 10). Seuraavassa on merkitty harmaalla se osa taulukkoa jossa etsitty ei voi olla:

// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Tutkitaan seuraavaksi jäljellä olevan etsintäalueen, eli indeksien 6-10 keskimmäistä indeksiä. Keskimmäinen indeksi löytyy ottamalla etsintäalueen pienimmän ja suurimman indeksin summa ja jakamalla se kahdella, eli (6+10)/2 = 16/2 = 8. Indeksi 8 on merkitty alle tähdellä.

                                                 *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Indeksissä 8 oleva luku on 24, joka ei ollut hakemamme luku. Koska luvut taulukossa ovat suuruusjärjestyksessä, ei etsittävä luku voi missään nimessä olla luvun 24 oikealla puolella. Voimme siis päätellä että kaikki indeksit, jotka ovat suurempia tai yhtäsuuria kuin 8, eivät missään nimessä sisällä hakemaamme arvoa. Etsintäalue rajautuu taas, harmaat alueet on käsitelty:

// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Etsintä jatkuu. Tutkitaan jäljellä olevan etsintäalueen, eli indeksien 6-7, keskimmäistä indeksiä. Keskimmäinen indeksi löytyy taas ottamalla etsintäalueen pienimmän ja suurimman indeksin summa ja jakamalla se kahdella, eli (6+7)/2 = 6,5, joka pyöristyy alaspäin luvuksi 6. Kohta on merkitty alle tähdellä.

                                         *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Indeksissä 6 on luku 17, joka on sama kuin hakemamme luku. Voimme lopettaa haun ja ilmoittaa että etsitty luku on taulukossa. Jos luku ei olisi ollut taulukossa -- esimerkiksi jos haettava luku olisi ollut 16, etsintäalue olisi jäänyt lopulta tyhjäksi.

                                         *
// indeksit    0   1   2   3   4    5    6   7   8   9  10
// luvut      -7  -3   3   7  11   15   17  21  24  28  30

Jotta binäärihaun idea tulee sinulle tutuksi, simuloi kynällä ja paperilla miten binäärihaku toimii kun taulukkona on alla oleva taulukko ja haet ensin lukua 33, sitten lukua 1.

// indeksit   0   1   2   3   4   5   6   7   8   9  10  11  12  13
// luvut     -5  -2   3   5   8  11  14  20  22  26  29  33  38  41

Binäärihaun avulla haemme alkiota puolittamalla aina tutkittavan alueen kahteen osaan. Tämä mahdollistaa hyvin tehokkaan hakemisen. Esimerkiksi taulukko, jossa on 16 alkiota, voidaan jakaa kahteen osaan korkeintaan 4 kertaa, eli 16 -> 8 -> 4 -> 2 -> 1. Toisaalta, taulukko, jossa on 16777216 alkiota, voidaan jakaa kahteen osaan korkeintaan 24 kertaa. Tämä tarkoittaa sitä, että binäärihaun avulla 16777216-alkioisessa taulukossa tarvitsee tarkastella korkeintaan 24 alkiota haetun alkion löytymiseksi.

Binäärihaun tehokkuutta voi tutkia logaritmien avulla. Kaksikantainen logaritmi (log2) luvusta 16777216 on 24 -- voimme siis laskea kaksikantaisen logaritmin avulla kuinka monta kertaa jonkun luvun voi puolittaa. Vastaavasti luvun 4294967296 kaksikantainen logaritmi, (log2 4294967296) on 32. Tämä tarkoittaa että 4294967296 eri arvoa sisältävästä järjestyksessä olevasta taulukosta hakeminen vaatisi korkeintaan 32 eri alkion tarkastamista. Tehokkuus on oleellinen osa tietojenkäsittelytiedettä. Esimerkiksi tietojenkäsittelytieteen ensimmäisen vuoden kurssi Tietorakenteet keskittyy tehokkaiden tietorakenteiden toteuttamiseen.

Tehtävä 130: Arvauspeli

Tehdään tässä tehtävässä tekoäly, joka arvaa pelaajan ajatteleman luvun. Tekoäly olettaa, että luku on välillä alaraja...yläraja. Pelin käynnistäjä antaa nämä rajat pelin toteuttavalle metodille parametrina. Tekoäly kysyy käyttäjältä kysymyksiä muotoa "Onko lukusi suurempi kuin X?" ja päättelee oikean luvun käyttäjän vastausten perusteella.

Tekoäly pitää kirjaa hakualueesta muuttujien alaraja ja yläraja avulla. Tekoäly kysyy aina, onko käyttäjän luku suurempi kuin näiden lukujen keskiarvo, jolloin vastauksen perusteella hakualue aina puolittuu. Lopulta alaraja ja yläraja ovat samat, ja käyttäjän ajattelema luku on paljastunut.

Seuraavassa esimerkissä käyttäjä valitsee luvun 44:

Ajattele jotain lukua väliltä 1...100.
Lupaan pystyä arvaamaan ajattelemasi luvun 7 kysymyksellä.

Esitän sinulle seuraavaksi sarjan kysymyksiä. Vastaa niihin rehellisesti.

Onko lukusi suurempi kuin 50? (k/e)
e
Onko lukusi suurempi kuin 25? (k/e)
k
Onko lukusi suurempi kuin 38? (k/e)
k
Onko lukusi suurempi kuin 44? (k/e)
e
Onko lukusi suurempi kuin 41? (k/e)
k
Onko lukusi suurempi kuin 43? (k/e)
k
Ajattelemasi luku on 44.

Yllä olevassa esimerkissä mahdollinen lukualue on aluksi 1...100. Kun käyttäjä kertoo, että luku ei ole yli 50, mahdollinen lukualue on 1...50. Kun käyttäjä kertoo, että luku on yli 25, mahdollinen lukualue on 26...50. Samanlainen päättely jatkuu, kunnes saavutaan lukuun 44.

Puolitus- eli binäärihaun mukaisesti tässä puolitetaan mahdollinen hakualue jokaisella kysymyksellä, jolloin kysymyksiä tarvitaan vähän. Jopa lukuvälillä 1..1000000 pitäisi kulua korkeintaan 20 kysymystä.

Pelin toteuttavan luokan Arvauspeli runko on seuraavanlainen:

public class Arvauspeli {

    private Scanner lukija;

    public Arvauspeli() {
        this.lukija = new Scanner(System.in);
    }

    public void pelaa(int alaraja, int ylaraja) {
        tulostaOhjeet(ylaraja, alaraja);

        // Kirjoita pelin koodi tänne
    }

    // tee metodit onkoSuurempiKuin ja keskiarvo tänne

    public void tulostaOhjeet(int ylaraja, int alaraja) {
        int kysymyksiaKorkeintaan = kuinkaMontaKertaaVoiJakaaKahteen(ylaraja - alaraja);

        System.out.println("Ajattele jotain lukua väliltä " + alaraja + "..." + ylaraja + ".");

        System.out.println("Lupaan pystyä arvaamaan ajattelemasi luvun " + kysymyksiaKorkeintaan + " kysymyksellä.");
        System.out.println("");
        System.out.println("Esitän sinulle seuraavaksi sarjan kysymyksiä. Vastaa niihin rehellisesti.");
        System.out.println("");
    }

    // apumetodi
    public int kuinkaMontaKertaaVoiJakaaKahteen(int luku) {
        // luodaan kaksikantainen logaritmi annetusta luvusta, logaritmeista
        // löytyy lisää tietoa mm. osoitteessa

        // http://www02.oph.fi/etalukio/pitka_matematiikka/kurssi8/maa8_teoria7.html

        // Alla vaihdamme kantalukua alkuperäisestä kaksikantaisiin logaritmeihin!
        return (int) (Math.log(luku) / Math.log(2)) + 1;
    }
}

Peli käynnistetään seuraavasti:

// luodaan peli-olio
Arvauspeli peli = new Arvauspeli();

// pelataan pari kierrosta
peli.pelaa(1,10);  // arvattava luku nyt välillä 1-10
peli.pelaa(10,99);  // arvattava luku nyt välillä 10-99

Toteutetaan tämä tehtävä askeleittain.

130.1 Onko suurempi kuin

Toteuta luokalle Arvauspeli metodi public boolean onkoSuurempiKuin(int luku), joka esittää käyttäjälle kysymyksen:

"Onko lukusi suurempi kuin annettu luku? (k/e)"

Metodi palauttaa arvon true jos käyttäjä vastaa "k", muulloin false.

Huom: metodin tulee lukea käyttäjän syöte luokan Arvauspeli oliomuuttujassa this.lukija olevaa lukijaa hyväksikäyttäen. Metodissa ei saa luoda uutta lukijaa!

Testaa metodisi toimintaa

Arvauspeli peli = new Arvauspeli();

System.out.println(peli.onkoSuurempiKuin(32));
System.out.println(peli.onkoSuurempiKuin(99));
Onko lukusi suurempi kuin 32? (k/e)
k
true
Onko lukusi suurempi kuin 99? (k/e)
e
false

130.2 Keskiarvo

Toteuta luokalle Arvauspeli metodi public int keskiarvo(int ekaLuku, int tokaLuku), joka laskee annettujen lukujen keskiarvon. Huomaa että Java pyöristää liukuluvut luvut automaattisesti alaspäin, tämä on meidän tapauksessamme täysin toivottavaa.

Arvauspeli peli = new Arvauspeli();

System.out.println(peli.keskiarvo(3, 4));
System.out.println(peli.keskiarvo(6, 12));
  3
  9

130.3 Arvauslogiikka

Kirjoita varsinainen arvauslogiikka luokan Arvauspeli metodin public void pelaa(int alaraja, int ylaraja) runkoon. Tarvitset ainakin toistolauseen, sekä kyselyn jossa kysyt onko käyttäjän ajattelema luku suurempi kuin ala- ja ylärajan keskiarvo. Käytä edellisten kohtien metodeja kyselyyn ja keskiarvon selvittämiseen. Muokkaa toistolauseessa ala- tai ylärajaa käyttäjän vastauksesta riippuen.

Jatka toistolausekkeen toistoa kunnes alaraja ja yläraja ovat samat! Voit testata peliä myös pienemmillä ala- ja ylärajan arvoilla:

Ajattele jotain lukua väliltä 1...4.
Lupaan pystyä arvaamaan ajattelemasi luvun 2 kysymyksellä.

Esitän sinulle seuraavaksi sarjan kysymyksiä. Vastaa niihin rehellisesti.

Onko lukusi suurempi kuin 2? (k/e)
k
Onko lukusi suurempi kuin 3? (k/e)
k
Ajattelemasi luku on 4.

Tehtävä 131: Binäärihaun toteutus

Testiautomaatista tulevassa rungossa on pohja binäärihaun toteutukselle. Luokka BinaariHaku sisältää metodin public static boolean hae(int[] taulukko, int etsittavaLuku), jonka tehtävänä on selvittää binäärihakua käyttäen, onko parametrina annettu luku parametrina annetussa järjestyksessä olevassa taulukossa.

Kuten huomaat, metodi hae ei toimi. Toteuta tehtäväpohjaan binäärihakualgoritmi -- tämä tehtävä on kolmen tehtäväpisteen arvoinen.

Ohjelman testausta varten on erillinen pääohjelma luokassa Main, jonka runko on seuraava:

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        // Tässä voit testata binäärihakua
        int[] taulukko = { -3, 2, 3, 4, 7, 8, 12 };
        Scanner lukija = new Scanner(System.in);

        System.out.print("Taulukon luvut: " + Arrays.toString(taulukko));
        System.out.println();

        System.out.print("Anna haettava luku: ");
        String etsittavaLuku = lukija.nextLine();
        System.out.println();

        boolean tulos = BinaariHaku.hae(taulukko, Integer.parseInt(etsittavaLuku));

        // Tulosta tässä binäärihaun tulos
    }
}

Ohjelman suoritus näyttää seuraavalta:

Taulukon luvut: [-3, 2, 3, 4, 7, 8, 12]

Anna haettava luku: 8

Luku 8 on taulukossa
Taulukon luvut: [-3, 2, 3, 4, 7, 8, 12]

Anna haettava luku: 99

Luku 99 ei ole taulukossa

Huom! Kertaa edellisen viikon kohta kaksiulotteisista taulukoista ennen seuraavan tehtävän tekemistä.

Tehtävä 132: Schelling

Thomas Schelling on yhdysvaltalainen taloustieteilijä, joka esitti ihmisten eriytymistä selittävän mallin.

Malli perustuu ajatukselle, että vaikka ihmiset asetettasiin satunnaisesti asumaan, he muuttavat pois jos he eivät pidä naapureistaan. Tässä tehtävässä täydennetään ohjelmaa, jonka avulla Schellingin mallia voidaan simuloida.

Simulaation suoritus alkaa kartasta, jossa ihmiset ovat satunnaisessa järjestyksessä.

Ja päättyy tyypillisesti tilanteeseen, missä samankaltaiset ihmiset ovat päätyneet samankaltaisten ihmisten luo.

Ohjelmasta puuttuu muutamia oleellisia toiminnallisuuksia: (1) kartan tyhjennys, (2) tyhjien paikkojen etsiminen, ja (3) tyytymättömien henkilöiden tunnistaminen. Kannattaa ennen aloitusta tutustua nykyiseen tehtävän koodiin -- ohjelmassa on mukana myös visualisointiin käytettävä komponentti.

132.1 Kartan tyhjentäminen ja tyhjien paikkojen etsiminen

Simulaatiomallissa käytetään kaksiulotteista taulukkoa asuinalueen kuvaamiseen. Taulukossa oleva arvo 0 kuvaa tyhjää paikkaa, kun taas luvut 1-4 erilaisia ihmisjoukkoja.

Toteuta ensin luokan Eriytymismalli metodiin public void asetaKaikkiTyhjiksi() toiminnallisuus, joka asettaa jokaisen taulukon data solun arvoksi 0.

Lisää tämän jälkeen metodiin public ArrayList<Piste> tyhjatPaikat() toiminnallisuus, joka tunnistaa tyhjät paikat (solut, joissa on arvo 0), luo jokaisesta Piste-olion, ja palauttaa ne listana. Huomaa, että kaksiulotteisessa taulukossa ensimmäinen ulottuvuus kuvaa y-koordinaattia, ja toinen x-koordinaattia (taulukko[y][x]).

132.2 Tyytymättömien hakeminen

Malli saa konstruktorin parametrina arvon tyytyvaisyysraja, joka kuvaa tyytyväisyyssuhdetta. Jos ruudussa (x, y) olevan henkilön naapureista on samankaltaisia yli tyytyvaisyysrajan verran, on henkilö tyytyväinen. Muuten henkilö on tyytymätön.

Naapureista tulee tarkastella kaikkia ruudun vieressä olevia ruutuja. Tyhjää ruutua ei oteta huomioon.

Toteuta metodi public ArrayList<Piste> haeTyytymattomat(), joka palauttaa tyytymättömät listana.

Kun ohjelma toimii, ihaile sen toimintaa :)

34 Taulukoista ja olioista

Jos tarvetta on, taulukkoon voi luonnollisesti laittaa minkä tahansa tyyppisen olion. Seuraavassa esimerkki taulukosta johon talletetaan Henkilo-olioita:

public static void main(String[] args) {
    Henkilo[] henkilot = new Henkilo[3];

    henkilot[0] = new Henkilo("Ada");
    henkilot[1] = new Henkilo("Antti");
    henkilot[2] = new Henkilo("Juhana");

    for (int i = 0; i < 30; i++) {
        henkilot[0].vanhene();
        henkilot[1].vanhene();
        henkilot[2].vanhene();
    }

    for (Henkilo henkilo : henkilot) {
        ilmoitaTaysiIkaisyys(henkilo);
    }
}

Alussa luodaan taulukko johon mahtuu 3 henkilöolioa. Laitetaan Ada lokeroon 0, Antti lokeroon 1 ja Juhana lokeroon 2. Vanhennetaan kaikkia 30 vuotta ja tarkastetaan kaikkien täysi-ikäisyys edellisen luvun metodia hyödyntäen.

Sama esimerkki ArrayListien avulla:

public static void main(String[] args) {
    ArrayList<Henkilo> henkilot = new ArrayList<>();

    henkilot.add(new Henkilo("Ada"));
    henkilot.add(new Henkilo("Antti"));
    henkilot.add(new Henkilo("Juhana"));

    for (int i = 0; i < 30; i++) {
        for (Henkilo henkilo : henkilot) {
            henkilo.vanhene();
        }

        // tai henkilot.get(0).vanhene();
        //     henkilot.get(1).vanhene();
        //     ...
    }

    for (Henkilo henkilo : henkilot) {
        ilmoitaTaysiIkaisyys(henkilo);
    }
}

Useimmissa sovellustilanteissa taulukon sijaan kannattaa käyttää ArrayListiä. Voi kuitenkin olla joitain tilanteita joissa taulukko riittää ja on hieman yksinkertaisempi käyttää; taulukko on myös hieman ArrayListiä tehokkaampi -- tämä tehokkuus tosin näkyy lähinnä vain tietorakenteiden ja algoritmien suunnittelussa, ja niissäkin harvemmin.

Viikko koostuu aina seitsemästä päivästä. Viikko-olio olisikin mielekästä koostaa tasan seitsemästä päiväoliosta. Koska päiva-olioita on aina täsmälleen 7, sopii taulukko tilanteeseen hyvin:

public class Paiva {
      private String nimi;
      // ...
}

public class Viikko {
    private Paiva[] paivat;

    public Viikko() {
        paivat = new Paiva[7];
        paivat[0] = new Paiva("Maanantai");
        paivat[1] = new Paiva("Tiistai");
        // ...
    }
}

35 Kurssipalaute ja tulevan fiilistelyä

Tehtävä 133: Kurssipalaute

Vaikka olemme saaneet paljon arvokasta palautetta eri tehtävistä TMC:n kautta, toivomme että annat palautetta kattavammin kaikista tähänastisita tehtävistä, materiaalista ja kurssin toiminnasta. Kaikki parannusehdotukset ovat myös erittäin arvokkaita!

Voit myös halutessasi antaa palautteen vasta kurssikokeen jälkeen, palautteen antaminen onnistuu tästä linkistä: https://ilmo.cs.helsinki.fi/kurssit/servlet/Valinta.

Jotta saat tehtävästä pisteet, aja tehtävän TMC-testit ja lähetä tehtävä palvelimelle. Anna palaute tehtävän laatikkoon :) -- kiitos paljon!

Ohjelmoinnin jatkokurssilla teemme matopelin. Fiilistellään tässä välissä vielä hieman ja pohditaan matopelin älyn rakentamista.

Tämän kurssin loppupuolella teemme matopelin. Fiilistellään tässä välissä vielä hieman ja pohditaan matopelin älyn rakentamista.

Tehtävä 134: Fiksu mato

Matopelin ideana on ohjata matoa (joka sattumalta on eräs Rho-bottimme inkarnaatio) siten, että mato saa kerättyä pelialueelta mahdollisimman monta punaista omenaa törmäämättä itseensä tai seiniin.

Tämä tehtävä on avoin tehtävä, jossa pääset pohtimaan matopelin tekoälyn toteuttamista. Tehtävän vaatimukset ovat ensisijaisesti se, että tekoälysi ei saa päätyä poikkeustilaan (esimerkiksi ArrayIndexOutOfBoundsException, joka tarkoittaa että yritetään hakea tietoa käytössä olevan alueen ulkopuolelta), ja että tekoälysi ohjaa matosi keräämään ainakin muutamia omenoita.

Tehtäväpohjassa on mukana luokka Main, missä on matopelin käynnistämiseen tarvittavat lähdekoodit -- luokkaa ei tarvitse muokata, sekä luokka Matoaly, johon toteutet älyä omenan jahtaamiseen.

Luokan Matoaly metodin annaSiirto tulee palauttaa merkkijono "YLOS", "ALAS", "VASEN" tai "OIKEA", jolla kerrotaan suunta, mihin madon tulee seuraavaksi liikkua.

Käytössäsi ovat seuraavat Matopeli-luokan metodit, joista saat apua älyn tekemiseen (luokka Matoaly saa Matopeli-luokan ilmentymän metodin annaSiirto parametrina).:

  1. ArrayList<Pala> madonPalat(); palauttaa palat, joista mato koostuu. Jokaisella palalla on metodit getX() ja getY(), joilla pääset käsiksi kyseisen palan sijaintiin.
  2. int matoX(); palauttaa madon pään x-koordinaatin.
  3. int matoY(); palauttaa madon pään y-koordinaatin.
  4. int omenaX(); palauttaa omenan x-koordinaatin.
  5. int omenaY(); palauttaa omenan y-koordinaatin.

Näiden lisäksi metodi int[][] annaAlusta() palauttaa kaksiulotteisen taulukon, joka sisältää matopelin tilanteen. Jos taulukosta halutaan koordinaatin (3, 2) tilanne (ensimmäinen luku on x-koordinaatti, toinen y-koordinaatti), taulukkoa käytetään seuraavasti:

int[][] taulukko = matopeli.annaAlusta();
int tilanne = taulukko[3][2];

System.out.println(tilanne);

Yllä olevassa esimerkissä tulostetaan x-koordinaatissa 3 ja y-koordinaatissa 2 oleva tilanne. Taulukon alkiot ovat merkitty seuraavasti: luku -2 tarkoittaa madon päätä, luku -1 madon muuta osaa, ja luku 8 omenaa. Luku 0 tarkoittaa, että kohta on vapaa.

Huom! Saat taulukon leveyden komennolla int leveys = taulukko.length;, taulukon korkeus taas löytyy komennolla int korkeus = taulukko[0].length;. Taulukon indeksöinti alkaa nollasta, eli ensimmäinen alkio on kohdassa taulukko[0][0];, ja viimeinen alkio on kohdassa taulukko[leveys-1][korkeus-1];.

Vinkki: Yksi tapa lähteä pohtimaan ongelmaa on miettiä suuntaa mihin madon pitäisi liikkua; tämän jälkeen voit alkaa pohtimaan miten häntää kannattaa välttää, jonka jälkeen voit aloittaa reunojen välttelemisen.

Vinkki2: Erittäin hyvin toimivan matoälyn tekeminen on haastavaa; tehokkaisiin lähestymistapoihin tutustutaan tarkemmin mm. kurssilla tietorakenteet.

Viikon 7 Loppukysely

Viikon 7 Loppukysely

 

Alla on esitetty viikkoon liittyviä väittämiä. Jos olet täysin eri mieltä väittämän kanssa, valitse 1. Jos olet täysin samaa mieltä väittämän kanssa, valitse 7. Muuten, valitse luku väliltä.

Täysin eri mieltä.
1
2
3
4
5
6
7
Täysin samaa mieltä.

 

1. Viikko oli työläs.
2. Viikko oli opettavainen.
3. Viikko oli turhauttava.
4. Viikko oli mukava.
5. Viikko oli vaikea.