Sisällysluettelo

Tehtävät

Kysely: Taustatiedot

Kysely: Taustatiedot

Käytämme tietoja kurssiemme kehittämiseen ja opetuksen tutkimukseen. Vaikka osa kyselyistä ovat samanlaisia kuin aiemmin näkemäsi kyselyt, vastaathan niihin silti.

Kysely: Oppimiskokemukset ja motivaatio

Tutkimus: Oppimiskokemukset ja motivaatio

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

Seuraavilla kysymyksillä kartoitetaan opiskelukokemuksiasi ja motivaatiotasi tällä kurssilla. Tässä kyselyssä ei ole oikeita tai vääriä vastauksia, vastaa niin tarkasti kuin pystyt. Käytä seuraavaa skaalaa vastausten antamiseen. Jos olet sitä mieltä, että väite pätee sinuun erittäin hyvin, valitse 7; jos olet sitä mieltä, että väite ei päde sinuun lainkaan, valitse 1. Jos väite on enemmän tai vähemmän totta kohdallasi, valitse sinua parhaiten kuvaava arvo lukujen 1 ja 7 väliltä.

 

Väite ei päde minuun lainkaan.
1
2
3
4
5
6
7
Väite pätee minuun erittäin hyvin.

 

34 Ohjelmoinnin perusteiden kertausta

Tässä luvussa kerrataan pikaisesti muutamia aiempina viikkoina tutuksi tulleita asioita. Ruostunutta ohjelmointitaitoa voi verestää myös täältä löytyvän tehtäväsarjan avulla.

34.1 Ohjelma, käskyt ja muuttujat

Tietokoneohjelma koostuu joukosta käskyjä, joita tietokone suorittaa yksi kerrallaan, ylhäältä alaspäin. Käskyillä on aina määrätty rakenne ja semantiikka. Javassa, eli kurssilla käyttämässämme ohjelmointikielessä, käskyjä luetaan ylhäältä alas vasemmalta oikealle. Ohjelmointikurssit aloitetaan usein esittelemällä ohjelma, joka tulostaa merkkijonon Hei maailma!. Alla on Java-kielellä kirjoitettu käsky Hei maailma! -merkkijonon tulostamiseksi.

System.out.println("Hei maailma!");

Käskyssä kutsutaan System-luokkaan liittyvän muuttujan println-metodia, joka tulostaa ensin parametrina annetun merkkijonon ja sen jälkeen rivinvaihdon. Metodille annetaan parametrina merkkijono Hei maailma!, jolloin tulostus on Hei maailma! jota seuraa rivinvaihto.

Ohjelmaan voi liittyä muuttujia joita voi käyttää osana ohjelman toimintaa. Alla on ohjelma, joka ensin esittelee kokonaislukutyyppisen muuttujan pituus johon asetetaan seuraavalla rivillä arvo 179. Tämän jälkeen tulostetaan muuttujan pituus arvo eli 179.

int pituus;
pituus = 179;
System.out.println(pituus);

Yllä ohjelman suoritus tapahtuisi rivi kerrallaan. Ensin suoritetaan rivi int pituus;, jossa esitellään muuttuja nimeltä pituus. Seuraavaksi suoritetaan rivi pituus = 179;, jossa asetetaan edellisellä rivillä esiteltyyn muuttujaan arvo 179. Tämän jälkeen suoritetaan rivi System.out.println(pituus);, jossa kutsutaan aiemmin näkemäämme tulostusmetodia, jolle annetaan parametrina muuttuja pituus. Metodi tulostaa muuttujan pituus sisällön eli arvon 179.

Yllä olevassa ohjelmassa ei oikeastaan ole tarvetta esitellä muuttujaa pituus ja asettaa siihen arvoa erillisillä riveillä. Muuttujan esittelyn ja siihen liittyvän arvon asetuksen voi tehdä myös samalla rivillä.

int pituus = 179;

Yllä olevaa riviä suoritettaessa esitellään ensin muuttuja pituus, johon asetetaan esittelyn yhteydessä arvo 179.

Kaikki tieto esiintyy oikeasti tietokoneen sisällä jonona bittejä, eli lukuja nolla ja yksi. Muuttujat ovat ohjelmointikielten tarjoama abstraktio, jolla voidaan käsitellä erilaisia arvoja helpommin. Muuttujia käytetään arvojen säilyttämiseen ja ohjelman tilan ylläpitoon. Javassa käytössämme on muun muassa alkeisarvoiset muuttujatyypit int (kokonaisluku), double (liukuluku), boolean (totuusarvo), char (merkki) sekä viitetyyppiset muuttujatyypit String (merkkijono), ArrayList (taulukko) ja kaikki luokat. Palaamme alkeistyyppisiin ja viitetyyppisiin muuttujiin ja niiden eroihin tarkemmin myöhemmin.

34.2 Muuttujien vertaileminen ja syötteen lukeminen

Ohjelmien toiminnallisuus rakennetaan kontrollirakenteiden avulla. Kontrollirakenteet mahdollistavat erilaiset toiminnot ohjelman muuttujien arvoista riippuen. Alla esimerkki if-else if-else -kontrollirakenteesta, jossa tehdään erilainen toiminto vertailun tuloksesta riippuen. Esimerkissä tulostetaan merkkijono Kaasua jos muuttujan nopeus arvo on pienempi kuin 110, merkkijono Jarrua jos muuttujan nopeus arvo on suurempi kuin 120 ja merkkijono Kruisaillaan muissa tapauksissa.

int nopeus = 105;

if(nopeus < 110) {
    System.out.println("Kaasua");
} else if (nopeus > 120) {
    System.out.println("Jarrua");
} else {
    System.out.println("Kruisaillaan");
}

Koska yllä olevassa esimerkissä muuttujan nopeus arvo on 105, tulostaa ohjelma aina merkkijonon Kaasua. Muistathan että merkkijonojen yhtäsuuruusvertailu tapahtuu merkkijonoon liittyvällä equals-metodilla. Alla on esimerkki jossa käytetään Javan Scanner-luokasta luotua olioita käyttäjän kirjoittaman syötteen lukemiseen. Ohjelma tarkistaa ovatko käyttäjän syöttämät merkkijonot samat.

Scanner lukija = new Scanner(System.in);

System.out.print("Syötä ensimmäinen merkkijono: ");
String ensimmainen = lukija.nextLine();

System.out.print("Syötä toinen merkkijono: ");
String toinen = lukija.nextLine();

System.out.println();

if (ensimmainen.equals(toinen)) {
    System.out.println("Kirjoittamasi merkkijonot ovat samat!");
} else {
    System.out.println("Kirjoittamasi merkkijonot eivät olleet samat!");
}

Ohjelman toiminta riippuu käyttäjän syötteestä. Alla esimerkki, punaisella värillä tarkoitetaan käyttäjän kirjoittamaa syötettä.

Syötä ensimmäinen merkkijono: porkkana
Syötä toinen merkkijono: salaatti

Kirjoittamasi merkkijonot eivät olleet samat!

34.3 Toistolausekkeet

Ohjelmissa tarvitaan usein toistoa. Tehdään ensin ns. while-true-break -toistolause, jota jatketaan niin pitkään kunnes käyttäjä syöttää merkkijonon salasana. Lause while(true) aloittaa toistolauseen, jota jatketaan kunnes kohdataan avainsana break.

Scanner lukija = new Scanner(System.in);

while(true) {
    System.out.print("Syötä salasana: ");
    String salasana = lukija.nextLine();

    if(salasana.equals("salasana")) {
        break;
    }
}

System.out.println("Kiitos!");
Syötä salasana: porkkana
Syötä salasana: salasana
Kiitos

While-toistolausekkeeseen voi asettaa totuusarvon true sijaan myös vertailun. Alla tulostetaan käyttäjän syöttämä merkkijono siten, että sillä on ylä- ja alapuolella tähtiä.

Scanner lukija = new Scanner(System.in);

System.out.print("Syötä merkkijono: ");
String merkkijono = lukija.nextLine();

int tahdenNumero = 0;
while(tahdenNumero < merkkijono.length()) {
    System.out.print("*");
    tahdenNumero = tahdenNumero + 1;
}
System.out.println();

System.out.println(merkkijono);

tahdenNumero = 0;
while(tahdenNumero < merkkijono.length()) {
    System.out.print("*");
    tahdenNumero = tahdenNumero + 1;
}
System.out.println();
Syötä merkkijono: porkkana
********
porkkana
********

Yllä olevan esimerkin pitäisi nostattaa kylmiä väreitä selkäpiissäsi. Kylmät väreet toivottavasti johtuvat siitä, että huomaat esimerkin rikkovan ohjelmoinnin perusteissa opittuja käytänteitä. Esimerkissä on turhaa toistoa joka tulee poistaa metodien avulla.

While-toistolauseen lisäksi käytössämme on myös for-toistolauseen kaksi versiota. Uudempaa for-lauseketta käytetään listojen läpikäyntiin.

ArrayList<String> tervehdykset = new ArrayList<>();
tervehdykset.add("Hei");
tervehdykset.add("Hallo");
tervehdykset.add("Hi");

for(String tervehdys: tervehdykset) {
    System.out.println(tervehdys);
}
Hei
Hallo
Hi

Perinteisempää for-lausetta käytetään samankaltaisissa tilanteissa kuin while-toistolausetta. Sitä voidaan käyttää esimerkiksi taulukoiden läpikäyntiin. Seuraavassa esimerkissä kerrotaan jokaisen taulukon luvut alkion sisältö kahdella ja lopuksi tulostetaan luvut uudempaa for-lausetta käyttäen.

int[] luvut = new int[] {1, 2, 3, 4, 5, 6};

for (int i = 0; i < luvut.length; i++) {
    luvut[i] = luvut[i] * 2;
}

for (int luku : luvut) {
    System.out.println(luku);
}
2
4
6
8
10
12

Perinteinen for-lauseke on erittäin hyödyllinen tapauksissa joissa käymme indeksejä yksitellen läpi. Alla oleva toistolauseke käy merkkijonon merkit yksitellen läpi, ja tulostaa merkkijono Hip! aina kun törmäämme merkkiin a.

String merkkijono = "saippuakauppias";
for (int i = 0; i < merkkijono.length(); i++) {
    if (merkkijono.charAt(i) == 'a') {
        System.out.println("Hip!");
    }
}
Hip!
Hip!
Hip!
Hip!

34.4 Metodit

Metodit ovat tapa pilkkoa ohjelman toiminnallisuutta pienempiin osakokonaisuuksiin. Kaikkien Java-ohjelmien suoritus alkaa pääohjelmametodista, joka määritellään lauseella public static void main(String[] args). Lause määrittelee staattisen, eli luokkaan liittyvän metodin, joka saa parametrina merkkijonotaulukon.

Ohjelmoija määrittelee metodeja ohjelman sisältävien toiminnallisuuksien abstrahoimiseksi. Ohjelmoidessa tulee tavoitella tilannetta, jossa ohjelmaa voidaan katsoa korkeammalta tasolta, jolloin pääohjelma koostuu joukosta itse määriteltyjen, hyvin nimettyjen metodien kutsuja. Metodit taas tarkentavat ohjelman toiminnallisuutta ja ehkä toteuttavat oman toiminnallisuutensa kutsumalla joitain muita metodeja.

Metodit joiden määrittelyssä on sana static, liittyvät metodin sisältävään luokkaan ja toimivat ns. apumetodeina. Metodit joiden määrittelyssä ei ole sanaa static liittyvät luokasta tehtyihin ilmentymiin, eli olioihin, ja voivat muokata yksittäisten olioiden sisäistä tilaa.

Metodilla on aina näkyvyysmääre (public, näkyy kaikille, private, näkyy vain luokan sisällä), paluutyyppi (void, ei palauta arvoa) sekä metodin nimi. Seuraavassa luodaan luokkaan liittyvä metodi public static void tulosta(String merkkijono, int kertaa), joka tulostaa merkkijonon halutun määrän kertoja. Käytetään tällä kertaa metodia System.out.print, joka toimii muuten samoin kuin System.out.println, mutta ei tulosta rivinvaihtoa.

public static void tulosta(String merkkijono, int kertaa) {
    for (int i = 0; i < kertaa; i++) {
        System.out.print(merkkijono);
    }
}

Yllä oleva metodi tulostaa parametrina annetun merkkijonon niin monta kertaa kuin parametrina annettu kokonaisluku kertoo.

Toistolausekkeisiin liittyvässä luvussa huomasimme että koodi sisälsi tähtien tulostamisen osalta ikävästi "copypastea". Metodien avulla voimme siirtää tähtien tulostamisen erillisen metodiin. Luodaan metodi public static void tulostaTahtia(int kertaa), joka tulostaa parametrina annetun määrän tähtiä. Metodi käyttää for-toistolausetta while:n sijaan.

public static void tulostaTahtia(int kertaa) {
    for(int i = 0; i < kertaa; i++) {
        System.out.print("*");
    }
    System.out.println();
}

Metodia hyödyntäessä aiemmin kauhistusta aihettanut esimerkkimme näyttää seuraavalta.

Scanner lukija = new Scanner(System.in);

System.out.print("Syötä merkkijono: ");
String merkkijono = lukija.nextLine();

tulostaTahtia(merkkijono.length());
System.out.println(merkkijono);
tulostaTahtia(merkkijono.length());

34.5 Luokka

Metodit toimivat ohjelman abstrahoimisessa tiettyyn pisteeseen asti, mutta ohjelmien kasvaessa suuremmiksi, ohjelmia halutaan pilkkoa vielä pienempiin ja loogisempiin kokonaisuuksiin. Luokkien avulla voimme määritellä ohjelmaan korkeamman tason käsitteitä ja käsitteisiin liittyviä toiminnallisuuksia. Jokainen Java-ohjelma vaatii toimiakseen luokan, eli yllä rakennettu Hei maailma! -esimerkki ei toimisi ilman luokkamäärittelyä. Luokka määritellään avainsanoilla public class LuokanNimi.

public class HeiMaailma {
    public static void main(String[] args) {
        System.out.println("Hei maailma!");
    }
}

Luokkia siis käytetään ohjelmassa esiintyvien käsitteiden ja niihin liittyvien toiminnallisuuksien määrittelyyn. Luokista voidaan luoda olioita, jotka ovat luokan ilmentymiä. Jokaisella tiettyyn luokkaan liittyvällä oliolla on sama rakenne, mutta oliohin liittyvien muuttujien arvot voivat olla erilaiset. Olioiden metodit käsittelevät olioiden tilaa, eli sisäisiä muuttujia.

Tutkitaan alla olevaa luokkaa Kirja, jolla on oliomuuttujat nimi (merkkijono) ja julkaisuvuosi (kokonaisluku).

public class Kirja {
    private String nimi;
    private int julkaisuvuosi;

    public Kirja(String nimi, int julkaisuvuosi) {
        this.nimi = nimi;
        this.julkaisuvuosi = julkaisuvuosi;
    }

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

    public int getJulkaisuvuosi() {
        return this.julkaisuvuosi;
    }
}

Alussa oleva määritelmä public class Kirja kertoo luokan nimen. Tätä seuraa oliomuuttujien määrittely. Oliomuuttujat ovat muuttujia, joista jokaisella luokan oliolla on oma toisista riippumaton kopionsa. On yleensä tarkoituksenmukaista piilottaa oliomuuttujat luokan käyttäjiltä, eli määritellä niille näkyvyysmääre private. Jos näkyvyysmääreeksi asetetaan public pääsee olion käyttäjä suoraan käsiksi oliomuuttujiin.

Luokasta luodaan olioita konstruktorilla. Konstruktori on metodi, joka alustaa olion (eli luo oliolle siihen liittyvät muuttujat) ja suorittaa konstruktorin sisällä olevat käskyt. Konstruktori on aina samanniminen kuin konstruktorin sisältävä luokka. Konstruktorissa public Kirja(String nimi, int julkaisuvuosi) luodaan uusi olio luokasta Kirja ja asetetaan siihen liittyviin muuttujiin parametrina annetut arvot.

Ylläolevalle luokalle on määritelty myös kaksi olioiden tietoja käsittelevää metodia. Metodi public String getNimi() palauttaa käsiteltävän olion nimen. Metodi public int getJulkaisuvuosi() palauttaa käsiteltävän olion julkaisuvuoden.

34.6 Olio

Olioita luodaan luokkaan määritellyn konstruktorin avulla. Ohjelmakoodissa konstruktoria kutsutaan new-käskyllä, joka palauttaa viitteen uuteen olioon. Oliot ovat luokista tehtyjä ilmentymiä. Tutkitaan ohjelmaa, joka luo kaksi eri kirjaa, jonka jälkeen tulostetaan olioihin liittyvien metodien getNimi palauttamat arvot.

Kirja jarkiJaTunteet = new Kirja("Järki ja tunteet", 1811);
Kirja ylpeysJaEnnakkoluulo = new Kirja("Ylpeys ja ennakkoluulo", 1813);

System.out.println(jarkiJaTunteet.getNimi());
System.out.println(ylpeysJaEnnakkoluulo.getNimi());
Järki ja tunteet
Ylpeys ja ennakkoluulo

Jokaisella oliolla on siis oma sisäinen tila. Tila muodostuu olioon liittyvistä oliomuuttujista. Oliomuuttujat voivat olla sekä alkeistyyppisiä muuttujia että viitetyyppisiä muuttujia. Jos olioon liittyy viitetyyppisiä muuttujia, voi olla että muutkin oliot viittaavat samoihin olioihin! Visualisoidaan tämä pankkiesimerkillä, jossa on tilejä ja henkilöitä.

public class Tili {
    private String tilitunnus;
    private int saldoSentteina;

    public Tili(String tilitunnus) {
        this.tilitunnus = tilitunnus;
        this.saldoSentteina = 0;
    }

    public void pane(int summa) {
        this.saldoSentteina += summa;
    }

    public int getSaldoSentteina() {
        return this.saldoSentteina;
    }

    // .. muita tiliin liittyviä metodeja
}
import java.util.ArrayList;

public class Henkilo {
    private String nimi;
    private ArrayList<Tili> tilit;

    public Henkilo(String nimi) {
        this.nimi = nimi;
        this.tilit = new ArrayList<>();
    }

    public void lisaaTili(Tili tili) {
        this.tilit.add(tili);
    }

    public int rahaaYhteensa() {
        int yhteensa = 0;
        for (Tili tili: this.tilit) {
            yhteensa += tili.getSaldoSentteina();
        }

        return yhteensa;
    }

    // ... muita henkilöön liittyviä metodeja
}

Jokaisella Henkilo-luokasta tehdyllä oliolla on oma nimi sekä oma lista tileistä. Luodaan seuraavaksi kaksi henkilöä ja kaksi tiliä. Toinen tileistä on vain yhden henkilön oma, toinen yhteinen.

Henkilo matti = new Henkilo("Matti");
Henkilo maija = new Henkilo("Maija");

Tili palkkatili = new Tili("NORD-LOL");
Tili kotitaloustili = new Tili("SAM-LOL");

matti.lisaaTili(palkkatili);
matti.lisaaTili(kotitaloustili);
maija.lisaaTili(kotitaloustili);

System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa());
System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa());
System.out.println();

palkkatili.pane(150000);

System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa());
System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa());
System.out.println();

kotitaloustili.pane(10000);

System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa());
System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa());
System.out.println();
Matin tileillä rahaa: 0
Maijan tileillä rahaa: 0

Matin tileillä rahaa: 150000
Maijan tileillä rahaa: 0

Matin tileillä rahaa: 160000
Maijan tileillä rahaa: 10000

Ensin kummankin henkilön tilit ovat tyhjiä. Kun palkkatilille, johon oliolla matti on viite, lisätään rahaa, kasvaa Matin tileillä oleva rahamäärä. Kun kotitaloustilille lisätään rahaa, kasvaa kummankin henkilön rahamäärä. Tämä johtuu siitä että sekä Matilla että Maijalla on "oikeus" kotitaloustilille, eli kummakin omassa oliomuuttujassa tilit on viite kotitaloustiliin. Palaamme alkeis- ja viitetyyppisten muuttujien eroon tarkemmin myöhemmin.

34.7 Ohjelmien rakenteesta

Ohjelmista tulee olla selkeitä niin ohjelmoijalle kuin muille ohjelmaa tarkasteleville. Selkeys saadaan aikaan sopivalla luokkarakenteella ja hyvien nimeämiskäytänteiden seuraamisella. Jokaisella luokalla tulee olla selkeä 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 metodilla 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. Hyvät ohjelmoijat ohjelmoivat koodia, jota he ja heidän työkaverinsa ymmärtävät myös viikkoja koodin kirjoittamisen jälkeenkin.

Ymmärrettävään koodiin liittyy niin kuvaava muuttujien, metodien ja luokkien nimeäminen kuin ohjelmakoodin ilmavuus ja johdonmukainen sisentäminen. Tutkitaan seuraavaa esimerkkiä käyttöliittymästä, jossa käyttäjä voi ostaa ja myydä esineitä. Vaikka esimerkissä ainoat ostettavat ja myytävät asiat ovat porkkanoita, eikä niistä pidetä kirjaa, voisi käyttöliittymää laajentaa esimerkiksi siten että sille annettaisiin myytävien tavaroiden varasto konstruktorin parametrina.

public class Kayttoliittyma {
    private Scanner lukija;

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

    public void kaynnista() {
        while (true) {
            String komento = lukija.nextLine();

            if (komento.equals("lopeta")) {
                break;
            } else if (komento.equals("osta")) {
                String luettu = null;
                while(true) {
                    System.out.print("Mitä ostetaan: ");
                    luettu = lukija.nextLine();
                    if(luettu.equals("porkkana") {
                        break;
                    } else {
                        System.out.println("Ei löydy!");
                    }
                }

                System.out.println("Ostettu!");
            } else if (komento.equals("myy")) {
                String luettu = null;
                while(true) {
                    System.out.print("Mitä myydään: ");
                    luettu = lukija.nextLine();
                    if(luettu.equals("porkkana") {
                        break;
                    } else {
                        System.out.println("Ei löydy!");
                    }
                }

                System.out.println("Myyty!");
            }
        }
    }
}

Huomaamme esimerkissä heti monta ongelmakohtaa. Ensimmäinen pulma liittyy kaynnista-metodin sisällön suuruuteen. Huomaamme että metodia voi pienentää siirtämällä muiden komentojen kuin lopeta-komennon käsittelyn erilliseen metodiin.

public class Kayttoliittyma {
    private Scanner lukija;

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

    public void kaynnista() {
        while (true) {
            String komento = lukija.nextLine();

            if (komento.equals("lopeta")) {
                break;
            } else {
                hoidaKomento(komento);
            }
        }
    }

    public void hoidaKomento(String komento) {
        if (komento.equals("osta")) {
            String luettu = null;
            while(true) {
                System.out.print("Mitä ostetaan: ");
                luettu = lukija.nextLine();
                if(luettu.equals("porkkana") {
                    break;
                } else {
                    System.out.println("Ei löydy!");
                }
            }

            System.out.println("Ostettu!");
        } else if (komento.equals("myy")) {
            String luettu = null;
            while(true) {
                System.out.print("Mitä myydään: ");
                luettu = lukija.nextLine();
                if(luettu.equals("porkkana") {
                    break;
                } else {
                    System.out.println("Ei löydy!");
                }
            }

            System.out.println("Myyty!");
        }
    }
}

Metodissa hoidaKomento on vielä toisteisuutta syötteen lukemiseen liittyen. Huomaamme että lukemisessa toistuu aina muutama merkkijono. Kun ostetaan kysytään "Mitä ostetaan: ", kun myydään kysytään "Mitä myydään: ". Molemmat lukemiskohdat odottavat merkkijonoa "porkkana" ja tulostavat "Ei löydy!" jos käyttäjä ei syötä merkkijonoa "Porkkana". Luodaan tätä varten erillinen metodi public String lueKayttajalta(String kysymys), joka kysyy käyttäjältä haluttua merkkijonoa. Huomaa että jos käyttöliittymässämme olisi käytössä jonkinlainen varastonhallintaolio, vertaisimme sen sisältämiä esineitä käyttäjän syötteeseen porkkanan sijasta.

public class Kayttoliittyma {
    private Scanner lukija;

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

    public void kaynnista() {
        while (true) {
            String komento = lukija.nextLine();

            if (komento.equals("lopeta")) {
                break;
            } else {
                hoidaKomento(komento);
            }
        }
    }

    public void hoidaKomento(String komento) {
        if (komento.equals("osta")) {
            String luettu = lueKayttajalta("Mitä ostetaan: ");
            System.out.println("Ostettu!");
        } else if (komento.equals("myy")) {
            String luettu = lueKayttajalta("Mitä myydään: ");
            System.out.println("Myyty!");
        }
    }

    public String lueKayttajalta(String kysymys) {
        while(true) {
            System.out.print(kysymys);
            String luettu = lukija.nextLine();

            if(luettu.equals("porkkana") {
                return luettu;
            } else {
                System.out.println("Ei löydy!");
            }
        }
    }
}

Ohjelma on nyt pilkottu sopiviksi osiksi. Huomaamme kuitenkin että kaynnista-metodin yhteydessä on turha else-haara. Jos ohjelman suoritus päätyy if-haaraan, suoritetaan komento break ja poistutaan toistolauseesta. Voimme siis poistaa else-haaran jolloin hoidaKomento-metodin suorittaminen on omalla rivillään. Samanlainen tilanne on myös metodissa lueKayttajalta. Siistitään se myös samalla.

public class Kayttoliittyma {
    private Scanner lukija;

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

    public void kaynnista() {
        while (true) {
            String komento = lukija.nextLine();

            if (komento.equals("lopeta")) {
                break;
            }

            hoidaKomento(komento);
        }
    }

    public void hoidaKomento(String komento) {
        if (komento.equals("osta")) {
            String luettu = lueKayttajalta("Mitä ostetaan: ");
            System.out.println("Ostettu!");
        } else if (komento.equals("myy")) {
            String luettu = lueKayttajalta("Mitä myydään: ");
            System.out.println("Myyty!");
        }
    }

    public String lueKayttajalta(String kysymys) {
        while(true) {
            System.out.print(kysymys);
            String luettu = lukija.nextLine();

            if(luettu.equals("porkkana") {
                return luettu;
            }

            System.out.println("Ei löydy!");
        }
    }
}

Yllä kuvaavaamme ohjelman pilkkomista pienempiin osiin kutsutaan refaktoroinniksi. Refaktoroinnissa ohjelman toiminta pysyy samana, mutta sisäinen rakenne muuttuu selkeämmäksi ja ylläpidettävämmäksi. Nykyinen versiomme on huomattavasti selkeämpi alkuperäiseen ohjelmaan verrattuna. Ohjelmassa on toki vieläkin parannettavaa. Esimerkiksi metodin hoidaKomento voi pilkkoa ostamis- ja myymistoiminnallisuutta hoitaviin metodeihin.

public class Kayttoliittyma {
    private Scanner lukija;

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

    public void kaynnista() {
        while (true) {
            String komento = lukija.nextLine();

            if (komento.equals("lopeta")) {
                break;
            }

            hoidaKomento(komento);
        }
    }

    public void hoidaKomento(String komento) {
        if (komento.equals("osta")) {
            komentoOsta();
        } else if (komento.equals("myy")) {
            komentoMyy();
        }
    }

    public void komentoOsta() {
        String luettu = lueKayttajalta("Mitä ostetaan: ");
        System.out.println("Ostettu!");
    }

    public void komentoMyy() {
        String luettu = lueKayttajalta("Mitä myydään: ");
        System.out.println("Myyty!");
    }

    public String lueKayttajalta(String kysymys) {
        while(true) {
            System.out.print(kysymys);
            String luettu = lukija.nextLine();

            if(luettu.equals("porkkana") {
                return luettu;
            }

            System.out.println("Ei löydy!");
        }
    }
}

Nyt ohjelma on rakenteeltaan riittävän selkeä, metodit ovat kuvaavasti nimettyjä ja jokaisella metodilla on oma pieni tehtävä, josta metodi huolehtii. Huomaa että emme lisänneet refaktoroidessa ohjelmaan uutta toiminnallisuutta, muokkasimme vain rakennetta selkeämmäksi.

34.8 Ohjelmoinnista ja ennenkaikkea harjoittelun tärkeydestä

Tietääksemme kukaan ei ole oppinut ohjelmoimaan luentoja kuuntelemalla. Ohjelmointitaidon kehittymisen kannalta harjoittelu ja kertaaminen on tärkeää. Ohjelmointitaitoa on verrattu niin kielten puhumiseen kuin instrumentin soittamiseen, kummassakin kehittyy vain harjoittelemalla. Oikeastaan, esimerkiksi hyvät viulistit eivät ole hyviä vain sen takia, että he ovat harjoitelleet paljon. Harjoittelua motivoi myös se, että se on hauskaa. Samaa voi sanoa ohjelmoijista.

Linus Torvaldsin sanoin "Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.".

Tohtori Luukkainen on kirjoittanut listan jota kannattaa seurata ohjelmoidessa ja siinä kehittyessä. Seuraa listan neuvoja kunnes osaat ne unissasikin.

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

34.9 Näkyvyysmääreet

Olemme tähän mennessä käyttäneet kahta erilaista näkyvyyteen liittyvää avainsanaa metodien ja oliomuuttujien määrittelyssä. Avainsana public asettaa metodit ja muuttujat kaikille näkyviksi. Esimerkiksi luokan metodit ja konstruktorit merkitään usein määreellä public, jolloin niitä voi kutsua luokan ulkopuolelta.

Avainsana private taas piilottaa metodit ja muuttujat luokan sisälle. Metodia, jolla on määre private, ei pysty kutsumaan luokan ulkopuolelta.

public class Kirja {
    private String nimi;
    private String sisalto;

    public Kirja(String nimi, String sisalto) {
        this.nimi = nimi;
        this.sisalto = sisalto;
    }

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

    public String getSisalto() {
        return this.sisalto;
    }

    // ...
}

Yllä olevasta Kirja-luokasta luotujen olioiden tietoihin pääsee käsiksi vain kirjan julkisten metodien kautta. Private-määreellä merkityt oliomuuttujat ovat näkyvillä ja käsiteltävissä vain luokan sisäisessä koodissa. Jos kirjalla olisi private-määreellinen metodi, ei sitäkään voisi käyttää muualta kuin Kirja-luokan sisältä.

Sitten itse asiaan, eli harjoitteluun!

Tehtävä 126: Hymiöt

Huom! Tehtävät löytyvät TMC:stä kurssin k2015-ohja alta. Palvelimen osoitteena käytetään https://tmc.mooc.fi/hy

Laadi tehtavapohjan mukana tulevalle luokalle Hymiot apumetodi private static void tulostaHymioityna(String merkkijono). Metodin tulee tulostaa annettu merkkijono hymiöillä ympyröitynä. Käytä hymiönä merkkijonoa :).

tulostaHymioityna("\\:D/");
:):):):):)
:) \:D/ :)
:):):):):)

Huomaa, että merkkijonoon on kirjoitettava \\ jotta saadaan tulostumaan merkki \.

Huom! Jos merkkijonon pituus on pariton, tulee ylimääräinen välilyönti lisätä annetun merkkijonon oikealle puolelle.

tulostaHymioityna("\\:D/");
tulostaHymioityna("87.");
:):):):):)
:) \:D/ :)
:):):):):)
:):):):):)
:) 87.  :)
:):):):):)

Kannattaa ensin miettiä montako hymiötä minkäkin pituiselle merkkijonolle tulee tulostaa. Merkkijonon pituuden saa selville siihen liittyvällä length-metodilla. Ala- ja ylärivin hymiöiden tulostamiseen auttaa toistolause, keskimmäisellä rivillä selviät normaalilla tulostuskomennolla. Pituuden parittomuuden voit tarkistaa jakojäännöksen avulla merkkijono.length() % 2 == 1.

Tehtävä 127: Merkkijonomuuntaja

Tässä tehtävässä luodaan merkkijonomuuntaja, joka koostuu kahdesta luokasta. Luokka Muunnos muuttaa yksittäiset merkit toiseksi, Muuntaja sisältää joukon Muunnoksia ja muuttaa merkkijonoja sisältämiensä Muunnos-olioiden avulla.

127.1 Muunnos-luokka

Luo luokka Muunnos, jolla on seuraavat toiminnot:

  • konstruktori public Muunnos(char muunnettava, char muunnettu) luo olion joka tekee muunnoksia merkiltä muunnettava merkille muunnettu
  • metodi public String muunna(String merkkijono) palauttaa muunnetun version annetusta merkkijonosta

Luokkaa käytetään seuraavalla tavalla:

String sana = "porkkana";
Muunnos muunnos1 = new Muunnos('a', 'b');
sana = muunnos1.muunna(sana);

System.out.println(sana);

Muunnos muunnos2 = new Muunnos('k', 'x');
sana = muunnos2.muunna(sana);

System.out.println(sana);

Yllä oleva esimerkki tulostaisi:

porkkbnb
porxxbnb

Vihje: voit tehdä merkkien korvaamisen kahdella tavalla, joko suoraan hyödyntäen luokan String metodia (etsi metodi itse!) tai käymällä merkkijonon läpi merkki merkiltä ja samalla muodostaen muunnetun merkkijonon.

Jos et käytä Stringin valmista metodia, niin kannattaa muistaa, että vaikka merkkijonoja vertaillaankin komennolla equals yksittäisiä merkkejä vertaillaan operaattorin == avulla:

String sana = "porkkana";

String korvattuAat = "";
for (int i = 0; i < sana.length(); i++) {
    char merkki = sana.charAt(i);
    if ( merkki == 'a' ) {
       korvattuAat += '*'
    }  else {
       korvattuAat += merkki;
    }
}

System.out.println(korvattuAat);  // tulostu porkk*n*

127.2 Muuntaja-luokka

Luo luokka Muuntaja, jolla on seuraavat toiminnot:

  • konstruktori public Muuntaja() luo uuden muuntajan
  • metodi public void lisaaMuunnos(Muunnos muunnos) lisää uuden muunnoksen muuntajaan
  • metodi public String muunna(String merkkijono) suorittaa merkkijonolle kaikki lisätyt muunnokset niiden lisäysjärjestyksessä ja palauttaa muunnetun merkkijonon

Luokkaa käytetään seuraavalla tavalla:

Muuntaja skanditPois = new Muuntaja();
skanditPois.lisaaMuunnos(new Muunnos('ä', 'a'));
skanditPois.lisaaMuunnos(new Muunnos('ö', 'o'));
System.out.println(skanditPois.muunna("ääliö älä lyö, ööliä läikkyy"));

Yllä oleva esimerkki tulostaisi:

aalio ala lyo, oolia laikkyy

Vihje: Muunnokset kannattaa tallettaa Muuntajan oliomuuttujana olevalle listalle (samaan tyyliin kuin peruskurssilla talletettiin esim. pelaajat joukkueeseen, puhelinnumerot puhelinluetteloon tai kirjat kirjastoon). Muunnos suoritetaan siten, että muunnettavalle merkkijonolle suoritetaan muunnokset yksi kerrallaan seuraavan esimerkin tapaan:

ArrayList<Muunnos> muunnokset = new ArrayList<>();

muunnokset.add(new Muunnos('a', 'b'));
muunnokset.add(new Muunnos('k', 'x'));
muunnokset.add(new Muunnos('o', 'å'));

String sana = "porkkana";

for (Muunnos muunnos : muunnokset) {
    sana = muunnos.muunna(sana);
}

System.out.println(sana);  // tulostuu pårxxbnb

MUISTUTUS kun lisäät ohjelmaasi ArrayList:in, Scanner:in tai Random:in ei Java tunnista luokkaa ellet "importoi" sitä lisäämällä ohjelmatiedoston alkuun:

import java.util.ArrayList;    // importoi ArrayListin
import java.util.*;            // importoi kaikki java.util:sissa olevat työkalut, mm. ArrayListin, Scannerin ja Randomin

Tehtävä 128: Laskin

Teemme tässä tehtävässä samantyylisen yksinkertaisen laskimen, joka oli jo ohjelmoinnin perusteiden viikon 1 materiaalissa. Tällä kertaa kiinnitämme kuitenkin huomiota ohjelman rakenteeseen. Erityisesti teemme main-metodista eli pääohjelmasta hyvin kevyen. Pääohjelmametodi ei tee oikeastaan mitään muuta kun käynistää ohjelman:

public class Paaohjelma {
    public static void main(String[] args) {
        Laskin laskin = new Laskin();
        laskin.kaynnista();
    }
}

Pääohjelma siis ainoastaan luo varsinaisen sovelluslogiikan toteuttavan olion ja käynnistää sen. Tämä on oikea tyyli tehdä ohjelmia ja tulemme jatkossa usein pyrkimään tähän rakenteeseen.

128.1 Lukija

Kommunikoidakseen käyttäjän kanssa laskin tarvitsee Scanner-olion. Kuten olemme huomanneet, on kokonaislukujen lukeminen scannerilla hieman työlästä. Teemme nyt erillisen luokan Lukija joka kapseloi sisälleen Scanner-olion.

Toteuta luokka Lukija ja lisää sille metodit

  • public String lueMerkkijono()
  • public int lueKokonaisluku()

Lukijan sisällä tulee olla oliomuuttujana Scanner-olio jota metodit käyttävät ohjelmoinnin perusteista tuttuun tyyliin. Muistathan että kokonaislukujen lukemisessa kannattaa ensin lukea koko rivi, jonka jälkeen rivi tulee muuttaa kokonaisluvuksi. Tässä on hyödyksi Integer-luokan metodi parseInt.

128.2 Sovellusrunko

Laskin toimii seuraavan esimerkin mukaan:

komento: summa
luku1: 4
luku2: 6
lukujen summa 10

komento: tulo
luku1: 3
luku2: 2
lukujen tulo 6

komento: lopetus

Tee ohjelmaasi sovelluslogiikasta huolehtiva luokka Laskin ja sille metodi public void kaynnista() jonka sisältö on täsmälleen seuraava:

public void kaynnista() {
    while (true) {
        System.out.print("komento: ");
        String komento = lukija.lueMerkkijono();
        if (komento.equals("lopetus")) {
            break;
        }

        if (komento.equals("summa")) {
            summa();
        } else if (komento.equals("erotus")) {
            erotus();
        } else if (komento.equals("tulo")) {
            tulo();
        }
    }

    statistiikka();
}

Laskimellamme on operaatiot summa, erotus, tulo.

Tee valmiiksi rungot metodeille summa, erotus, tulo ja statistiikka. Kaikkien tulee olla tyyppiä private void, eli metodit ovat vain laskimen sisäisessä käytössä.

Lisää laskimelle oliomuuttuja jonka tyyppi on Lukija, ja luo lukija konstruktorissa. Laskimessa ei saa olla erikseen Scanner-tyyppistä muuttujaa!

128.3 Sovelluslogiikan toteutus

Toteuta nyt metodit summa, erotus ja tulo siten, että ne toimivat yllä olevan esimerkin mukaan. Esimerkissä kysytään aina ensin komento, jonka jälkeen kysytään 2 lukua käyttäjältä, suoritetaan haluttu operaatio, ja tulostetaan operaation arvo. Huomaa, että lukujen kysyminen tapahtuu metodiensumma, erotus ja tulo sisällä! Metodit käyttävät lukujen kysymiseen lukija-olioa. Metodien runko on siis seuraavanlainen:

private void summa() {
   System.out.print("luku1: ");
   int luku1 = // luetaan luku käyttäen lukija-olioa
   System.out.print("luku2: ");
   int luku2 = // luetaan luku käyttäen lukija-olioa
   // tulostetaan tulos ylläolevan esimerkin tapaan
}

128.4 Statistiikka

Metodissa kaynnista olevan while-silmukan jälkeen kutsutaan metodia statistiikka. Metodin on tarkoitus tulostaa kuinka montaa kertaa laskin-oliolla suoritettiin joku laskutoimenpide. Esim:

komento: summa
luku1: 4
luku2: 6
lukujen summa 10

komento: tulo
luku1: 3
luku2: 2
lukujen tulo 6

komento: lopetus
Laskuja laskettiin 2

Toteuta metodi private void statistiikka(), ja tee statistiikan keräämiseen tarvittavat muutokset muualle Laskin-luokan koodiin.

Huom: jos ohjelmalle annetaan virheellinen komento (eli joku muu kuin summa, erotus, tulo, tai lopetus), ei laskin reagoi komentoon millään tavalla vaan jatkaa kysymällä seuraavaa komentoa. Statistiikka ei saa laskea virheellistä komentoa laskutoimenpiteeksi.

komento: integraali
komento: erotus
luku1: 3
luku2: 2
lukujen erotus 1

komento: lopetus
Laskuja laskettiin 1

Bonustehtävä (ei testata): Syötteen lukeminen toistuu samanlaisena kaikissa kolmessa laskuoperaation suorittavassa metodissa. Posta koodistasi toisteisuus sopivan apumetodin avulla. Metodi voi palauttaa käyttäjältä luetut 2 lukua esim. taulukossa.

35 Alkeis- ja viitetyyppiset muuttujat

Java on vahvasti tyypitetty kieli, eli kaikilla sen muuttujilla on tyyppi. Muuttujien tyypit voidaan jakaa kahteen kategoriaan: alkeis- ja viitetyyppisiin muuttujiin. Kummankin kategorian tyypisillä muuttujilla on oma "lokero", joka sisältää niihin liittyvän tiedon. Alkeistyyppisillä muuttujilla lokeroon talletetaan muuttujien konkreettinen arvo, kun taas viitetyyppisten muuttujien lokero sisältää viitteen muuttujaan liittyvään konkreettiseen olioon.

35.1 Alkeistyyppiset muuttujat

Alkeistyyppisen muuttujan arvo tallennetaan muuttujaa varten luotuun lokeroon. Jokaisella alkeistyyppisellä muuttujalla on oma lokero ja oma arvo. Muuttujalle luodaan uusi lokero silloin kun se esitellään (esim. int numero;). Lokeroon asetetaan arvo sijoitusoperaatiolla =. Alla on esimerkki alkeistyyppisen int (kokonaisluku) -muuttujan esittelemisestä ja arvon asettamisesta samassa lausekkeessa.

int numero = 42;

Alkeistyyppisiä muuttujia ovat muun muassa int, double, char, boolean sekä harvemmin käyttämämme short, float, byte ja long. Myös void on alkeistyyppi, mutta sillä ei ole omaa lokeroa tai arvoa. Void-tyyppiä käytetään silloin kun halutaan ilmaista että metodi ei palauta mitään arvoa.

Esitellään seuraavaksi kaksi alkeistyyppistä muuttujaa ja asetetaan niihin arvot.

int vitonen = 5;
int kutonen = 6;

Yllä esiteltyjen alkeistyyppisten muuttujien nimet ovat vitonen ja kutonen. Muuttujaa vitonen luodessa sitä varten luotuun lokeroon asetetaan arvo 5 (int vitonen = 5;). Muuttujaa kutonen luodessa sitä varten luotuun lokeroon asetetaan arvo 6 (int kutonen = 6;). Muuttujat vitonen ja kutonen ovat kumpikin int-tyyppisiä, eli kokonaislukuja.

Alkeistyyppiset muuttujat voi visualisoida laatikkoina joiden sisälle kuhunkin muuttujaan liittyvä arvo on tallennettu:

Tarkastellaan seuraavaksi alkeistyyppisten muuttujien arvojen kopioitumista.

int vitonen = 5;
int kutonen = 6;

vitonen = kutonen; // muuttuja vitonen sisältää nyt arvon 6, eli arvon joka oli muuttujassa kutonen
kutonen = 64; // muuttuja kutonen sisältää nyt arvon 64

// muuttuja vitonen sisältää vieläkin arvon 6

Yllä esitellään muuttujat vitonen ja kutonen ja asetetaan niihin arvot. Tämän jälkeen muuttujan vitonen lokeroon kopioidaan muuttujan kutonen lokeron sisältämä arvo (vitonen = kutonen;). Tässä vaiheessa muuttujan vitonen lokeroon kopioituu muuttujan kutonen sisältämä arvo. Jos muuttujan kutonen arvoa muutetaan tämän jälkeen, ei muuttujan vitonen sisältämä arvo muutu: muuttujan vitonen arvo on sen omassa lokerossa eikä liity muuttujan kutonen lokerossa olevaan arvoon millään tavalla. Lopputilanne kuvana.

Alkeistyyppinen muuttuja metodin parametrina ja paluuarvona

Kun alkeistyyppinen muuttuja annetaan metodille parametrina, asetetaan metodin parametrille kopio annetun muuttujan lokerossa olevasta. Käytännössä myös metodin parametreillä on omat lokerot, joihin arvo kopioidaan kuten asetuslauseessa. Katsotaan seuraavaa metodia lisaaLukuun(int luku, int paljonko).

public int lisaaLukuun(int luku, int paljonko) {
    return luku + paljonko;
}

Metodi lisaaLukuun saa kaksi parametria, kokonaisluvut luku ja paljonko. Metodi palauttaa uuden luvun, joka on annettujen parametrien summa. Tutkitaan vielä metodin kutsumista.

int omaLuku = 10;
omaLuku = lisaaLukuun(omaLuku, 15);
// muuttuja omaLuku sisältää nyt arvon 25

Esimerkissä kutsutaan lisaaLukuun-metodia muuttujalla omaLuku ja arvolla 15. Metodin muuttujiin luku ja paljonko kopioituvat arvot 10, eli muuttujan omaLuku sisältö, ja 15. Metodi palauttaa muuttujien luku ja paljonko summan, eli 10 + 15 = 25.

Huom! Edellisessä esimerkissä muuttujan omaLuku arvo muuttuu ainoastaan koska metodin lisaaLukuun palauttama arvo asetetaan siihen sijoituslausekkeella (omaLuku = lisaaLukuun(omaLuku, 15);). Jos metodin lisaaLukuun kutsu olisi seuraavanlainen, ei muuttujan omaLuku arvo muutu.

int omaLuku = 10;
lisaaLukuun(omaLuku, 15);
// muuttuja omaLuku sisältää vieläkin arvon 10

Minimi- ja maksimiarvot

Eri tietotyypeillä on omat minimi- ja maksimiarvonsa, eli arvot joita pienempiä tai suurempia ne eivät voi olla. Tämä johtuu Javan (ja useimpien ohjelmointikielten) sisäisestä tiedon esitysmuodosta, jossa tietotyyppien koot on ennalta määrätty.

Alla muutama Javan alkeistyyppi ja niiden minimi- ja maksimiarvot

Muuttujatyyppi Selitys Minimiarvo Maksimiarvo
int Kokonaisluku -2 147 483 648 (Integer.MIN_VALUE) 2 147 483 647 (Integer.MAX_VALUE)
long Iso kokonaisluku -9 223 372 036 854 775 808 (Long.MIN_VALUE) 9 223 372 036 854 775 807 (Long.MAX_VALUE)
boolean Totuusarvo true tai false
double Liukuluku Double.MIN_VALUE Double.MAX_VALUE

Pyöristysvirheet

Liukulukuja käyttäessä kannattaa muistaa että liukuluvun arvo on aina arvio oikeasta arvosta. Koska liukuluvun, kuten kaikkien muidenkin alkeistyyppien sisältämä tietomäärä on rajoitettu, voidaan huomata yllättäviäkin pyöristysvirheitä. Esimerkiksi seuraava tilanne.

double eka = 0.39;
double toka = 0.35;
System.out.println(eka - toka);

Esimerkki tulostaa arvon 0.040000000000000036. Ohjelmointikielet tarjoavat usein työkalut liukulukujen tarkempaa käsittelyä varten. Esimerkiksi Javassa on luokka BigDecimal, johon voi asettaa äärettömän pitkiä liukulukuja.

Liukulukuja vertaillessa pyöristysvirheisiin varaudutaan usein vertaamalla arvojen etäisyyttä toisistaan. Esimerkiksi edellisen esimerkin muuttujia käytettäessä vertailu eka - toka == 0.04 ei tuota toivottua tulosta pyöristysvirheen takia.

double eka = 0.39;
double toka = 0.35;

if((eka - toka) == 0.04) {
    System.out.println("Vertailu onnistui!");
} else {
    System.out.println("Vertailu epäonnistui!");
}
Vertailu epäonnistui!

Arvon etäisyyttä jostain luvusta voi tarkastella esimerkiksi seuraavasti. Apufunktio Math.abs palauttaa sille annetun luvun itseisarvon.

double eka = 0.39;
double toka = 0.35;

double etaisyys = 0.04 - (eka - toka);

if(Math.abs(etaisyys) < 0.0001) {
    System.out.println("Vertailu onnistui!");
} else {
    System.out.println("Vertailu epäonnistui!");
}

35.2 Viitetyyppi

Viitetyyppiset muuttujat tallentavat niihin liittyvän tiedon viitteen taakse eli "langan päähän". Viitetyyppisten muuttujien lokerossa on viite tiedon sisältävään paikkaan. Toisin kuin alkeistyyppisillä muuttujilla, viitetyyppisillä muuttujilla ei ole rajoitettua arvoaluetta, koska niiden oikea arvo tai tieto on viitteen takana. Oleellinen ero alkeistyyppisiin muuttujiin on se, että usea viitetyyppinen muuttuja voi viitata samaan olioon.

Tutkitaan kahden viitetyyppisen muuttujan luontia. Esimerkeissä käytetään seuraavaa luokkaa Laskuri:

public class Laskuri {
    private int arvo;

    public Laskuri(int alkuarvo) {  // Konstruktori
        this.arvo = alkuarvo;
    }

    public void kasvataArvoa() {
        this.arvo = this.arvo + 1;
    }

    public int annaArvo() {
        return arvo;
   }
}

Pääohjelma:

Laskuri bonusLaskuri = new Laskuri(5);
Laskuri axeLaskuri = new Laskuri(6);

Esimerkissä luodaan ensin viitetyyppinen muuttuja bonusLaskuri. Komentoa new kutsuessa varataan tila muuttujan tietoa varten, suoritetaan new-kutsua seuraavan konstruktorin koodi, ja palautetaan viite juuri luotuun olioon. . Palautettu viite asetetaan sijoitusoperaattorilla = muuttujaan bonusLaskuri. Sama tapahtuu muuttujalle nimeltä axeLaskuri. Kuvana viitetyyppi kannattaa ajatella siten, että muuttuja sisältää "langan" tai "nuolen", jonka päässä on olio itse. Muuttuja ei siis sisällä oliota, vaan viitteen olioon liittyviin tietoihin.

Tutkitaan seuraavaksi viitetyyppisen muuttujan kopioitumista.

Laskuri bonusLaskuri = new Laskuri(5);
Laskuri axeLaskuri = new Laskuri(6);

bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite,
                           // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6

Viitetyyppistä muuttujaa kopioitaessa (yllä bonusLaskuri = axeLaskuri;) muuttujan viite kopioituu. Yllä muuttujan bonusLaskuri lokeroon kopioituu muuttujan axeLaskuri lokerossa oleva viite. Nyt kummatkin oliot viittaavat samaan paikkaan!

Jatketaan yllä olevaa esimerkkiä ja asetetaan muuttujaan axeLaskuri uusi viite, joka osoittaa kutsulla new Laskuri(10) luotuun olioon.

Laskuri bonusLaskuri = new Laskuri(5);
Laskuri axeLaskuri = new Laskuri(6);

bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite,
                           // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6

axeLaskuri = new Laskuri(10); // muuttujaan axeLaskuri asetetaan uusi viite, joka osoittaa
                              // new Laskuri(10) - kutsulla luotuun Laskuri-olioon

// muuttuja bonusLaskuri sisältää vieläkin viitteen Laskuri-olioon, joka sai konstruktorissaan arvon 6

Esimerkissä tehdään käytännössä samat operaatiot kuin alkeistyyppi-kappaleessa olevassa asetusesimerkissä. Äskeisessä esimerkissä kopioimme viitetyyppisten muuttujien viitteitä, kun taas alkeistyyppisiin muuttujiin liittyvässä esimerkissä kopioimme alkeistyyppien arvoja. Kummassakin tapauksessa siis lokeron sisältö kopioidaan, alkeistyyppisten muuttujien lokero sisältää arvon, viitetyyppisten muuttujien lokero sisältää viitteen.

Edellisen esimerkin lopussa kukaan ei viittaa Laskuriolioon, joka sai arvokseen 5 sen konstruktorissa. Javassa oleva roskienkeruumekanismi käy ajallaan poistamassa tällaiset turhat oliot. Lopputilanne kuvana:

Tarkastellaan vielä kolmatta esimerkkiä, joka näyttää viite- ja alkeistyyppisten muuttujien oleellisen eron.

Laskuri bonusLaskuri = new Laskuri(5);
Laskuri axeLaskuri = new Laskuri(6);

bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite,
                           // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6

axeLaskuri.kasvataArvoa(); // kasvatetaan axeLaskuri-viitteen takana olevan olion arvoa yhdellä

System.out.println(bonusLaskuri.annaArvo());
System.out.println(axeLaskuri.annaArvo());
7
7

Koska asetuksen bonusLaskuri = axeLaskuri; jälkeen bonusLaskuri-muuttuja viittaa samaan olioon kuin axeLaskuri-muuttuja, on kummankin laskurin arvo 7 vaikka kasvatuksia on tehty vain yksi. Tämä johtuu siitä että kummatkin laskurit viittaavat samaan olioon.

Kuvana tilanne on ehkä selkeämpi. Kutsu axeLaskuri.kasvataArvoa() kasvattaa muuttujan axeLaskuri viittaaman olion sisältämää muuttujan arvo arvoa yhdellä. Koska muuttuja bonusLaskuri viittaa samaan olioon, palauttaa kutsu bonusLaskuri.annaArvo() saman muuttujan arvon, jota aiempi kutsu axeLaskuri.kasvataArvoa() kasvatti.

Seuraavassa esimerkissä on kolme viitetyyppistä muuttujaa, jotka kaikki osoittavat samaan Laskuri-olioon.

Laskuri bonus = new Laskuri(5);
Laskuri ihq = bonus;
Laskuri lennon = bonus;

Esimerkissä luodaan vain yksi Laskuri-olio, mutta kaikki kolme Laskuri-tyyppistä muuttujaa osoittavat lopussa siihen. Tällöin kaikki metodikutsut viitteille bonus, ihq ja lennon muokkaavat samaa oliota. Vielä kerran: viitetyyppisiä muuttujia kopioitaessa viitteet kopioituvat. Kuvana:

Katsotaan kopioitumista vielä esimerkillä.

Laskuri bonus = new Laskuri(5);
Laskuri ihq = bonus;
Laskuri lennon = bonus;

lennon = new Laskuri(3);

Kun muuttujan lennon sisältö, eli viite muuttuu, se ei vaikuta muuttujien bonus tai ihq sisältämiin viitteisiin. Muuttujan arvoa asetettaessa muutetaan aina vain muuttujan oman lokeron sisältöä. Kuvana:

Viitetyyppinen muuttuja metodin parametrina

Kun viitetyyppinen muuttuja annetaan parametrina metodille, luodaan metodin parametrille kopio annetusta muuttujan viitteestä. Parametrilla on siis oma lokero, johon viite kopioidaan. Alkeistyyppisistä muuttujista poiketen kopioimme viitteen, emmekä arvoa, eli voimme muokata viitteen takana olevaa oliota myös metodin sisällä. Oletetaan että metodimme on alla esitelty public void lisaaLaskuriin(Laskuri laskuri, int paljonko).

public void lisaaLaskuriin(Laskuri laskuri, int paljonko) {
    for (int i = 0; i < paljonko; i++) {
        laskuri.kasvataArvoa();
    }
}

Metodille lisaaLaskuriin annetaan kaksi parametria, viitetyyppinen muuttuja ja alkeistyyppinen muuttuja. Kumpaankin muuttujaan liittyvän lokeron sisältö kopioidaan metodin parametrien omiin lokeroihin. Viitetyyppiselle parametrille laskuri kopioituu viite ja alkeistyyppiselle parametrille paljonko kopioituu arvo. Metodi kutsuu Laskuri-tyyppisen parametrin metodia kasvataArvoa() paljonko-muuttujan sisältämän arvon määrän. Tutkitaan vielä metodin kutsumista.

int kertoja = 10;

Laskuri bonus = new Laskuri(10);
lisaaLaskuriin(bonus, kertoja);
// muuttujan bonus sisäinen arvo on nyt 20

Esimerkissä kutsutaan lisaaLaskuriin()-metodia muuttujilla bonus ja kertoja. Metodin parametriin laskuri ja paljonko kopioituvat siis viitetyyppisen muuttujan bonus viite, ja alkeistyyppisen muuttujan kertoja arvo 10. Metodi suorittaa metodissa olevalle muuttujalle laskuri paljonko muuttujan määrittelemän määrän kasvataArvoa()-metodikutsuja. Tämä kuvana:

Metodissa on siis pääohjelmasta täysin erilliset muuttujat!

Viitetyyppisestä muuttujasta kopioituu metodin sisäiseen muuttujaan viite, eli metodin sisäinen muuttuja viittaa vieläkin samaan olioon. Alkeistyyppisestä muuttujasta kopioituu arvo, eli metodin sisäisellä muuttujalla on täysin oma arvonsa.

Metodi näkee saman laskurin johon muuttuja bonus viittaa, eli metodin tekemä muutos vaikuttaa suoraan olioon. Alkeistyyppien suhteen tilanne on toinen, eli metodille tulee ainoastaan kopio muuttujan kertoja arvosta. Metodista käsin ei siis voi muuttaa alkeistyyppisten muuttujien arvoja.

Viitetyyppinen muuttuja metodin paluuarvona

Kun metodi palauttaa viitetyyppisen muuttujan, palauttaa se viitteen muualla sijaitsevaan olioon. Metodin palauttaman viitetyyppisen muuttujan voi asettaa muuttujalle samalla tavalla kuin normaalikin asetus tapahtuu, eli yhtäsuuruusmerkin avulla. Katsotaan metodia public Laskuri luoLaskuri(int alkuarvo), joka luo uuden viitetyyppisen muuttujan.

public Laskuri luoLaskuri(int alkuarvo) {
    return new Laskuri(alkuarvo);
}

Metodi luoLaskuri palauttaa metodissa luotuun olioon viittaavan viitteen uusiLaskuri. Uusi olio luodaan aina metodia kutsuttaessa, seuraavassa esimerkissä luomme kaksi erillistä Laskuri-tyyppistä oliota.

Laskuri bonus = luoLaskuri(10);
Laskuri lennon = luoLaskuri(10);

Metodi luoLaskuri luo aina uuden Laskuri-tyyppisen olion. Ensimmäisessä kutsussa, eli kutsussa Laskuri bonus = luoLaskuri(10); asetetaan metodin palauttama viite viitetyyppiseen muuttujaan bonus. Toisessa metodikutsussa luodaan uusi viite, joka asetetaan muuttujaan lennon. Muuttujat bonus ja lennon eivät sisällä samaa viitettä, sillä metodi luo aina uuden olion ja palauttaa viitteen luotuun olioon.

36 Static ja ei-static

Kerrataan ja täsmennetään Ohjelmoinnin perusteiden luvussa 30 käsiteltyä asiaa. Staattisuudella ja ei-staattisisuudella erotetaan se, mihin muuttuja tai metodi liittyy. Staattiset metodit liittyvät aina luokkaan, kun taas ei-staattiset metodit voivat muokata olion omia muuttujia.

36.1 Static, luokkakirjastot ja final

Static-määreen saavat metodit eivät liity olioihin vaan luokkiin. On mahdollista määritellä myös luokkakohtaisia muuttujia lisäämällä muuttujan eteen määre static. Esimerkiksi Integer.MAX_VALUE, Long.MIN_VALUE ja Double.MAX_VALUE ovat kaikki staattisia muuttujia. Staattisia muuttujia ja metodeja käytetään luokan nimen kautta, esimerkiksi LuokanNimi.muuttuja tai LuokanNimi.metodi().

Luokkakirjastoksi kutsutaan luokkaa, jossa on yleiskäyttöisiä metodeja ja muuttujia. Esimerkiksi Javan Math-luokka on luokkakirjasto. Se tarjoaa muun muassa Math.PI-muuttujan. Omien luokkakirjastojen toteuttaminen on usein hyödyllistä. Esimerkiksi Helsingin Seudun Liikenne (HSL) voisi pitää lippujensa hintoja luokkakirjastossa, josta ne löytyisi tarvittaessa.

public class HslHinnasto {
    public static final double KERTALIPPU_AIKUINEN = 2.50;
    public static final double RAITIOVAUNULIPPU_AIKUINEN = 2.50;
}

Avainsana final muuttujan määrittelyssä kertoo ettei muuttujaan voi asettaa uutta arvoa kun se on kerran asetettu. Final-tyyppiset muuttujat ovat vakioita, ja niiden tulee sisältää aina arvo. Esimerkiksi suurimman kokonaisluvun kertova luokkamuuttuja Integer.MAX_VALUE on vakiotyyppinen luokkamuuttuja.

Jos käytössämme on yllä esitelty luokka HslHinnasto, voivat kaikki ohjelmat, jotka tarvitsevat kerta- tai raitiovaunulipun hintaa, päästä niihin käsiksi HslHinnasto-luokan kautta. Seuraavassa esimerkissä esitellään luokka Ihminen, jolla on metodi onkoRahaaKertalippuun(), joka käyttää HslHinnasto-luokasta löytyvää lipun hintaa.

public class Ihminen {
    private String nimi;
    private double rahat;
    // muut oliomuuttujat

    // konstruktori

    public boolean onkoRahaaKertalippuun() {
        if(this.rahat >= HslHinnasto.KERTALIPPU_AIKUINEN) {
            return true;
        }

        return false;
    }

    // muut luokkaan Ihminen liittyvät metodit
}

Metodi public boolean onkoRahaaKertalippuun() vertaa luokan Ihminen oliomuuttujaa rahat HslHinnasto-luokan staattiseen muuttujaan KERTALIPPU_AIKUINEN. Metodia onkoRahaaKertalippuun() voi kutsua vain olioviitteen kautta. Esimerkiksi:

Ihminen matti = new Ihminen();

if (matti.onkoRahaaKertalippuun()) {
    System.out.println("Ostetaan kertalippu.");
} else {
    System.out.println("Mennään pummilla.");
}

Huomaa nimeämiskäytäntö! Kaikki vakiot eli final-määreellä varustetut muuttujat kirjoitetaan ISOLLA_JA_ALAVIIVOILLA.

Staattiset metodit toimivat vastaavasti. Esimerkiksi Luokka HslHinnasto saattaisi kapseloida muuttujat ja antaa vain aksessorit niihin. Aksessoriksi kutsutaan metodia, jolla voi joko lukea muuttujan arvon tai sijoittaa muuttujalle uuden arvon.

public class HslHinnasto {
  private static final double KERTALIPPU_AIKUINEN = 2.50;
  private static final double RAITIOVAUNULIPPU_AIKUINEN = 2.50;

  public static double annaKertalipunHinta() {   // Aksessori
    return KERTALIPPU_AIKUINEN;
  }

  public static double annaRaitiovaunulipunHinta() {   // Aksessori
    return RAITIOVAUNULIPPU_AIKUINEN;
  }
}

Tällöin Ihminen-luokan toteutuksessa tulee kutsua metodia annaKertalipunHinta() sen sijaan että kutsuttaisiin muuttujaa suoraan.

public class Ihminen {
    private String nimi;
    private double rahat;
    // muut oliomuuttujat

    // konstruktori

    public boolean onkoRahaaKertalippuun() {
        if(this.rahat >= HslHinnasto.annaKertalipunHinta()) {
            return true;
        }

        return false;
    }

    // muut luokkaan Ihminen liittyvät metodit
}

Vaikka Java periaatteessa mahdollistaa staattisten muuttujien käytön, ei niille useinkaan ole tarvetta. Usein staattisten muuttujien käyttö aiheuttaa ongelmia ohjelman rakenteelle, sillä staattiset muuttujat toimivat pahamaineisten globaalien muuttujien tapaan. Tällä kurssilla käytämme ainoastaan vakioarvoisia eli final määreen omaavia staattisia muuttujia!

36.2 Ei-static

Ei-staattiset metodit ja muuttujat liittyvät olioihin. Oliomuuttujat, eli attribuutit määritellään luokan alussa. Kun olio luodaan new-kutsulla, kaikille oliomuuttujille varataan tila olioon liittyvän viitteen päähän. Muuttujien arvot ovat oliokohtaisia, eli jokaisella oliolla on omat muuttujien arvot. Tutkitaan taas luokkaa Ihminen, jolla on oliomuuttujat nimi ja rahat.

public class Ihminen {
  private String nimi;
  private double rahat;

  // muut tiedot
}

Kun luokasta Ihminen luodaan uusi ilmentymä, alustetaan myös siihen liittyvät muuttujat. Jos viitetyyppistä muuttujaa nimi ei alusteta, saa se arvokseen null-viitteen. Lisätään luokan Ihminen toteutukseen vielä konstruktori ja muutama metodi.

public class Ihminen {
    private String nimi;
    private double rahat;

    // konstruktori
    public Ihminen(String nimi, double rahat) {
        this.nimi = nimi;
        this.rahat = rahat;
    }

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

    public double getRahat() {
        return this.rahat;
    }

    public void lisaaRahaa(double summa) {
        if(summa > 0) {
          this.rahat += summa;
        }
    }

    public boolean onkoRahaaKertalippuun() {
        if(this.rahat >= HslHinnasto.annaKertalipunHinta()) {
            return true;
        }

        return false;
    }
}

Konstruktori Ihminen(String nimi, double rahat) luo uuden ihmisolion ja palauttaa viitteen siihen. Metodi getNimi() palauttaa viitteen nimi-olioon, ja getRahat()-metodi palauttaa alkeistyyppisen muuttujan rahat. Metodi lisaaRahaa(double summa) lisää oliomuuttujaan rahat parametrina annetun summan jos parametrin arvo on suurempi kuin 0.

Oliometodeja kutsutaan olion viitteen kautta. Seuraava koodiesimerkki luo uuden Ihmis-olion, lisää sille rahaa, ja lopuksi tulostaa sen nimen. Huomaa että metodikutsut ovat muotoa olionNimi.metodinNimi()

Ihminen matti = new Ihminen("Matti", 5.0);
matti.lisaaRahaa(5);

if (matti.onkoRahaaKertalippuun()) {
    System.out.println("Ostetaan kertalippu.");
} else {
    System.out.println("Mennään pummilla.");
}

Esimerkki tulostaa "Ostetaan kertalippu".

Metodit luokan sisällä

Luokan sisäisiä ei-staattisia metodeja voi kutsua myös ilman olio-etuliitettä metodiin liittyvissä luokissa. Esimerkiksi seuraava toString()-metodi Ihminen luokalle, joka kutsuu olioon liittyvää metodia getNimi().

public class Ihminen {
    // aiemmin toteutetun luokan sisältö

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

Metodi toString() kutsuu siis luokan sisäistä juuri käsiteltävään olioon liittyvää getNimi()-metodia. Etuliite this korostaa kutsun liittyvän juuri tähän olioon.

Ei-staattiset metodit voivat kutsua myös staattisia, eli luokkakohtaisia metodeja. Toisaalta, luokkakohtaiset metodit eivät voi kutsua oliokohtaisia metodeja ilman viitettä itse olioon, sillä ilman viitettä ei päästä käsiksi olioon liittyviin tietoihin.

Muuttujat metodien sisällä

Metodien sisällä määriteltävät muuttujat ovat metodien suorituksessa käytettäviä apumuuttujia, eikä niitä tule sekoittaa oliomuuttujiin. Alla esimerkki metodista, jossa luodaan metodiin paikallinen muuttuja. Muuttuja indeksi on olemassa ja käytössä vain metodin suorituksen ajan.

public class ... {
    ...

    public static void tulostaTaulukko(String[] taulukko) {
        int indeksi = 0;

        while(indeksi < taulukko.length) {
            System.out.println(taulukko[indeksi]);
            indeksi++;
        }
    }
}

Metodissa tulostaTaulukko() luodaan apumuuttuja indeksi jota käytetään taulukon läpikäynnissä. Muuttuja indeksi on olemassa vain metodin suorituksen ajan.

Tehtävä 129: Tavara, Matkalaukku ja Lastiruuma

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

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

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

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

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

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

129.6 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 matinLaukku = new Matkalaukku(10);
        matinLaukku.lisaaTavara(kirja);
        matinLaukku.lisaaTavara(puhelin);

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

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

        System.out.println(lastiruuma);
    }
}

Ohjelman tulostuksen tulisi olla seuraava:

2 matkalaukkua (7 kg)

129.7 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 matinLaukku = new Matkalaukku(10);
        matinLaukku.lisaaTavara(kirja);
        matinLaukku.lisaaTavara(puhelin);

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

        Lastiruuma lastiruuma = new Lastiruuma(1000);
        lastiruuma.lisaaMatkalaukku(matinLaukku);
        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)

129.8 Paljon tiiliskiviä

Testataan vielä, että lastiruuman toiminta on oikea eikä maksimipaino pääse ylittymään. Tee Main-luokkaan metodi public static void lisaaMatkalaukutTiiliskivilla(Lastiruuma lastiruuma), joka lisää parametrina annettuun lastiruumaan 100 matkalaukkua, joissa jokaisessa on yksi tiiliskivi. Tiiliskivien painot ovat 1, 2, 3, ..., 100 kg.

Ohjelman runko on seuraava:

public class Main {
    public static void main(String[] args) {
        Lastiruuma lastiruuma = new Lastiruuma(1000);
        lisaaMatkalaukutTiiliskivilla(lastiruuma);
        System.out.println(lastiruuma);
    }

    public static void lisaaMatkalaukutTiiliskivilla(Lastiruuma lastiruuma) {
        // 100 matkalaukun lisääminen, jokaiseen tulee tiiliskivi
    }
}

Ohjelman tulostus on seuraava:

44 matkalaukkua (990 kg)

37 Hajautustaulu (HashMap)

Hajautustaulu on yksi Javan yleishyödyllisistä tietorakenteista. Hajautustaulun ideana on laskea olioon liittyvälle avaimelle, eli yksilöivälle arvolle (esimerkiksi henkilötunnus, opiskelijanumero, puhelinnumero), indeksi hajautustaulun sisältämästä taulukosta. Indeksin avulla Avaimen muuttamista indeksiksi kutsutaan hajautukseksi, joka tarkoittaa indeksin laskemista. Hajautus tapahtuu aina tietyn hajautusfunktion avulla, joka takaa että tietyllä avaimella saadaan aina sama indeksi.

Avaimen perusteella lisääminen ja hakeminen mahdollistaa erittäin nopean hakemisen. Sen sijaan että tutkisimme taulukon alkiot järjestyksessä (pahimmassa tapauksessa joudumme käymään kaikki alkiot läpi), tai etsisimme arvoa binäärihaulla (pahimmassa tapauksessa käymme taulukon kokoon liittyvän logaritmisen määrän alkoita läpi), voimme periaatteessa katsoa tasan yhtä taulukon indeksiä ja tarkistaa onko indeksiin tallennettu arvoa vai ei.

Hajautustaulu käyttää avaimen arvon laskemiseen Object-luokassa määriteltyä hashCode()-metodia, jonka jokainen toteutettu luokka perii. Emme kuitenkaan tutustu hajautustaulun toteutukseen tarkemmin tällä kurssilla. Perintään tutustumme muutaman viikon kuluttua.

Javan luokka HashMap kapseloi eli piilottaa hajautustaulun toteutuksen, ja tarjoaa valmiit metodit sen käyttöön.

Hajautustaulua luodessa tarvitaan kaksi tyyppiparametria, avainmuuttujan tyyppi ja tallennettavan olion tyyppi. Seuraava esimerkki käyttää avaimena String-tyyppistä oliota, ja tallennettavana oliona String-tyyppistä oliota.

HashMap<String, String> numerot = new HashMap<>();
numerot.put("Yksi", "Uno");
numerot.put("Kaksi", "Dos");

String kaannos = numerot.get("Yksi");
System.out.println(kaannos);

System.out.println(numerot.get("Kaksi"));
System.out.println(numerot.get("Kolme"));
System.out.println(numerot.get("Uno"));
Uno
Dos
null
null

Esimerkissä luodaan hajatustaulu, jonka avaimena ja tallennettavana oliona merkkijono. Hajautustauluun lisätään tietoa put()-metodilla, joka saa parametreikseen viitteet avaimeen ja tallennettavaan olioon. Metodi get()-palauttaa parametrina annettuun avaimeen liittyvän viitteen tai arvon null jos avaimella ei löydy viitettä.

Hajautustaulussa tiettyä avainta vastaa aina yksi arvo. Jos jo olemassaolevalla avaimella tallennetaan uusi arvo, katoaa vanha arvo hajautustaulusta.

HashMap<String, String> numerot = new HashMap<>();
numerot.put("Yksi", "Uno");
numerot.put("Kaksi", "Dos");
numerot.put("Yksi", "Ein");

String kaannos = numerot.get("Yksi");
System.out.println(kaannos);

System.out.println(numerot.get("Kaksi"));
System.out.println(numerot.get("Kolme"));
System.out.println(numerot.get("Uno"));

Koska avain "Yksi" asetetaan uudestaan, on esimerkin tulostus nyt seuraavanlainen.

Ein
Dos
null
null

Tehtävä 130: Lempinimet

Luo main-metodissa uusi HashMap<String,String>-olio. Tallenna tähän HashMappiin seuraavien henkilöiden nimet ja lempinimet niin, että nimi on avain ja lempinimi on arvo. Käytä pelkkiä pieniä kirjaimia.

  • matin lempinimi on mage
  • mikaelin lempinimi on mixu
  • arton lempinimi on arppa

Tämän jälkeen hae HashMapistä mikaelin lempinimi ja tulosta se.

Testit edellyttävät että kirjoitat nimet pienellä alkukirjaimella.

37.1 Kirjojen haku hajautustaulun avulla

Tutkitaan hajautustaulun toimintaa seuraavaksi kirjastoesimerkin avulla. Kirjastosta voi hakea kirjoja kirjan nimen perusteella, nimi toimii siis kirjan avaimena. Jos annetulle nimelle löytyy kirja, saadaan siihen liittyvä viite ja samalla kirjan tiedot. Luodaan ensin esimerkkiluokka Kirja, jolla on oliomuuttujina nimi ja kirjaan liittyvä sisältö.

public class Kirja {
    private String nimi;
    private String sisalto;
    private int julkaisuvuosi;

    public Kirja(String nimi, int julkaisuvuosi, String sisalto) {
        this.nimi = nimi;
        this.julkaisuvuosi = julkaisuvuosi;
        this.sisalto = sisalto;
    }

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

    public void setNimi(String nimi) {
        this.nimi = nimi;
    }

    public int getJulkaisuvuosi() {
        return this.julkaisuvuosi;
    }

    public void setJulkaisuvuosi(int julkaisuvuosi) {
        this.julkaisuvuosi = julkaisuvuosi;
    }

    public String getSisalto() {
        return this.sisalto;
    }

    public void setSisalto(String sisalto) {
        this.sisalto = sisalto;
    }

    public String toString() {
        return "Nimi: " + this.nimi + " (" + this.julkaisuvuosi + ")\n"
                + "Sisältö: " + this.sisalto;
    }
}

Luodaan seuraavaksi hajautustaulu, joka käyttää avaimena kirjan nimeä eli String-tyyppistä oliota, ja tallentaa viitteitä Kirja-olioihin.

HashMap<String, Kirja> kirjahakemisto = new HashMap<>();

Yllä oleva hajautustaulu käyttää avaimena String-oliota. Laajennetaan esimerkkiä siten, että kirjahakemistoon lisätään kaksi kirjaa, "Järki ja tunteet" ja "Ylpeys ja ennakkoluulo".

Kirja jarkiJaTunteet = new Kirja("Järki ja tunteet", 1811, "...");
Kirja ylpeysJaEnnakkoluulo = new Kirja("Ylpeys ja ennakkoluulo", 1813, "....");

HashMap<String, Kirja> kirjahakemisto = new HashMap<>();
kirjahakemisto.put(jarkiJaTunteet.getNimi(), jarkiJaTunteet);
kirjahakemisto.put(ylpeysJaEnnakkoluulo.getNimi(), ylpeysJaEnnakkoluulo);

Kirjahakemistosta voi hakea kirjoja kirjan nimellä. Haku kirjalla "Viisasteleva sydän" ei tuota osumaa, jolloin hajautustaulu palauttaa null-viitteen. Kirja "Ylpeys ja ennakkoluulo" kuitenkin löytyy.

Kirja kirja = kirjahakemisto.get("Viisasteleva sydän");
System.out.println(kirja);
System.out.println();
kirja = kirjahakemisto.get("Ylpeys ja ennakkoluulo");
System.out.println(kirja);
null

Nimi: Ylpeys ja ennakkoluulo (1813)
Sisältö: ...

Hajautustaulu on hyödyllinen silloin kun tiedetään avain minkä perusteella halutaan hakea. Avaimet ovat aina yksilöllisiä, joten saman avaimen taakse ei voi tallettaa montaa eri oliota. Tallennettava olio voi toki olla lista tai toinen hajautustaulukko!

37.2 Kirjasto

Yllä olevan kirjahakemiston ongelmana on se, että kirjoja haettaessa täytyy muistaa kirjan nimi merkki merkiltä oikein. Javan valmis String-luokka tarjoaa meille välineet tähänkin. Metodi toLowerCase() muuttaa merkkijonon kirjaimet pieniksi, ja metodi trim() poistaa merkkijonon alusta ja lopusta tyhjät merkit (esimerkiksi välilyönnit). Tietokoneen käyttäjät usein kirjoittavat tekstin alkuun tai loppuun vahingossa välilyöntejä.

String teksti = "Ylpeys ja ennakkoluulo ";
teksti = teksti.toLowerCase(); // teksti nyt "ylpeys ja ennakkoluulo "
teksti = teksti.trim() // teksti nyt "ylpeys ja ennakkoluulo"

Luodaan luokka Kirjasto, joka kapseloi kirjat sisältävän hajautustaulun ja mahdollistaa kirjoitusasusta riippumattoman kirjojen haun. Lisätään Kirjasto-luokalle metodit lisaaKirja(Kirja kirja) ja poistaKirja(String kirjanNimi). Huomaamme jo nyt että merkkijonon siistimistä tarvitsisi useammassa metodissa, joten tehdään siitä erillinen metodi private String siistiMerkkijono(String merkkijono).

public class Kirjasto {
    private HashMap<String, Kirja> hakemisto;

    public Kirjasto() {
        this.hakemisto = new HashMap<>();
    }

    public void lisaaKirja(Kirja kirja) {
        String nimi = siistiMerkkijono(kirja.getNimi());

        if(this.hakemisto.containsKey(nimi)) {
            System.out.println("Kirja on jo kirjastossa!");
        } else {
            hakemisto.put(nimi, kirja);
        }
    }

    public void poistaKirja(String kirjanNimi) {
        kirjanNimi = siistiMerkkijono(kirjanNimi);

        if(this.hakemisto.containsKey(kirjanNimi)) {
            this.hakemisto.remove(kirjanNimi);
        } else {
            System.out.println("Kirjaa ei löydy, ei voida poistaa!");
        }
    }

    private String siistiMerkkijono(String merkkijono) {
        if (merkkijono == null) {
            return "";
        }

        merkkijono = merkkijono.toLowerCase();
        return merkkijono.trim();
    }
}

Toteutetaan kirjan hakutoiminnallisuus siten, että kirjaa haetaan hajautusrakenteesta sen nimellä.

public Kirja haeKirja(String kirjanNimi) {
    kirjanNimi = siistiMerkkijono(kirjanNimi);
    return this.hakemisto.get(kirjanNimi);
}

Yllä oleva metodi palauttaa haetun kirjan jos sellainen löytyy, muulloin null-arvon. Voimme myös käydä kaikki hakemiston avaimet läpi yksitellen, etsien esimerkiksi alkuosaa kirjan nimestä. Tällä tavalla etsiessä menetämme kuitenkin hajautustaulun nopeusedun, sillä huonoimmassa tapauksessa joudumme käymään kaikkien kirjojen nimet läpi. Hakeminen alkuosan perusteella onnistuisi hajautustaulun keySet()-metodin avulla. Metodi keySet() palauttaa avaimet joukossa, jonka voi käydä läpi for-each -toistolauseella.

public Kirja haeKirjaNimenAlkuosalla(String kirjanAlkuosa) {
    kirjanAlkuosa = siistiMerkkijono(kirjanAlkuosa);

    for (String avain: this.hakemisto.keySet()) {
        if (avain.startsWith(kirjanAlkuosa)) {
            return this.hakemisto.get(avain);
        }
    }

    return null;
}

Jätämme yllä olevan metodin kuitenkin pois kirjastostamme. Kirjastosta puuttuu oleellisista toiminnoista vielä kirjojen listaaminen. Luodaan metodi public ArrayList<Kirja> kirjalista(), joka palauttaa listan kirjaston kirjoista. Metodi kirjalista hyödyntää hajautustaulun tarjoamaa values()-metodia. Metodi values() palauttaa kokoelman kirjaston kirjoista, jonka voi antaa parametrina ArrayList-luokan konstruktorille.

public class Kirjasto {
    private HashMap<String, Kirja> hakemisto;

    public Kirjasto() {
        this.hakemisto = new HashMap<>();
    }

    public Kirja haeKirja(String kirjanNimi) {
        kirjanNimi = siistiMerkkijono(kirjanNimi);
        return this.hakemisto.get(kirjanNimi);
    }

    public void lisaaKirja(Kirja kirja) {
        String nimi = siistiMerkkijono(kirja.getNimi());

        if(this.hakemisto.containsKey(nimi)) {
            System.out.println("Kirja on jo kirjastossa!");
        } else {
            this.hakemisto.put(nimi, kirja);
        }
    }

    public void poistaKirja(String kirjanNimi) {
        kirjanNimi = siistiMerkkijono(kirjanNimi);

        if(this.hakemisto.containsKey(kirjanNimi)) {
            this.hakemisto.remove(kirjanNimi);
        } else {
            System.out.println("Kirjaa ei löydy, ei voida poistaa!");
        }
    }

    public ArrayList<Kirja> kirjalista() {
        return new ArrayList<>(this.hakemisto.values());
    }

    private String siistiMerkkijono(String merkkijono) {
        if (merkkijono == null) {
            return "";
        }

        merkkijono = merkkijono.toLowerCase();
        return merkkijono.trim();
    }
}

Yksi ohjelmoinnin periaatteista on ns. DRY-periaate (Don't Repeat Yourself), jolla pyritään välttämään saman koodin olemista useassa paikassa. Merkkijonon pieneksi muuttaminen ja trimmaus, eli tyhjien merkkien poisto alusta ja lopusta, olisi toistunut useasti kirjastoluokassamme ilman metodia siistiMerkkijono. Toistuvaa koodia ei usein huomaa ennen kuin sitä on jo kirjoittanut, jolloin sitä päätyy koodiin lähes pakosti. Tässä ei kuitenkaan ole mitään pahaa. Tärkeintä on että siistit koodiasi sitä mukaa kun huomaat siistimistä vaativia tilanteita.

37.3 Alkeistyyppiset muuttujat hajautustaulussa

Huomaa että hajautustaulun avain ja tallennettava olio ovat aina viitetyyppisiä. Jos haluat käyttää alkeistyyppisiä muuttujia avaimena tai tallennettavana arvona, on niille olemassa myös viitetyyppiset vastineet. Alla on esitelty muutama.

Alkeistyyppi Viitetyyppinen vastine
int Integer
double Double
char Character

Java kapseloi alkeistyyppiset muuttujat viitetyyppisiksi muuttujiksi esimerkiksi ArrayList ja HashMap-tietorakennetta käytettäessä. Vaikka numero 1 on alkeistyyppinen muuttuja (tyyppiä int), tulee sitä käyttää Integer-tyyppisenä seuraavasti.

HashMap<Integer, String> taulu = new HashMap<>();
taulu.put(1, "Ole!");

Alkeistyyppisten muuttujien automaattista muunnosta viitetyyppisiksi kutsutaan Javassa auto-boxingiksi, eli automaattiseksi "laatikkoon" asettamiseksi. Vastaava onnistuu myös toisinpäin. Voimme luoda metodin, joka palauttaa hajautustaulun sisältämän kokonaisluvun. Seuraavassa esimerkissä olevassa metodissa lisaaBongaus tapahtuu automaattinen tyyppimuunnos.

public class Rekisteribongaus {
    private HashMap<String, Integer> bongatut;

    public Rekisteribongaus() {
        this.bongatut = new HashMap<>();
    }

    public void lisaaBongaus(String nimi, int numero) {
        this.bongatut.put(nimi, numero);
    }

    public int viimeisinBongaus(String nimi) {
        this.bongatut.get(nimi);
    }
}

Vaikka hajautustaulu sisältää Integer-tyyppisiä olioita, osaa Java muuntaa tietyt viitetyyppiset muuttujat niiden alkeistyyppisiksi vastineiksi. Esimerkiksi Integer-oliot muuttuvat tarpeen vaatiessa int-tyyppisiksi muuttujiksi. Tässä piilee kuitenkin vaara! Jos yritämme muuttaa null-viitettä numeroksi, näemme virheen java.lang.reflect.InvocationTargetException. Kun teemme automaattista muunnosta, tulee varmistaa että muunnettava arvo ei ole null. Yllä olevassa ohjelmassa oleva viimeisinBongaus-metodi tulee korjata esimerkiksi seuraavasti.

public int viimeisinBongaus(String nimi) {
    if(this.bongatut.containsKey(nimi) {
        return this.bongatut.get(nimi);
    }

    return 0;
}

Tehtävä 131: Velkakirja

Luo luokka Velkakirja, jolla on seuraavat toiminnot:

  • konstruktori public Velkakirja() luo uuden velkakirjan
  • metodi public void asetaLaina(String kenelle, double maara) tallettaa velkakirjaan merkinnän lainasta tietylle henkilölle.
  • metodi public double paljonkoVelkaa(String kuka) palauttaa velan määrän annetun henkilön nimen perusteella

Luokkaa käytetään seuraavalla tavalla:

Velkakirja matinVelkakirja = new Velkakirja();
matinVelkakirja.asetaLaina("Arto", 51.5);
matinVelkakirja.asetaLaina("Mikael", 30);

System.out.println(matinVelkakirja.paljonkoVelkaa("Arto"));
System.out.println(matinVelkakirja.paljonkoVelkaa("Joel"));

Yllä oleva esimerkki tulostaisi:

51.5
0

Ole tarkkana tilanteessa, jossa kysytään velattoman ihmisen velkaa. Kertaa luvun 4.3 lopun esimerkki!

Huom! Velkakirjan ei tarvitse huomioida vanhoja lainoja. Kun asetat uuden velan henkilölle jolla on vanha velka, vanha velka unohtuu.

Velkakirja matinVelkakirja = new Velkakirja();
matinVelkakirja.asetaLaina("Arto", 51.5);
matinVelkakirja.asetaLaina("Arto", 10.5);

System.out.println(matinVelkakirja.paljonkoVelkaa("Arto"));
10.5

Tehtävä 132: Sanakirja

Tässä tehtäväsarjassa toteutetaan sanakirja, josta voi hakea suomen kielen sanoille englanninkielisiä käännöksiä. Sanakirjan tekemisessä käytetään HashMap-tietorakennetta.

132.1 Luokka Sanakirja

Toteuta luokka nimeltä Sanakirja. Luokalla on aluksi seuraavat metodit:

  • public String kaanna(String sana) metodi palauttaa parametrinsa käännöksen. Jos sanaa ei tunneta, palautetaan null.
  • public void lisaa(String sana, String kaannos) metodi lisää sanakirjaan uuden käännöksen

Toteuta luokka Sanakirja siten, että sen ainoa oliomuuttuja on HashMap-tietorakenne.

Testaa sanakirjasi toimintaa:

Sanakirja sanakirja = new Sanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
sanakirja.lisaa("cembalo", "harpsichord");

System.out.println(sanakirja.kaanna("apina"));
System.out.println(sanakirja.kaanna("porkkana"));
monkey
null

132.2 Sanojen lukumäärä

Lisää sanakirjaan metodi public int sanojenLukumaara(), joka palauttaa sanakirjassa olevien sanojen lukumäärän.

Sanakirja sanakirja = new Sanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
System.out.println(sanakirja.sanojenLukumaara());

sanakirja.lisaa("cembalo", "harpsichord");
System.out.println(sanakirja.sanojenLukumaara());
2
3

132.3 Kaikkien sanojen listaaminen

Lisää sanakirjaan metodi public ArrayList<String> kaannoksetListana() joka palauttaa sanakirjan sisällön listana avain = arvo muotoisia merkkijonoja.

Sanakirja sanakirja = new Sanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
sanakirja.lisaa("cembalo", "harpsichord");

ArrayList<String> kaannokset = sanakirja.kaannoksetListana();
for(String kaannos: kaannokset) {
    System.out.println(kaannos);
}
banaani = banana
apina = monkey
cembalo = harpsichord

Vihje: saat käytyä kaikki HashMapissa olevat avaimet läpi metodin keySet avulla seuraavasti:

HashMap<String, String> sanaparit = new HashMap<>();

sanaparit.put("apina", "eläin");
sanaparit.put("etelä", "ilmansuunta");
sanaparit.put("sauerkraut", "ruoka");

for (String avain : sanaparit.keySet()) {
    System.out.print( avain + " " );
}

// tulostuu apina etelä sauerkraut

// vastaavasti avaimia vastaavat arvot saadaan tulostettua seuraavasti
for (String avain : sanaparit.keySet()) {
    System.out.print( sanaparit.get(avain) + " ");
}

// tulostuu eläin ilmansuunta ruoka

132.4 Tekstikäyttöliittymän alku

Harjoitellaan tässäkin tehtävässä erillisen tekstikäyttöliittymän tekemistä. Luo luokka Tekstikayttoliittyma, jolla on seuraavat metodit

  • konstruktori public Tekstikayttoliittyma(Scanner lukija, Sanakirja sanakirja)
  • metodi public void kaynnista(), joka käynnistää tekstikäyttöliittymän.

Tekstikäyttöliittymä tallettaa konstruktorin parametrina saamansa lukijan ja sanakirjan oliomuuttujiin. Muita oliomuuttujia ei tarvita. Käyttäjän syötteen lukeminen tulee hoitaa konstruktorin parametrina saatua lukija-olioa käyttäen! Myös kaikki käännökset on talletettava konstruktorin parametrina saatuun sanakirja-olioon. Tekstikäyttöliittymä ei saa luoda olioita itse!

HUOM: vielä uudelleen edellinen, eli Tekstikäyttöliittymä ei saa luoda itse skanneria vaan sen on käytettävä parametrina saamaansa skanneria syötteiden lukemiseen!

Tekstikäyttöliittymässä tulee aluksi olla vain komento lopeta, joka poistuu tekstikäyttöliittymästä. Jos käyttäjä syöttää jotain muuta, käyttäjälle sanotaan "Tuntematon komento".

Scanner lukija = new Scanner(System.in);
Sanakirja sanakirja = new Sanakirja();

Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja);
kayttoliittyma.kaynnista();
Komennot:
  lopeta - poistuu käyttöliittymästä

Komento: apua
Tuntematon komento.

Komento: lopeta
Hei hei!

132.5 Sanojen lisääminen ja kääntäminen

Lisää tekstikäyttöliittymälle komennot lisaa ja kaanna. Komento lisaa lisää kysyy käyttäjältä sanaparin ja lisää sen sanakirjaan. Komento kaanna kysyy käyttäjältä sanaa ja tulostaa sen käännöksen.

Scanner lukija = new Scanner(System.in);
Sanakirja sanakirja = new Sanakirja();

Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja);
kayttoliittyma.kaynnista();
Komennot:
  lisaa - lisää sanaparin sanakirjaan
  kaanna - kysyy sanan ja tulostaa sen käännöksen
  lopeta - poistuu käyttöliittymästä

Komento: lisaa
Suomeksi: porkkana
Käännös: carrot

Komento: kaanna
Anna sana: porkkana
Käännös: carrot

Komento: lopeta
Hei hei!

38 Kohti testauksen automatisointia

Ohjelman testaaminen käsin on toivottoman 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 yllä olevassa tehtävässä luotua ohjelmaa voi testata automaattisesti.

String syote = "kaanna\n" + "apina\n"  +
               "kaanna\n" + "juusto\n" +
               "lisaa\n"  + "juusto\n" + "cheese\n" +
               "kaanna\n" + "juusto\n" +
               "lopeta\n";

Scanner lukija = new Scanner(syote);
Sanakirja sanakirja = new Sanakirja();

Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja);
kayttoliittyma.kaynnista();

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

Komennot:
  lisaa - lisää sanaparin sanakirjaan
  kaanna - kysyy sanan ja tulostaa sen käännöksen
  lopeta - poistuu käyttöliittymästä

Komento: Anna sana: Tuntematon sana!

Komento: Anna sana: Tuntematon sana!

Komento: Suomeksi: Käännös:
Komento: Anna sana: Käännös: cheese

Komento: Hei hei!

Merkkijonon antaminen Scanner-luokalle korvaa näppäimistöltä luettavan syötteen merkkijonolla. 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 käyttäjän yhteen nextLine()-komentoon antamaa syötettä.

Testityötettä on helppo muuttaa, esim. seuraavassa syötetään lisää uusia sanoja sanakirjaan:

String syote = "lisaa\n"  + "juusto\n" +     "cheese\n" +
                   "lisaa\n"  + "olut\n"   +     "beer\n" +
                   "lisaa\n"  + "kirja\n"  +     "book\n" +
                   "lisaa\n"  + "tietokone\n" +  "computer\n" +
                   "lisaa\n"  + "auto\n"   +     "car\n" +
                   "lopeta\n";

Kun haluat testata ohjelmasi toimintaa jälleen käsin, vaihda Scanner-olion konstruktorin parametriksi System.in, eli järjestelmän syötevirta.

Ohjelman toiminnan oikeellisuus pitää edelleen tarkastaa itse 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öhemmin kurssin aikana.

39 Java API

Kurssilla käyttämämme Java-ohjelmointikieli koostuu kolmesta osasta. Ensimmäinen osa on ohjelmointikielen syntaksi ja semantiikka: muuttujien määrittelytapa, kontrollirakenteiden muoto, ja muuttujien ja luokkien rakenne ja niiden toimintatapa. Toinen osa on JVM, eli Java Virtual Machine, jota käytetään ohjelmien suorittamiseen. Java-ohjelmat käännetään tavukoodiksi, jota voidaan suorittaa missä tahansa koneessa olevan JVM:n avulla. Emme oikeastaan ole törmänneet ohjelmien kääntämiseen, sillä ohjelmointiympäristöt tekevät sen ohjelmoijien puolesta. Silloin tällöin ohjelmointiympäristö ei toimi odotetulla tavalla, ja saatamme joutua valitsemaan clean & build, joka poistaa vanhat lähdekoodit ja kääntää ohjelman uudestaan. Kolmantena osana on API (Application Programming Interface), eli ohjelmointirajapinta tai standardikirjasto.

API on ohjelmointikielen tarjoama joukko valmiita luokkia, joita ohjelmoija voi käyttää omissa projekteissaan. Esimerkiksi luokat ArrayList, Arrays, Collections, ja String ovat kaikki osa Javan valmista APIa. Javan version 8 API-kuvaus löytyy osoitteesta http://docs.oracle.com/javase/8/docs/api/. Osoitteessa olevan sivuston vasemmassa laidassa on Javan valmiille luokille luokkakuvaus. Etsiessäsi sivulta luokkaa ArrayList, löydät sivun http://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html, joka kuvaa ArrayList-luokan rakenteen, konstruktorit, ja metodit.

NetBeans osaa näyttää luokkaan liittyvän APIn tarvittaessa. Kun kirjoitat luokan nimen ja lisäät siihen liittyvän import-lauseen, voit klikata luokan nimeä oikealla hiirennapilla ja valita Show Javadoc. Tämä avaa luokkaan liittyvän API-kuvauksen selaimessa.

Tehtävä 133: Lentokenttä

Jokaisella viikolla on yksi laajempi tehtävä, jossa pääset vapaasti suunnittelemaan ohjelman rakenteen, käyttöliittymän ulkomuoto ja vaaditut komennot on määritelty ennalta. Ohjelmoinnin jatkokurssin ensimmäinen vapaasti suunniteltava tehtävä on Lentokenttä.

Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!

Lentokenttä-tehtävässä toteutetaan lentokentän hallintasovellus. Lentokentän hallintasovelluksessa hallinnoidaan lentokoneita ja lentoja. Lentokoneista tiedetään aina tunnus ja kapasiteetti. Lennoista tiedetään lennon lentokone, lähtöpaikan tunnus (esim. HEL) ja kohdepaikan tunnus (esim. BAL).

Sekä lentokoneita että lentoja voi olla useita. Sama lentokone voi myös lentää useaa eri lentoa (useaa eri reittiä). Sovelluksen tulee toimia kahdessa vaiheessa. Ensin lentokentän työntekijä syöttää lentokoneiden ja lentojen tietoja hallintakäyttöliittymässä.

Kun käyttäjä poistuu hallintakäyttöliittymässä, avautuu käyttäjälle mahdollisuus lentopalvelun käyttöön. Lentopalvelussa on kolme toimintoa; lentokoneiden tulostaminen, lentojen tulostaminen, ja lentokoneen tietojen tulostaminen. Tämän lisäksi käyttäjä voi poistua ohjelmasta valitsemalla vaihtoehdon x. Jos käyttäjä syöttää epäkelvon komennon, kysytään komentoa uudestaan.

Lentokentän hallinta
--------------------

Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 1
Anna lentokoneen tunnus: HA-LOL
Anna lentokoneen kapasiteetti: 42
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 1
Anna lentokoneen tunnus: G-OWAC
Anna lentokoneen kapasiteetti: 101
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: HA-LOL
Anna lähtöpaikan tunnus: HEL
Anna kohdepaikan tunnus: BAL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: G-OWAC
Anna lähtöpaikan tunnus: JFK
Anna kohdepaikan tunnus: BAL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> 2
Anna lentokoneen tunnus: HA-LOL
Anna lähtöpaikan tunnus: BAL
Anna kohdepaikan tunnus: HEL
Valitse toiminto:
[1] Lisää lentokone
[2] Lisää lento
[x] Poistu hallintamoodista
> x

Lentopalvelu
------------

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 1
G-OWAC (101 henkilöä)
HA-LOL (42 henkilöä)
Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 2
HA-LOL (42 henkilöä) (HEL-BAL)
HA-LOL (42 henkilöä) (BAL-HEL)
G-OWAC (101 henkilöä) (JFK-BAL)

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> 3
Mikä kone: G-OWAC
G-OWAC (101 henkilöä)

Valitse toiminto:
[1] Tulosta lentokoneet
[2] Tulosta lennot
[3] Tulosta lentokoneen tiedot
[x] Lopeta
> x

Huom1: Testien kannalta on oleellista että käyttöliittymä toimii täsmälleen kuten yllä kuvattu. Ohjelman tulostamat menut kannattaneekin copypasteta tästä ohjelmakoodiin. Testit eivät oleta, että ohjelmasi on varautunut epäkelpoihin syötteisiin. Tämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen.

Huom2: älä käytä luokkein nimissä skandeja, ne saattavat aiheuttaa ongelmia testeihin!

Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan.

Vielä uudelleen varoitus: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!