(Muutettu viimeksi 10.10.2011, sivu perustettu 24.9.2011.)
Nämä harjoitukset liittyvät oppimateriaalin päälukuihin 1-4.
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:
// 6. harjoitukset, tehtävä 1.1, Oili Opiskelija
Tämän sarjan tehtävät palautetaan sähköpostitse "pajasuurmestari" Arto Vihavaiselle (arto.vihavainen@helsinki.fi). Koko sarja lähtetään yhtenä sähköpostina. Lähetä siis vasta, kun olet tehnyt kaikki kohdat, jotka aioit tehdä! Posti on otsikoitava täsmälleen muodossa "OHPE V6, TEHTÄVÄ 1" (ilman sitaatteja). Automaattinen filtteri ohjaa postit oikeaan paikkaansa. Virheellisesti otsikoituja viestejä ei oteta lainkaan huomioon.
Pisteet saa kunnollisista selityksistä: lepsuilusta tai ympäripyöreistä käsienheilutteluista pisteitä ei saa. Anna kielen rakenteita kyselevissä kohdissa myös pieni ohjelmaesimerkki!
Huomaa että tehtävä merkitään tehdyksi vain, jos tehtävän jokaisen kohdan selitys on oikein ja ymmärrettävissä – esimerkiksi tehtävässä 1.1. tulee osata selittää kaikki termit kohdasta yksi kohtaan kuusi. Valitettavasti resurssit eivät riitä henkilökohtaiseen palautteeseen, mutta asoiden pitäisi selvitä kurssimateriaalista.
Älä tee tätä tehtäväsarjaa pajassa, jos siellä on ruuhkaa!
Jos et pääse alkuun, ajattele selittäväsi käsitteitä kurssikaverillesi, joka on ohjelmointitaidoiltaan hieman sinua jäljessä. Tähän tehtävään ei saa henkilökohtaista ohjausta pajassa eikä henkilökohtaista palautetta sähköpostitse, mutta kuten todettu asioiden pitäisi selvitä kurssimateriaalista. Ja kurssimateriaalin esitystaso ja tyyli myös riittää vastaukseksi!
Selitä omin sanoin, mitä olio-ohjelmoinnissa tarkoitetaan kapseloinnilla (encapsulation) ja mitä hyötyä tällaisesta "vaivannäöstä" on.
Viidennellä harjoitusviikolla ohjelmoitiin LyyraKortti-luokka. Kortissa on metodit edullisesti ja maukkaasti syömistä sekä rahan lataamista varten.
Viime viikon tyylillä tehdyssä Lyyra-kortissa oli kuitenkin ongelma. Kortti tiesi lounaiden hinnan ja osasi sen ansiosta vähentää saldoa oikean määrän. Entä kun hinnat nousevat? Tai jos myyntivalikoimaan tulee uusia tuotteita? Hintojen muuttaminen tarkoittaisi, että kaikki jo käytössä olevat Lyyra-kortit pitäisi korvata uusilla, uudet hinnat tuntevilla korteilla.
Voisi olla parempi ratkaisu on tehdä kortit "tyhmiksi", hinnoista ja myytävistä tuotteista tietämättömiksi pelkän saldon säilyttäjiksi. Kaikki äly kannattaakin ehkä laittaa erillisiin olioihin, kassapäätteisiin.
Toteutetaan ensin Lyyra-kortista "tyhmä" versio. Kortilla on ainoastaan metodit saldon kysymiseen, rahan lataamiseen ja rahan ottamiseen. Täydennä allaoleva luokka ohjeen mukaan:
public class LyyraKortti { private double saldo; public LyyraKortti(double saldo) { this.saldo = saldo; } public double saldo() { return saldo; } public void lataaRahaa(double lisays) { this.saldo += lisays; } public boolean otaRahaa(double maara){ // toteuta metodi siten että se ottaa kortilta rahaa vain jos saldo on vähintään maara // onnistuessaan metodi palauttaa true ja muuten false } }
Testipääohjelma:
public class Main { public static void main(String[] args) { LyyraKortti pekanKortti = new LyyraKortti(10); System.out.println("rahaa " + pekanKortti.saldo() ); boolean onnistuiko = pekanKortti.otaRahaa(8); System.out.println("onnistuiko otto: " + onnistuiko ); System.out.println("rahaa " + pekanKortti.saldo() ); onnistuiko = pekanKortti.otaRahaa(4); System.out.println("onnistuiko otto: " + onnistuiko ); System.out.println("rahaa " + pekanKortti.saldo() ); } }
Tulostuksen kuuluisi olla seuraavanlainen
rahaa 10.0 onnistuiko otto: true rahaa 2.0 onnistuiko otto: false rahaa 2.0
Unicafessa asioidessa asiakas maksaa joko käteisellä tai Lyyra-kortilla. Myyjä käyttää kassapäätettä kortin veloittamiseen ja käteismaksujen hoitamiseen. Tehdään ensin kassapäätteestä käteismaksuihin sopiva versio.
Kassapäätteen runko:
public class Kassapaate { private double rahaa; // kassassa olevan käteisen määrä private int edulliset; private int maukkaat; public Kassapaate() { // kassassa on aluksi 1000 euroa rahaa } public double syoEdullisesti(double maksu) { // edullinen lounas maksaa 2.40 euroa. // päivitetään kassan rahamäärää ja palautetaan vaihtorahat } public double syoMaukkaasti(double maksu) { // maukas lounas maksaa 4.00 euroa. // päivitetään kassan rahamäärää ja palautetaan vaihtorahat } public String toString() { // palautetaan merkkijonona kassan rahamäärä sekä tieto myydyistä lounaista } }
Kassapäätteessä on aluksi rahaa 1000 euroa. Toteuta ylläolevan rungon metodit ohjeen ja allaolevan pääohjelman esimerkkitulosteen mukaan toimiviksi.
public class Main { public static void main(String[] args) { Kassapaate unicafeExactum = new Kassapaate(); double vaihtorahaa = unicafeExactum.syoEdullisesti(10); System.out.println("vaihtorahaa jäi " + vaihtorahaa ); vaihtorahaa = unicafeExactum.syoEdullisesti(5); System.out.println("vaihtorahaa jäi " + vaihtorahaa ); vaihtorahaa = unicafeExactum.syoMaukkaasti(4); System.out.println("vaihtorahaa jäi " + vaihtorahaa ); System.out.println( unicafeExactum ); } }
vaihtorahaa jäi 7.6 vaihtorahaa jäi 2.6 vaihtorahaa jäi 0.0 kassassa rahaa 1008.8 edullisia lounaita myyty 2 maukkaita lounaita myyty 1
Laajennetaan kassapäätettä siten että myös kortilla voi maksaa. Teemme kassapäätteelle siis metodit joiden parametrina kassapääte saa lyyrakortin jolta se vähentää valitun lounaan hinnan. Seuraavassa uusien metodien rungot ja ohje niiden toteuttamiseksi:
public class Kassapaate { // ... public boolean syoEdullisesti(LyyraKortti kortti) { // edullinen lounas maksaa 2.40 euroa. // jos kortilla on tarpeeksi rahaa, vähennetään hinta kortilta ja palautetaan true // muuten palautetaan false } public boolean syoMaukkaasti(LyyraKortti kortti) { // maukas lounas maksaa 4.00 euroa. // jos kortilla on tarpeeksi rahaa, vähennetään hinta kortilta ja palautetaan true // muuten palautetaan false } // ... }
Huom: Kortilla maksaminen ei lisää kassapäätteessä olevan käteisen määrää.
Seuraavassa testipääohjelma ja haluttu tulostus:
public class Main { public static void main(String[] args) { Kassapaate unicafeExactum = new Kassapaate(); double vaihtorahaa = unicafeExactum.syoEdullisesti(10); System.out.println("vaihtorahaa jäi " + vaihtorahaa ); LyyraKortti antinKortti = new LyyraKortti(7); boolean onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti); System.out.println("riittikö raha: " + onnistuiko); onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti); System.out.println("riittikö raha: " + onnistuiko); onnistuiko = unicafeExactum.syoEdullisesti(antinKortti); System.out.println("riittikö raha: " + onnistuiko); System.out.println( unicafeExactum ); } }
vaihtorahaa jäi 7.6 riittikö raha: true riittikö raha: false riittikö raha: true kassassa rahaa 1002.4 edullisia lounaita myyty 2 maukkaita lounaita myyty 1
Viime viikon tehtävässä 2.2 kehoitettiin poistamaan Lyyra-kortin kahdesta eri metodista samantapainen koodi uuden metodin avulla. On hyvin todennäköistä, että Kassapaate-luokassasi on nyt "copypaste"-koodia. Poista se viime viikon tehtävän 2.2. ohjeita soveltaen.
On todennäköistä, että lounaiden hinnat muuttuvat tulevaisuudessa. Tee koodistasi sellainen, että lounaan hinnat 2.4 euroa ja 4.0 euroa eivät esiinny kassapäätteen koodissa kuin korkeintaan yhdessä kohdassa. Valitse kohta siten, että hintojen muuttaminen on mahdollisimman helppoa.
Lisätään vielä kassapäätteelle metodi jonka avulla kortille voidaan ladata lisää rahaa. Muista, että rahan lataamisen yhteydessä ladattava summa viedään kassapäätteeseen. Metodin runko:
public void lataaRahaaKortille(LyyraKortti kortti, double summa) { // ... }
Testipääohjelma ja haluttu tulostus:
public class Main { public static void main(String[] args) { Kassapaate unicafeExactum = new Kassapaate(); System.out.println( unicafeExactum ); LyyraKortti antinKortti = new LyyraKortti(2); System.out.println("kortilla rahaa " + antinKortti.saldo() + " euroa"); boolean onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti); System.out.println("riittikö raha: " + onnistuiko); unicafeExactum.lataaRahaaKortille(antinKortti, 100); onnistuiko = unicafeExactum.syoMaukkaasti(antinKortti); System.out.println("riittikö raha: " + onnistuiko); System.out.println("kortilla rahaa " + antinKortti.saldo() + " euroa"); System.out.println( unicafeExactum ); } }
kassassa rahaa 1000.0 edullisia lounaita myyty 0 maukkaita lounaita myyty 0 kortilla rahaa 2.0 euroa riittikö raha: false riittikö raha: true kortilla rahaa 98.0 euroa kassassa rahaa 1100.0 edullisia lounaita myyty 0 maukkaita lounaita myyty 1
Harjoitellaan erilaisia taulukoihin liittyviä peruskäsitteitä ja -tekniikoita.
Tee staattinen metodi private static int taulukonPienin(int[] taulukko)
,
joka palautaa arvonaan taulukon pienimmän alkion.
int[] luvut = {8, 3, 7, 12, -3, 2, 4}; System.out.println("Pienin alkio on " + taulukonPienin(luvut));
Pienin alkio on -3
Huom: Älä missään nimessä tässä tehtävässä järjestä taulukkoa! Kaksikin hyvää syytä: 1) Muu ohjelma saattaa luottaa taulukon säilyttävän alkuperäisen järjestyksensä. 2) Etsiminen peräkkäishaulla on lineaarinen operaatio, järjestäminen (opituilla tavoilla) on neliöinen; siis paljon työläämpi operaatio. Kolmaskin syy on: järjestämällä tehty ratkaisu ei kelpaa palautusautomaatille.
Tee staattinen metodi private static int taulukonPienimmanIndeksi(int[] taulukko)
,
joka palautaa arvonaan taulukon pienimmän alkion indeksin.
Entä jos pienin ei ole yksikäsitteinen?
int[] luvut = {8, 3, 7, 12, -3, 2, 4}; System.out.println("Pienimmän indeksi on " + taulukonPienimmanIndeksi(luvut));
Pienimmän indeksi on 4
Tee staattinen metodi private static String taulukkoTekstiksi(int[] taulukko)
,
joka palauttaa arvonaan taulukon sisällön merkkijonona seuraavan esimerkin mukaisesti.
int[] luvut = {8, 3, 7, 12, -3, 2, 4}; System.out.println(taulukkoTekstiksi(luvut));
8,3,7,12,-3,2,4
Huomaa ettei viimeistä lukua seuraa pilkku.
Tee staattinen metodi
private static boolean onkoJarjestyksessa(int[] taulukko)
,
joka palauttaa arvon true, jos taulukon alkiot ovat
nousevassa suuruusjärjestyksessä, muuten false.
Esimerkki:
int[] luvut1 = {8, 3, 7, 12, -3, 2, 4}; System.out.println(onkoJarjestyksessa(luvut1)); int[] luvut2 = {8, 9, 9, 199, 213}; System.out.println(onkoJarjestyksessa(luvut2)); int[] luvut3 = {1, 1, 1}; System.out.println(onkoJarjestyksessa(luvut3));
Tulostus:
false true true
Tee staattinen metodi
private static boolean onkoKahtaSamaa(int[] taulukko)
,
joka selvittää, onko taulukossa vähitään jokin luku kahteen kertaan.
Esimerkki:
int[] luvut1 = {8, 3, 7, 12, -3, 2, 4}; System.out.println(onkoKahtaSamaa(luvut1)); int[] luvut2 = {8, 9, 9, 199, 213}; System.out.println(onkoKahtaSamaa(luvut2));
Tulostus:
false true
Vihje: algoritmin hahmotelma:
Luennoilla tutustuttiin yksinkertaiseen vaihtojärjestämiseen. Siinä taulukon joka indeksikohtaan etsittiin "jäljellä olevista pienin". Aina kun havaittiin epäjärjestys, arvoja vaihdettiin. Tätä algoritmia voidaan hivenen tehostaa tuossa viimeksi mainitussa kohdassa: etsitään edelleenkin "jäljellä olevista" pienin, mutta tehdäänkin tarvittaessa tuo vaihto vain "jäljellä olevista pienimmän" kanssa, ei aina, kun havaittiin epäjärjestys.
Tämän järjestämisalgoritmin nimi on vaihtojärjestäminen.
Toteuta vaihtojärjestäminen staattisena metodina
public static void jarjesta(int[] taulukko)
Algoritmin toimintaa voidaan kuvailla seuraavaan luettelon tapaan. [Otetaan tässä vaiheessa mukaan testitarkoituksia varten aputulostus, joka tietenkin poistetaan lopullisesta tuotteesta. Usein testitulostukset on tapana "kommentoida pois" ohjelmatekstistä! Silloin ne on myös helppo palauttaa, jos/kun algoritmin myöhemmin havaitaan toimivan väärin!]
Kokeile metodin toimintaa seuraavalla esimerkillä:
int[] luvut = {8, 3, 7, 12, -3, 2, 4}; jarjesta(luvut);
Ohjelman pitäisi tulostaa seuraavaan tapaan:
8,3,7,12,-3,2,4 -3,3,7,12,8,2,4 -3,2,7,12,8,3,4 -3,2,3,12,8,7,4 -3,2,3,4,8,7,12 -3,2,3,4,7,8,12 -3,2,3,4,7,8,12
Luennolla mittailtiin kolmen järjestysalgoritmin nopeutta eri tilanteissa. Täydennä materiaalin antamaa mittausohjelmaa VertaileJarjAlgoritmeja.java edellisen tehtävän algoritmilla ja selvitä erityisesti, onko vaihtojärjestäminen merkittävästi nopeampi kuin luennoilla esitetty yksinkertainen vaihtojärjestäminen.
Huom: Suorita vertailu useita kertoja ja laske vaikka keskiarvo, koska satunnaiset seikat saattavat vaikuttaa mittaustuloksiin! Tällainen on esimerkiksi koneen mahdollinen verkkoviestintä mittauksen aikana.
Huom: Jos haluat mitata hyvin tarkasti tai jos Windowsissa tuo mittausohjelma antaa kovin epämääräisiä tuloksia, voit käyttää nanosekunnin tarkuudella mittaavaa versiota VertaileJarjAlgoritmejaB.java.
Luennolla väitettiin, että binäärihaku olisi muka jotenkin kovin nopea tapa hakea järjestetystä taulukosta vastausta kysymykseen, missä jos missään indeksissä sijaitsee kysytty arvo.
Ota edellisen tehtävän mittausohjelmasta mallia ja kehittele oma mittausohjelma. Binäärihaun algoritmi löytyy materiaalin luvusta 4. Ohjelma voi keskustella käyttäjän kanssa vaikkapa seuraavaan tyyliin:
Peräkkäishaun ja binäärihaun vertailua. Miten suuresta taulukosta haetaan? 100000 Montako hakua tehdään? 10000 Peräkkäishaku: 246 ms. Binäärihaku: 31 ms.
Vähän sormituntumaa matriiseihin.
Tee staattinen metodi
private static int matriisinPienin(int[][] matriisi)
,
joka palautaa arvonaan matriisin pienimmän alkion arvon.
Esimerkki:
int[][] matriisi = { {5, 2, -3}, {9, -1, 4} }; System.out.println("Pienin alkio on " + matriisinPienin(matriisi));
Pienin alkio on -3
Tee staattinen metodi
private void tulostaMatriisi(int[][] taulukko)
,
joka kirjoittaa matriisin standarditulosvirtaan seuraavan esimerkin
tapaan:
int[][] matriisi = { {5, 2, -3}, {9, -1, 4} }; tulostaMatriisi(matriisi);
Tulostus:
5, 2, -3 9, -1, 4
Tee staattinen metodi
private void indeksienSummaAlkioihin(int[][] taulukko)
,
joka sijoittaa matriisin jokaisen alkion arvoksi alkion indeksien
summan.
Esimerkki:
int[][] matriisi = new int[4][5]; indeksienSummaAlkioihin(matriisi); tulostaMatriisi(matriisi);
Tulostus:
0, 1, 2, 3, 4 1, 2, 3, 4, 5 2, 3, 4, 5, 6 3, 4, 5, 6, 7
Monet tehtävät edellä on tehty ohjatusti vaiheittain. Nyt on aika kokeilla omia siipiä! Pyri tekemään myös seuraavat tehtävät askelittain etenemällä.
Tee sovellus, joka kysyy ensin nimien lukumäärän ja sitten nimet yksi kerrallaan standardisyöttövirrasta. Lopuksi ohjelma tulostaa nimet String-luokan compareTo()-aksessorin antamassa "aakkosjärjestyksessä". Nimien alussa ja lopussa olevat turhat välilyönnit on poistettava.
Vihje: Muokkaa suosikkijärjestämisalgoritmisi sellaiseksi, jossa int-vertailun sijalla on compareTo()-vertailu.
Tee ohjelma, joka ensin kysyy syötettävien kokonaislukujen lukumäärän, sitten lukee tuon määrän lukuja taulukkoon ja lopuksi tarjoaa seuraavan palvelun: Ohjelmalle annetaan lukuja yksi kerrallaan ja ohjelma selvittää, löytyykö luku taulukosta. Etsimiseen on käytettävä binäärihakua. Muista mitä binäärihaku edellyttää taulukon järjestykseltä!
Lotto on numeroveikkaus, jossa arvotaan 7 numeroa ja 3 lisänumeroa 39 numerosta. Loton voittoluokat ovat 7 oikein, 6 ja lisänumero oikein, 6 oikein, 5 oikein ja 4 oikein. Tee ohjelma joka ensin arpoo oikean lottorivin. Sitten ohjelmalta voi kysellä, onko jokin lottorivi oikein. Kyselyitä voi olla useampia. Suunnittele itse, miten ohjelman toiminta päättyy. Satunnaisluvun väliltä 1-39 saat arvottua vaikkapa seuraavasti:
int arvottu = (int)(39*Math.random()) + 1;
Kyselija on olio, "laite", joka helpottaa tietokilpailun kysymysten tekemistä ja vastausten analysointia. Sen API on seuraavanlainen:
kysymyksenAlku kysytty sana kysymyksenLoppu
OIKEIN!
tai:
VÄÄRIN! Oikea vastaus on "oikeaVastaus".
Ks. myös esimerkki alla.
Käyttöesimerkki selventää konstruktorin ja aksessoreiden käyttöä:
Kyselija kilpa = new Kyselija("Mitä on italian", "suomeksi?"); kilpa.samaistaIsotJaPienet(true); boolean oikein; oikein = kilpa.kysy("tutti", "kaikki"); if (oikein) System.out.println("Hienoa, hyvä alku!"); // kuvaruudulla tapahtuu: // // Kone: Mitä on italian "tutti" suomeksi? // Käyttäjä: Kaikki // Kone: OIKEIN! // Kone: Hienoa, hyvä alku! kilpa.kysy("ferro", "rauta"); // Arvon palauttavaa metodia voi // kutsua myös arvoa käyttämättä! // kuvaruudulla tapahtuu: // // Kone: Mitä on italian "ferro" suomeksi? // Käyttäjä: rAuTA // Kone: OIKEIN! oikein = kilpa.kysy("matto", "hullu"); if (!oikein) System.out.println("No mitäs nyt?"); // kuvaruudulla tapahtuu: // // Kone: Mitä on italian "matto" suomeksi? // Käyttäjä: matto // Kone: VÄÄRIN! Oikea vastaus on "hullu". // Kone: No mitäs nyt? System.out.println("Kysymyksiä oli " + kilpa.montakoKysymystä() + ", oikeita vastauksia " + kilpa.montakoOikein() + "."); // kuvaruudulla tapahtuu: // // Kone: Kysymyksiä oli 3, oikeita vastauksia 2. kilpa.kysy("madre", "äiti"); kilpa.kysy("cavallo", "hevonen"); kilpa.kysy("birra", "olut"); // ... jne., jne...
Ohjelmoi myös sovellus, joka havainnollistaa Kyselija-luokan käyttöä tietokilpailun toteuttamisessa. Voit tietenkin valita itseäsi kiinnostavan aihepiirin. Huomaa että tämä tehtävä ei ole taulukkotehtävä vaikka tässä osassa onkin.
Vastaa kurssikyselyyn osoitteessa http://ilmo.cs.helsinki.fi/kurssit/servlet/Valinta. Valitse Ohjelmoinnin perusteet. Muista myös lähettää lomake! Lähetysnäppäin on lomakkeen lopussa. Vastaukset ovat anonyymejä, joten ei tarvitse ujostella... Ja tästäkin tehtävästä silti saa "rastin", kunhan vain vakuuttaa ohjaajan siitä, että vastattu on.
Vastauksilla ihan oikeasti on merkitystä laitoksen opetusta kehitettäessä! Myös tuo viimeinen kohta, vapaa teksti, on mitä tärkein: siellä voi antaa täsmäpalautetta ja loistavia kehitysideoita!