Oppimateriaali perustuu kevään 2010 kurssiversion materiaaliin ja Arto Wiklan ohjelmointisivustoon. Materiaalin copyright © (aakkosjärjestyksessä): Matti Luukkainen, Matti Paksula, Arto Vihavainen, Arto Wikla. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

Ohpe-harjoitukset s2010: 5/6 (4.-8.10.)

(Muutettu viimeksi 4.10.2010, sivu perustettu 30.9.2010.)

Nämä harjoitukset liittyvät oppimateriaalin lukuihin III Oliot ja kapselointi, luokka olion mallina ja IV Ohjelmointitekniikkaa: parametrien ja syöttötietojen tarkistamista.

Harjoitustehtävien otsikoilla on värikoodaus: Vihreät tehtävät on syytä tehdä joka tapauksessa. Värittämättömiä ei ole ihan pakko tehdä, mutta nekin ovat hyvin hyödyllisiä ja myös vaikuttavat pisteisiin. Keltaiset tehtävät ovat vähän haastavampia. Nekin lasketaan mukaan harjoituspisteitä määrättäessä, mutta ilmankin niitä harjoituksista voi saada maksimipisteet.

Huom: Jokaisen ohjelmatiedoston alkuun on kirjoitettava kommenttina harjoituskerta, tehtävän numero ja tekijän nimi tyyliin:

// 5. harjoitukset, tehtävä 1.7, Oili Opiskelija

Matkakortti

NetBeansin käyttäjän on järkevää ohjelmoida kaikki tämän kohdan tehtävät yhteen ja samaan projektiin.

Matkakortti.java

Ohjelmoi luokka Matkakortti. Matkakortilla on omistaja (String) ja arvo (double). Toteuta piilotettujen kenttien ja konstruktorin lisäksi Matkakortille myös toString-metodi, joka tuottaa esimerkin mukaisen merkkiesityksen Matkakortti-oliosta.

Esittele luokan Matkakortti toteutusta seuraavan ohjelman avulla.

public class Hkl {
  public static void main(String[] args) {
    Matkakortti matinKortti = new Matkakortti("Matti");
    Matkakortti artonKortti = new Matkakortti("Arto");
    
    System.out.println(matinKortti);
    System.out.println(artonKortti);
  }
}

Ohjelman tulosteen pitää olla seuraavan näköinen:

Omistaja Matti, arvoa 0.0 euroa
Omistaja Arto, arvoa 0.0 euroa

Lataajalaite.java

Matkakortille ladataan arvoa ja aikaa Lataajalaitteen avulla. Ohjelmoi luokka Lataajalaite, jossa on ainakin aksessorimetodi lataaArvoa(...) arvon lataamista varten. (Ajan lataaminen ohjelmoidaan vasta myöhemmin.)

Metodille annetaan parametrina matkakortti ja ladattava arvo.

Huomaa että Matkakortti-luokkaa pitää nyt täydentää asettavalla aksessorilla, "setterillä", jolla piilossa pidettyä tietorakennetta voidaan muuttaa. Miten suhtaudut negatiiviseen matkakortille lisättävään arvoon Matkakortti-luokan setterissä? Entä Lataajalaite-luokan metodissa lataaArvoa(...)? Kenellä on vastuu mistäkin?

Seuraavassa esittelyohjelmassa luodaan kolme lataajalaitetta. Kokeile toteutuksiasi ensin tällä ohjelmalla:

public class Hkl {
  public static void main(String[] args) {
    Matkakortti matinKortti = new Matkakortti("Matti");
    Matkakortti artonKortti = new Matkakortti("Arto");
    Lataajalaite rautatietori = new Lataajalaite();
    Lataajalaite hakaniemi = new Lataajalaite();
    Lataajalaite sornainen = new Lataajalaite();

    System.out.println(matinKortti);
    System.out.println(artonKortti);

    rautatietori.lataaArvoa(artonKortti, 4);

    hakaniemi.lataaArvoa(matinKortti, 35);

    System.out.println(matinKortti);
    System.out.println(artonKortti);
  }
}

Tulosteen pitäisi olla seuraavanlainen:

Omistaja Matti, arvoa 0.0 euroa
Omistaja Arto, arvoa 0.0 euroa
Omistaja Matti, arvoa 35.0 euroa
Omistaja Arto, arvoa 4.0 euroa

Lukijalaite.java

Kulkuvälineeseen menevän matkustajan matkakortti luetaan lukijalaitteella. Ohjelmoi luokka Lukijalaite, jolla on voi maksaa joko ratikkalipun, Helsingin sisäisen bussilipun tai seutulipun. Tee Lukijalaite-luokkaan metodi ostaLippu(...), joka veloittaa halutun lipputyypin hinnan kortilta. Lipputyyppi annetaan parametrina (0 ratikkalippu, 1 Helsingin sisäinen, 2 seutu). Metodi palauttaa totuusarvona tiedon siitä riittikö kortilla raha matkaan. Jos raha riittää, metodi vähentää kortilta matkaan kuluneen rahamäärän. Miten käsittelet virheelliset parametrit?

HKL:n hinnat matkakorttia käytettäessä:

Seuraavassa Arto matkustaa kutosratikalla käyttäen ratikkalippua. Matti taas ostaa seutulipun bussikuskilta. Lopussa Arto yrittää vielä ostaa seutulipun.

public class Hkl {
  public static void main(String[] args) {
    Matkakortti matinKortti = new Matkakortti("Matti");
    Matkakortti artonKortti = new Matkakortti("Arto");
    Lataajalaite rautatietori = new Lataajalaite();
    Lataajalaite hakaniemi = new Lataajalaite();
    Lataajalaite sornainen = new Lataajalaite();
    Lukijalaite ratikka6 = new Lukijalaite();
    Lukijalaite bussi242 = new Lukijalaite();

    rautatietori.lataaArvoa(artonKortti, 4);

    hakaniemi.lataaArvoa(matinKortti, 35);

    System.out.println();
    System.out.println(matinKortti);
    System.out.println(artonKortti);
    System.out.println();

    boolean tark = ratikka6.ostaLippu(artonKortti, 0);
    if (tark) {
      System.out.println("Arton rahat riittivät ratikkalippuun.");
    }
    else {
      System.out.println("Artolla ei ole rahaa ratikkalippuun.");
    }

    tark = bussi242.ostaLippu(matinKortti, 2);
    if (tark) {
      System.out.println("Matin rahat riittivät seutulippuun.");
    }
    else {
      System.out.println("Matilla ei ole rahaa seutulippuun.");
    }

    System.out.println();
    System.out.println(matinKortti);
    System.out.println(artonKortti);
    System.out.println();

    tark = ratikka6.ostaLippu(artonKortti, 2);
    if (tark) {
      System.out.println("Arton rahat riittivät seutulippuun.");
    }
    else {
      System.out.println("Artolla ei ole rahaa seutulippuun.");
    }
  }
}
Omistaja Matti, arvoa 35.0 euroa
Omistaja Arto, arvoa 4.0 euroa

Arton rahat riittivät ratikkalippuun.
Matin rahat riittivät seutulippuun.

Omistaja Matti, arvoa 31.63 euroa
Omistaja Arto, arvoa 2.7199999999999998 euroa

Artolla ei ole rahaa seutulippuun.

Kioski.java

Tähän asti matkakortit ovat ilmestyneet tyhjästä. Muutetaan tilanne sellaiseksi, että matkakortti ostetaan kioskista. Ohjelmoi siis luokka Kioski, jossa on ainakin metodi ostaMatkakortti(...). Metodi saa parametrina ostajan nimen ja palauttaa luomansa Matkakortti-olion arvonaan.

Seuraavassa luodaan ensin Eiran Q-kioski, josta Matti ja Arto hankkivat matkakortit:

  // ...
  Lataajalaite rautatietori = new Lataajalaite();
  Lataajalaite hakaniemi = new Lataajalaite();
  Lataajalaite sornainen = new Lataajalaite();
  Lukijalaite ratikka6 = new Lukijalaite();
  Lukijalaite bussi242 = new Lukijalaite(); 

  Kioski eiranQkiska = new Kioski();

  Matkakortti artonKortti = eiranQkiska.ostaMatkakortti("Arto");  
  Matkakortti matinKortti = eiranQkiska.ostaMatkakortti("Matti");

  rautatietori.lataaArvoa(artonKortti, 4);

  hakaniemi.lataaArvoa(matinKortti, 35); 
  // ...

Laajenna Kioskia

Täydennä luokkaa Kioski siten, että matkakortin ostometodilla voi myös pyytää kioskia lataamaan kortille rahaa:

public class Hkl {
  public static void main(String[] args) {
    Kioski eiranQkiska = new Kioski();

    Matkakortti artonKortti = eiranQkiska.ostaMatkakortti("Arto");
    Matkakortti matinKortti = eiranQkiska.ostaMatkakortti("Matti", 10);
    ...
  }
}

Kertakortti.java

Matkakorttivalikoimaan kuuluu myös ns. kertakortteja. Kertakortille ladataan ostettaessa tietty määrä matkoja. Kun matkat on käytetty, kertakortti heitetään pois. Kertakorttiin ei liity omistajan nimeä. Ohjelmoi luokka Kertakortti.

Laajenna lukijalaitetta siten, että matkoja voi maksaa myös kertakorteilla. Lukijalaitteet käyttävät saman nimistä metodia sekä Matka- että Kertakorttien käsittelyyn.

Esimerkissa luodaan Kertakortti, jolla 1 ratikkamatka, 3 HKL-matkaa, mutta ei seutumatkoja. Kortilla yritetään tehdä ensin seutumatka ja sen jälkeen tehdään onnistuneesti HKL-matka.

public class Hkl {
  public static void main(String[] args) {
    Lukijalaite ratikka6 = new Lukijalaite();

    // kertakortti, jolle ladattuna 3 HKL:n kertamatkaa
    Kertakortti kerta = new Kertakortti(1, 3, 0);

    System.out.println(kerta);

    boolean tark = ratikka6.ostaLippu(kerta, 2);
    if (tark) {
      System.out.println("Seutumatka veloitettu.");
    }
    else {
      System.out.println("Kertakortilla ei ole seutulippua.");
    }

    tark = ratikka6.ostaLippu(kerta, 1);
    if (tark) {
      System.out.println("HKL-matka veloitettu.");
    }
    else {
      System.out.println("Kertakortilla ei ole HKL-matkaa.");
    }

    System.out.println(kerta);
  }
}
Kertakortti: 1 ratikkamatkaa 3 HKL-matkaa ja 0 seutumatkaa jäljellä
Kertakortilla ei ole seutulippua.
HKL-matka veloitettu.
kertakortti: 1 ratikkamatkaa 2 HKL-matkaa ja 0 seutumatkaa jäljellä

Laajenna Kioskia

Täydennä Kioski-luokkaa siten, että myös Kertakortit voi ostaa Kioskeista.

Kertakortin hankinta tapahtuu seuraavan esimerkin mukaan. Metodille ostaKertakortti(...) annetaan ostettavien ratikka-, HKL- ja seutumatkojen määrä parametreina:

public class Hkl {
  public static void main(String[] args) {
    // ...
    Kioski eiranQkiska = new Kioski();
    Kertakortti kerta = eiranQkiska.ostaKertakortti(1,3,0);
    // ...
  }
}

Viimeinen voimassaolopäivä

Laajennetaan Matkakortti-luokkaa siten, että kortilla on myös viimeinen voimassaolopäivä. Voimassaolopäivä esitetään kahtena kokonaislukuna, toinen ilmaisee päivän ja toinen kuukauden. Vuosiluvuista viis veisataan tässä tehtävässä. Matkakortin arvo on alussa 0 ja voimassaolopäivää ei ole. Olematon voimassaolopäivä voidaan merkitä esim. päivämäärällä "nollas nollatta". Laajenna Matkakortin toString-metodia siten, että seuraavan esimerkin tulostus toteutuu. Miten suhtaudut virheellisiin päiväyksiin?

public class Hkl {
  public static void main(String[] args) {
    Matkakortti matinKortti = new Matkakortti("Matti");
    Matkakortti artonKortti = new Matkakortti("Arto");

    System.out.println(matinKortti);
    System.out.println(artonKortti);

    matinKortti.asetaViimeinenVoimassaolopaiva(28, 6);
    artonKortti.asetaViimeinenVoimassaolopaiva(16, 3);
    System.out.println(matinKortti);
    System.out.println(artonKortti);
  }
}
Omistaja Matti, arvoa 0.0 euroa, aikaa 0.0. asti
Omistaja Arto, arvoa 0.0 euroa, aikaa 0.0. asti
Omistaja Matti, arvoa 0.0 euroa, aikaa 28.6. asti
Omistaja Arto, arvoa 0.0 euroa, aikaa 16.3. asti

Ajan tarkastaminen lukijalaitteella

Laajenna lukijalaitetta siten, että lukijalla voi tarkastaa onko kortilla aikaa jäljellä. Lukijan on siis tunnettava nykyinen päivämäärä. Tee lukijalle metodi, jolla päivämäärä etenee yhdellä päivällä. Voit olettaa, että jokaisessa kuussa on 30 päivää. Miten suhtaudut kosntruktorille annettaviin virheellisiin päiväyksiin?

public class Hkl {
  public static void main(String[] args) {
    // Lukijalaitteen konstruktorille annetaan aloituspäivämäärä, tässä 30.9.
    Lukijalaite ratikka9 = new Lukijalaite(30, 9);
 
    Matkakortti artonKortti = new Matkakortti("Arto");
    boolean voimassa = ratikka9.onkoVoimassa(artonKortti);
    if (!voimassa) {
      System.out.println("Osta lippu pummi!");
    }

    // klo 23.59
    ratikka9.seuraavaPaiva();
    // nyt on päiväys on 1.10.
  }
}

Ajan lataaminen lataajalaitteella

Laajenna Lataajalaite-luokkaa siten, että kortille voidaan ladata aikaa eli asettaa uusi eräpäivä. Miten suhtaudut virheellisiin päiväyksiin?

public class Hkl {
  public static void main(String[] args) {
    // ...
    Lataajalaite kurvinGrilli = new Lataajalaite();
    kurvinGrilli.lataaAikaa(matinKortti, 30, 12);
    // ...
  }
}

Paivamaara.java

Ohjelmoi luokka Paivamaara, joka tietää monesko päivä on. Luokalla on metodi, seuraavaPaiva(), joka vie päivämäärää yhdellä eteenpäin. Konstruktorissa asetetaan aloituspäivämäärä. Luokalla on myös metodit, jolla voi kysyä menossa olevaa päivää ja kuukautta. Karkausvuosia ei tarvitse ottaa huomioon eli helmikuussa olkoon aina 28 päivää. Miten suhtaudut kosntruktorille annettaviin virheellisiin päiväyksiin?

Muuta Lukijalaite-luokkaa siten, että lukijalaite tuntee päivämääräolion, eli käytännössä lukijalaite saa konstruktorissa viitteen Paivamaara-olioon. Kun lukijalaite tarkastaa onko matkakortti voimassa, se tarkastaa päivämäärän päivämääräoliolta ja vertaa sitä matkakortin eräpäivään.

public class Hkl {
  public static void main(String[] args) {
  
    Paivamaara paiva = new Paivamaara(30, 9);
    // ...
    // kaikki lukijalaitteet käyttävät samaa päiväoliota
    Lukijalaite ratikka1 = new Lukijalaite(paiva);
    Lukijalaite ratikka3T = new Lukijalaite(paiva);
    Lukijalaite ratikka3B = new Lukijalaite(paiva);
    Lukijalaite ratikka4 = new Lukijalaite(paiva);
    // ...
    // klo 23.59:
    paiva.seuraavaPaiva();
    // nyt on 1.10.
    // huom: koska kaikki lukijalaitteet kysyvät päiväyksen samalta oliolta,
    // tuntevat kaikki nyt uuden päivämäärän!
    // ...
  }
}

Paivamaara-olio ja Lataajalaite

Kehittele Lataajalaite-luokkaa siten, että sekin tuntee nykyisen päivämäärän. Kuormita lataaAikaa-metodi sellaiseksi, että sille voi antaa joko tiedon, kuinka monta päivää nykypäivästä eteenpäin kortille lisätään, tai mikä on uusi eräpäivä.

public class Hkl {
  public static void main(String[] args) {
    // nyt on 30.9.
    Paivamaara paiva = new Paivamaara(30, 9);
    // ...
    // kaikki lukijalaitteet ja lataajalaitteet käyttävät samaa päiväoliota
    Lukijalaite ratikka1 = new Lukijalaite(paiva);
    Lukijalaite ratikka3T = new Lukijalaite(paiva);
    Lukijalaite ratikka3B = new Lukijalaite(paiva);
    Lukijalaite ratikka4 = new Lukijalaite(paiva);
    Lataajalaite hakaniemi = new Lataajalaite(paiva);
    Lataajalaite sornainen = new Lataajalaite(paiva);

    Matkakortti matinKortti = new Matkakortti("Matti");
    Matkakortti artonKortti = new Matkakortti("Arto");

    // arto lataa 20 päivää aikaa kortille,
    // 20 päivää lasketaan paivamaara-olion tietämästä nykyisestä päiväyksestä
    sornainen.lataaAikaa(artonKortti, 20);

    // matti lataa aikaa siten, että hän kertoo uuden eräpäivän olevan 24.5
    sornainen.lataaAikaa(matinKortti, 24, 12);

    // klo 23.59:
    paiva.seuraavaPaiva();
    // nyt on 1.10.
    // huom: koska kaikki lukijalaitteet ja lataajalaitteet
    // kysyvät päiväyksen samalta oliolta, tuntevat kaikki nyt uuden
    // päivämäärän!
    // ...
  }
}

Uhkapeli

Rakennellaan ensin välineitä ja sitten sovellus. NetBeansin käyttäjän lienee järkevää ohjelmoida tämän kohdan tehtävät yhteen ja samaan projektiin.

NumeronArpoja.java

NumeronArpoja-olio on kone, joa palvelee tuottamalla satunnaisen luvun väliltä 1 — annettu yläraja. Luokan yksinkertanen API on

Ohjelmoi luokkaan myös yksinkertainen pääohjelma, joka esittelee NumeronArpoja-olioiden käyttöä.

Vihje: Satunnaisluvun väliltä 1 — ylaraja saa arvottua seuraavasti:

  int arvottuLuku = (int)(ylaraja*Math.random()) + 1;

VarmaIntLukija.java

Ei ole kivaa, kun ohjelman suoritus kaatuu syötteiden virheisiin. Erityisen kriittistä tämä on tällaisen nyt tekeillä olevan "uhkapeliohjelman" tapauksessa, jossa ohjelman käyttäjän tunteet muistakin syistä saattavat kuohahtaa yli.

Kokonaisluvuksi tarkoitettu syöte voi olla ainakin kahdella periaatteessa erilaisella tavalla virheellinen:

  1. Syöte ei ole kokonaisluku, ehkä ei edes numeerinen.
  2. Syöte on kokonaisluku, mutta arvoltaan sopimaton

Toteuta luokka VarmaIntLukija:

VarmaIntLukija-olio ei keskustele käyttäjän kanssa mulloin kuin virhetilanteessa. Siispä VarmaIntLukija-oliota käyttävän sovelluksen huoleksi jää esimerkiksi syötteen pyytäminen. Ja sovelluksen puolellahan sitä tiedetäänkin, mitä pyydetään!

VarmaIntLukija-oliota voisi ohjelmoinnissa käyttää seuraavaan tapaan:

  ...
  VarmaIntLukija yksViivaKymppi = new VarmaIntLukija(1, 10);
  ...
  System.out.println("Arvaa luku!");
  int arvaus = yksViivaKymppi.lueInt();
  // nyt arvaus on kokonaisluku väliltä 1-10
  System.out.println("Arvauksesi siis oli " + arvaus + ".");
  ...

Ja tuon ohjelmakohdan suoritus voisi puolestaan näyttää seuraavalta:

...
Arvaa luku!
kissantassu
"kissantassu" ei ole kokonaisluku välillä 1-10! Yritä uudelleen.
666
666  ei ole kokonaisluku välillä 1-10! Yritä uudelleen.
7
Arvauksesi siis oli 7.
...

Uhkapeli.java

Tämä tehtävä tehdään käyttäen edellisissä tehtävissä ohjelmoituja välineitä NumeronArpoja ja VarmaIntLukija!

Toteuta seuraava tietokonepeli: Ensin ohjelma arpoo jonkin kokonaisluvun 1, 2, ..., 10 paljastamatta sitä ohjelman käyttäjälle. Sitten käyttäjä syöttää ohjelmalle kolme arvausta siitä, minkä luvun ohjelma on arponut.

Lopuksi ohjelma selvittää ja ilmoittaa pelin tuloksen:

Mikään ei estä kayttäjää syöttämästä eli arvaamasta samaa lukua useampaankin kertaan; tällöin sekä riski hävitä että mahdollinen voitto kasvavat.

Tehtävän ratkaisuksi riittää toteuttaa yhden luvun arvaaminen, ts. yksi pelikerta.

Uhkapelikasino.java

Laajenna edellisen tehtävän uhkapeliohjlemaa siten, että pelaaja voi pelata haluamansa määrän kierroksia. Ohjelma pitää kirjaa pelaajan voitoista ja tappioista. Päätä itse, miten ohjelma keskustelee käyttäjän kanssa, miten pelaaminen päättyy ja millaisia raportteja tulostetaan.

Jos haluat, voit mallintaa myös pelaajan hallussa olevan alkupääoman ja seurata pääoman muutoksia.