(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
NetBeansin käyttäjän on järkevää ohjelmoida kaikki tämän kohdan tehtävät yhteen ja samaan projektiin.
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
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
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.
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); // ...
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); ... } }
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ä
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); // ... } }
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
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. } }
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); // ... } }
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! // ... } }
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! // ... } }
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-olio on kone, joa palvelee tuottamalla satunnaisen luvun väliltä 1 — annettu yläraja. Luokan yksinkertanen API on
ylaraja
:
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;
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:
Toteuta luokka VarmaIntLukija:
alaraja
— ylaraja
.
alaraja
— ylaraja
.
Aksessorin pitää virheellisen syötteen tapauksessa antaa virheilmoitus ja pyytää
sisukkaasti uutta lukua kunnes saa kelvollisen.
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. ...
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.
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.