Tehtävät
Seitsemännen osion tavoitteet

Seitsemäs osa on viimeinen osa kurssilla Ohjelmoinnin perusteet. Kahdeksas osa aloittaa Ohjelmoinnin jatkokurssin. Kurssit kannattaa suorittaa "putkeen" -- Ohjelmoinnin MOOC sisältää molemmat osat.

Seitsemännessä osassa pohditaan ohjelmien kommentointia, rakennetta sekä ohjelmien koostamista useammista paloista. Tutustumme myös lyhyesti automaattiseen testaamiseen (teema tulee tutummaksi Ohjelmoinnin jatkokurssilla) sekä opimme kirjoittamaan tietoa tiedostoihin. Kertaaminen on myös suuressa roolissa.

Ohjeet kolmanteen konekokeeseen löytyvät tämän osan lopusta.

Kurssin väli/loppukysely

Kurssiin kuuluu kyselyitä, joiden tavoitteena on parantaa kurssia. Vastaa kyselyyn osoitteessa https://elomake.helsinki.fi/lomakkeet/78143/lomake.html.

Käy vastaamassa ensin kyselyyn ja ruksaa sen jälkeen allaoleva tekstikenttä. Jos allaolevan tekstikentän ruksaaminen ei onnistu, varmista että olet kirjautunut tälle sivulle. Kirjautuminen onnistuu sivun oikeasta ylälaidasta.

Ohjelmien rakenteesta

Aloitetaan seitsemäs osa muutamalla sanalla lähdekoodin kommentoinnista sekä ymmärrettävyydestä.

Lähdekoodin kommentointi

Lähdekoodiin voidaan lisätä kommentteja joko yhdelle riville kahden vinoviivan jälkeen // kommentti tai useammalle riville vinoviivan ja tähden sekä tähden ja vinoviivan rajaamalle alueelle /* kommentti */.

/*
Tulostaa luvut kymmenestä yhteen. Jokainen
luku tulostetaan omalle rivilleen.
*/
int luku = 10;
while (luku > 0) {
    System.out.println(luku);
    luku--; // sama kuin luku = luku - 1;
}

Kommenteilla on useita käyttötarkoituksia. Ohjelmointikurssilla ohjelmointia opettelevan kannattaa käyttää kommentteja ohjelman toiminnallisuuden itselleen selittämiseen. Kun yllä oleva lähdekoodi on selitetty kommenteissa rivi riviltä auki, näyttää se esimerkiksi seuraavalta.

/*
Tulostaa luvut kymmenestä yhteen. Jokainen
luku tulostetaan omalle rivilleen.
*/

// Luodaan kokonaislukutyyppinen muuttuja nimeltä
// luku, johon asetetaan arvo 10.
int luku = 10;

// Toistolauseen lohkon suoritusta jatketaan kunnes
// muuttujan luku arvo on nolla tai pienempi kuin nolla.
// Suoritus ei lopu _heti_ kun muuttujaan luku asetetaan
// arvo nolla, vaan vasta kun toistolauseen ehtolauseke
// evaluoidaan seuraavan kerran. Tämä tapahtuu aina lohkon
// suorituksen jälkeen.
while (luku > 0) {
    // tulostetaan muuttujassa luku oleva arvo sekä rivinvaihto
    System.out.println(luku);
    // vähennetään yksi luku-muuttujan arvosta
    luku--; // sama kuin luku = luku - 1;
}

Kommentit eivät vaikuta ohjelman suoritukseen, eli ohjelma toimii kommenttien kanssa täysin samalla tavalla kuin ilman kommentteja.

Edellä käytetty ohjelmoinnin opetteluun tarkoitettu kommentointityyli on kuitenkin ohjelmistokehitykseen kelpaamaton. Ohjelmistoja rakennettaessa lähdekoodin tulee kommentoida itse itsensä. Tällöin ohjelman toiminnallisuus tulee ilmi luokkien, metodien ja muuttujien nimistä.

Edelliset esimerkit voidaan yhtä hyvin kommentoida kapseloimalla ohjelmakoodi sopivasti nimettyn metodin sisään. Alla on kaksi esimerkkiä yllä olevan koodin kapseloivista metodeista -- toinen metodeista on hieman yleiskäyttöisempi kuin toinen.

public void tulostaLuvutKymmenestaYhteen() {
    int luku = 10;
    while (luku > 0) {
        System.out.println(luku);
        luku--;
    }
}
public void tulostaLuvutIsoimmastaPienimpaan(int mista, int mihin) {
    while (mista >= mihin) {
        System.out.println(mista);
        mista--;
    }
}

Kommenteista ja ymmärrettävyydestä

Alla on hieman kryptisempi ohjelma.

Tutustu ohjelmaan ja yritä selvittää mitä ohjelma tekee ennen materiaalissa etenemistä. Alla olevan ohjelman suorituksen selvittämisessä kannattaa käyttää esimerkiksi kynää ja paperia. Kun käytössäsi on kynä ja paperi, aloita ohjelmakoodin läpi käyminen rivi riviltä kuin olisit tietokone. Kirjaa jokaisen rivin jälkeen ylös ohjelman käyttämissä muuttujissa tapahtuneet muutokset.

int[] t = {12, 14, 18, 40, 41, 42, 47, 52, 59};
int x = 42;

int a = 0;
int b = t.length - 1;
while (a <= b) {
    int c = a + (b - a) / 2;
    if (x < t[c]) {
        b = c - 1;
    } else if (x > t[c]) {
        a = c + 1;
    } else {
        System.out.println(c);
    }
}

System.out.println("-1");

Kun olet kokeillut ohjelman toiminnan seuraamista yllä olevalla ohjelmalla, toista harjoitus alla olevalla ohjelmalla. Alla olevassa ohjelmassa muuttujien nimet on muutettu kuvaavammiksi.

int[] taulukko = {12, 14, 18, 40, 41, 42, 47, 52, 59};
int haettava = 42;

int alaraja = 0;
int ylaraja = taulukko.length - 1;
while (alaraja <= ylaraja) {
    int keskikohta = alaraja + (ylaraja - alaraja) / 2;
    if (haettava < taulukko[keskikohta]) {
        ylaraja = keskikohta - 1;
    } else if (haettava > taulukko[keskikohta]) {
        alaraja = keskikohta + 1;
    } else {
        System.out.println(keskikohta);
    }
}

System.out.println("-1");

Lähdekoodi, missä muuttujien nimet on selkeitä, on helpommin ymmärrettävää kuin lähdekoodi, missä muuttujien nimet eivät kuvaa niiden tarkoitusta. Haluamme ohjelmasta kuitenkin version, joka on nopeasti ymmärrettävissä. Luodaan siitä metodi ja nimetään metodi sopivasti.

public int binaariHaku(int[] taulukko, int haettava) {
    int alaraja = 0;
    int ylaraja = taulukko.length - 1;
    while (alaraja <= ylaraja) {
        int keskikohta = alaraja + (ylaraja - alaraja) / 2;
        if (haettava < taulukko[keskikohta]) {
            ylaraja = keskikohta - 1;
        } else if (haettava > taulukko[keskikohta]) {
            alaraja = keskikohta + 1;
        } else {
            return keskikohta;
        }
    }

    return -1;
}

Lähdekoodi on nyt ymmärrettävissä suoraan metodin määrittelystä: public void binaariHaku(int[] taulukko, int haettava). Kyseessä on binäärihakualgoritmi, joka etsii taulukosta annettua lukua. Metodimäärittely ei kuitenkaan kerro binäärihakuun liittyvistä oletuksista tai sen palautusarvoista.

Korjataan tilanne kommentilla. Yllä esitetyn binäärihakualgoritmin toiminnan ehtona on se, että taulukko on järjestyksessä pienimmästä suurimpaan. Jos etsittävä luku löytyy, algoritmi palauttaa luvun indeksin. Jos lukua taas ei löydy, algoritmi palauttaa luvun -1.

Käytämme alla ohjelman dokumentointiin liittyvää kommentointitapaa, missä kommentti alkaa vinoviivalla ja kahdella tähdellä sekä päättyy yhteen tähteen ja vinoviivaan /** kommentti */. Ohjelmointiympäristöt näyttävät metodeihin liittyvät dokumenttikommentit muunmuassa lähdekoodin automaattisen täydennyksen yhteydessä.

/**
  Binäärihaku etsii parametrina annetusta taulukosta parametrina annettua lukua.
  Jos etsittävä luku löytyy, metodi palauttaa luvun indeksin taulukossa. Jos
  etsittävää lukua ei löydy, metodi palauttaa arvon -1. Metodi olettaa, että
  taulukko on järjestetty pienimmästä arvosta suurimpaan.
*/

public int binaariHaku(int[] taulukko, int haettava) {
    int alaraja = 0;
    int ylaraja = taulukko.length - 1;
    while (alaraja <= ylaraja) {
        int keskikohta = alaraja + (ylaraja - alaraja) / 2;
        if (haettava < taulukko[keskikohta]) {
            ylaraja = keskikohta - 1;
        } else if (haettava > taulukko[keskikohta]) {
            alaraja = keskikohta + 1;
        } else {
            return keskikohta;
        }
    }

    return -1;
}

Alla olevassa kuvassa näytetään miten ohjelmointiympäristö näyttää metodiin liittyvän kommentin. Oletuksena on, että hakualgoritmi on luokassa Hakualgoritmit. Kun luokasta on tehty olio, ja ohjelmoija alkaa kirjoittamaan metodin nimeä, näyttää ohjelmointiympäristö metodiin aiemmin liitetyn dokumentaation.

Ohjelmointiympäristö näyttää metodiin liitetyn kommentin.

 

Kommentteja käytetään siis ensisijaisesti luokkien sekä metodien yleisen toiminnallisuuden kuvaamisessa sen sijaan, että kerrottaisiin yksityiskohtaisesti mitä ohjelma tekee. Yksityiskohtainen ohjelman toiminnan avaaminen on kuitenkin hyvä tapa selittää ohjelmakoodia itselleen. Yleisesti ottaen voidaan ajatella niin, että vaikeasti ymmärrettävät ohjelmat kannattaa pilkkoa luokkiin ja metodeihin, jotka kuvaavat ohjelman rakennetta. Dokumentointi ja kommentointi niiltä osin, mitkä eivät ole luokkien tai metodien nimistä selviä, on tärkeää -- esimerkiksi metodien paluuarvot sekä niiden toimintaan liittyvät oletukset on hyvä dokumentoida.

Sovellus ja sen osat

Edellä puhuimme kommenteista sekä ohjelman pilkkomisesta luokkiin ja metodeihin, jotka kuvaavat ohjelman rakennetta. Seuraava katkelma on Edsger W. Dijkstran artikkelista On the role of scientific thought.

Let me try to explain to you, what to my taste is characteristic for all intelligent thinking. It is, that one is willing to study in depth an aspect of one's subject matter in isolation for the sake of its own consistency, all the time knowing that one is occupying oneself only with one of the aspects. We know that a program must be correct and we can study it from that viewpoint only; we also know that it should be efficient and we can study its efficiency on another day, so to speak. In another mood we may ask ourselves whether, and if so: why, the program is desirable. But nothing is gained - on the contrary! - by tackling these various aspects simultaneously. It is what I sometimes have called "the separation of concerns", which, even if not perfectly possible, is yet the only available technique for effective ordering of one's thoughts, that I know of. This is what I mean by "focusing one's attention upon some aspect": it does not mean ignoring the other aspects, it is just doing justice to the fact that from this aspect's point of view, the other is irrelevant. It is being one- and multiple-track minded simultaneously.

Ohjelmoijan tulee pystyä tarkastelemaan ohjelmaansa eri näkökulmista ilman, että muut ohjelman osa-alueet vievät keskittymistä. Käyttöliittymään tulee voida keskittyä ilman, että ohjelmoijan tulee keskittyä sovelluksen ydinlogiikkaan. Vastaavasti ohjelmassa ja ongelma-alueessa esiintyviin käsitteisiin tulee voida keskittyä ilman, että ohjelmoijan tarvitsee välittää käyttöliittymästä. Vastaavasti ohjelmassa käytettävien algoritmien tehokkuus on oma "huolenaihe", johon ohjelmoijan tulee voida keskittyä ilman huolta muista osa-alueista.

Samaa ajatusta voidaan jatkaa vastuiden näkökulmasta. Robert "Uncle Bob" C. Martin kuvaa blogissaan termiä "single responsibility principle" seuraavasti.

When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function. You want to isolate your modules from the complexities of the organization as a whole, and design your systems such that each module is responsible (responds to) the needs of just that one business function.

[..in other words..] Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Selkeys saadaan aikaan sopivalla luokkarakenteella sekä nimeämiskäytänteiden seuraamisella. Jokaisella luokalla tulee olla vastuu, johon liittyviä tehtäviä luokka hoitaa. Metodeja käytetään toiston vähentämiseen ja luokkien sisäisten toimintojen jäsentämiseen. Myös metodeilla tulee olla selkeä vastuu eli metodien ei tule olla liian pitkiä ja liian montaa asiaa tekeviä. Liian montaa asiaa tekevät monimutkaiset metodit tuleekin pilkkoa useiksi pienemmiksi apumetodeiksi joita alkuperäinen metodi kutsuu.

Hyvin harva ohjelma kirjoitetaan vain kerran

Ohjelmistoja kehittäessä keskitytään tyypillisesti niihin ohjelmiston ominaisuuksiin, jotka tuovat eniten arvoa ohjelmiston käyttäjälle. Nämä ominaisuudet sovitaan yhdessä ohjelmiston kehittäjän sekä loppukäyttäjän kanssa, mikä mahdollistaa ominaisuuksien järjestämisen tärkeysjärjestykseen.

Ohjelmistoille on tyypillistä se, että ohjelmistoon liittyvät toiveet sekä ominaisuuksien tärkeysjärjestys muuttuu ohjelmiston elinkaaren aikana. Tämä johtaa siihen, että osia ohjelmistosta kirjoitetaan uudestaan, osia siirrellään paikasta toiseen ja osia poistetaan kokonaan.

Ohjelmoijan näkökulmasta tämä tarkoittaa ensisijaisesti sitä, että ohjelmisto kehittyy jatkuvasti. Uudelleenkirjoitettavat osat tulevat tyypillisesti paremmiksi, sillä ohjelmoija oppii ongelma-alueesta siihen liittyviä ratkaisuja kehittäessään. Samalla tämä tarkoittaa sitä, että ohjelmoijan tulee myös säilyttää kokonaiskuva ohjelman rakenteesta, sillä joitain osia saatetaan myös uudelleenkäyttää muissa osissa ohjelmistoa.

Yleisesti ottaen voidaan todeta, että hyvin harva ohjelma kirjoitetaan vain kerran. Tätä ajatusta jatkaen on hyvä pyrkiä tilanteeseen, missä ohjelman käyttäjä pääsee kokeilemaan sitä mahdollisimman nopeasti -- tällöin muutostoiveiden kerääminen myös alkaa nopeasti. Ohjelmistoja tehdessä onkin hyvä usein luoda ensin Proof of Concept-sovellus, jolla voidaan kokeilla idean toimivuutta. Jos idea on hyvä, sitä jatkokehitetään -- samalla myös ohjelma ja kehittyy.

Ongelmasta kokonaisuuteen ja takaisin osiin

Tarkastellaan erään ohjelman rakennusprosessia. Ohjelma kysyy käyttäjältä sanoja kunnes käyttäjä syöttää saman sanan uudestaan. Ohjelma käyttää taulukkoa sanojen tallentamiseen -- ohjelmassa on tehty oletus, että käyttäjä voi syöttää korkeintaan 1000 sanaa.

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

Rakennetaan ohjelma osissa. Eräs haasteista on se, että on vaikea päättää miten lähestyä tehtävää, eli miten ongelma tulisi jäsentää osaongelmiksi, ja mistä osaongelmasta kannattaisi aloittaa. Yhtä oikeaa vastausta ei ole -- joskus on hyvä lähteä pohtimaan ongelmaan liittyviä käsitteitä ja niiden yhteyksiä, joskus taas ohjelman tarjoamaa käyttöliittymää.

Käyttöliittymän hahmottelu voisi lähteä liikenteeseen luokasta Kayttoliittyma. Käyttöliittymä käyttää Scanner-oliota, jonka sille voi antaa. Tämän lisäksi käyttöliittymällä on käynnistämiseen tarkoitettu metodi.

public class Kayttoliittyma {
    private Scanner lukija;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
    }

    public void kaynnista() {
        // tehdään jotain
    }
}

Käyttöliittymän luominen ja käynnistäminen onnistuu seuraavasti.

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

Toisto ja lopetus

Ohjelmassa on (ainakin) kaksi "aliongelmaa". Ensimmäinen on sanojen toistuva lukeminen käyttäjältä kunnes tietty ehto toteutuu. Tämä voitaisiin hahmotella seuraavaan tapaan.

public class Kayttoliittyma {
    private Scanner lukija;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
    }

    public void kaynnista() {

        while (true) {
            System.out.print("Anna sana: ");
            String sana = lukija.nextLine();

            if (pitää lopettaa) {
                break;
            }

        }

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

Sanojen kysely jatkuu kunnes käyttäjä syöttää jo aiemmin syötetyn sanan. Täydennetään ohjelmaa siten, että se tarkistaa onko sana jo syötetty. Vielä ei tiedetä miten toiminnallisuus kannattaisi tehdä, joten tehdään siitä vasta runko.

public class Kayttoliittyma {
    private Scanner lukija;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
    }

    public void kaynnista() {

        while (true) {
            System.out.print("Anna sana: ");
            String sana = lukija.nextLine();

            if (onJoSyotetty(sana)) {
                break;
            }

        }

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

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

        return false;
    }
}

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

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

Oleellisten tietojen tallentaminen

Toinen osaongelma on aiemmin syötettyjen sanojen muistaminen. Taulukko sopii mainiosti tähän tarkoitukseen -- myös ArrayList kävisi mainiosti, mutta harjoitellaan taulukkojen käyttöä. Taulukon lisäksi tarvitaan muuttuja, joka pitää kirjaa taulukkoon lisättyjen sanojen lukumäärästä ja samalla seuraavaksi taulukkoon lisättävän sanan indeksistä. Lisätään nämä toistaiseksi luokan Kayttoliittyma oliomuuttujiksi.

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ä while-silmukkaan taulukkoa ja sanojen laskuria päivittävät rivit:

while (true) {
    System.out.print("Anna sana: ");
    String sana = lukija.nextLine();

    if (onJoSyotetty(sana)) {
        break;
    }

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

Kayttoliittyma näyttää kokonaisuudessaan seuraavalta.

public class Kayttoliittyma {
    private Scanner lukija;
    private String[] aiemmatSanat;
    private int sanojaSyotetty;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
        this.aiemmatSanat = new String[1000];
        this.sanojaSyotetty = 0;
    }

    public void kaynnista() {

        while (true) {
            System.out.print("Anna sana: ");
            String sana = lukija.nextLine();

            if (onJoSyotetty(sana)) {
                break;
            }

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

        }

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

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

        return false;
    }
}

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

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

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 boolean onJoSyotetty(String sana) {
    for (int i = 0; i < this.sanojaSyotetty; i++) {
        if (sana.equals(this.aiemmatSanat[i])) {
            return true;
        }
    }

    return false;
}

Nyt sovellus toimii kutakuinkin halutusti.

Oliot luonnollisena osana ongelmanratkaisua

Rakensimme äsken 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 class Kayttoliittyma {
    private Scanner lukija;
    private String[] aiemmatSanat;
    private int sanojaSyotetty;

    public Kayttoliittyma(Scanner lukija) {
        this.lukija = lukija;
        this.aiemmatSanat = new String[1000];
        this.sanojaSyotetty = 0;
    }

    public void kaynnista() {

        while (true) {
            System.out.print("Anna sana: ");
            String sana = lukija.nextLine();

            if (onJoSyotetty(sana)) {
                break;
            }

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

        }

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

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

        return false;
    }
}

Ohjelman käyttämät apumuuttujat, eli taulukko aiemmatSanat ja kokonaisluku sanojaSyotetty ovat yksityiskohtia käyttöliittymän kannalta. Käyttöliittymän 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 käyttöliittymän metodi kaynnista on seuraavanlainen:

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

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

    aiemmatSanat.lisaa(sana);
}

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

Käyttöliittymän kannalta Sanajoukolla kannattaisi siis olla 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 käyttöliittymän luettavuus on huomattavasti parempi kuin taulukkoa käyttäen.

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

Alustava toteutus 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 käyttöliittymä näyttää siistiltä. Kaikki "likaiset yksityiskohdat" (taulukko ja lukumäärä sanoista) on saatu siivottua eli kapseloitua olion sisälle.

Muokataan käyttöliittymää niin, että se käyttää Sanajoukkoa. Sanajoukko annetaan käyttöliittymälle samalla tavalla parametrina kuin Scanner.

public class Kayttoliittyma {
    private Scanner lukija;
    private Sanajoukko sanajoukko;

    public Kayttoliittyma(Scanner lukija, Sanajoukko sanajoukko) {
        this.lukija = lukija;
        this.sanajoukko = sanajoukko;
    }

    public void kaynnista() {

        while (true) {
            System.out.print("Anna sana: ");
            String sana = lukija.nextLine();

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

            sanajoukko.lisaa(sana);
        }

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

Ohjelman käynnistäminen tapahtuu nyt seuraavasti:

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

    Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija, joukko);
    kayttoliittyma.kaynnista();
}

Luokan sisäisen toteutuksen muuttaminen

Sanojen muistaminen 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 ainoastaan "kapseloi" ArrayList:in. 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 Sanajoukkoon muuttamatta käyttöliittymän käyttävien metodien nimiä, ei käyttöliittymää tarvitsisi muuttaa mitenkään.

Oleellista on tässä se, että Sanajoukko-luokkaan tehdyt sisäiset muutokset eivät vaikuta luokkaan Käyttöliittymä. Kun vaihdoimme taulukon listaan, käyttöliittymä ei muutu millään tavalla. Tämä johtuu siitä, että käyttöliittymä käyttää sanajoukkoa sen tarjoamien metodien -- eli julkisten rajapintojen -- kautta.

Uusien toiminnallisuuksien toteuttaminen: palindromit

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

public void kaynnista() {

    while (true) {
        System.out.print("Anna sana: ");
        String sana = lukija.nextLine();

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

        sanajoukko.lisaa(sana);
    }

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

Käyttöliittymä säilyy siistinä ja palindromien laskeminen jää Sanajoukko-olion huoleksi. Metodin toteutus voisi olla esimerkiksi 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() {
        return (int) this.joukonSanat.stream().filter(s -> onPalindromi(s)).count();
    }

    public 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;
    }
}

Metodissa palindromeja käytetään sekä apumetodia onPalindromi että virran filter-metodia. Virran count-metodi palauttaa long-tyyppisen kokonaisluvun, joka tulee muuntaa int-tyyppiseksi ennen sen palautusta metodista.

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.

Neuvoja ohjelmointiin

Seuraa näitä neuvoja aina ohjelmoidessasi.

  • 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
  • Astu tarvittaessa askel taaksepäin ja mieti kokonaisuutta. Jos ohjelma ei toimi, voi olla hyvä idea palata aiemmin toimineeseen tilaan. Käänteisesti voidaan sanoa, että rikkinäinen ohjelma korjaantuu harvemmin lisäämällä siihen lisää koodia.

Ohjelmoijat noudattavat näitä käytänteitä sen takia että ohjelmointi olisi helpompaa. Käytänteiden noudattaminen tekee myös ohjelmien lukemisesta, ylläpitämisestä ja muokkaamisesta helpompaa muille.

Tässä tehtävässä toteutetaan ohjelma kurssipistetilastojen tulostamiseen. Ohjelmalle syötetään pisteitä (kokonaislukuja nollasta sataan), ja ohjelma tulostaa niiden perusteella arvosanoihin liittyviä tilastoja. Syötteiden lukeminen lopetetaan kun käyttäjä syöttää luvun -1. Lukuja, jotka eivät ole välillä [0-100] ei tule ottaa huomioon tilastojen laskemisessa.

Pisteiden keskiarvot

Kirjoita ohjelma, joka lukee käyttäjältä kurssin yhteispisteitä kuvaavia kokonaislukuja. Luvut väliltä [0-100] ovat hyväksyttäviä ja luku -1 lopettaa syötteen. Muut luvut ovat virhesyötteitä, jotka tulee jättää huomiotta. Kun käyttäjä syöttää luvun -1, tulostetaan (1) syötettyjen yhteispisteiden keskiarvo ja (2) hyväksyttyyn arvosanaan riittävien yhteispisteiden keskiarvo.

Hyväksytyn arvosanan saa vähintään 51 kurssipisteellä. Voit olettaa, että käyttäjä kirjoittaa aina vähintään yhden välillä [0-100] olevan kokonaisluvun.

Syötä yhteispisteet, -1 lopettaa:
-42
24
42
72
80
52
-1
Pisteiden keskiarvo (kaikki): 54.0
Pisteiden keskiarvo (hyväksytyt): 68.0
Syötä yhteispisteet, -1 lopettaa:
50
51
52
-1
Pisteiden keskiarvo (kaikki): 51.0
Pisteiden keskiarvo (hyväksytyt): 51.5

Hyväksyttyjen prosenttiosuus

Täydennä edellisessä osassa toteuttamaasi ohjelmaa siten, että ohjelma tulostaa myös hyväksymisprosentin. Hyväksymisprosentti lasketaan kaavalla 100 * hyväksytyt / osallistujat.

Syötä yhteispisteet, -1 lopettaa:
50
51
52
-1
Pisteiden keskiarvo (kaikki): 51.0
Pisteiden keskiarvo (hyväksytyt): 51.5
Hyväksymisprosentti: 66.66666666666667
Syötä yhteispisteet, -1 lopettaa:
102
-4
33
66
99
1
-1
Pisteiden keskiarvo (kaikki): 49.75
Pisteiden keskiarvo (hyväksytyt): 82.5
Hyväksymisprosentti: 50.0

Arvosanajakauma

Täydennä ohjelmaa siten, että ohjelma tulostaa myös arvosanajakauman. Arvosananajakauma muodostetaan seuraavasti.

pistemääräarvosana
< 51hylätty eli 0
< 611
< 712
< 813
< 914
>= 915

Jokainen koepistemäärä muutetaan arvosanaksi yllä olevan taulukon perusteella. Jos syötetty pistemäärä ei ole välillä [0-100], jätetään se huomiotta.

Arvosanajakauma tulostetaan tähtinä. Esim jos arvosanaan 5 oikeuttavia koepistemääriä on 1 kappale, tulostuu rivi 5: *. Jos johonkin arvosanaan oikeuttavia pistemääriä ei ole, ei yhtään tähteä tulostu, alla olevassa esimerkissä näin on mm. nelosten kohdalla.

Syötä yhteispisteet, -1 lopettaa:
102
-2
1
33
66
99
-1
Pisteiden keskiarvo (kaikki): 49.75
Pisteiden keskiarvo (hyväksytyt): 82.5
Hyväksymisprosentti: 50.0
Arvosanajakauma:
5: *
4:
3:
2: *
1:
0: **

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.

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

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

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.

Voit halutessasi hyödyntää seuraavaa runkoa metodin tekemiseen.

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

kirjasto.haeKirjaNimekkeella("Cheese").forEach(k -> System.out.println(k));

System.out.println("---");
kirjasto.haeKirjaJulkaisijalla("Pong Group").forEach(k -> System.out.println(k));

System.out.println("---");
kirjasto.haeKirjaJulkaisuvuodella(1851).forEach(k -> System.out.println(k));
Cheese Problems Solved, Woodhead Publishing, 2007
The Stinky Cheese Man and Other Fairly Stupid Tales, Penguin Group, 1992
---
---
Battle Axes, Tom A. Hawk, 1851

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. Hyödyt tehtävässä todennäköisesti ongelma-aluetta sopivasti kuvaavista luokista.

Ensiaskeleet automaattiseen testaamiseen

Ohjelman testaaminen käsin on usein työlästä. Syötteen antaminen on kuitenkin mahdollista automatisoida esimerkiksi syöttämällä Scanner-oliolle luettava merkkijono. Alla on annettu esimerkki siitä, miten edellisessä luvussa luotua ohjelmaa voi testata automaattisesti. Ohjelmassa syötetään ensin viisi merkkijonoa, jonka jälkeen syötetään aiemmin nähty merkkijono. Tämän jälkeen yritetään syöttää vielä uusi merkkijono. Merkkijonoa "kuusi" ei pitäisi esiintyä sanajoukossa.

Testisyötteen voi antaa merkkijonona Scanner-oliolle konstruktorissa.

String syote = "yksi\n" + "kaksi\n"  +
               "kolme\n" + "nelja\n" +
               "viisi\n" + "yksi\n"  +
               "kuusi\n";

Scanner lukija = new Scanner(syote);
Sanajoukko joukko = new Sanajoukko();

Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija, joukko);
kayttoliittyma.kaynnista();

if (joukko.sisaltaa("kuusi")) {
    System.out.println("Joukkoon lisättiin arvo, jota sinne ei olisi pitänyt lisätä.");
}

Ohjelma tulostus näyttää vain ohjelman antaman tulostuksen, ei käyttäjän tekemiä komentoja.

Anna sana:
Anna sana:
Anna sana:
Anna sana:
Anna sana:
Anna sana:
Annoit saman sanan uudestaan!
Sanoistasi 0 oli palindromeja

Merkkijonon antaminen Scanner-luokan konstruktorille korvaa näppäimistöltä luettavan syötteen. Merkkijonomuuttujan syote sisältö siis "simuloi" käyttäjän antamaa syötettä. Rivinvaihto syötteeseen merkitään \n:llä. Jokainen yksittäinen rivinvaihtomerkkiin loppuva osa syote-merkkijonossa siis vastaa yhtä nextLine()-komentoon annettua syötettä.

Kun haluat testata ohjelmasi toimintaa jälleen käsin, vaihda Scanner-olion konstruktorin parametriksi System.in, eli järjestelmän syötevirta. Voit toisaalta halutessasi myös vaihtaa testisyötettä, sillä kyse on merkkijonosta.

Ohjelman toiminnan oikeellisuus tulee edelleen tarkastaa ruudulta. Tulostus voi olla aluksi hieman hämmentävää, sillä automatisoitu syöte ei näy ruudulla ollenkaan. Lopullinen tavoite on automatisoida myös ohjelman tulostuksen oikeellisuden tarkastaminen niin hyvin, että ohjelman testaus ja testituloksen analysointi onnistuu "nappia painamalla". Palaamme aiheeseen myöhemmissä osissa.

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ä ohjelma ei kuitenkaan toimi halutusti. Ota selvää miksi ja korjaa ohjelma.

Tiedostoon tallentaminen

Tutustuimme edellisessä osassa tiedostojen lukemiseen. Tarkastellaan seuraavaksi lukemisen lisäksi myös tiedostoon kirjoittamista. Kertaa aluksi edellisestä osasta tiedoston lukeminen.

Tässä tehtävässä tehdään sovellus tiedoston rivi- ja merkkimäärän laskemiseen.

Rivien laskeminen

Tee luokka Analyysi, jolla on konstruktori public Analyysi(String tiedosto). Toteuta luokalle metodi public int rivimaara(), joka palauttaa konstruktorille annetun tiedoston rivimäärän. Metodi ei saa olla "kertakäyttöinen", eli sen pitää tuottaa oikea tulos myös usealla peräkkäisellä kutsulla.

Merkkien laskeminen

Toteuta luokkaan Analyysi metodi public int merkkeja(), joka palauttaa luokan konstruktorille annetun tiedoston merkkien määrän. Kuten edellä, metodi ei saa olla "kertakäyttöinen", eli sen pitää tuottaa oikea tulos myös usealla peräkkäisellä kutsulla.

Huomaa, että rivinvaihdot tulee laskea myös merkeiksi -- tiedoston lukijat tuppaavat syömään nämä rivin vaihdot.

Voit päättää itse miten reagoidaan jos konstruktorin parametrina saatua tiedostoa ei ole olemassa.

Projektissa on testausta varten tiedosto testitiedosto.txt. Ohjelmasta avatessa tiedoston nimeksi tulee antaa src/testitiedosto.txt. Tiedoston sisältö on seuraava:

rivejä tässä on 3 ja merkkejä
koska rivinvaihdotkin ovat
merkkejä
  

Ohjelman toiminta testaustiedostolla:

File tiedosto = new File("src/testitiedosto.txt");
Analyysi analyysi = new Analyysi(tiedosto);
System.out.println("Rivejä: " + analyysi.rivimaara());
System.out.println("Merkkejä: " + analyysi.merkkeja());
Rivejä: 3
Merkkejä: 67

Tehdään luokka Sanatutkimus, jolla voi tehdä erilaisia tutkimuksia tiedoston sisältämille sanoille. Tässä kohtaa on hyvä myös kerrata osassa 5 käsiteltyjä merkkijonojen metodeja.

Kotimaisten kielten tutkimuskeskus (Kotus) on julkaissut netissä suomen kielen sanalistan. Tässä tehtävässä käytetään listan muokattua versiota, joka löytyy tehtäväpohjasta src-hakemistosta nimellä sanalista.txt, eli suhteellisesta polusta "src/sanalista.txt". Koska sanalista on varsin pitkä, on projektissa testausta varten myös pienilista.txt joka löytyy polusta "src/pienilista.txt".

Mikäli sinulla on ongelmia ääkkösellisten sanojen kanssa (mac ja windows käyttäjät), lue tiedoston rivit antamalla lukijalle tieto tiedoston merkistöstä seuraavasti: Files.lines(Paths.get(tiedosto), StandardCharsets.UTF_8). Ongelmat liittyvät erityisesti testien suoritukseen.

Sanojen määrä

Luo Sanatutkimus-luokalle konstruktori public Sanatutkimus(String tiedosto) joka luo uuden Sanatutkimus-olion, joka tutkii parametrina annettavaa tiedostoa.

Tee luokkaan metodi public int sanojenMaara(), joka lukee tiedostossa olevat sanat ja tulostaa niiden määrän. Tässä vaiheessa sanoilla ei tarvitse tehdä mitään, riittää laskea niiden määrä. Voit olettaa tässä tehtävässä, että tiedostossa on vain yksi sana riviä kohti.

z-kirjain

Tee luokkaan metodi public ArrayList<String> kirjaimenZSisaltavatSanat(), joka palauttaa tiedoston kaikki sanat, joissa on z-kirjain. Tällaisia sanoja ovat esimerkiksi jazz ja zombi.

n-pääte

Tee luokkaan metodi public ArrayList<String> kirjaimeenNPaattyvatSanat(), joka palauttaa tiedoston kaikki sanat, jotka päättyvät n-kirjaimeen. Tällaisia sanoja ovat esimerkiksi aakkonen ja valkokaulustyöläinen.

Huom! Jos luet tiedoston uudestaan ja uudestaan jokaisessa metodissa huomaat viimeistään tässä vaiheessa copy-paste koodia. Kannattaa miettiä olisiko tiedoston lukeminen helpompi tehdä osana konstruktoria tai metodina, jota konstruktori kutsuu. Metodeissa voitaisiin käyttää tällöin jo luettua listaa ja luoda siitä aina uusi, hakuehtoihin sopiva lista.

Palindromit

Tee luokkaan metodi public ArrayList<String> palindromit(), joka palauttaa tiedoston kaikki sanat, jotka ovat palindromeja. Tällaisia sanoja ovat esimerkiksi ala ja enne.

Kaikki vokaalit

Tee luokkaan metodi public ArrayList<String> kaikkiVokaalitSisaltavatSanat(), joka palauttaa tiedoston kaikki sanat, jotka sisältävät kaikki suomen kielen vokaalit (aeiouyäö). Tällaisia sanoja ovat esimerkiksi myöhäiselokuva ja ympäristönsuojelija.

Tiedostoon kirjoittaminen on melko suoraviivaista. Kuten lukemisessa, tarvitsemme tiedon tiedoston polusta eli minne tiedosto luodaan tai missä olevaan tiedostoon kirjoitetaan. Tämän lisäksi tarvitsemme myös kirjoitettavan tiedon, joka esitetään listana merkkijonoja. Kirjoitettaessa päätetään myös tiedoston käyttämä muoto, tyypillisesti StandardCharsets.UTF_8. Aivan kuten lukeminen, myös kirjoittaminen voi aiheuttaa virhetilanteen, johon tulee varautua. Tämä tehdään try { ... } catch (Exception e) { ... } -rakenteella.

Alla olevassa esimerkissä luetaan ensin käyttäjältä kirjoitettava sisältö. Tämän jälkeen luetaan tiedoston nimi, johon merkkijono kirjoitetaan. Lopulta merkkijono kirjoitetaan tiedostoon.

Scanner lukija = new Scanner(System.in);

ArrayList<String> rivit = new ArrayList<>();
System.out.println("Mitä kirjoitetaan, tyhjä lopettaa?");

while (true) {
    String kirjoitettava = lukija.nextLine();
    if (kirjoitettava.isEmpty()) {
        break;
    }

    rivit.add(kirjoitettava);
}

System.out.println("Minne kirjoitetaan?");
String tiedosto = lukija.nextLine();

// tiedostoon kirjoittaminen
try {
    Files.write(Paths.get(tiedosto), rivit, StandardCharsets.UTF_8);
} catch (Exception e) {
    System.out.println("Virhe tiedostoon kirjoittamisessa: " + e.getMessage());
}

Yllä oleva esimerkki luo tarvittaessa uuden tiedoston. Jos tiedosto on jo olemassa, tiedoston vanha sisältö poistetaan ennen tiedostoon kirjoittamista.

Jos halutaan, että tiedostoon vain lisätään -- eli että tiedoston vanhaa sisältöä ei poisteta, lisätään kirjoituskomentoon parametri StandardOpenOption.APPEND. Tällöin edellisen ohjelman kirjoitusosa on seuraavanlainen.

try {
    Files.write(Paths.get(tiedosto), rivit, StandardCharsets.UTF_8, StandardOpenOption.APPEND);
} catch (Exception e) {
    System.out.println("Virhe tiedostoon kirjoittamisessa: " + e.getMessage());
}

Tiedoston lukemisen ja tiedostoon kirjoittamisen voi eriyttää omaksi luokakseen. Alla tiedostoon kirjoittamiseen löytyy sekä rivin että listan kirjoittamiseen liittyvät metodit. Allaolevat esimerkit näyttävät myös miten luokan sisällä olevat metodit voivat kutsua muita luokan sisällä olevia metodeja -- jos metodit ovat saman nimisiä, kutsuttava metodi päätellään metodin parametreista ja niiden tyypeistä.

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;

public class Tiedostonkasittelija {

    public ArrayList<String> lueTiedosto(String tiedosto) {
        ArrayList<String> rivit = new ArrayList<>();
        try {
            Files.lines(Paths.get(tiedosto)).forEach(rivi -> rivit.add(rivi));
        } catch (IOException e) {
            System.out.println("Lukeminen epäonnistui. Virhe: " + e.getMessage());
        }

        return rivit;
    }

    public void kirjoitaTiedostoon(String tiedosto, String rivi) {
        kirjoitaTiedostoon(tiedosto, merkkijonoListana(rivi));
    }

    public void kirjoitaTiedostoon(String tiedosto, ArrayList<String> rivit) {
        try {
            Files.write(Paths.get(tiedosto), rivit, StandardCharsets.UTF_8);
        } catch (IOException e) {
            System.out.println("Kirjoittaminen epäonnistui. Virhe: " + e.getMessage());
        }
    }

    public void kirjoitaTiedostoon(String tiedosto, String rivi) {
        lisaaTiedostoon(tiedosto, merkkijonoListana(rivi));
    }

    public void lisaaTiedostoon(String tiedosto, ArrayList<String> rivit) {
        try {
            Files.write(Paths.get(tiedosto), rivit,
                StandardCharsets.UTF_8, StandardOpenOption.APPEND);
        } catch (IOException e) {
            System.out.println("Lisääminen epäonnistui. Virhe: " + e.getMessage());
        }
    }

    public ArrayList<String> merkkijonoListana(String merkkijono) {
        ArrayList<String> rivit = new ArrayList<>();
        rivit.add(rivi);

        return rivit;
    }
}

Nyt ohjelmoijan ei enää tarvitse välittää kirjoittamiseen ja lukemiseen liittyvistä yksityiskohdista. Alla olevassa esimerkissä kirjoitetaan tiedostoon "paivakirja.txt" kaksi riviä.

public static void main(String[] args) {
    Tiedostonkasittelija tiedosto = new Tiedostonkasittelija();
    tiedosto.lisaaTiedostoon("paivakirja.txt", "Rakas päiväkirja, tänään oli kiva päivä.");
    tiedosto.lisaaTiedostoon("paivakirja.txt", "Rakas päiväkirja, tänäänkin oli kiva päivä.");
}

Luo luokka Sensuroija, joka saa konstruktorin parametrina sensuroitavan merkkijonon. Luo luokalle tämän jälkeen metodi public void sensuroi(String lahdetiedosto, String kohdetiedosto), joka (1) lukee lähdetiedoston, (2) poistaa luetusta datasta kaikki sanat, joissa esiintyy sensuroijalle konstruktorin parametrina annettu merkkijono, ja (3) tallentaa jäljelle jääneet sanat kohdetiedostoon.

Voit olettaa, että syötetiedostossa jokainen rivi sisältää yhden sanan. Tehtäväpohjan mukana on edellisessäkin tehtävässä nähtyjä sanalistoja, joita voit käyttää testaamiseen.

Mikäli testeissä on omituisia ongelmia merkistön (mac ja windows -käyttäjät) kanssa, voit kokeilla merkistön lisäämistä lukemiseen ja tallentamiseen. Tämä onnistuu seuraavilla esimerkeillä:

Files.write(Paths.get(tiedosto), rivit, StandardCharsets.UTF_8);
Files.lines(Paths.get(tiedosto), StandardCharsets.UTF_8)

Kertausta

Tässä tehtävässä harjoitellaan lahjojen pakkaamista. Tehdään luokat Lahja ja Pakkaus. Lahjalla on nimi ja paino, ja Pakkaus sisältää lahjoja.

Lahja-luokka

Tee luokka Lahja, josta muodostetut oliot kuvaavat erilaisia lahjoja. Tallennettavat tiedot ovat tavaran nimi ja paino (kg).

Lisää luokkaan seuraavat metodit:

  • Konstruktori, jolle annetaan parametrina lahjan nimi ja paino
  • Metodi public String getNimi(), joka palauttaa lahjan nimen
  • Metodi public int getPaino(), joka palauttaa lahjan painon
  • Metodi public String toString(), joka palauttaa merkkijonon muotoa "nimi (paino kg)"

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Lahja kirja = new Lahja("Aapiskukko", 2);

        System.out.println("Lahjan nimi: " + kirja.getNimi());
        System.out.println("Lahjan paino: " + kirja.getPaino());

        System.out.println("Lahja: " + kirja);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Lahjan nimi: Aapiskukko
Lahjan paino: 2
Lahja: Aapiskukko (2 kg)

Pakkaus-luokka

Tee luokka Pakkaus. Pakkaus voi sisältää äärettömän määrän lahjoja, jonka lisäksi se tarjoaa metodin lahjojen yhteispainon laskemiseen.

Lisää luokkaan seuraavat metodit:

  • Parametriton konstruktori
  • Metodi public void lisaaLahja(Lahja lahja), joka lisää parametrina annettavan lahjan pakkaukseen. Metodi ei palauta mitään arvoa.

Tavarat kannattaa tallentaa ArrayList-olioon:

ArrayList<Lahja> lahjat = new ArrayList<>();

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Lahja kirja = new Lahja("Aapiskukko", 2);

        Pakkaus paketti = new Pakkaus();
        paketti.lisaaLahja(kirja);
        System.out.println(paketti.getPaino());
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

2

Tässä tehtäväsarjassa tehdään luokat Tavara, Matkalaukku ja Lastiruuma, joiden avulla harjoitellaan olioita, jotka sisältävät toisia olioita.

Tavara-luokka

Tee luokka Tavara, josta muodostetut oliot vastaavat erilaisia tavaroita. Tallennettavat tiedot ovat tavaran nimi ja paino (kg).

Lisää luokkaan seuraavat metodit:

  • Konstruktori, jolle annetaan parametrina tavaran nimi ja paino
  • Metodi public String getNimi(), joka palauttaa tavaran nimen
  • Metodi public int getPaino(), joka palauttaa tavaran painon
  • Metodi public String toString(), joka palauttaa merkkijonon muotoa "nimi (paino kg)"

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);

        System.out.println("Kirjan nimi: " + kirja.getNimi());
        System.out.println("Kirjan paino: " + kirja.getPaino());

        System.out.println("Kirja: " + kirja);
        System.out.println("Puhelin: " + puhelin);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Kirjan nimi: Aapiskukko
Kirjan paino: 2
Kirja: Aapiskukko (2 kg)
Puhelin: Nokia 3210 (1 kg)

Matkalaukku-luokka

Tee luokka Matkalaukku. Matkalaukkuun liittyy tavaroita ja maksimipaino, joka määrittelee tavaroiden suurimman mahdollisen yhteispainon.

Lisää luokkaan seuraavat metodit:

  • Konstruktori, jolle annetaan maksimipaino
  • Metodi public void lisaaTavara(Tavara tavara), joka lisää parametrina annettavan tavaran matkalaukkuun. Metodi ei palauta mitään arvoa.
  • Metodi public String toString(), joka palauttaa merkkijonon muotoa "x tavaraa (y kg)"

Tavarat kannattaa tallentaa ArrayList-olioon:

ArrayList<Tavara> tavarat = new ArrayList<>();

Luokan Matkalaukku tulee valvoa, että sen sisältämien tavaroiden yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi lisättävän tavaran vuoksi, metodi lisaaTavara ei saa lisätä uutta tavaraa laukkuun.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku matkalaukku = new Matkalaukku(5);
        System.out.println(matkalaukku);

        matkalaukku.lisaaTavara(kirja);
        System.out.println(matkalaukku);

        matkalaukku.lisaaTavara(puhelin);
        System.out.println(matkalaukku);

        matkalaukku.lisaaTavara(tiiliskivi);
        System.out.println(matkalaukku);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

0 tavaraa (0 kg)
1 tavaraa (2 kg)
2 tavaraa (3 kg)
2 tavaraa (3 kg)

Kielenhuoltoa

Ilmoitukset "0 tavaraa" ja "1 tavaraa" eivät ole kovin hyvää suomea – paremmat muodot olisivat "ei tavaroita" ja "1 tavara". Tee tämä muutos luokkaan Matkalaukku.

Nyt edellisen ohjelman tulostuksen tulisi olla seuraava:

ei tavaroita (0 kg)
1 tavara (2 kg)
2 tavaraa (3 kg)
2 tavaraa (3 kg)

Kaikki tavarat

Lisää luokkaan Matkalaukku seuraavat metodit:

  • metodi tulostaTavarat, joka tulostaa kaikki matkalaukussa olevat tavarat
  • metodi yhteispaino, joka palauttaa tavaroiden yhteispainon

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku matkalaukku = new Matkalaukku(10);
        matkalaukku.lisaaTavara(kirja);
        matkalaukku.lisaaTavara(puhelin);
        matkalaukku.lisaaTavara(tiiliskivi);

        System.out.println("Matkalaukussa on seuraavat tavarat:");
        matkalaukku.tulostaTavarat();
        System.out.println("Yhteispaino: " + matkalaukku.yhteispaino() + " kg");
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Matkalaukussa on seuraavat tavarat:
Aapiskukko (2 kg)
Nokia 3210 (1 kg)
Tiiliskivi (4 kg)
Yhteispaino: 7 kg

Muokkaa myös luokkaasi siten, että käytät vain kahta oliomuuttujaa. Toinen sisältää maksimipainon, toinen on lista laukussa olevista tavaroista.

Raskain tavara

Lisää vielä luokkaan Matkalaukku metodi raskainTavara, joka palauttaa painoltaan suurimman tavaran. Jos yhtä raskaita tavaroita on useita, metodi voi palauttaa minkä tahansa niistä. Metodin tulee palauttaa olioviite. Jos laukku on tyhjä, palauta arvo null.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("Tiiliskivi", 4);

        Matkalaukku matkalaukku = new Matkalaukku(10);
        matkalaukku.lisaaTavara(kirja);
        matkalaukku.lisaaTavara(puhelin);
        matkalaukku.lisaaTavara(tiiliskivi);

        Tavara raskain = matkalaukku.raskainTavara();
        System.out.println("Raskain tavara: " + raskain);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Raskain tavara: Tiiliskivi (4 kg)

Lastiruuma-luokka

Tee luokka Lastiruuma, johon liittyvät seuraavat metodit:

  • konstruktori, jolle annetaan maksimipaino
  • metodi public void lisaaMatkalaukku(Matkalaukku laukku), joka lisää parametrina annetun matkalaukun lastiruumaan
  • metodi public String toString(), joka palauttaa merkkijonon muotoa "x matkalaukkua (y kg)"

Tallenna matkalaukut sopivaan ArrayList-rakenteeseen.

Luokan Lastiruuma tulee valvoa, että sen sisältämien matkalaukkujen yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi uuden matkalaukun vuoksi, metodi lisaaMatkalaukku ei saa lisätä uutta matkalaukkua.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku adanLaukku = new Matkalaukku(10);
        adanLaukku.lisaaTavara(kirja);
        adanLaukku.lisaaTavara(puhelin);

        Matkalaukku pekanLaukku = new Matkalaukku(10);
        pekanLaukku.lisaaTavara(tiiliskivi);

        Lastiruuma lastiruuma = new Lastiruuma(1000);
        lastiruuma.lisaaMatkalaukku(adanLaukku);
        lastiruuma.lisaaMatkalaukku(pekanLaukku);

        System.out.println(lastiruuma);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

2 matkalaukkua (7 kg)

Lastiruuman sisältö

Lisää luokkaan Lastiruuma metodi public void tulostaTavarat(), joka tulostaa kaikki lastiruuman matkalaukuissa olevat tavarat.

Seuraavassa on luokan käyttöesimerkki:

public class Main {
    public static void main(String[] args) {
        Tavara kirja = new Tavara("Aapiskukko", 2);
        Tavara puhelin = new Tavara("Nokia 3210", 1);
        Tavara tiiliskivi = new Tavara("tiiliskivi", 4);

        Matkalaukku adanLaukku = new Matkalaukku(10);
        adanLaukku.lisaaTavara(kirja);
        adanLaukku.lisaaTavara(puhelin);

        Matkalaukku pekanLaukku = new Matkalaukku(10);
        pekanLaukku.lisaaTavara(tiiliskivi);

        Lastiruuma lastiruuma = new Lastiruuma(1000);
        lastiruuma.lisaaMatkalaukku(adanLaukku);
        lastiruuma.lisaaMatkalaukku(pekanLaukku);

        System.out.println("Ruuman matkalaukuissa on seuraavat tavarat:");
        lastiruuma.tulostaTavarat();
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

Ruuman matkalaukuissa on seuraavat tavarat:
Aapiskukko (2 kg)
Nokia 3210 (1 kg)
tiiliskivi (4 kg)

Kertaa ennen tätä tehtävää edellisen osan kaksiulotteisiin taulukkoihin liittynyt osa.

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 ole tyytyväisiä naapureihinsa.

Tässä tehtävässä pohditaan Schellingin mallia sekä kehitetään siihen liittyvää simulaatio-ohjelmaa.

Simulaation suoritus alkaa tilanteesta, missä ihmiset ovat asetettu satunnaisesti asumaan.

Tilanne, missä ihmiset asuvat satunnaisesti.

 

Kun simulaatio etenee, päädytään ennen pitkää tilanteeseen, missä samankaltaiset ihmiset ovat muuttaneet samankaltaisten ihmisten luo.

Ihmiset ovat muuttaneet sopivampiin paikkoihin.

 

Simulaatio-ohjelmasta puuttuu muutamia oleellisia toiminnallisuuksia: (1) kartan tyhjennys, (2) tyhjien paikkojen etsiminen, sekä (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.

Kartan tyhjentäminen ja tyhjien paikkojen etsiminen

Simulaatiomallissa käytetään kaksiulotteista taulukkoa asuinalueen kuvaamiseen. Taulukossa oleva arvo 0 kuvaa tyhjää paikkaa ja luvut 1-5 kuvaavat eri ryhmiä.

Toteuta ensin luokan Eriytymismalli metodiin public void asetaKaikkiTyhjiksi() toiminnallisuus, joka asettaa jokaisen taulukon taulukko 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ä käytössä olevassa kaksiulotteisessa taulukossa ensimmäinen ulottuvuus kuvaa x-koordinaattia, ja toinen y-koordinaattia (taulukko[x][y]).

Tyytymättömien hakeminen

Mallille voidaan asettaa parametri tyytyvaisyysraja. Tyytyväisyysrajalla kuvataan samaan ryhmään kuuluvien naapureiden minimimäärää, millä henkilö on tyytyväinen sijaintiinsa. Jos ruudussa (x, y) olevan henkilön naapureista on samankaltaisia yhtä paljon tai yli tyytyvaisyysrajan, on henkilö tyytyväinen. Muissa tapauksissa henkilö on tyytymätön.

Naapureista tulee tarkastella kaikkia ruudun vieressä olevia ruutuja. Alueen ulkopuolella olevat ruudut (esim. -1, 0) tulee käsitellä tyhjänä ruutuna (ei samankaltainen).

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

Kun metodi on toteutettu, ihaile ohjelman toimintaa :)

Vaikka karttamme on suorakulmio, voisi sen yhtä hyvin piirtää vaikkapa Helsingin muotoiseksi.

Uno on korttipeli, missä jokaisella pelaajalla on kädessään kortteja. Kortteja pelataan vuorotellen siten, että jokainen pelaaja pelaa aina yhden kortin kerrallaan. Pelin voittaja on se, jonka kädestä loppuu kortit ensimmäisenä kesken.

Suurimmalla osalla korteista on väri -- Punainen, Vihreä, Sininen tai Keltainen -- ja kortin saa pelata edellisen pelaajan pelaaman kortin jälkeen jos kortilla on sama väri tai numero kuin edellisellä kortilla. Pelissä on lisäksi joukko erikoiskortteja. Osalla niistä on väri ja ne saa pelata vain jos edellisessä kortissa on sama väri. Osalla erikoiskorteista ei ole väriä. Värittömät kortit saa pelata minkä tahansa kortin jälkeen (pelaaminen tapahtuu kuitenkin aina omalla vuorolla).

  • Värilliset erikoiskortit ovat "Ohitus", "Suunnanvaihto" ja "Nosta 2". Kun pelaaja pelaa kortin "Ohitus", seuraavalta pelaajalta jää vuoro välistä. Kortti "Suunnanvaihto" kääntää pelin suuntaa (esim. myötäpäivästä vastapäivään), ja "Nosta 2" lisää seuraavalle pelaajalle kaksi korttia sekä jättää häneltä vuoron välistä.
  • Värittömät erikoiskortit ovat "Villi kortti" ja "Villi kortti + Nosta 4". Molemmat kortit antavat kortin pelaavalle pelaajalle mahdollisuuden valita seuraavaksi pelattava väri. Tämän lisäksi kortti "Villi kortti + Nosta 4" lisää seuraavalle pelaajalle 4 korttia sekä jättää häneltä vuoron välistä.

Pelin tavoitteena on saada kortit loppumaan. Kun tämä tapahtuu, ensimmäisenä korttinsa loppuun pelannut pelaaja saa pisteitä muiden pelaajien käteen jääneistä korteista. Pisteet lasketaan seuraavasti: numerokortit ovat kortissa olevan numeron arvoisia, värilliset erikoiskortit ovat 20 pisteen arvoisia, ja värittömät erikoiskortit ovat 50 pisteen arvoisia. Uusia pelejä pelataan kunnes joku pelaajista saavuttaa 500 pistettä.

Tässä tehtävässä rakennetaan Uno-peliä varten tekoäly. Ennen aloittamista, kokeile tehtäväpohjassa olevan pelin käynnistämistä ja pelaamista. Peli käynnistetään tehtäväpohjan Main-luokasta.


Tekoäly tulee toteuttaa luokkaan Tekoalypelaaja. Luokalle on valmiiksi määriteltynä rajapinta (palaamme tähän myöhemmissä osissa) ja kolme metodia.

  • Metodi public int pelaa(ArrayList<Kortti>, omatKortit, Kortti paallimmaisin, String vari, Pelitilanne tilanne) on tekoälyn ydin ja sen tehtävänä on päättää mikä kortti pelataan seuraavaksi. Metodi saa parametrina pelaajan kädessä olevat kortit (omatKortit), viimeksi pelatun kortin minkä päälle pelataan (paallimmaisin), edellisen pelaajan valitseman värin jos viimeksi pelattu kortti oli villi kortti (vari), sekä yleisen pelitilanteen (tilanne). Pelitilanne-luokasta kerrotaan enemmän tehtävänannon lopussa.
    Tehtävänäsi on toteuttaa ohjelma, joka palauttaa pelattavan kortin indeksin. Jos käytettävissä ei ole yhtäkään sopivaa korttia, metodin tulee palauttaa arvo -1, jolloin nostetaan kortti. Huomaa, että pelissä tulee pelata kortti jos se vain on mahdollista.

  • Metodia public String valitseVari(ArrayList<Kortti> omatKortit) kutsutaan kun tekoäly pelaa villin kortin eli vaihtaa pelattavaa väriä. Metodin tulee palauttaa joku seuraavista merkkijonoista: "Punainen", "Vihreä", "Sininen", "Keltainen".

  • Metodi public String nimi() kertoo tekoälysi nimen, jonka saat luonnollisesti keksiä itse. Tekoälyn nimi saattaa ilmestyä jonkinlaisiin turnauslistoihin, eli pidä nimi ns. kilttinä.

Tehtävänäsi on tutustua ohjelmaan sekä toteuttaa luokkaan Tekoalypelaaja tekoäly, joka toimii oikein, eli se pelaa aina sallitun kortin. Oikein toimiva tekoäly on kahden tehtäväpisteen arvoinen.

Kun olet saanut toteutettua oikein toimivan tekoälyn, viilaa sitä paremmaksi.

Kun tehtävän määräaika on ohi, jokaisen palauttamaa (toimivaa) tekoälyä peluutetaan kaikkia muita tekoälyjä vastaan useampaan otteeseen. Parhaille tekoälyille on tiedossa myös palkintoja.


Vinkkejä viilaamiseen:

  • Koska häviötilanteessa käden pisteet menevät voittajalla, saattaa olla hyvä idea pyrkiä pelaamaan ensin kortit, joiden pistearvo on suuri.
  • Toisaalta, esimerkiksi villit kortit saattavat olla erittäin hyödyllisiä myöhemmässä vaiheessa peliä, eli niistä kannattanee pitää kiinni.
  • Tai no, ehkä villeistä korteista ei kannata pitää ikuisesti kiinni, koska ne kuitenkin ovat 50 pisteen arvoisia.
  • Kun pelaat villin kortin ja valitset värin, voi olla ihan fiksua valita väri, jota sinulla on paljon kädessä.
  • Toiisaalta sitten, jos kädessäsi on punaiset 0, 1 ja 6 ja vihreä 9 sekä vihdeä nosta 2, voi olla kuitenkin ihan fiksua yrittää ensin hankkiutua eroon vihreistä korteista, sillä niiden pistearvo on hyvin suuri.
  • Pohdi tilannetta, missä pöydällä on punainen 5 ja kädessäsi on punainen 3 sekä sininen 5. Kannattaako sinun pelata punainen vai vihreä kortti? Tässä on pohdittavana sekä mahdollinen pistesaldo että syy punaisen kortin pöydällä olemiseen. Entäpä jos vastustaja on pelannut punaisen kortin koska hänellä on punaisia kortteja jäljellä?
  • Jokaista nollakorttia löytyy pelistä vain yksi, mutta jokaista numerokorttia on kaksi. Nollan pelaaminen ei vaikuta käden pisteisiin, mutta sen pelaaminen saattaa johtaa tilanteeseen, missä on epätodennäköisempää että jollain muulla on sama numero. Tällöin todennäköisyys värin vaihdolle saattaa olla pienempi..
  • Kaikkiin näihin vinkkeihin varmaankin vaikuttaa myös kädessä olevien korttien määrä. Pienellä määrällä kortteja voi olla ehkäpä hyvä ottaa riskejä ja yrittää voittaa peli. Toisaalta sitten, jos kädessä on paljon kortteja, kannattaa ehkä unohtaa kyseisen kierroksen voitto ja pyrkiä minimoimaan vastustajalle menevien pisteiden määrä.
  • Ainiin, muutkin osallistujat lukevat näitä vinkkejä.. Löytyisiköhän joillekin edeltäville vinkeille jonkinlainen käänteisstrategia?

Pelitilanne-luokka sisältää havaintoja tähän asti kuluneesta pelistä sekä lisätietoa kehittyneemmille tekoälyille. Se tarjoaa seuraavat metodit:

  • Metodi public String getSuunta() palauttaa pelin tämänhetkisen suunnan. Suunta on joko "Myötäpäivään" eli eteenpäin tai "Vastapäivään" eli taaksepäin.
  • Metodi public int getOmaIndeksi() palauttaa oman tekoälyn indeksin. Indeksiä käytetään seuraavissa metodeissa.
  • Metodi public int[] getPelaajienPisteet() kertoo tämänhetkisen pistetilanteen kaikille pelaajille.
  • Metodi public int[] getPelaajienKorttienLukumaarat() kertoo pelaajien tämänhetkisen korttien lukumäärän.
  • Metodi public String[] getPelaajienViimeksiPelaamatVarit() kertoo pelaajien viimeksi pelaamat värit. Alkiossa on arvo null jos pelaaja ei ole vielä pelannut korttia.

Tehtävän alkuperäinen idea: Stephen Davies, UMW

Hengähdä hetkeksi ja mieti kurssin alkua. Olet todennäköisesti kehittynyt huimasti.

Lopuksi

Huom! Jos et vielä vastannut osoitteessa https://elomake.helsinki.fi/lomakkeet/78143/lomake.html olevaan kyselyyn, tee se nyt.

Sisällysluettelo