Sisällysluettelo

Tehtävät

HUOM: Helsingin Yliopiston logolla merkatut tehtävät ovat kaikille yliopistoon MOOCin kautta hakeville pakollisia.

23 Nopea kertaus

Aloitetaan viikko muutamalla viime viikon tärkeimpiä teemoja kertaavalla tehtävällä. Kertaa tarvittaessa luku 24.10 ennen tehtävää 94 ja luvut 24.6 ja 24.12 ennen tehtävää 95.

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ä 94: Puhelinmuistio

Tehtävässä tehdään yksinkertainen puhelinmuistio.

94.1 Henkilö

Tee ensin luokka Henkilo Luokan pitää toimia seuraavan esimerkin osoittamalla tavalla:

public static void main(String[] args) {
    Henkilo pekka = new Henkilo("Pekka Mikkola", "040-123123");

    System.out.println( pekka.haeNimi() );
    System.out.println( pekka.haeNumero() );

    System.out.println( pekka );

    pekka.vaihdaNumeroa("050-333444");
    System.out.println( pekka );
}
  

Tulostuu:

Pekka Mikkola
040-123123
Pekka Mikkola  puh: 040-123123
Pekka Mikkola  puh: 050-333444
  

Tee siis luokalle

  • metodi public String toString(), joka palauttaa henkilön merkkijonoesityksen (yo. esimerkin tapaan muotoiltuna)
  • konstruktori, jolla asetetaan henkilölle nimi ja puhelinnumero
  • public String haeNimi(), joka palauttaa nimen
  • public String haeNumero(), joka palauttaa puhelinnumeron
  • metodi public void vaihdaNumeroa(String uusiNumero), joka muuttaa henkilön puhelinnumeroa

94.2 Henkilöiden lisäys puhelinmuistioon

Tee luokka Puhelinmuistio joka tallettaa sisällään olevaan ArrayListiin Henkilo-olioita. Tässä vaiheessa luokalle tehdään seuraavat metodit:

  • public void lisaa(String nimi, String numero) luo Henkilo-olion ja lisää sen puhelinmuistion ArrayListiin.
  • public void tulostaKaikki(), tulostaa puhelinmuistion sisällön

Esimerkki muistion toiminnasta:

public static void main(String[] args) {
    Puhelinmuistio muistio = new Puhelinmuistio();

    muistio.lisaa("Pekka Mikkola", "040-123123");
    muistio.lisaa("Antti Laaksonen", "045-456123");
    muistio.lisaa("Juhana Laurinharju", "050-222333");

    muistio.tulostaKaikki();
}
  

Ohjelman tulostus oikein toteutetuilla luokilla on:

Pekka Mikkola  puh: 040-123123
Antti Laaksonen  puh: 045-456123
Juhana Laurinharju  puh: 050-222333
  

94.3 Numerojen haku muistiosta

Tehdään puhelinmuistiolle metodi public String haeNumero(String nimi), joka palauttaa parametrina annetun henkilön numeron. Jos henkilö ei ole muistiossa, palautetaan merkkijono "numero ei tiedossa". Esimerkki metodin toiminnasta:

public static void main(String[] args) {
    Puhelinmuistio muistio = new Puhelinmuistio();
    muistio.lisaa("Pekka Mikkola", "040-123123");
    muistio.lisaa("Antti Laaksonen", "045-456123");
    muistio.lisaa("Juhana Laurinharju", "050-222333");

    String numero = muistio.haeNumero("Pekka Mikkola");
    System.out.println( numero );

    numero = muistio.haeNumero("Martti Tienari");
    System.out.println( numero );
}
    

Tulostuu:

040-123123
numero ei tiedossa

Tehtävä 95: Raha

Maksukortti-tehtävässä käytimme rahamäärän tallettamiseen double-tyyppistä oliomuuttujaa. Todellisissa sovelluksissa näin ei kannata tehdä, sillä kuten jo olemme nähneet, doubleilla laskenta ei ole tarkkaa. Onkin järkevämpää toteuttaa rahamäärän käsittely oman luokkansa avulla. Seuraavassa on luokan runko:

public class Raha {

    private final int euroa;
    private final int senttia;

    public Raha(int euroa, int senttia) {
        this.euroa = euroa;
        this.senttia = senttia;
    }

    public int eurot(){
      return euroa;
    }

    public int sentit(){
        return senttia;
    }

    public String toString() {
        String nolla = "";
        if (senttia <= 10) {
            nolla = "0";
        }

        return euroa + "." + nolla + senttia + "e";
    }
}

Määrittelyssä pistää silmään oliomuuttujien määrittelyn yhteydessä käytetty sana final, tällä saadaan aikaan se, että oliomuuttujien arvoa ei pystytä muuttamaan sen jälkeen kun ne on konstruktorissa asetettu. Raha-luokan oliot ovatkin muuttumattomia eli immutaabeleita, eli jos halutaan esim. kasvattaa rahamäärää, on luotava uusi olio, joka kuvaa kasvatettua rahasummaa.

Luomme seuraavassa muutaman operaation rahojen käsittelyyn.

95.1 Plus

Tee ensin metodi public Raha plus(Raha lisattava), joka palauttaa uuden raha-olion, joka on arvoltaan yhtä suuri kuin se olio jolle metodia kutsuttiin ja parametrina oleva olio yhteensä.

Metodin runko on seuraavanlainen:

public Raha plus(Raha lisattava) {
    Raha uusi = new Raha(...); // luodaan uusi Raha-olio jolla on oikea arvo

    // palautetaan uusi Raha-olio
    return uusi;
}

Seuraavassa esimerkkejä metodin toiminnasta

Raha a = new Raha(10,0);
Raha b = new Raha(5,0);

Raha c = a.plus(b);

System.out.println(a);  // 10.00e
System.out.println(b);  // 5.00e
System.out.println(c);  // 15.00e

a = a.plus(c);          // HUOM: tässä syntyy uusi Raha-olio, joka laitataan "a:n langan päähän"
//       vanha a:n langan päässä ollut 10 euroa häviää ja Javan roskien kerääjä korjaa sen pois

System.out.println(a);  // 25.00e
System.out.println(b);  // 5.00e
System.out.println(c);  // 15.00e

95.2 Vähemmän

Tee metodi public boolean vahemman(Raha verrattava), joka palauttaa true jos raha-olio jolle metodia kutsutaan on arvoltaan pienempi kuin raha-olio, joka on metodin parametrina.

Raha a = new Raha(10,0);
Raha b = new Raha(3,0);
Raha c = new Raha(5,0);

System.out.println(a.vahemman(b));  // false
System.out.println(b.vahemman(c));  // true

95.3 Miinus

Tee metodi public Raha miinus(Raha vahentaja), joka palauttaa uuden raha-olion, jonka arvoksi tulee sen olion jolle metodia kutsuttiin ja parametrina olevan olion arvojen erotus. Jos erotus olisi negatiivinen, tulee luotavan raha-olion arvoksi 0.

Seuraavassa esimerkkejä metodin toiminnasta

Raha a = new Raha(10,0);
Raha b = new Raha(3,50);

Raha c = a.miinus(b);

System.out.println(a);  // 10.00e
System.out.println(b);  // 3.50e
System.out.println(c);  // 6.50e

c = c.miinus(a);        // HUOM: tässä syntyy uusi Raha-olio, joka laitataan "c:n langan päähän"
//       vanha c:n langan päässä ollut 6.5 euroa häviää ja Javan roskien kerääjä korjaa sen pois

System.out.println(a);  // 10.00e
System.out.println(b);  // 3.50e
System.out.println(c);  // 0.00e

23.1 Merkkijonot ovat immutaabeleja

Javan String-oliot ovat Raha-luokan olioiden tyyliin muuttumattomia, eli immutaabelena. Jos esim. merkkijonon perään katenoidaan eli liitetään +-operaatiolla uusi merkkijono, ei alkuperäistä merkkijonoa pidennetä vaan syntyy uusi merkkijono-olio:

String jono = "koe";
jono + "häntä";

System.out.println( jono );  // koe

Merkkijonoa ei siis voi muuttaa, mutta voimme ottaa katenoimalla syntyvän uuden merkkijonon talteen vanhaan muuttujaan:

String jono = "koe";
jono = jono + "häntä";   // tai jono += "häntä";

System.out.println( jono );  // koehäntä
  

Nyt siis muuttuja jono viittaa uuteen merkkijono-olioon, joka luotiin yhdistämällä muuttujan aiemmin viittaama merkkijono "koe" ja merkkijono "häntä". Merkkijono-olioon "koe" ei enää viitata.

24 Taulukko

Olemme käyttäneet kurssin aikana lukemattomia kertoja ArrayList:ejä erilaisten olioiden säilömiseen. ArrayList on helppokäyttöinen sillä se tarjoaa paljon valmiita työvälineitä jotka helpottavat ohjelmoijan elämää: muun muassa automaattisen listan kasvatuksen, jonka ansioista listalta ei lopu tila kesken (ellei lista kasva niin suureksi että se käyttää loppuun ohjelmalle varatun muistimäärän).

ArrayListin helppokäyttöisyydesta huolimatta ohjelmissa on joskus tarvetta ArrayListin esi-isälle eli taulukolle.

Taulukko on olio, joka voidaan käsittää eräänlaisena järjestettynä lokerikkona arvoille. Taulukon pituus tai koko on lokerikon paikkojen lukumäärä, eli kuinka monta arvoa taulukkoon voi laittaa. Taulukon arvoja kutsutaan taulukon alkioiksi. ArrayLististä poiketen taulukon kokoa (eli sen alkioiden määrää) ei voi muuttaa, taulukon kasvattaminen vaatii siis aina uuden taulukon luomista ja vanhassa olevien alkioiden kopiointia uuteen.

Taulukon voi luoda kahdella eri tavalla. Tutustutaan ensin tapaan jossa taulukolle annetaan sisältö luomisen yhteydessä. Kolmen alkion kokonaislukutyyppinen taulukko määritellään seuraavasti:

int[] luvut = {100, 1, 42};

Taulukko-olion tyyppi merkitään int[], joka tarkoittaa taulukkoa, jonka alkiot ovat tyyppiä int. Taulukko-olion nimi on esimerkissä luvut ja se sisältää kolme lukua {100, 1, 42}. Taulukko alustetaan lohkolla, jossa taulukkoon asetettavat arvot on esitelty pilkulla eriteltyinä.

Taulukon arvot voivat olla mitä tahansa aikaisemmin nähtyjä muuttujatyyppejä. Alla on esitelty ensin merkkijonoja sisältävä taulukko, jonka jälkeen esitellään liukulukuja sisältävä taulukko.

String[] merkkijonotaulukko = {"Matti P.", "Matti V."};
double[] liukulukutaulukko = {1.20, 3.14, 100.0, 0.6666666667};

Taulukon alkioihin viitataan indeksillä, joka on kokonaisluku. Indeksi kertoo alkion paikan taulukossa. Taulukon ensimmäinen alkio on paikassa nolla, seuraava kohdassa yksi ja niin edelleen. Taulukon tietyssä indeksissä olevaa arvoa tutkittaessa indeksi annetaan taulukko-olion nimen perään hakasulkeiden sisällä.

// indeksi       0   1    2    3   4   5     6     7
int[] luvut = {100,  1,  42,  23,  1,  1, 3200, 3201};

System.out.println(luvut[0]);  // tulostaa luvun taulukon indeksistä 0, eli luvun 100
System.out.println(luvut[2]);  // tulostaa luvun taulukon indeksistä 2, eli luvun 42

Yllä olevan taulukon koko (eli pituus) on 8.

Huomaat todennäköisesti että ArrayListin metodi get käyttäytyy hyvin samalla tavalla kuin taulukon tietystä indeksistä haku. Taulukon kohdalla vain syntaksi, eli merkintätapa, on erilainen.

Yksittäisen arvon asettaminen taulukon tiettyyn paikkaan tapahtuu kuten arvon asetus tavalliseen muuttujaan, mutta taulukkoon asetettaessa paikka eli indeksi tulee kertoa. Indeksi kerrotaan hakasulkeiden sisällä.

int[] luvut = {100,1,42};

luvut[0] = 1;    // asetetaan luku 1 indeksiin 0
luvut[1] = 101;  // asetetaan luku 101 indeksiin 1

// luvut-taulukko on nyt {1,101,42}

Jos indeksillä osoitetaan taulukon ohi, eli alkioon jota ei ole olemassa, niin saadaan virheilmoitus ArrayIndexOutOfBoundsException, joka kertoo että indeksiä johon osoitimme ei ole olemassa. Taulukon ohi, eli indeksiin joka on pienempi kuin 0 tai suurempi tai yhtäsuuri kuin taulukon koko ei siis saa viitata.

Huomaamme, että taulukko on selvästi sukua ArrayList:ille. Aivan kuten listoilla, myös taulukossa alkiot ovat tietyssä paikassa!

24.1 Taulukon läpikäynti

Taulukko-olion koon saa selville kirjoittamalla koodiin taulukko.length, huomaa että ilmauksessa ei tule käyttää sulkuja eli taulukko.length() ei toimi!

Taulukon alkioiden läpikäynti on helppo toteuttaa while-komennon avulla:

int[] luvut = {1, 8, 10, 3, 5};

int i = 0;
while (i < luvut.length ) {
    System.out.println(luvut[i]);
    i++;
}
  

Esimerkissä käydään muuttujan i avulla läpi indeksit 0, 1, 2, 3, ja 4, ja tulostetaan taulukon kussakin indeksissä olevan muuttujan arvo. Ensin siis tulostuu luvut[0], sitten luvut[1] jne. Muuttujan i kasvatus loppuu kun koko taulukko on käyty läpi, eli kun sen arvo on yhtäsuuri kuin taulukon pituus.

Taulukon läpikäynnissä ei ole aina todellista tarvetta taulukon indeksien luetteluun, vaan ainoa kiinnostava asia ovat taulukon arvot. Tällöin voidaan käyttää aiemmin tutuksi tullutta for-each-rakennetta arvojen läpikäyntiin. Nyt toistolauseen rungossa annetaan vain muuttujan nimi, johon kukin taulukon arvo asetetaan vuorollaan, ja taulukon nimi kaksoispisteellä erotettuna.

    int[] luvut = {1,8,10,3,5};

for (int luku : luvut) {
    System.out.println(luku);
  }
String[] nimet = {"Juhana L.", "Matti P.", "Matti L.", "Pekka M."};

    for (String nimi : nimet) {
        System.out.println(nimi);
    }
}

Huom: for-each-tyylisellä läpikäynnillä taulukon alkioihin ei voi asettaa arvoja! Seuraavaksi nähtävällä for-lauseen toisella muodolla sekin onnistuu.

24.2 for-komennon toinen muoto

Olemme toistaiseksi käyttäneet toistolauseissa whileä tai for-lauseen ns. "for-each"-muotoa. Toistolauseesta for on olemassa myös toinen muoto, joka on kätevä erityisesti taulukoiden käsittelyn yhteydessä. Seuraavassa tulostetaan for-toistolauseen avulla luvut 0, 1 ja 2:

for (int i = 0; i < 3; i++ ) {
    System.out.println(i);
}

Esimerkin for toimii täsmälleen samalla tavalla kuin alla oleva while:

int i = 0;  // toistossa käytettävän muuttujan alustus
while ( i < 3 ) {  // toistoehto
    System.out.println(i);
    i++;   // toistossa käytettävän muuttujan päivitys
}

for-komento, kuten yllä esitelty for (int i = 0; i < 3; i++ ) sisältää kolme osaa: looppimuuttujien alustus; toistoehto; looppimuuttujien päivitys:

  • Ensimmäisessä osassa alustetaan toistolauseessa käytettävät muuttujat. Yllä olevassa esimerkissä alustimme muuttujan i lauseella int i=0. Ensimmäinen osa suoritetaan vain kerran, juuri for:in suorituksen alussa.
  • Toisessa osassa määritellään toistoehto, joka määrittelee miten kauan for:in yhteydessä olevassa koodilohkossa olevaa koodia suoritetaan. Esimerkissämme toistoehto oli i < 3. Toistoehdon voimassaolo tarkastetaan ennen jokaista for:in toistokertaa. Toistoehto toimii täsmälleen samoin kuin while:n toistoehto.
  • Kolmas osa, joka esimerkissämme on i++ suoritetaan aina kertaalleen forin koodilohkon suorituksen jälkeen.

for on hieman while:ä selkeämpi tapa toteuttaa toistoja joissa toistojen määrä perustuu esim. laskurin kasvatukseen. Taulukkojen läpikäynnissä tilanne on yleensä juuri tämä. Seuraavassa tulostetaan taulukon luvut sisältö for:illa

int[] luvut = {1, 3, 5, 9, 17, 31, 57, 105};

for(int i = 3; i < 7; i++) {
    System.out.println(luvut[i]);
}

Forilla voidaan aloittaa läpikäynti luonnollisesti muualtakin kuin nollasta ja läpikäynti voi edetä "ylhäältä alas". Esimerkiksi taulukon paikoissa 6, 5, 4, ja 3 olevat alkiot voidaan tulostaa seuraavasti:

int[] luvut = {1, 3, 5, 9, 17, 31, 57, 105};

for(int i = 6; i>2 ; i--) {
    System.out.println(luvut[i]);
}

24.3 for ja taulukon pituus

Kaikkien taulukon alkioiden läpikäynti for:in avulla onnistuu seuraavasti:

int[] luvut = {1, 8, 10, 3, 5};

for (int i = 0; i < luvut.length; i++ ) {
    System.out.println(luvut[i]);
}

Huomaa, että toistoehdossa i < luvut.length verrataan looppimuuttujan arvoa taulukolta kysyttyyn pituuteen. Ehtoa ei kannata missään tapauksessa "kovakoodata" tyyliin i < 5 sillä yleensä taulukon pituudesta ei ole etukäteen varmuutta.

24.4 Taulukko parametrina

Taulukkoja voidaan käyttää metodin parametrina aivan kuten muitakin olioita. Huomaa, että kuten kaikkien olioiden tapauksessa, metodi saa parametrina viitteen taulukkoon, eli kaikki metodissa tapahtuvat taulukon sisältöön vaikuttavat muutokset näkyvät myös pääohjelmassa.

public static void listaaAlkiot(int[] kokonaislukuTaulukko) {

    System.out.println("taulukon alkiot ovat: ");
    for( int luku : kokonaislukuTaulukko) {
        System.out.print(luku + " ");
    }

    System.out.println("");
}

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

Kuten jo tiedämme, parametrin nimi metodin sisällä voi olla aivan vapaasti valittu, nimen ei tarvitse missään tapauksessa olla sama kuin kutsuvassa. Edellä taulukkoa kutsutaan metodin sisällä nimellä kokonaislukuTaulukko, metodin kutsuja taas näkee saman taulukon luvut-nimisenä.

Tehtävä 96: Taulukon lukujen summa

Tee metodi public static int laskeTaulukonLukujenSumma(int[] taulukko), joka palauttaa taulukossa olevien lukujen summan.

Ohjelman runko on seuraava:

public class Main {
    public static void main(String[] args) {
        // Tässä voit testata metodia
        int[] taulukko = {5, 1, 3, 4, 2};
        System.out.println(laskeTaulukonLukujenSumma(taulukko));
    }

    public static int laskeTaulukonLukujenSumma(int[] taulukko) {
        // Kirjoita koodia tänne
        return 0;
    }
}

Ohjelman tulostus on seuraava:

15

Huom: tämän tehtävän metodi samoin kuin muutamien seuraavien tehtävien taulukkoa käsittelevät metodit ovat viikkojen 2 ja 3 tapaan static eli staattisia metodeja. Karkeasti ottaen tämä johtuu siitä, että metodi ei liity mihinkään olioon, vaan saa kaiken datan jota se käyttää eli tässä tapauksessa taulukon, parametrina. Luvussa 28 palaamme tarkemmin aiheeseen staattiset vs. olioihin liittyvät metodit.

Tehtävä 97: Tyylikäs tulostus

Tee metodi public static void tulostaTyylikkaasti(int[] taulukko), joka tulostaa taulukossa olevat luvut tyylikkäästi. Lukujen väliin tulee pilkku ja välilyönti. Viimeisen luvun jälkeen ei pilkkua tule.

Ohjelman runko on seuraava:

public class Main {
    public static void main(String[] args) {
        // Tässä voit testata metodia
        int[] taulukko = {5, 1, 3, 4, 2};
        tulostaTyylikkaasti(taulukko);
    }

    public static void tulostaTyylikkaasti(int[] taulukko) {
        // Kirjoita koodia tänne
    }
}

Ohjelman tulostus on seuraava:

5, 1, 3, 4, 2

24.5 Uuden taulukon luonti

Jos taulukon koko ei ohjelmassa ole aina sama, eli se riippuu esim. käyttäjän syötteestä, ei äsken esitelty taulukon luontitapa kelpaa. Taulukko on mahdollista luoda myös siten, että sen koko määritellään muuttujan avulla:

int alkioita = 99;
int[] taulukko = new int[alkioita];

Yllä luodaan int-tyyppinen taulukko, jossa on 99 paikkaa. Tällä vaihtoehtoisella tavalla taulukon luominen tapahtuu siis kuten muidenkin olioiden luominen, eli new-komennolla. Komentoa new seuraa taulukon sisältämien muuttujien tyyppi, ja hakasuluissa taulukon koko.

int alkioita = 99;
int[] taulukko = new int[alkioita]; //luodaan muuttujan alkioita sisältämän arvon kokoinen taulukko

if(taulukko.length == alkioita) {
    System.out.println("Taulukon pituus on " + alkioita);
} else {
    System.out.println("Jotain epätodellista tapahtui. Taulukon pituus on eri kuin " + alkioita);
}

Seuraavassa esimerkissä on ohjelma, joka kysyy käyttäjältä lukujen määrän ja joukon lukuja. Tämän jälkeen ohjelma tulostaa luvut uudestaan samassa järjestyksessä. Käyttäjän antamat luvut tallennetaan taulukkoon.

System.out.print("Kuinka monta lukua? ");
int lukuja = Integer.parseInt(lukija.nextLine());

int[] luvut = new int[lukuja];

System.out.println("Anna luvut:");
for(int i = 0; i < lukuja; i++) {
    luvut[i] = Integer.parseInt(lukija.nextLine());
}

System.out.println("Luvut uudestaan:");
for(int i = 0; i < lukuja; i++) {
    System.out.println(luvut[i]);
}

Eräs ohjelman suorituskerta voisi olla seuraavanlainen:

Kuinka monta lukua? 4
Anna luvut:
4
8
2
1
Luvut uudestaan:
4
8
2
1

24.6 Taulukko paluuarvona

Koska metodit voivat palauttaa olioita, voivat ne palauttaa myös taulukkoja. Eräs merkkijonotaulukon palauttava metodi on seuraavannäköinen -- huomaa että taulukkoihin voi aivan hyvin siis laittaa myös olioita.

public static String[] annaMerkkijonoTaulukko() {
    String[] opet = new String[3];

    opet[0] = "Bonus";
    opet[1] = "Ihq";
    opet[2] = "Lennon";

    return opet;
}

public static void main(String[] args){
    String[] opettajat = annaMerkkijonoTaulukko();

    for ( String opettaja : opettajat){
        System.out.println( opettaja );
    }
}

Tehtävä 98: Taulukon kopiointi ja kääntaminen

98.1 Kopiointi

Tee metodi public static int[] kopioi(int[] taulukko) joka luo kopion parametrina saadusta taulukosta. Vihje: koska metodin on luotava taulukosta kopio, tulee metodin sisällä luoda uusi taulukko ja kopioida vanhan taulukon sisältö uudelle taulukolle alkio alkiolta.

Seuraavassa esimerkki metodin käytöstä (koodissa myös Arrays-luokan tarjoama kätevä apuväline taulukon sisällön tulostamiseen):

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

    // muutetaan kopioa
    kopio[0] = 99;

    // tulostetaan molemmat
    System.out.println( "alkup: " + Arrays.toString(alkuperainen));
    System.out.println( "kopio: " + Arrays.toString(kopio));
  }

Kuten tulostuksesta huomaa, ei kopioon tehty muutos vaikuta alkuperäiseen:

alkup: [1, 2, 3, 4]
kopio: [99, 2, 3, 4]

98.2 Kääntäminen

Tee metodi public static int[] kaanna(int[] taulukko) joka luo käänteisessä järjestyksessä olevan kopion parametrinaan saamastaan taulukosta.

Eli jos parametrina on taulukko jossa esim. luvut 5, 6, 7 palauttaa metodi uuden taulukon jonka sisältönä luvut 7, 6, 5. Parametrina oleva taulukko ei saa muuttua.

Seuraavassa esimerkki metodin käytöstä:

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

    // tulostetaan molemmat
    System.out.println( "alkup: " +Arrays.toString(alkuperainen));
    System.out.println( "käännetty: " +Arrays.toString(kaannetty));
}

Tulostuksesta pitäisi selvitä, että alkuperäinen taulukko on muuttumaton:

alkup: [1, 2, 3, 4]
käännetty: [4, 3, 2, 1]

25 Lohkoista ja sisäkkäisistä toistolauseista

Aaltosululla { alkavaa ja aaltosululla } koodia sisältävää aluetta siis sanotaan lohkoksi. Kuten on jo nähty, lohkoja käytetään muun muassa ehto- ja toistolauseiden alueen rajaamisessa. Tärkeä lohkon ominaisuus on se, että lohkossa määritellyt muuttujat ovat voimassa vain lohkon sisällä.

Seuraavassa esimerkissä määritellään ehtolausekkeeseen liittyvän lohkon sisällä tekstimuuttuja lohkonSisallaMaariteltyTeksti, joka on olemassa vain lohkon sisällä. Lohkossa esitellyn muuttujan tulostus lohkon ulkopuolella ei toimi!

int luku = 5;

if( luku == 5 ){
    String lohkonSisallaMaariteltyTeksti = "Jea!";
}

System.out.println(lohkonSisallaMaariteltyTeksti); // ei toimi!

Lohkossa voidaan toki käyttää ja muuttaa sen ulkopuolella määriteltyjä muuttujia.

int luku = 5;

if( luku == 5 ) {
    luku = 6;
}

System.out.println(luku); // tulostaa luvun 6

Lohkon sisällä voi olla mitä tahansa koodia. Esimerkiksi for-toistolauseen määrittelemän lohkon sisällä voi olla toinen for-toistolauseke tai vaikkapa while-toistolauseke. Tarkastellaan seuraavaa ohjelmaa:

for(int i = 0; i < 3; i++) {
    System.out.print(i + ": ");

    for(int j = 0; j < 3; j++) {
        System.out.print(j + " ");
    }

    System.out.println();
}

Ohjelman tulostus on seuraava:

0: 0 1 2
1: 0 1 2
2: 0 1 2

Eli mitä ohjelmassa tapahtuukaan? Jos ajatellaan pelkkää ulommaista for:ia, on toiminnallisuus helppo ymmärtää:

for(int i = 0; i < 3; i++) {
    System.out.print(i + ": ");

    // sisemmäinen for

    System.out.println();
}

Eli ensin i=0 ja tulostuu 0: ja rivinvaihto. Tämän jälkeen i kasvaa ja tulostuu ykkönen, jne., eli ulompi for saa aikaan seuraavan:

0:
1:
2:

Myös sisempi for on helppo ymmärtää erillään. Se saa aina aikaan tulosteen 0 1 2. Kun yhdistämme nämä kaksi, huomaamme, että sisin for suorittaa tulosteensa aina juuri ennen uloimman for:in tulostamaa rivinvaihtoa.

25.1 for-toistolauseen ulkopuolella määriteltyjen muuttujien käyttö toiston ehtona

Tutkitaan seuraavaa muunnosta edelliseen esimerkkiin:

for(int i = 0; i < 3; i++) {
    System.out.print(i + ": ");

    for(int j = 0; j <= i; j++) {
        System.out.print(j + " ");
    }

    System.out.println();
}

Sisemmän for:in toistojen määrä riippuukin nyt ulomman for:in muuttujan i arvosta. Eli kun i=0, tulostaa sisin looppi luvun 0, kun i=1, tulostaa sisempi toistolauseke 0 1, jne. Koko ohjelman tulostus on seuraava:

0: 0
1: 0 1
2: 0 1 2

Seuraava ohjelma tulostaa lukujen 1..10 kertotaulun.

for(int i = 1; i <= 10; i++) {

    for(int j = 1; j <= 10; j++) {
        System.out.print(i * j + " ");
    }

    System.out.println();
}

Tulostus näyttää seuraavalta:

1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
4 8 12 16 20 24 28 32 36 40
5 10 15 20 25 30 35 40 45 50
6 12 18 24 30 36 42 48 54 60
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64 72 80
9 18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100

Ylimmällä rivillä luvun 1 kertotaulu. Alussa i=1 ja sisimmän loopin muuttuja j saa arvot 1...10. Jokaisella i, j arvoparilla tulostetaan niiden tulo. Eli alussa i=1, j=1, sitten i=1, j=2, ..., i=1, j=10 seuraavaksi i=2, j=1, jne.

Kertotaulu-ohjelman voi toki pilkkoa pienempiin osiin. Voimme määritellä metodit public void tulostaKertotaulunRivi(int kerroin, int montakokertaa) ja public void tulostaKertotaulu(int mihinAsti), jolloin ohjelman rakenne olisi esimerkiksi seuraavanlainen seuraavanlainen:

public class Kertotaulu {

    public void tulosta(int mihinAsti) {
        for(int i = 1; i <= mihinAsti; i++) {
            tulostaKertotaulunRivi(i, mihinAsti);

            System.out.println();
        }
    }


    public void tulostaKertotaulunRivi(int kerroin, int montakokertaa) {
        for(int i = 1; i <= montakokertaa; i++) {
            System.out.print(i * kerroin + " ");
        }
    }
}

Nyt kutsu new Kertotaulu().tulosta(5); tulostaa allaolevan kertotaulun.

1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
4 8 12 16 20
5 10 15 20 25

Tehtävä 99: Taulukko tähtinä

Kirjoita metodi public static void tulostaTaulukkoTahtina(int[] taulukko), joka tulostaa jokaista taulukossa olevaa lukua vastaavan pituisen rivin tähtiä.

Ohjelman runko on seuraava:

public class Main {
    public static void main(String[] args) {
        // Tässä voit testata metodia
        int[] taulukko = {5, 1, 3, 4, 2};
        tulostaTaulukkoTahtina(taulukko);
    }

    public static void tulostaTaulukkoTahtina(int[] taulukko) {
        // Kirjoita tulostuskoodi tänne
    }
}

Edellisen esimerkin mukaisella syötteellä ohjelman tulostus on seuraava:

*****
*
***
****
**

Eli koska taulukon nollannessa paikassa on luku 5, tulee ensimmäiselle riville 5 tähteä. Seuraavalla 1 tähti jne.

Tehtävä 100: Tähtitaivas

Luodaan ohjelma tähtitaivaan tulostamiseen. Tähtitaivaan tähtien määrä kerrotaan tiheyden avulla. Esimerkiksi jos tähtitaivaan tiheys on 0.2, on noin 20% tähtitaivaasta peitettynä tähdillä. Pääset harjoittelemaan siis myös satunnaislukujen käyttöä.

Käytä tähtien tulostamiseen *-merkkiä. Alla on esimerkki lopullisen Tahtitaivas-luokan käytöstä ja käyttöä vastaavasti tulostuksesta.

Tahtitaivas tahtitaivas = new Tahtitaivas(0.1, 39, 10);
tahtitaivas.tulosta();
System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());
System.out.println("");

tahtitaivas = new Tahtitaivas(0.2, 15, 6);
tahtitaivas.tulosta();
System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());
  
*     *                  *
*             * *         *      **
*
*       *      *         *  *
*     *                     *
*            * *                   *
*  * *           *          * *  **
*  *
*               *
*                             *
Tähtiä: 36

* * *     *
* *   *
*     *
*  *       *
*       *   * *
* ** **     *
Tähtiä: 22
  

Huom! tehtävissä kannattaa käyttää for-lauseketta. Vaikka edellinen luku puhuukin sisäkkäisistä toistolauseista, tässä tehtävässä "sisempi" toisto piilotetaan metodin sisälle.

100.1 Tahtitaivas-luokka ja yhden rivin tulostaminen

Luo luokka Tahtitaivas, jolla on kolme oliomuuttujaa: tiheys (double), leveys (int), ja korkeus (int). Luo luokalle myös kolme konstruktoria:

  • public Tahtitaivas(double tiheys) Luo tähtitaivas-olion, jolla on parametrina annettu tiheys, leveys saa arvon 20, korkeus saa arvon 10.
  • public Tahtitaivas(int leveys, int korkeus) Luo tähtitaivas-olion, jolla on parametrina annetut leveys ja korkeus, tiheys saa arvon 0.1.
  • public Tahtitaivas(double tiheys, int leveys, int korkeus) Luo tähtitaivas-olion, jolla on parametrina annetut tiheys, leveys ja korkeus.

Lisää luokalle Tahtitaivas metodi tulostaRivi, joka tulostaa yhden rivin. Rivin leveyden määrää oliomuuttuja leveys. Oliomuuttuja tiheys kertoo todennäköisyyden tähdelle. Arvo jokaisen merkin kohdalla tulostetaanko tähti vai ei Random-luokan nextDouble-metodin avulla.

Testaa ohjelmaasi, esimerkkinä seuraava kutsu ja esimerkkitulostus.

Tahtitaivas tahtitaivas = new Tahtitaivas(0.1);
tahtitaivas.tulostaRivi();
  
*  *                  *
  

100.2 Tähtitaivaan tulostus

Luo Tahtitaivas-luokalle metodi tulosta, joka tulostaa koko tähtitaivaan. Käytä tässä hyödyksesi aiempaa tulostaRivi-metodia.

Tahtitaivas tahtitaivas = new Tahtitaivas(8, 4);
tahtitaivas.tulosta();
  
*

*
*

100.3 Tähtien laskeminen

Lisää Tahtitaivas-luokalle oliomuuttuja tahtiaViimeTulostuksessa (int) ja metodi tahtiaViimeTulostuksessa(), joka palauttaa viime tulostuksessa tulostuneiden tähtien lukumäärän. Toteuta ohjelmaasi tähtien laskeminen.

Tahtitaivas tahtitaivas = new Tahtitaivas(8, 4);
tahtitaivas.tulosta();
System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());
System.out.println("");

tahtitaivas.tulosta();
System.out.println("Tähtiä: " + tahtitaivas.tahtiaViimeTulostuksessa());


*

Tähtiä: 1

*
*
*

Tähtiä: 3

26 static:illa vai ilman?

Kun aloimme olioden käytön, materiaalissa oli neuvo jättää sana static pois olioiden metodien määrittelystä. Viikkoon 3 asti taas kaikissa metodeissa esiintyi määre static. Mistä on kysymys?

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

public class Ohjelma {

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

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

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

        System.out.println();

        nollaaTaulukko(luvut);

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

Huomaamme, että metodin määrittelyssä on nyt määre static. Syynä on se, että metodi ei liity mihinkään olioon vaan kyseessä on luokkametodi. Luokkametodeja kutsutaan usein myös staattisiksi metodeiksi. Toisin kuin olioiden metodit (joilla ei ole määrettä static), staattiseen metodiin ei liity mitään olioa. Staattinen metodi ei siis voi viitata this-määreellä olioon itseensä toisin kuin oliometodit.

Staattiselle metodille voi toki antaa olion parametrina. Staattinen metodi ei voi käsitellä mitään muita lukuja, merkkijonoja, taulukoita tai oliota kuin niitä, jotka on sille parametrina annettu tai jotka ovat staattisia luokkamuuttujia. Toisin sanoin, staattisen metodin kutsuja antaa metodille aina käsiteltävät arvot ja oliot.

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

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

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

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

        System.out.println();

        TaulukonKasittely.nollaaTaulukko(luvut);

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

Toisen luokan sisällä määriteltyä staattista metodia kutsutaan nyt muodossa TaulukonKasittely.nollaaTaulukko(parametri);.

26.1 Milloin staattisia metodeja tulisi käyttää

Kaikki olion tilaa käsittelevät metodit tulee määritellä normaaleina oliometodeina. Esim. edellisillä viikoilla määrittelemiemme luokkien Henkilo, Paivays, Kello, Joukkue, ... kaikki metodit tulee määritellä normaaleina oliometodeina eli ei static:eina.

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

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

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

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

        return true;
    }

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

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

Koska metodit käsittelevät olioa, ei niitä voi määrittää static:eiksi eli "olioon kuulumattomiksi". Jos näin yritetään tehdä, ei metodi toimi:

public class Henkilo {
    //...

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

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

Eli milloin staattista metodia sitten kannattaa käyttää? Tarkastellaan luvusta 23 tuttua henkilöolioita käsittelevää esimerkkiä:

public class Ohjelma {
    public static void main(String[] args) {
        Henkilo pekka = new Henkilo("Pekka");
        Henkilo antti = new Henkilo("Antti");
        Henkilo juhana = new Henkilo("Juhana");

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

        antti.vanhene();

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

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

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

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

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

public class Main {

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

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

        antti.vanhene();

        ilmoitaTaysiIkaisyys(antti);

        ilmoitaTaysiIkaisyys(pekka);

        ilmoitaTaysiIkaisyys(juhana);
    }

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

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

Tehtävä 101: Kirjaston tietojärjestelmä

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

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

101.1 Kirja

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

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

Testaa luokan toimintaa:

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

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

101.2 Kirjasto

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

Kirjasto kirjasto = new Kirjasto();

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

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

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

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

101.3 Hakutoiminnallisuus

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

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

public class Kirjasto {
    // ...

    public ArrayList<Kirja> haeKirjaNimikkeella(String nimike) {
        ArrayList<Kirja> loydetyt = new ArrayList<Kirja>();

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

        return loydetyt;
    }
}

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

Kirjasto kirjasto = new Kirjasto();

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

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

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

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

101.4 Paranneltu hakutoiminnallisuus

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

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

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

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

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

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

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

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

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

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

Tehtävä 102: Arvosanajakauma

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

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

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

    // ...

    teeJotain(lukija);
}

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

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

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

Pisteiden syöttö toimii seuraavasti:

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

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

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

Arvosananajakauma muodostetaan seuraavasti:

  • Jokainen koepistemäärä muutetaan arvosanaksi samalla kaavalla kuin tehtävässä 18. Jos syötetty pistemäärä ei ole välillä 0-60, ei sitä huomioida mitenkään.
  • Arvosanat tulostetaan tähtinä. Esim jos arvosanaan 5 oikeuttavia koepistemääriä on 2 kappaletta, tulostuu rivi 5: **. Jos johonkin arvosanaan oikeuttavia pistemääriä ei ole, ei yhtään tähteä tulostu, esimerkissämme näin oli nelosten kohdalla, tulostui rivi jossa on ainoastaan 4:

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

Tehtävä 103: Lintubongarin tietokanta

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

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

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

Ohjelmasi täytyy toteuttaa seuraavat komennot:

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

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

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

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

28 Debuggeri ja muuta hyödyllistä

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

29 Taulukon järjestäminen

Palaamme jälleen taulukkojen pariin.

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

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

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

import java.util.Arrays;

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

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

int[] luvut = {-3, -111, 7, 42};
Arrays.sort(luvut);
for(int luku: luvut) {
    System.out.println(luku);
}
-111
-3
7
42

29.2 Järjestämisalgoritmin toteuttaminen

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

Tehtävä 104: Järjestäminen

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

104.1 Pienin

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

Metodin runko on seuraava:

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

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

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

104.2 Pienimmän indeksi

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

Metodin runko on seuraava:

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

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

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

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

104.3 Pienimmän indeksi taulukon loppuosassa

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

Metodin runko on seuraava:

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

HUOM: parametrina olevaa taulukkoa ei saa muuttaa!

Seuraava koodi esittelee metodin toimintaa:

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

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

104.4 Lukujen vaihtaminen

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

Metodin runko on seuraava:

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

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

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

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

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

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

104.5 Järjestäminen

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

Vaihtojärjestämisen idea on seuraava:

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

Toisin sanoen:

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

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

Metodin runko on seuraava:

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

Testaa metodin toimintaa ainakin seuraavalla esimerkillä:

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

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

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

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

30 Etsintä

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

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

    return false;
}

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

    return false;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Tehtävä 105: Arvauspeli

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

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

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

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

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

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

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

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

Pelin toteuttavan luokan Arvauspeli runko on seuraavanlainen:

public class Arvauspeli {

    private Scanner lukija;

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

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

        // Kirjoita pelin koodi tänne
    }

    // tee metodit onkoSuurempiKuin ja keskiarvo tänne

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

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

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

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

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

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

Peli käynnistetään seuraavasti:

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

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

Toteutetaan tämä tehtävä askeleittain.

105.1 Onko suurempi kuin

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

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

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

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

Testaa metodisi toimintaa

Arvauspeli peli = new Arvauspeli();

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

105.2 Keskiarvo

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

Arvauspeli peli = new Arvauspeli();

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

105.3 Arvauslogiikka

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

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

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

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

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

Tehtävä 106: Binäärihaun toteutus

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

Metodi hae ei kuitenkaan toimi vielä. Viimeistele metodin toteutus oikeaksi binäärihauksi.

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


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

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

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

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

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

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

Ohjelman suoritus näyttää seuraavalta:

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

Anna haettava luku: 8

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

Anna haettava luku: 99

Luku 99 ei ole taulukossa

31 Taulukoista ja olioista

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

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

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

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

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

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

Sama esimerkki ArrayListien avulla:

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

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

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

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

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

Useimmissa sovellustilanteissa taulukon sijaan kannattaa käyttää ArrayListiä. Voi kuitenkin olla joitain tilanteita joissa taulukko riittää ja on hieman yksinkertaisempi käyttää.

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

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

public class Viikko {
    private Paiva[] paivat;

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

32 Kurssipalaute

Palaute

Tehtävä 107: Kurssipalaute

Vaikka olemme saaneet paljon arvokasta palautetta eri tehtävistä TMC:n kautta, toivomme että annatte palautetta kattavammin kaikista tähänastisita tehtävistä, materiaalista yms.

Jotta saat merkatuksi tämän tehtävän, aja tehtävän TMC-testit ja lähetä tehtävä palvelimelle.

Palaute

Vaikka olemme saaneet paljon arvokasta palautetta eri tehtävistä TMC:n kautta, toivomme että annatte palautetta kattavammin kaikista tähänastisita tehtävistä, materiaalista yms.

Jotta saat merkatuksi tämän tehtävän, aja tehtävän TMC-testit ja lähetä tehtävä palvelimelle.

33 Tulevan fiilistelyä

Kevään lopulla teemme koko matopelin itse. Fiilistellään tässä välissä hieman, ja pohditaan matopelin älyn rakentamista.

Tehtävä 108: Fiksu mato

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

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

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

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

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

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

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

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

System.out.println(tilanne);

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

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

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

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