Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

Ohpe-harjoitukset s2011: 5/6 (3.-7.10.)

(Muutettu viimeksi 1.10.2011, sivu perustettu 19.9.2011.)

Nämä harjoitukset liittyvät oppimateriaalin lukuihin 2 Oliot ja kapselointi ja 3 Ohjelmointitekniikkaa: standardisyöttövirta ja Scanner

Kaikki harjoitustehtävät on syytä tehdä. Jotkin tehtävät on merkitty keltaisella värillä. Ne ovat ehkä hieman muita haastavampia. Ilman niitäkin harjoituksista voi saada maksimipisteet, mutta ne lasketaan silti mukaan harjoituspisteitä määrättäessä – ne voivat siis korvata joitakin haasteettomampia tehtäviä tms. Mutta ennen kaikkea noista keltaisista tehtävistä sitä vasta oppiikin!

Huom:

Lyyra-kortti

Tässä tehtäväsäsarjassa tehdään luokka LyyraKortti, jonka tarkoituksena on jäljitellä Lyyra-kortin käyttämistä Unicafessa.

Luokan runko

Projektiin tulee kuulumaan kaksi kooditiedostoa:

Kun luot projektin NetBeansissa, anna projektin nimeksi (Project Name) LyyraKortti ja pääluokan nimeksi (Main Class) Main. Lisää sitten projektiin uusi luokka painamalla projektin nimestä hiiren oikealla napilla vasemmalla olevasta projektilistasta ja valitsemalla New->Java Class. Anna luokan nimeksi (Class Name) LyyraKortti.

Uuden luokan luonti

Tee ensin LyyraKortti-olion konstruktori, jolle annetaan kortin alkusaldo ja joka tallentaa sen olion sisäiseen muuttujaan. Tee sitten toString-metodi, joka palauttaa kortin saldon muodossa "Kortilla on rahaa X euroa".

Seuraavassa on luokan LyyraKortti runko:

public class LyyraKortti {
  private double saldo;

  public LyyraKortti(double alkusaldo) {
    // kirjoita koodia tähän
  }

  public String toString() {
    // kirjoita koodia tähän
  }
}

Seuraava pääohjelma testaa luokkaa:

public class Main {
  public static void main(String[] args) {
    LyyraKortti kortti = new LyyraKortti(50);
    System.out.println(kortti);
  }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 50.0 euroa

Kortilla maksaminen

Täydennä LyyraKortti-luokkaa seuraavilla metodeilla:

  public void syoEdullisesti() {
    // kirjoita koodia tähän
  }

  public void syoMaukkaasti() {
    // kirjoita koodia tähän
  }

Metodin syoEdullisesti tulisi vähentää kortin saldoa 2,40 eurolla ja metodin syoMaukkaasti tulisi vähentää kortin saldoa 4,00 eurolla.

Seuraava pääohjelma testaa luokkaa:

public class Main {
  public static void main(String[] args) {
    LyyraKortti kortti = new LyyraKortti(50);
    System.out.println(kortti);
    kortti.syoEdullisesti();
    System.out.println(kortti);
    kortti.syoMaukkaasti();
    kortti.syoEdullisesti();
    System.out.println(kortti);
  }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 50.0 euroa
Kortilla on rahaa 47.6 euroa
Kortilla on rahaa 41.2 euroa

Ei-negatiivinen saldo

Mitä tapahtuu, jos kortilta loppuu raha kesken? Ei ole järkevää, että saldo muuttuu negatiiviseksi. Muuta metodeita syoEdullisesti ja syoMaukkaasti niin, että ne eivät vähennä saldoa, jos saldo menisi negatiiviseksi.

Seuraava pääohjelma testaa luokkaa:

public class Main {
  public static void main(String[] args) {
    LyyraKortti kortti = new LyyraKortti(5);
    System.out.println(kortti);
    kortti.syoMaukkaasti();
    System.out.println(kortti);
    kortti.syoMaukkaasti();
    System.out.println(kortti);
  }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 5.0 euroa
Kortilla on rahaa 1.0 euroa
Kortilla on rahaa 1.0 euroa

Siis toinen metodin syoMaukkaasti kutsu ei vaikuttanut saldoon, koska saldo olisi mennyt negatiiviseksi.

Kortin lataaminen

Lisää LyyraKortti-luokkaan seuraava metodi:

  public void lataaRahaa(double rahamaara) {
    // kirjoita koodia tähän
  }

Metodin tarkoituksena on kasvattaa kortin saldoa parametrina annetulla rahamäärällä. Kuitenkin kortin saldo saa olla korkeintaan 150 euroa, joten jos ladattava rahamäärä ylittäisi sen, saldoksi tulisi tulla silti tasan 150 euroa.

Seuraava pääohjelma testaa luokkaa:

public class Main {
  public static void main(String[] args) {
    LyyraKortti kortti = new LyyraKortti(10);
    System.out.println(kortti);
    kortti.lataaRahaa(15);
    System.out.println(kortti);
    kortti.lataaRahaa(10);
    System.out.println(kortti);
    kortti.lataaRahaa(200);
    System.out.println(kortti);
  }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 10.0 euroa
Kortilla on rahaa 25.0 euroa
Kortilla on rahaa 35.0 euroa
Kortilla on rahaa 150.0 euroa

Monta korttia

Tee pääohjelma, jossa luodaan kaksi LyyraKortti-oliota: yksi Pekalle (alkusaldo 20 euroa) ja toinen Matille (alkusaldo 30 euroa). Tee ohjelmassa seuraavat asiat:

Pääohjelman runko on seuraava:

public class Main {
  public static void main(String[] args) {
    LyyraKortti pekanKortti = new LyyraKortti(20);
    LyyraKortti matinKortti = new LyyraKortti(30);
    // kirjoita koodia tähän
  }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Pekka: Kortilla on rahaa 16.0 euroa
Matti: Kortilla on rahaa 27.6 euroa
Pekka: Kortilla on rahaa 36.0 euroa
Matti: Kortilla on rahaa 23.6 euroa
Pekka: Kortilla on rahaa 31.200000000000003 euroa
Matti: Kortilla on rahaa 73.6 euroa

Saldo kokonaislukuna

Tosielämässä kortin saldon tallennus double-muuttujaan ei olisi hyvä idea, koska double-arvoissa esiintyy pyöristysvirheitä. Sellainen esiintyy jopa yllä olevassa esimerkissä: Pekan kortilla kuuluisi olla lopuksi 31,2 euroa, mutta siellä onkin muka 31,200000000000003 euroa.

Yksi ratkaisu ongelmaan on käyttää double-muuttujan sijasta int-muuttujaa ja tallentaa rahamäärä sentteinä. Tee tarvittavat muutokset luokkaan.

Lyyra-kortin parantelua

Täydennetään edellisen tehtäväsarjan LyyraKortti-luokkaa.

Monta konstruktoria

Lisää luokkaan LyyraKortti uusi konstruktori, jolla ei ole parametreja. Tässä tapauksessa kortin aloitussaldon tulee olla 20 euroa.

Seuraava pääohjelma testaa luokkaa:

public class Main {
  public static void main(String[] args) {
    LyyraKortti kortti1 = new LyyraKortti();
    System.out.println(kortti1);
    LyyraKortti kortti2 = new LyyraKortti(30);
    System.out.println(kortti2);
  }
}

Ohjelman tulisi tuottaa seuraava tulostus:

Kortilla on rahaa 20.0 euroa
Kortilla on rahaa 30.0 euroa

Sama koodi vain kerran

Metodien syoEdullisesti ja syoMaukkaasti ongelmana on, että niissä on samantapainen tarkistus sen varalta, että kortin saldo uhkaa mennä negatiiviseksi. On selkeämpää ja turvallisempaa, että tarkistus ohjelmoidaan vain yhteen paikkaan.

Tee apumetodi vahennaSaldoa, jonka tehtävänä on vähentää kortin saldoa annetulla rahamäärällä. Metodin runko on seuraava:

  private void vahennaSaldoa(double rahamaara) {
    // kirjoita koodia tähän
  }

Tee tämäkin metodi niin, että metodi ei muuta saldoa, jos saldo menisi negatiiviseksi.

Toteuta nyt metodit syoEdullisesti ja syoMaukkaasti niin, että ne kutsuvat metodia vahennaSaldoa. Nyt negatiivisen saldon tarkistus on vain yhdessä kohdassa. (Näin mahdollinen muuttaminen ja korjaaminen on helpompaa ja turvallisempaa!)

Muista myös testata, että metodit toimivat tämän muutoksen jälkeen.

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, joka 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;

NumeronArpoja-konetta siis käytetaan seuraavaan tapaan:

// ...
NumeronArpoja alleSatanen = new NumeronArpoja(100);
int luku = alleSatanen.getSeuraavaLuku();
           // jokin luvuista 1, 2, ..., 100
// ...
NumeronArpoja vuosiluku = new NumeronArpoja(2011);
int vuosi = vuosiluku.getSeuraavaLuku();
           // jokin luvuista 1, 2, ..., 2011
// ...
NumeronArpoja noppa = new NumeronArpoja(6);
System.out.println("10 nopanheittoa:");
for (int i = 0; i < 10; i++) {
    System.out.println(noppa.getSeuraavaLuku());
} 

RajoitettuIntLukija.java

Ohjelmoi "kone" RajoitettuIntLukija, jolla voi pyytää käyttäjältä kokonaislukua annetulta kokonaislukuväliltä. Luokan ilmentymä eli RajoitettuIntLukija-olio kyselee ohjelman käyttäjältä ponnahdusikkunan avulla oikean kokoista lukua niin pitkään, että käyttäjä ymmärtää antaa kelvollisen.

RajoitettuIntLukija-luokan API on seuraavanlainen:

Miten voisit varautua virheeseen, jossa yritetään luoda epäkelpo RajoitettuIntLukija-olio? Esimerkiksi jos yritetään asettaa: alaraja=3 ja ylaraja=1. Toistaiseksi voisit ehkä tehdä "rikkinäisen" olion, joka palauttaa aina alarajan. Tämä ei ole hyvä ratkaisu, mutta riittäköön toistaiseksi. Asiaan palataan vielä...

RajoitettuIntLukija-olioita voisi ohjelmoinnissa käyttää seuraavaan tapaan:

  // ...
  RajoitettuIntLukija neljastaKymppiin = new RajoitettuIntLukija(4, 10);
  RajoitettuIntLukija itsArvoTonni = new RajoitettuIntLukija(-1000, 1000);
  // ...
  Pop.ilmoita("Anna kouluarvosana!");
  int arvosana = neljastaKymppiin.lueInt();
  // nyt arvosana on kokonaisluku väliltä 4-10
  Pop.ilmoita("Arvosana on " + arvosana);
  // ...
  Pop.ilmoita("Anna luku, jolle |luku| <= 1000!");
  int luku = itsArvoTonni.lueInt();
  // nyt luku on kokonaisluku väliltä -1000 - 1000
  Pop.ilmoita("Luku on " + luku);
  // ...

Ja tuon ohjelmakohdan suoritus voisi puolestaan näyttää seuraavalta. Huomaa, miten virheestä ei ilmoiteta, ellei virhettä tule.

kuva kuva kuva kuva kuva kuva kuva kuva kuva

Uhkapeli.java

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

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 uhkapeliohjelmaa 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. Pelaajalla on siis lompakko tai tili, jota voitot ja tappiot päivittävät. Ja jos tili tyhjenee, pelaaminen loppuu, koska tässä kasinossa ei suvaita velaksi pelaamista.

Klassinen syöttö ja tulostus standardivirroin

Kurssilla on tähän saakka tulostettu tietoja ponnahdusikkuinoin ja myös kirjoitettu standarditulosvirtaan. Kaikki tietojen lukeminen on toteutettu ponnahdusikkunoin. Nyt on aika harjoitella "klassista tapaa" ohjelman syötteiden ja tulosteiden käsittelyssä. Tässä tehtäväsarjassa ei saa käyttää ponnahdusikkunoita. Tässä tehtäväsarjassa ei tarvitse varautua virheellisiin syötteisiin. Kokeile silti, miten käy...

Palataan hetkeksi uudelleen alun tunnelmiin ja silloisiin pikku tehtäviin:

Summaaja.java

(Vrt. 1. viikon tehtävä 3.1)

Ohjelma pyytää kaksi kokonaislukua ja ilmoittaa niiden summan.

Esimerkkisuoritus (käyttäjän kirjoittama teksti on kursiivilla):

Anna ensimmäinen luku!
6
Anna toinen luku!
8
Niiden summa on 14

Ikatervehdys.java

(Vrt. 1. viikon tehtävä 3.5)

Tee ohjelma, joka kysyy käyttäjältä nimen iän. Sitten ohjelma tervehtii käyttäjää ja kertoo samalla tämän iän.

Esimerkkisuoritus: (käyttäjän kirjoittama teksti on kursiivilla):

Mikä on nimesi?
Matti
Mikä on ikäsi
14
Hei Matti, olet siis 14!

NegPosLkm.java

(Vrt. 2. viikon tehtävä 4.4)

Negatiivisten ja positiivisten kokonaislukujen määrä: Ohjelma pyytää käyttäjältä kokonaislukuja. Luku nolla ilmaisee, että syöttöluvut loppuivat. Ohjelman tehtävänä on laskea syötteen positiivisten ja negatiivisten lukujen lukumäärä.

Esimerkkisuoritus: (käyttäjän kirjoittama teksti on kursiivilla):

Positiivisten ja negatiivisten laskenta
***************************************

Anna lukuja! Nolla lopettaa!
4
-2
-33
9
123
31
7
0

Positiivisia: 5
Negatiivisia: 2

Arvosanajakauma.java

(Vrt. 3. viikon tehtävä 6.5)

Opettaja syöttää opiskelijoiden koepistemäärät. Kukin syötetty koepistemäärä muutetaan vastaavaksi arvosanaksi. Jokainen laskettu arvosana kasvattaa yhdellä arvosanojen lukumääriä laskevan taulukon oikeaa alkiota. Negatiivinen syöttöluku lopettaa pisteiden syötön.

Esimerkkisuoritus: (käyttäjän kirjoittama teksti on kursiivilla):

Arvosanajakauman laskenta
=========================
Anna pistemäärät! (Negatiivinen syöte lopettaa.)
44
35
17
56
7
54
-1

Arvosanajakauma:
5: **
4: 
3: *
2: *
1: 
0: **

Hyväksyttyjä 66.66666667 % osallistujista

Opiskelijoita kurssilla

Tässä tehtäväsarjassa mallinnetaan liioitellun yksinkertaisesti kurssikirjanpitoa. Harjoittelun ytimessä on luokka, joka sisältää taulukollisen toisen luokan ilmentymiä.

Opiskelija.java

Ohjelmoi luokka Opiskelija. API:

Käyttöesimerkki:

Opiskelija maija = new Opiskelija("Maija");
maija.setPisteet(54);

System.out.println(maija.getNimi());    // Maija
System.out.println(maija.getPisteet()); // 54
System.out.println(maija);              // Maija: 54

Kurssi.java

Kurssikirjapitoa varten tarvitaan luokka, jonka ilmentymä sisältää taulukollisen opiskelijoita. Ohjelmoi tähän tarkoitukseen luokka Kurssi, jonka API on:

Luokkamäärittelyn hahmottelua:

public class Kurssi {

  private Opiskelija[] opiskelijat;
 
  public Kurssi(int opiskelijaLkm) {
    opiskelijat = // ... oikean kokoinen Opiskelija-taulukko
  }

  public void kysyOpiskelijat() {
    // käy läpi opiskelijat-talukko: 0 - taulukon viimeinen alkio {
         // kysele opiskelijan tiedot
         // luo Opiskelija-olio ja sijoita se taulukkoon
    // }
  }

  public boolean asetaPisteet(String nimi, int pisteet) {
    // käy läpi opiskelijat-talukkoa kunnes (jos) löytyy
    //   Opiskelija-olio, jolle opiskelijat[i].getNimi().equals(nimi)
    // jos löytyi, aseta opiskelijat[i].setPisteet(pisteet)
    // palauta arvo true tai false riippuen siitä, löytyikö opiskelija
  }

  public String toString() {
    // String tulos = "";
    // käy läpi opiskelijat-talukko: 0 - taulukon viimeinen alkio
    //   tulos = tulos + opiskelijat[i] + "\n";
    // palauta tulos
  }
}

Käyttöesimerkki

Kurssi ohpe = new Kurssi(6);
ohpe.kysyOpiskelijat(); 
  // syötetään Liisa, Maija, Pekka, Antti, Evita, Barbara

if (ohpe.asetaPisteet("Maija", 54)) 
  System.out.println("Maijan pisteiden asetus onnistui.");
else
  System.out.println("Maijan pisteiden asetus ei onnistunut.");

if (ohpe.asetaPisteet("Petteri", 4)) 
  System.out.println("Petterin pisteiden asetus onnistui.");
else
  System.out.println("Petterin pisteiden asetus ei onnistunut.");

System.out.println(ohpe);

Tulostuksen pitäisi näyttää seuraavalta:

Maijan pisteiden asetus onnistui.
Petterin pisteiden asetus ei onnistunut.
Liisa: 0
Maija: 54
Pekka: 0
Antti: 0
Evita: 0
Barbara: 0

Kurssikirjanpitosovellus

Laadi luokkia Opiskelija ja Kurssi käyttäen vuorovaikutteinen (eli keskusteleva eli interaktiivinen) yksinkertainen sovellus kurssikirjanpitoon. Ensin ohjelma kyselee opiskelijoiden lukumäärän, sitten kaikkien nimet. Kun ohjelma on saanut kurssin muodostettua, se tarjoaa pistemäärien päivityspalvelun:

Annetaan nimi. Jos nimi on tyhjä merkkijono ("") päivitykset päättyvät. Jos nimi ei ollut tyhjä, kysytään nimeen liitettävät pisteet. Tätä jatketaan siis tyhjään merkkijonoon saakka. Virheellistä pistemäärää ei hyväksytä. Sovellus antaa sellaisesta selkeän virheilmoituksen ja tarjoaa korjaamismahdollisuuden.

Lopuksi ohjelma tulostaa kurssin tulokset muodossa, jonka Kurssi-luokan toString-metodi tarjoaa.