Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.
Tekijät: Arto Vihavainen ja Matti Luukkainen
Pieni johdanto olio-ohjelmointiin ennen aloitusta.
Proseduraalisessa ohjelmoinnissa, eli tähän asti opiskelemassamme ohjelmointityylissä, ohjelma jäsennellään jakamalla se pienempiin osiin eli metodeihin. Metodi toimii ohjelman erillisenä osana, ja sitä voi kutsua mistä tahansa ohjelmastan sisältä. Metodia kutsuttaessa ohjelman suoritus siirtyy metodin alkuun, ja suorituksen päätyttyä palataan takaisin siihen kohtaan mistä metodia kutsuttiin.
Olio-ohjelmoinnissa, kuten proseduraalisessa ohjelmoinnissa, pyritään jakamaan ohjelma pieniin osiin. Olio-ohjelmoinnissa pienet osat ovat olioita. Jokaisella oliolla on oma yksittäinen vastuunsa eli se sisältää joukon yhteenkuuluvaa tietoa ja toiminnallisuutta. Olio-ohjelmat koostuvat useista olioista, joiden yhteinen toiminta määrittelee järjestelmän toiminnan.
Olemme käyttäneet jo monia Javan valmiita olioita. Esimerkiksi ArrayList
:it ovat olioita. Jokainen yksittäinen lista koostuu yhteenkuuluvasta tiedosta, eli olion tilasta. Listaolioihin liittyy toiminnallisuutta, eli metodt joilla olion tilaa voidaan muuttaa. Esimerkiksi seuraavassa ohjelmanpätkässä on kaksi ArrayList
-olioa kaupungit
ja maat
:
public static void main(String[] args) { ArrayList<String> kaupungit = new ArrayList<String>(); ArrayList<String> maat = new ArrayList<String>(); maat.add("Suomi"); maat.add("Saksa"); maat.add("Hollanti"); kaupungit.add("Berliini"); kaupungit.add("Nijmegen"); kaupungit.add("Turku"); kaupungit.add("Helsinki"); System.out.println("maita " + maat.size() ); System.out.println("kaupunkeja " + kaupungit.size() ); }
Sekä maat
-olio että kaupungit
-olio elää omaa elämäänsä. Molempien "tila" on toisten olioiden tilasta riippumaton. Esim. olion maat
tila koostuu listalla olevista merkkijonoista "Suomi", "Saksa" ja "Hollanti" ja todennäköisesti myös tiedosta kuinka monta maata listalla on.
Olioon liittyvää metodikutsua tehdessä (esimerkiksi maat.add("Suomi");
) pisteen vasemmalle puolelle tulee sen olion nimi, jolle metodia kutsutaan, oikealle metodin nimi. Kun kysytään montako merkkijonoa listalla maat
on, kutsu on muotoa maat.size()
eli kutsutaan maat
oliolle sen metodia size
. Metodin palauttama tulos riippuu olion maat
tilasta, eli muut oliot kuten kaupungit
eivät vaikuta metodin suoritukseen millään tavalla.
Olemme jo useasti käyttämään komentoa new
. Esimerkiksi listan (ArrayList
) ja lukijan (Scanner
) luominen on tapahtunut new
-komennolla. Syy on se, että kumpikin näistä on Luokkia, joista olio luodaan. Java-kielessä oliot luodaan aina new
-komennolla muutamaa poikkeusta lukuunottamatta.
Yksi näistä poikkeuksista ovat merkkijonot, joiden luomiseen ei aina tarvita new
:iä. Tuttu tapa merkkijonon luomiseen on oikeastaan Javan lyhennysmerkintä new
:in käytölle. Merkkijonon voi luoda myös new:illä kuten muutkin oliot:
String teksti = "tekstiä"; // lyhennysmerkintä merkkijono-olion luomiselle String toinenTeksti = new String("lisää tekstiä");
On myös tilanteita, missä Javan valmis kalusto kutsuu new
:iä piilossa ohjelmoijalta.
On selvää että kaikki oliot eivät ole keskenään samankaltaisia. Esim. ArrayList
-oliot poikkeavat ratkaisevasti String
-olioista. Kaikilla ArrayList
:eillä on samat metodit (add
, contains
, remove
, size
, ...) ja vastaavasi kaikilla String
-olioilla on samat metodit (substring
, length
, charAt
, ...). ArrayList
- ja String
-olioiden metodit eivät ole samat sillä ne ovat eri tyyppisiä olioita.
Tietyn olioryhmän tyyppiä kutsutaan luokaksi. ArrayList
on luokka, String
on luokka, Scanner
on luokka, jne... Oliot taas ovat luokasta tehtyjä ilmentymiä.
Luokan kaikilla olioilla on samat metodit, sekä samankaltainen tila. Esim. kaikkien ArrayList
-olioiden tila koostuu listalle tallennetuista alkioista. String
-olioiden tila taas koostuu merkkijonon muodostavista kirjaimista.
Luokka määrittelee minkälaisia luokan oliot ovat:
Luokka kuvaa siitä luotavien olioiden "rakennuspiirustukset".
Otetaan analogia tietokoneiden ulkopuoleisesta maailmasta. Rintamamiestalot lienevät kaikille suomalaisille tuttuja. Voidaan ajatella, että jossain on olemassa piirustukset jotka määrittelevät minkälainen rintamamiestalo on. Piirrustukset ovat luokka, eli ne määrittelevät luokasta luotavien olioiden luonteen:
Yksittäiset oliot eli rintamamiestalot on tehty samojen piirustusten perusteella, eli ne ovat saman luokan ilmentymiä. Yksittäisten olioiden tila eli ominaisuudet (esim. seinien väri, katon rakennusmateriaali ja väri, kivijalan väri, ovien rakennusmateriaali ja väri, ...) vaihtelevat. Seuraavassa yksi "rintamamiestalo-luokan olio":
Luokasta luodaan olio aina kutsumalla olion luovaa metodia eli konstruktoria komennon new
avulla. Esimerkiksi Scanner
-luokasta luodaan uusi ilmentymä eli olio kun kutsutaan new Scanner(..)
:
Scanner lukija = new Scanner(System.in);
Konstruktorit saavat parametreja kuten muutkin metodit.
Tehtäväpohjan mukana tulee valmis luokka Tili
.
Luokan Tili
olio esittää pankkitiliä, jolla on saldo
(eli jossa on jokin määrä rahaa). Tilejä käytetään näin:
Tili artonTili = new Tili("Arton tili",100.00); Tili artonSveitsilainenTili = new Tili("Arton tili Sveitsissä",1000000.00); System.out.println("Alkutilanne"); System.out.println(artonTili); System.out.println(artonSveitsilainenTili); artonTili.otto(20); System.out.println("Arton tilin saldo on nyt: "+artonTili.saldo()); artonSveitsilainenTili.pano(200); System.out.println("Arton toisen tilin saldo on nyt: "+artonSveitsilainenTili.saldo()); System.out.println("Lopputilanne"); System.out.println(artonTili); System.out.println(artonSveitsilainenTili);
Tee ohjelma, joka luo tilin jonka saldo on 100.0, panee tilille 20.0 ja tulostaa tilin. Huom! tee kaikki nämä operaatiot täsmälleen tässä järjestyksessä.
Tee ohjelma joka:
"Matin tili"
saldolla 1000"Oma tili"
saldolla 0
Yllä siirsit rahaa tililtä toiselle. Tehdään seuraavaksi metodi joka tekee saman!
Toteuta ohjelmapohjaan metodi public static void tilisiirto(Tili
mista, Tili minne, double paljonko)
joka siirtää rahaa
tililtä toiselle. Sinun ei tarvitse tarkistaa
että mista
-tilin saldo riittää.
Tämän jälkeen tee main
-metodissasi seuraavaa:
"tili A"
saldolla 100.0"tili B"
saldolla 0.0"tili C"
saldolla 0.0Luokka määritellään jotain mielekästä kokonaisuutta varten. Usein "mielekäs kokonaisuus" kuvaa jotain reaalimaailman asiaa. Jos tietokoneohjelman pitää käsitellä henkilötietoja, voisi olla mielekästä määritellä erillinen luokka Henkilo
joka kokoaa yhteen henkilöön liittyvät metodit ja ominaisuudet.
Aloitetaan. Oletetaan että meillä on projektirunko jossa on tyhjä pääohjelma:
public class Main { public static void main(String[] args) { } }
Luomme nyt projektiimme eli ohjelmaamme uuden luokan. Tämä tapahtuu valitsemalla NetBeansissa vasemmalta projects-kohdasta hiiren oikealla napilla new ja java class. Avautuvaan dialogiin annetaan luokalle nimi.
Kuten muuttujien ja metodien nimien, myös luokan nimen on aina oltava mahdollisimman kuvaava. Joskus ohjelmoinnin edetessä luokka elää ja muuttaa muotoaan. Tällaisissa tilanteissa luokan voi nimetä uudelleen ( ks. NetBeans ohje ks. NetBeans ohje ).
Luodaan luokka nimeltä Henkilo
. Luokasta muodostuu oma tiedostonsa Henkilo.java
. Eli ohjelma koostuu nyt kahdesta tiedostosta, sillä pääohjelma on omassa tiedostossaan. Aluksi luokka on tyhjä:
public class Henkilo { }
Luokan tulee määritellä mitä metodeja ja ominaisuuksia luokan olioilla on. Päätetään, että jokaisella henkilöllä on nimi ja ikä. Nimi on luonnollista esittää merkkijonona, eli Stringinä, ja ikä taas kokonaislukuna. Lisätään nämä rakennuspiirustuksiimme:
public class Henkilo { private String nimi; private int ika; }
Määrittelimme yllä että kaikilla Henkilo
-luokan olioilla on nimi
ja ika
. Määritettely tapahtuu hiukan kuten normaalin muuttujan määrittely. Eteen on kuitenkin nyt laitettu avainsana private
. Tämä tarkoittaa sitä, että nimi ja ikä eivät näy suoraan olion ulkopuolelle vaan ovat "piilossa" olion sisällä. Olion sisälle piilottamista kutsutaan kapseloinniksi.
Luokan sisälle määriteltyjä muuttujia kutsutaan oliomuuttujiksi tai olion kentiksi tai olion attribuuteiksi. Rakkaalla lapsella on monta nimeä.
Eli olemme määritelleet rakennuspiirustukset -- luokan -- henkilöoliolle. Kaikilla henkilöolioilla on muuttujat nimi
ja ika
. Henkilöiden "tila" koostuu niiden nimeen ja ikään asetetuista arvoista.
Luotavalle oliolle halutaan asettaa luontivaiheessa jokin alkutila. Itse määritellyn olion luominen tapahtuu hyvin samaan tapaan kuin Javan valmiiden olioiden kuten ArrayList
:ien luominen. Oliot siis luodaan new
-komennolla. Olion synnyttämisen yhteydessä olisikin kätevä pystyä antamaan arvo joillekin olioiden muuttujille. Esim. henkilön synnytyksessä olisi kätevää pystyä antamaan nimi jo syntymähetkellä:
public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); // ... }
Tämä onnistuu määrittelemällä olion synnyttävä metodi, eli konstruktori. Seuraavassa on määritelty Henkilo
-luokalle konstruktori, joka luo uuden Henkilo
-olion. Konstruktorissa luotavan henkilön iäksi asetetaan 0 ja nimeksi parametrina tuleva merkkijono:
public class Henkilo { private String nimi; private int ika; public Henkilo(String nimiAlussa) { this.ika = 0; this.nimi = nimiAlussa; } }
Konstruktori on siis nimeltään sama kuin luokan nimi. Yllä luokka (class) on Henkilo
, ja konstruktori public Henkilo(String nimiAlussa)
. Konstruktorille parametrina tuleva arvo tulee sulkuihin konstruktorin nimen perään. Konstruktorin voi ajatella olevan metodi, jonka Java suorittaa kun olio luodaan sanomalla new Henkilo("Pekka");
Aina kun jostain luokasta luodaan olio, kutsutaan kyseisen luokan konstruktoria.
Muutama huomio: konstruktorin sisällä on komento this.ika = 0
. Tässä asetetaan arvo 0 juuri tämän olion, eli "this"-olion sisäiselle muuttujalle ika
. Toinen komento this.nimi = nimiAlussa;
taas asettaa juuri tämän olion sisäiselle muuttujalle nimi
arvoksi parametrina annetun merkkijonon. Olion muuttujat ika
ja nimi
näkyvät konstruktorissa ja muuallakin olion sisällä automaattisesti. Niihin viitataan this
-etuliitteellä. Koska niissä on private-määre, niin olion ulkopuolelle ne eivät näy.
Vielä yksi huomio: jos ohjelmoija ei tee luokalle konstruktoria, tekee Java automaattisesti luokalle oletuskonstruktorin. Oletuskonstruktori on konstruktori joka ei tee mitään. Eli jos konstruktoria ei jostain syystä tarvita, ei sellaista tarvitse ohjelmoida.
Alkaa olla korkea aika päästä käyttämään Henkilo
-olioita. Osaamme luoda olion ja alustaa olion muuttujat. Järkevään toimintaan pystyäkseen olioilla on oltava myös metodeja. Tehdään Henkilo
-luokalle metodi jonka avulla olio tulostaa itsensä ruudulle:
public class Henkilo { private String nimi; private int ika; public Henkilo(String nimiAlussa) { this.ika = 0; this.nimi = nimiAlussa; } public void tulostaHenkilo() { System.out.println(this.nimi + ", ikä " + this.ika + " vuotta"); } }
Metodi siis kirjoitetaan luokan sisälle, metodin nimen eteen tulee public void
sillä metodin on tarkoitus näkyä ulkomaailmalle ja metodi ei palauta mitään. Huomaa, että sana static
ei nyt ilmene missään. Olioiden yhteydessä static
-määrettä ei käytetä. Selitämme ensi viikolla hieman tarkemmin mistä tässä on kysymys.
Metodin tulostaHenkilo
sisällä on yksi koodirivi joka käyttää hyvakseen oliomuuttujia nimi
ja ika
. Juuri tämän olion muuttujiin viitataan taas etuliitteellä this
. Kaikki olion muuttujat ovat siis näkyvillä ja käytettävissä metodin sisällä.
Luodaan pääohjelmassa kolme henkilöä ja pyydetään niitä tulostamaan itsensä:
public class Main { public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); Henkilo martin = new Henkilo("Martin"); pekka.tulostaHenkilo(); antti.tulostaHenkilo(); martin.tulostaHenkilo(); } }
Tulostuu:
Pekka, ikä 0 vuotta Antti, ikä 0 vuotta Martin, ikä 0 vuotta
Sama screencastina:
Luo luokka Tuote
joka esittää kaupan tuotetta jolla on hinta, lukumäärä ja nimi.
Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen 073.Tuote kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) Tuote
.
Luokalla tulee olla:
public Tuote(String nimiAlussa, double hintaAlussa, int maaraAlussa)
public void tulostaTuote()
joka tulostaa tuotteen tiedot tässä muodossa:
Banaani, hinta 1.1, 13 kpl
Lisätään aiemmin rakentamallemme Henkilölle metodi, joka kasvattaa henkilön ikää vuodella:
public class Henkilo { // ... public void vanhene() { this.ika = this.ika + 1; } }
Metodi siis kirjoitetaan tulostaHenkilo
-metodin tapaan luokan Henkilo
sisälle. Metodissa kasvatetaan oliomuuttujan ika
arvoa yhdellä.
Kutsutaan metodia ja katsotaan mitä tapahtuu:
public class Main { public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); pekka.tulostaHenkilo(); antti.tulostaHenkilo(); System.out.println(""); pekka.vanhene(); pekka.vanhene(); pekka.tulostaHenkilo(); antti.tulostaHenkilo(); } }
Ohjelman tulostus on seuraava:
Pekka, ikä 0 vuotta Antti, ikä 0 vuotta Pekka, ikä 2 vuotta Antti, ikä 0 vuotta
Eli "syntyessään" molemmat oliot ovat nollavuotiaita (konstruktorissa suoritetaan mm. rivi this.ika = 0;
). Olion pekka
metodia vanhene
kutsutaan kaksi kertaa. Kuten tulostus näyttää, tämä saa aikaan sen että Pekan ikä on vanhenemisen jälkeen 2 vuotta. Kutsumalla metodia Pekkaa vastaavalle oliolle, toisen henkilöolion ikä ei muutu.
Jokaisella oliolla on siis oma sisäinen tilansa.
Aivan kuiten edellisellä viikolla käsittelemämme olioihin liittymättömät metodit, myös olioihin liittyvät metodit voivat palauttaa arvon. Lisätään Henkilölle metodi joka palauttaa henkilön iän:
public class Henkilo { // ... public int palautaIka() { return this.ika; } }
Eli koska kyseessä olioon liittyvä metodi, ei määrittelyssä ole sanaa static. Olioiden arvon palauttavia metodeja käytetään kuten mitä tahansa arvon palauttavia metodeja:
public class Main { public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); pekka.vanhene(); pekka.vanhene(); antti.vanhene(); System.out.println( "Pekan ikä: "+pekka.palautaIka() ); System.out.println( "Antin ikä: "+antti.palautaIka() ); int yht = pekka.palautaIka() + antti.palautaIka(); System.out.println( "Pekka ja Antti yhteensä "+yht+ " vuotta" ); } }
Ohjelman tulostus on seuraava:
Pekan ikä 2 Antin ikä 1 Pekka ja Antti yhteensä 3 vuotta
Luo luokka Kertoja
jolla on:
public Kertoja(int luku)
.public int kerro(int toinenLuku)
joka palauttaa sille annetun luvun toinenLuku
kerrottuna konstruktorille annetulla luvulla luku
.Esimerkki luokan käytöstä:
Kertoja kolmellaKertoja = new Kertoja(3); System.out.println("kolmellaKertoja.kerro(2): " + kolmellaKertoja.kerro(2)); Kertoja neljallaKertoja = new Kertoja(4); System.out.println("neljallaKertoja.kerro(2): " + neljallaKertoja.kerro(2)); System.out.println("kolmellaKertoja.kerro(1): " + kolmellaKertoja.kerro(1)); System.out.println("neljallaKertoja.kerro(1): " + neljallaKertoja.kerro(1));
Tulostus
kolmellaKertoja.kerro(2): 6 neljallaKertoja.kerro(2): 8 kolmellaKertoja.kerro(1): 3 neljallaKertoja.kerro(1): 4
Tehtäväpohjan mukana tulee osittain valmiiksi toteutettu luokka VahenevaLaskuri
:
public class VahenevaLaskuri { private int arvo; // oliomuuttuja joka muistaa laskurin arvon public VahenevaLaskuri(int arvoAlussa) { this.arvo = arvoAlussa; } public void tulostaArvo() { System.out.println("arvo: " + this.arvo); } public void vahene() { // kirjoita tänne metodin toteutus // laskurin arvon on siis tarkoitus vähentyä yhdellä } // ja tänne muut metodit }
Seuraavassa esimerkki miten pääohjelma käyttää vähenevää laskuria:
public class Paaohjelma { public static void main(String[] args) { VahenevaLaskuri laskuri = new VahenevaLaskuri(10); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); } }
Pitäisi tulostua:
arvo: 10 arvo: 9 arvo: 8
VahenevaLaskuri
-luokan konstruktorille annetaan parametrina alkuarvo. Esimerkin oliota laskuri
luodessa laskurille välitetään parametrina arvo 10
. Esimerkin laskuri
-olioon liittyvään oliomuuttujaan arvo
asetetaan siis aluksi arvo 10
. Laskurin arvon voi tulostaa metodilla tulostaArvo()
. Laskurilla tulee myös olla metodi vahene()
joka vähentää laskurin arvoa yhdellä.
Täydennä luokan runkoon metodin vahene()
toteutus sellaiseksi, että se vähentää kutsuttavan olion oliomuuttujan arvo
arvoa yhdellä. Kun olet toteuttanut metodin vahene()
, edellisen esimerkin pääohjelman tulee toimia esimerkkitulosteen mukaan.
Täydennä metodin vahene()
toteutus sellaiseksi, ettei laskurin arvo mene koskaan negatiiviseksi. Eli jos laskurin arvo on jo 0, ei vähennys sitä enää vähennä:
public class Paaohjelma { public static void main(String[] args) { VahenevaLaskuri laskuri = new VahenevaLaskuri(2); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); } }
Tulostuu:
arvo: 2 arvo: 1 arvo: 0 arvo: 0
Tee laskurille metodi public void nollaa()
joka nollaa laskurin arvon, esim:
public class Paaohjelma { public static void main(String[] args) { VahenevaLaskuri laskuri = new VahenevaLaskuri(100); laskuri.tulostaArvo(); laskuri.nollaa(); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); } }
Tulostuu:
arvo: 100 arvo: 0 arvo: 0
Tee laskurille metodi public void palautaAlkuarvo()
, joka palauttaa laskurille arvon joka sillä oli alussa:
public class Paaohjelma { public static void main(String[] args) { VahenevaLaskuri laskuri = new VahenevaLaskuri(100); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); laskuri.vahene(); laskuri.tulostaArvo(); laskuri.nollaa(); laskuri.tulostaArvo(); laskuri.palautaAlkuarvo(); laskuri.tulostaArvo(); } }
Tulostuu:
arvo: 100 arvo: 99 arvo: 98 arvo: 0 arvo: 100
Vihje jotta alkuarvon voi palauttaa, se täytyy "muistaa" toisen oliomuuttujan avulla! Joudut siis lisäämään ohjelmaan oliomuuttujan johon talletetaan laskurin alussa saama arvo.
Kumpulan kampuksella Helsingissä toimivaan Unicafe-nimiseen gourmet-ravintolaan tarvitaan uusi ruokalista. Keittiömestari tietää ohjelmoinnista, ja haluaa listan hallinnointiin tietokonejärjestelmän. Toteutetaan tässä tehtävässä järjestelmän sydän, luokka Ruokalista.
Tehtäväpohjan mukana tulee Main
-luokka, jossa voit testata ruokalistan toimintaa. Ruokalistan toteuttamista varten saat seuraavanlaisen tehtäväpohjan:
import java.util.ArrayList; public class Ruokalista { private ArrayList<String> ateriat; public Ruokalista() { this.ateriat = new ArrayList<String>(); } // toteuta tänne tarvittavat metodit }
Ruokalistaoliolla on siis oliomuuttujana ArrayList, jonka on tarkoitus tallentaa ruokalistalla olevien ruokalajien nimet.
Ruokalistan tulee tarjota metodit public void lisaaAteria(String ateria)
, public void tulostaAteriat()
, ja public void tyhjennaRuokalista()
.
Toteuta metodi public void lisaaAteria(String ateria)
, joka lisää uuden aterian ruokalistan ateriat
-listaan. Jos lisättävä ateria on jo listassa, sitä ei lisätä uudelleen.
Toteuta metodi public void tulostaAteriat()
, joka tulostaa ateriat. Esimerkiksi kolmen aterian lisäyksen jälkeen tulostuksen tulee olla seuraavanlainen.
ensimmäisenä lisätty ateria toisena lisätty ateria kolmantena lisätty ateria
Toteuta metodi public void tyhjennaRuokalista()
joka tyhjentää ruokalistan. ArrayList
-luokalla on metodi josta on tässä hyötyä. NetBeans osaa vihjata käytettävissä olevista metodeista kun kirjoitat olion nimen ja pisteen. Yritä kirjoittaa ateriat.
metodirungon sisällä ja katso mitä käy.
Jatketaan taas Henkilo
-luokan laajentamista. Luokan tämänhetkinen versio on seuraava:
public class Henkilo { private String nimi; private int ika; public Henkilo(String nimiAlussa) { this.ika = 0; this.nimi = nimiAlussa; } public void tulostaHenkilo() { System.out.println(this.nimi + ", ikä " + this.ika + " vuotta"); } public void vanhene() { this.ika = this.ika + 1; } }
Tehdään henkilölle metodi, jonka avulla voidaan selvittää onko henkilö täysi-ikäinen. Metodi palauttaa totuusarvon -- joko true
tai false
:
public class Henkilo { // ... public boolean taysiIkainen(){ if ( this.ika < 18 ) { return false; } return true; } /* huom. metodin voisi kirjoittaa lyhyemmin seuraavasti: public boolean taysiIkainen(){ return this.ika >= 18; } */ }
Ja testataan:
public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); int i = 0; while ( i < 30 ) { pekka.vanhene(); i++; } antti.vanhene(); System.out.println(""); if ( antti.taysiIkainen() ) { System.out.print("täysi-ikäinen: "); antti.tulostaHenkilo(); } else { System.out.print("alaikäinen: "); antti.tulostaHenkilo(); } if ( pekka.taysiIkainen() ) { System.out.print("täysi-ikäinen: "); pekka.tulostaHenkilo(); } else { System.out.print("alaikäinen: "); pekka.tulostaHenkilo(); } }
alaikäinen: Antti, ikä 1 vuotta täysi-ikäinen: Pekka, ikä 30 vuotta
Viritellään ratkaisua vielä hiukan. Nyt henkilön pystyy "tulostamaan" ainoastaan siten, että nimen lisäksi tulostuu ikä. On tilanteita, joissa haluamme tietoon pelkän olion nimen. Eli tehdään tarkoitusta varten oma metodi:
public class Henkilo { // ... public String getNimi() { return this.nimi; } }
Metodi getNimi
palauttaa oliomuuttujan nimi
kutsujalle. Metodin nimi on hieman erikoinen. Javassa on usein tapana nimetä oliomuuttujan palauttava metodi juuri näin, eli getMuuttujanNimi
. Tälläisiä metodeja kutsutaan usein "gettereiksi".
Muotoillaan pääohjelma käyttämään uutta "getteri"-metodia:
public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); int i = 0; while ( i < 30 ) { pekka.vanhene(); i++; } antti.vanhene(); System.out.println(""); 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 " ); } }
Tulostus alkaa olla jo aika siisti:
Antti on alaikäinen Pekka on täysi-ikäinen
Olemme syyllistyneet edellä osittain huonoon ohjelmointityyliin tekemällä metodin jonka avulla olio tulostetaan, eli metodin tulostaHenkilo
. Suositeltavampi tapa on määritellä oliolle metodi jonka palauttaa olion "merkkijonoesityksen". Merkkijonoesityksen palauttavan metodin nimi on Javassa aina toString
. Määritellään seuraavassa henkilölle tämä metodi:
public class Henkilo { // ... public String toString() { return this.nimi + ", ikä " + this.ika + " vuotta"; } }
Metodi toString
toimii kuten tulostaHenkilo
, mutta se ei itse tulosta mitään vaan palauttaa merkkijonoesityksen, jotta metodin kutsuja voi halutessaan suorittaa tulostamisen.
Metodia käytetään hieman yllättävällä tavalla:
public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka"); Henkilo antti = new Henkilo("Antti"); int i = 0; while ( i < 30 ) { pekka.vanhene(); i++; } antti.vanhene(); System.out.println( antti ); // sama kun System.out.println( antti.toString() ); System.out.println( pekka ); // sama kun System.out.println( pekka.toString() ); }
Periaatteena on, että System.out.println
-metodi pyytää olion merkkijonoesityksen ja tulostaa sen. Merkkijonoesityksen palauttavan toString
-metodin kutsua ei tarvitse kirjoittaa itse, sillä Java lisää sen automaattisesti. Ohjelmoijan kirjoittaessa:
System.out.println( antti );
Java täydentää suorituksen aikana kutsun muotoon:
System.out.println( antti.toString() );
Käy niin, että oliolta pyydetään sen merkkijonoesitys. Olion palauttama merkkijonoesitys tulostetaan normaaliin tapaan System.out.println
-komennolla.
Voimme nyt poistaa turhaksi käyneen tulostaOlio
-metodin.
Olioscreencastin toinen osa:
Helsingin Yliopiston opiskelijaruokaloissa eli Unicafeissa opiskelijat maksavat lounaansa käyttäen maksukorttia.
Tässä tehtäväsäsarjassa tehdään luokka Maksukortti
, jonka tarkoituksena on jäljitellä Unicafeissa tapahtuvaa maksutoimintaa.
Projektiin tulee kuulumaan kaksi kooditiedostoa:
Tehtäväpohjan eli projektin Viikko4_077.Maksukortti mukana tulee kooditiedosto Paaohjelma
jonka sisällä on main
-metodi.
Lisää projektiin uusi luokka nimeltä Maksukortti
. Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen Viikko4_077.Maksukortti kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) Maksukortti
.
Tee ensin Maksukortti
-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 Maksukortti
runko:
public class Maksukortti { private double saldo; public Maksukortti(double alkusaldo) { // kirjoita koodia tähän } public String toString() { // kirjoita koodia tähän } }
Seuraava pääohjelma testaa luokkaa:
public class Paaohjelma { public static void main(String[] args) { Maksukortti kortti = new Maksukortti(50); System.out.println(kortti); } }
Ohjelman tulisi tuottaa seuraava tulostus:
Kortilla on rahaa 50.0 euroa
Täydennä Maksukortti
-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.50 eurolla ja metodin syoMaukkaasti
tulisi vähentää kortin saldoa 4.30 eurolla.
Seuraava pääohjelma testaa luokkaa:
public class Paaohjelma { public static void main(String[] args) { Maksukortti kortti = new Maksukortti(50); System.out.println(kortti); kortti.syoEdullisesti(); System.out.println(kortti); kortti.syoMaukkaasti(); kortti.syoEdullisesti(); System.out.println(kortti); } }
Ohjelman tulisi tuottaa kutakuinkin seuraava tulostus:
Kortilla on rahaa 50.0 euroa Kortilla on rahaa 47.5 euroa Kortilla on rahaa 40.7 euroa
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 Paaohjelma { public static void main(String[] args) { Maksukortti kortti = new Maksukortti(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 0.7 euroa Kortilla on rahaa 0.7 euroa
Yllä toinen metodin syoMaukkaasti
kutsu ei vaikuttanut saldoon, koska saldo olisi mennyt negatiiviseksi.
Lisää Maksukortti
-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 Paaohjelma { public static void main(String[] args) { Maksukortti kortti = new Maksukortti(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
Muuta metodia lataaRahaa
vielä siten, että jos yritetään ladata negatiivinen rahamäärä, ei kortilla oleva arvo muutu.
Seuraava pääohjelma testaa luokkaa:
public class Paaohjelma { public static void main(String[] args) { Maksukortti kortti = new Maksukortti(10); System.out.println(kortti); kortti.lataaRahaa(-15); System.out.println(kortti); } }
Ohjelman tulisi tuottaa seuraava tulostus:
Kortilla on rahaa 10.0 euroa Kortilla on rahaa 10.0 euroa
Tee pääohjelma, joka sisältää seuraavan tapahtumasarjan:
Pääohjelman runko on seuraava:
public class Main { public static void main(String[] args) { Maksukortti pekanKortti = new Maksukortti(20); Maksukortti matinKortti = new Maksukortti(30); // kirjoita koodia tähän } }
Ohjelman tulisi tuottaa seuraava tulostus:
Pekka: Kortilla on rahaa 15.7 euroa Matti: Kortilla on rahaa 27.5 euroa Pekka: Kortilla on rahaa 35.7 euroa Matti: Kortilla on rahaa 23.2 euroa Pekka: Kortilla on rahaa 30.7 euroa Matti: Kortilla on rahaa 73.2 euroa
Saatat huomata, että osassa luvuista ilmenee pyöristysvirheitä. Esimerkiksi pekan saldo 30.7 saattaa tulostua muodossa 30.700000000000003
. Tämä liittyy siihen, että liukuluvut kuten double
tallennetaan oikeasti binäärimuodossa, eli nollina ja ykkösinä vain rajattua määrää lukuja käyttäen. Koska liukulukuja on ääretön määrä (keksitkö miksi? kuinka monta liuku- tai desimaalilukua mahtuu vaikkapa lukujen 5 ja 6 väliin?), ei kaikkia voi esittää rajatulla määrällä nollia ja ykkösiä, ja tietokone joutuu rajoittamaan tallennustarkkuutta.
Jatketaan taas Henkilo
-luokan parissa. Päätetään että haluamme laskea henkilöiden painoindeksejä. Tätä varten teemme henkilölle metodit pituuden ja painon asettamista varten, sekä metodin joka laskee painoindeksin. Henkilön uudet ja muuttuneet osat seuraavassa:
public class Henkilo { private String nimi; private int ika; private int paino; private int pituus; public Henkilo(String nimiAlussa) { this.ika = 0; this.paino = 0; this.pituus = 0; this.nimi = nimiAlussa; } public void setPituus(int uusiPituus) { this.pituus = uusiPituus; } public void setPaino(int uusiPaino) { this.paino = uusiPaino; } public double painoIndeksi(){ double pituusPerSata = this.pituus / 100.0; return this.paino / ( pituusPerSata * pituusPerSata ); } // ... }
Eli henkilölle lisättiin oliomuuttujat pituus
ja paino
. Näille voi asettaa arvon metodeilla setPituus
ja setPaino
. Jälleen käytössä Javaan vakiintunut nimeämiskäytäntö, eli jos metodin tehtävänä on ainoastaan asettaa arvo oliomuuttujaan, on metodi tapana nimetä setMuuttujanNimi
:ksi. Arvon asettavia metodeja kutsutaan usein "settereiksi". Seuraavassa käytämme uusia metodeja:
public static void main(String[] args) { Henkilo matti = new Henkilo("Matti"); Henkilo juhana = new Henkilo("Juhana"); matti.setPituus(180); matti.setPaino(86); juhana.setPituus(175); juhana.setPaino(64); System.out.println( matti.getNimi() + ", painoindeksisi on " + matti.painoIndeksi() ); System.out.println( juhana.getNimi() + ", painoindeksisi on " + juhana.painoIndeksi() ); }
Tulostus:
Matti, painoindeksisi on 26.54320987654321 Juhana, painoindeksisi on 20.897959183673468
Edellä metodissa setPituus
asetetaan oliomuuttujaan pituus
parametrin uusiPituus
arvo:
public void setPituus(int uusiPituus) { this.pituus = uusiPituus; }
Parametrin nimi voisi olla myös sama kuin oliomuuttujan nimi, eli seuraava toimisi myös:
public void setPituus(int pituus) { this.pituus = pituus; }
Nyt metodissa pituus
tarkottaa nimenomaan pituus-nimistä parametria ja this.pituus
saman nimistä oliomuuttujaa. Esim. seuraava ei toimisi sillä koodi ei viittaa ollenkaan oliomuuttujaan pituus:
public void setPituus(int pituus) { // EI TOIMI ÄLÄ TEE NÄIN!!! pituus = pituus; }
Desimaalien määrä edellisessä tulostuksessa on hieman liioiteltu. Yksi tapa päästä määräämään tulostettavien desimaalien määrä on seuraava:
System.out.println( matti.getNimi() + ", painoindeksisi on " + String.format( "%.2f", matti.painoIndeksi() ) ); System.out.println( juhana.getNimi() + ", painoindeksisi on " + String.format( "%.2f", juhana.painoIndeksi() ) );
Eli jos luku
on liukuluku, voi siitä tehdä komennolla String.format( "%.2f", luku )
merkkijonon, jossa luku on otettu mukaan kahden desimaalin tarkkuudella. Pisteen ja f:n välissä oleva numero säätää mukaan tulevan desimaalien määrän.
Nyt tulostus on siistimpi:
Matti, painoindeksisi on 26,54 Juhana, painoindeksisi on 20,90
String.format
ei ehkä ole kaikkein monikäyttöisin tapa Javassa tulostuksen muotoiluun. Se kuitenkin lienee yksinkertaisin ja kelpaa meille hyvin nyt.
Tässä tehtävässä tehdään luokka YlhaaltaRajoitettuLaskuri
ja sovelletaan sitä kellon tekemiseen.
Tehdään luokka YlhaaltaRajoitettuLaskuri
. Luokan olioilla on seuraava toiminnallisuus:
seuraava
kasvattaa laskurin arvoa.
Mutta jos laskurin arvo ylittää ylärajan, sen arvoksi tulee 0.toString
palauttaa laskurin arvon merkkijonona.Tehtäväpohjassa on valmiina pääohjelmaa varten tiedosto Paaohjelma
. Aloita tekemällä luokka YlhaaltaRajoitettuLaskuri
vastaavasti kuin Maksukortti-tehtävässä. Näin tehdään myös tulevissa tehtäväsarjoissa.
Luokan rungoksi tulee seuraava:
public class YlhaaltaRajoitettuLaskuri { private int arvo; private int ylaraja; public YlhaaltaRajoitettuLaskuri(int ylarajanAlkuarvo) { // kirjoita koodia tähän } public void seuraava() { // kirjoita koodia tähän } public String toString() { // kirjoita koodia tähän } }
Vihje: et voi palauttaa toStringissä suoraan kokonaislukutyyppisen oliomuuttujan laskuri
arvoa. Kokonaislukumuuttujasta arvo
saa merkkijonomuodon esim. lisäämällä sen eteen tyhjän merkkijonon eli kirjoittamalla "" + arvo
.
Seuraavassa on pääohjelma, joka käyttää laskuria:
public class Paaohjelma { public static void main(String[] args) { YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(4); System.out.println("arvo alussa: " + laskuri ); int i = 0; while ( i < 10) { laskuri.seuraava(); System.out.println("arvo: " + laskuri ); i++; } } }
Laskurille asetetaan konstruktorissa ylärajaksi 4, joten laskurin arvo on luku 0:n ja 4:n väliltä. Huomaa, miten metodi seuraava
vie laskurin arvoa eteenpäin, kunnes se pyörähtää 4:n jälkeen 0:aan:
Ohjelman tulostuksen tulisi olla seuraava:
arvo alussa: 0 arvo: 1 arvo: 2 arvo: 3 arvo: 4 arvo: 0 arvo: 1 arvo: 2 arvo: 3 arvo: 4 arvo: 0
Tee toString
-metodista sellainen, että se lisää arvon merkkijonoesitykseen etunollan, jos laskurin arvo on vähemmän kuin 10. Eli jos laskurin arvo on esim. 3, palautetaan merkkijono "03", jos arvo taas on esim. 12, palautetaan normaaliin tapaan merkkijono "12".
Muuta pääohjelma seuraavaan muotoon ja varmista, että tulos on haluttu.
public class Paaohjelma { public static void main(String[] args) { YlhaaltaRajoitettuLaskuri laskuri = new YlhaaltaRajoitettuLaskuri(14); System.out.println("arvo alussa: " + laskuri ); int i = 0; while ( i < 16){ laskuri.seuraava(); System.out.println("arvo: " + laskuri ); i++; } } }
arvo alussa: 00 arvo: 01 arvo: 02 arvo: 03 arvo: 04 arvo: 05 arvo: 06 arvo: 07 arvo: 08 arvo: 09 arvo: 10 arvo: 11 arvo: 12 arvo: 13 arvo: 14 arvo: 00 arvo: 01
Käyttämällä kahta laskuria voimme muodostaa kellon. Tuntimäärä on laskuri, jonka yläraja on 23, ja minuuttimäärä on laskuri jonka yläraja on 59. Kuten kaikki tietävät, kello toimii siten, että aina kun minuuttimäärä pyörähtää nollaan, tuntimäärä kasvaa yhdellä.
Tee ensin laskurille metodi arvo
, joka palauttaa laskurin arvon:
public int arvo() { // kirjoita koodia tähän }
Tee sitten kello täydentämällä seuraava pääohjelmarunko (kopioi tämä pääohjelmaksesi sekä täydennä tarvittavilta osin kommenttien ohjaamalla tavalla):
public class Paaohjelma { public static void main(String[] args) { YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23); int i = 0; while ( i < 121 ) { System.out.println( tunnit + ":" + minuutit); // tulostetaan nykyinen aika // minuuttimäärä kasvaa // jos minuuttimäärä menee nollaan, tuntimäärä kasvaa i++; } } }
Jos kellosi toimii oikein, sen tulostus näyttää suunnilleen seuraavalta:
00:00 00:01 ... 00:59 01:00 01:01 01:02 ... 01:59 02:00
Laajenna kelloasi myös sekuntiviisarilla. Tee lisäksi luokalle YlhaaltaRajoitettuLaskuri
metodi asetaArvo
, jolla laskurille pystyy asettamaan halutun arvon. Jos laskurille yritetään asettaa kelvoton arvo eli negatiivinen luku tai ylärajaa suurempi luku, ei laskurin arvo muutu.
Tämän metodin avulla voit muuttaa kellon ajan heti ohjelman alussa haluamaksesi.
Voit testata kellon toimintaa seuraavalla ohjelmalla
import java.util.Scanner; public class Paaohjelma { public static void main(String[] args) { Scanner lukija = new Scanner(System.in); YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23); System.out.print("sekunnit: "); int sek = // kysy sekuntien alkuarvo käyttäjältä System.out.print("minuutit: "); int min = // kysy minuuttien alkuarvo käyttäjältä System.out.print("tunnit: "); int tun = // kysy tuntien alkuarvo käyttäjältä sekunnit.asetaArvo(sek); minuutit.asetaArvo(min); tunnit.asetaArvo(tun); int i = 0; while ( i < 121 ) { // lisää edelliseen myös sekuntiviisari i++; } } }
Kokeile laittaa kellosi alkamaan ajasta 23:59:50 ja varmista, että vuorokauden vaihteessa kello toimii odotetusti!
Bonus-tehtävä: ikuisesti käyvä kello (tehtävää ei palauteta!)
Ennen kuin alat tekemään tätä tehtävää, palauta jo tekemäsi kello!
Muuta pääohjelmasi seuraavaan muotoon:
public class Paaohjelma { public static void main(String[] args) throws Exception { YlhaaltaRajoitettuLaskuri sekunnit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri minuutit = new YlhaaltaRajoitettuLaskuri(59); YlhaaltaRajoitettuLaskuri tunnit = new YlhaaltaRajoitettuLaskuri(23); sekunnit.asetaArvo(50); minuutit.asetaArvo(59); tunnit.asetaArvo(23); while ( true ) { System.out.println( tunnit + ":" + minuutit + ":" + sekunnit ); Thread.sleep(1000); // lisää kellon aikaa sekunnilla eteenpäin } } }
Nyt kello käy ikuisesti ja kasvattaa arvoaan sekunnin välein. Sekunnin odotus tapahtuu komennolla Thread.sleep(1000);
, komennon parametri kertoo nukuttavan ajan millisekunteina. Jotta komento toimisi, pitää main:in esittelyriville tehdä pieni lisäys: public static void main(String[] args) throws Exception {
, eli tummennettuna oleva throws Exception
.
Saat ohjelman lopetettua painamalla NetBeans-konsolin (eli sen osan johon kello tulostaa arvonsa) vasemmalla laidalla olevasta punaisesta laatikosta.
Tärkeitä kommentteja liittyen olioiden käyttöön. Lue nämä ehdottomasti.
Olio-ohjelmoinnissa on kyse pitkälti käsitteiden eristämisestä omiksi kokonaisuuksikseen tai toisin ajatellen abstraktioiden muodostamisesta. Voisi ajatella, että on turha luoda oliota jonka sisällä on ainoastaan luku, eli että saman voisi tehdä suoraan int
-muuttujilla. Asia ei kuitenkaan ole näin. Jos kello koostuu pelkästään kolmesta int-muuttujasta joita kasvatellaan, muuttuu ohjelma lukijan kannalta epäselvemmäksi, koodista on vaikea "nähdä" mistä on kysymys. Aiemmin materiaalissa mainitsimme jo kokeneen ja kuuluisan ohjelmoijan Kent Beckin neuvon "Any fool can write code that a computer can understand. Good programmers write code that humans can understand", eli koska viisari on oikeastaan oma selkeä käsitteensä, on siitä ohjelman ymmärrettävyyden parantamiseksi hyvä tehdä oma luokka, eli YlhaaltaRajoitettuLaskuri
.
Käsitteen erottaminen omaksi luokaksi on monellakin tapaa hyvä idea. Ensinnäkin tiettyjä yksityiskohtia (esim. laskurin pyörähtäminen) saadaan piilotettua luokan sisään (eli abstrahoitua). Sen sijaan että kirjoitetaan if-lause ja sijoitusoperaatio, riittää, että laskurin käyttäjä kutsuu selkeästi nimettyä metodia seuraava()
. Aikaansaatu laskuri sopii kellon lisäksi ehkä muidenkin ohjelmien rakennuspalikaksi, eli selkeästä käsitteestä tehty luokka voi olla monikäyttöinen. Suuri etu saavutetaan myös sillä, että koska laskurin toteutuksen yksityiskohdat eivät näy laskurin käyttäjille, voidaan yksityiskohtia tarvittaessa muuttaa.
Totesimme että kello sisältää kolme viisaria, eli koostuu kolmesta käsitteestä. Oikeastaan kello on itsekin käsite ja teemme ensi viikolla luokan Kello, jotta voimme luoda selkeitä Kello-olioita. Kello tulee siis olemaan olio jonka toiminta perustuu "yksinkertaisimpiin" olioihin eli viisareihin. Tämä on juuri olio-ohjelmoinnin suuri idea: ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista.
Nyt otamme varovaisia ensiaskelia oliomaailmassa. Kurssin lopussa oliot alkavat kuitenkin olla jo selkärangassa ja nyt ehkä käsittämättömältä tuntuva lausahdus, ohjelma rakennetaan pienistä selkeistä yhteistoiminnassa olevista olioista alkaa tuntua meistä ehkä järkeenkäyvältä ja itsestäänselvältä.
Olio voi kutsua myös omia metodeitaan. Jos esim. halutaan, että toString-metodin palauttama merkkijonoesitys kertoisi myös henkilön painoindeksin, kannattaa toString
:istä kutsua olion omaa metodia painoIndeksi
:
public String toString() { return this.nimi + ", ikä " + this.ika + " vuotta, painoindeksini on " + this.painoIndeksi(); }
Eli kun olio kutsuu omaa metodiaan, riittää etuliite this ja pelkkä metodin nimi. Vaihtoehtoinen tapa on tehdä oman metodin kutsu muodossa painoIndeksi()
jolloin ei korosteta, että kutsutaan "olion itsensä" metodia painoindeksi:
public String toString() { return this.nimi + ", ikä " + this.ika + " vuotta, painoindeksini on " + painoIndeksi(); }
Olioscreencastin kolmas osa:
Nyt on aika harjoitella lisää olioiden tekemistä käytännössä:
Tee luokka Lukutilasto
(tiedosto luomaasi luokkaa varten on tehtäväpohjassa valmiina), joka tuntee seuraavat toiminnot :
lisaaLuku
lisää uuden luvun tilastoon
haeLukujenMaara
kertoo lisättyjen lukujen määrän
Luokan ei tarvitse tallentaa mihinkään lisättyjä lukuja, vaan riittää muistaa niiden määrä. Metodin lisaaLuku
ei tässä vaiheessa tarvitse edes ottaa huomioon, mikä luku lisätään tilastoon, koska ainoa tallennettava asia on lukujen määrä.
Luokan runko on seuraava:
public class Lukutilasto { private int lukujenMaara; public Lukutilasto() { // alusta tässä muuttuja lukujenMaara } public void lisaaLuku(int luku) { // kirjoita koodia tähän } public int haeLukujenMaara() { // kirjoita koodia tähän } }
Seuraava ohjelma esittelee luokan käyttöä:
public class Paaohjelma { public static void main(String[] args) { Lukutilasto tilasto = new Lukutilasto(); tilasto.lisaaLuku(3); tilasto.lisaaLuku(5); tilasto.lisaaLuku(1); tilasto.lisaaLuku(2); System.out.println("Määrä: " + tilasto.haeLukujenMaara()); } }
Ohjelman tulostus on seuraava:
Määrä: 4
Laajenna luokkaa seuraavilla toiminnoilla:
summa
kertoo lisättyjen lukujen summan (tyhjän lukutilaston summa on 0)
keskiarvo
kertoo lisättyjen lukujen keskiarvon (tyhjän lukutilaston keskiarvo on 0)
Luokan runko on seuraava:
public class Lukutilasto { private int lukujenMaara; private int summa; public Lukutilasto() { // alusta tässä muuttujat maara ja summa } public void lisaaLuku(int luku) { // kirjoita koodia tähän } public int haeLukujenMaara() { // kirjoita koodia tähän } public int summa() { // kirjoita koodia tähän } public double keskiarvo() { // kirjoita koodia tähän } }
Seuraava ohjelma esittelee luokan käyttöä:
public class Main { public static void main(String[] args) { Lukutilasto tilasto = new Lukutilasto(); tilasto.lisaaLuku(3); tilasto.lisaaLuku(5); tilasto.lisaaLuku(1); tilasto.lisaaLuku(2); System.out.println("Määrä: " + tilasto.haeLukujenMaara()); System.out.println("Summa: " + tilasto.summa()); System.out.println("Keskiarvo: " + tilasto.keskiarvo()); } }
Ohjelman tulostus on seuraava:
Määrä: 4 Summa: 11 Keskiarvo: 2.75
Tee ohjelma, joka kysyy lukuja käyttäjältä, kunnes käyttäjä antaa luvun -1. Sitten ohjelma ilmoittaa lukujen summan.
Ohjelmassa tulee käyttää Lukutilasto
-olioa summan laskemiseen.
HUOM: älä muuta Lukutilasto-luokaa millään tavalla!
Anna lukuja: 4 2 5 4 -1 Summa: 15
Muuta edellistä ohjelmaa niin, että ohjelma laskee myös parillisten ja parittomien lukujen summaa.
HUOM: Määrittele ohjelmassa kolme Lukutilasto-olioa ja laske ensimmäisen avulla kaikkien lukujen summa, toisen avulla parillisten lukujen summa ja kolmannen avulla parittomien lukujen summa.
Jotta testi toimisi, on oliot luotava pääohjelmassa edellä mainitussa järjestyksessä (eli ensin kaikkien summan laskeva olio, toisena parillisten summan laskeva ja viimeisenä parittomien summan laskeva olio)!
HUOM: älä muuta Lukutilasto-luokaa millään tavalla!
Ohjelman tulee toimia seuraavasti:
Anna lukuja: 4 2 5 2 -1 Summa: 13 Parillisten summa: 8 Parittomien summa: 5
Ohjelmoidessa satunnaisuutta tarvitaan silloin tällöin. Satunnaisuutta, esimerkiksi sään arvaamattomuutta tai tietokoneen tietokonepeleissä tekemien yllättäviä siirtoja, voidaan useimmiten simuloida tietokoneelta pyydettävien satunnaislukujen avulla. Satunnaislukujen pyytäminen onnistuu käyttäen Javasta löytyvää Random
-luokkaa. Random-luokkaa voi käyttää seuraavalla tavalla.
import java.util.Random; public class Arvontaa { public static void main(String[] args) { Random arpoja = new Random(); // luodaan arpoja apuväline int i = 0; while (i < 10) { // Arvotaan ja tulostetaan jokaisella kierroksella satunnainen luku System.out.println(arpoja.nextInt(10)); i++; } } }
Yllä olevassa koodissa luodaan ensin Random
-luokan ilmentymä käyttäen avainsanaa new
-- samoin kuin muitakin olioita luodessa. Random-olio tarjoaa metodin nextInt
, jolle annetaan parametrina kokonaisluku. Metodi palauttaa satunnaisen kokonaisluvun väliltä 0..(annettu kokonaisluku - 1).
Ohjelman tulostus voisi olla vaikka seuraavanlainen:
2 2 4 3 4 5 6 0 7 8
Tarvitsemme liukulukuja esimerkiksi todennäköisyyslaskennan yhteydessä. Tietokoneella todennäköisyyksiä simuloidaan yleensä väliltä [0..1] olevilla luvuilla. Random-oliolta saa satunnaisia liukulukuja metodilla nextDouble
. Tarkastellaan seuraavia säämahdollisuuksia:
Luodaan edellä olevista arvioista sääennustaja.
import java.util.ArrayList; import java.util.Random; public class SaaEnnustaja { private Random random; public SaaEnnustaja() { this.random = new Random(); } public String ennustaSaa() { double todennakoisyys = this.random.nextDouble(); if (todennakoisyys <= 0.1) { return "Sataa räntää"; } else if (todennakoisyys <= 0.4) { // 0.1 + 0.3 return "Sataa lunta"; } else { // loput, 1.0 - 0.4 = 0.6 return "Aurinko paistaa"; } } public int ennustaLampotila() { return (int) ( 4 * this.random.nextGaussian() - 3 ); } }
Metodi ennustaLampotila
on monella tapaa mielenkiintoinen. Metodin sisällä tehtävä kutsu this.random.nextGaussian()
on tavallinen metodikutsu, jonka kaltaisia olemme nähneet aikaisemminkin. Kiinnostavaa tässä Random
-luokan ilmentymän tarjoamassa metodissa on se, että metodin palauttama luku on normaalijakautunut (jos et koe mielenkiintoa satunnaisuuden eri lajeihin se ei haittaa!).
public int ennustaLampotila() { return (int) ( 4 * this.random.nextGaussian() - 3 ); }
Edellisessä lausekkeessa kiinnostava on myös osa (int)
. Tämä kohta lausekkeessa muuttaa suluissa olevan liukuluvun kokonaisluvuksi. Vastaavalla menetelmällä voidaan muuttaa myös kokonaislukuja liukuluvuiksi kirjoittamalla (double) kokonaisluku
. Tätä kutsutaan eksplisiittiseksi tyyppimuunnokseksi.
Luodaan vielä pääohjelma josta luokkaa SaaEnnustaja
käytetään.
public class Ohjelma { public static void main(String[] args) { SaaEnnustaja ennustaja = new SaaEnnustaja(); // käytetään listaa apuvälineenä ArrayList<String> paivat = new ArrayList<String>(); Collections.addAll(paivat, "Ma", "Ti", "Ke", "To", "Pe", "La", "Su"); System.out.println("Seuraavan viikon sääennuste:"); for(String paiva : paivat) { String saaEnnuste = ennustaja.ennustaSaa(); int lampotilaEnnuste = ennustaja.ennustaLampotila(); System.out.println(paiva + ": " + saaEnnuste + " " + lampotilaEnnuste + " astetta."); } } }
Ohjelman tulostus voisi olla esimerkiksi seuraavanlainen:
Seuraavan viikon sääennuste: Ma: Sataa lunta 1 astetta. Ti: Sataa lunta 1 astetta. Ke: Aurinko paistaa -2 astetta. To: Aurinko paistaa 0 astetta. Pe: Sataa lunta -3 astetta. La: Sataa lunta -3 astetta. Su: Aurinko paistaa -5 astetta
Tehtäväpohjassa on luokka Noppa
, jolla on seuraavat toiminnot:
Noppa(int tahkojenMaara)
luo uuden noppa-olion annetulla nopan tahkojen (eri oman numeronsa sisältämien "puolien") määrälläheita
kertoo nopanheiton tuloksen (tulos riippuu tahkojen määrästä)Luokan runko on seuraava:
import java.util.Random; public class Noppa { private Random random = new Random(); private int tahkojenMaara; public Noppa(int tahkojenMaara) { // Alusta muuttuja tahkojenMaara tässä } public int heita() { // arvotaan luku väliltä 1-tahkojenMaara } }
Täydennä luokkaa Noppa
siten, että noppa palauttaa jokaisella heitolla arvotun luvun väliltä 1...tahkojen määrä
. Seuraavassa noppaa testaava pääohjelma:
public class Ohjelma { public static void main(String[] args) { Noppa noppa = new Noppa(6); int i = 0; while ( i < 10 ) { System.out.println( noppa.heita() ); i++; } } }
Tulostus voisi olla esimerkiksi seuraava:
1 6 3 5 3 3 2 2 6 1
Tehtävänäsi on täydentää luokkaa SalasananArpoja
, jossa on seuraavat toiminnot:
SalasananArpoja
luo uuden olion, joka käyttää annettua salasanan pituuttaluoSalasana
palauttaa uuden, merkeistä a-z muodostetun konstruktorin parametrin määräämän pituisen salasanan Luokan runko on seuraava:
import java.util.Random; public class SalasananArpoja { // Määrittele muuttuja tässä public SalasananArpoja(int salasananPituus) { // Alusta muuttuja tässä } public String luoSalasana() { // Kirjoita tähän koodi, joka palauttaa uuden salasanan } }
Seuraavassa ohjelma joka käyttää SalasananArpoja-olioa:
public class Ohjelma { public static void main(String[] args) { SalasananArpoja arpoja = new SalasananArpoja(13); System.out.println("Salasana: " + arpoja.luoSalasana()); System.out.println("Salasana: " + arpoja.luoSalasana()); System.out.println("Salasana: " + arpoja.luoSalasana()); System.out.println("Salasana: " + arpoja.luoSalasana()); } }
Ohjelman tulostus voisi näyttää seuraavalta:
Salasana: mcllsoompezvs Salasana: urcxboisknkme Salasana: dzaccatonjcqu Salasana: bpqmedlbqaopq
Vihje1: näin muutat kokonaisluvun luku kirjaimeksi:
int luku = 17; char merkki = "abcdefghijklmnopqrstuvxyz".charAt(luku);
Vihje2: tehtävän 78 vihjeestä lienee tässäkin tehtävässä apua.
Tehtävänäsi on täydentää luokkaa LottoRivi
, joka arpoo viikon lottonumerot. Lottonumerot ovat väliltä 1–39 ja niitä arvotaan 7. Lottorivi koostuu siis 7:stä eri numerosta väliltä 1–39. Luokassa on seuraavat toiminnot:
LottoRivi
luo uuden LottoRivi-olion joka sisältää uudet, arvotut numerotnumerot
palauttaa tämän lottorivin lottonumerotarvoNumerot
arpoo riville uudet numerotsisaltaaNumeron
kertoo onko arvotuissa numeroissa annettu numeroLuokan runko on seuraava:
import java.util.ArrayList; import java.util.Random; public class LottoRivi { private ArrayList<Integer> numerot; public LottoRivi() { // Alustetaan lista numeroille this.numerot = new ArrayList<Integer>(); // Arvo numerot heti LottoRivin luomisen yhteydessä this.arvoNumerot(); } public ArrayList<Integer> numerot() { return this.numerot; } public void arvoNumerot() { // Kirjoita numeroiden arvonta tänne käyttämällä metodia sisaltaaNumeron() } public boolean sisaltaaNumeron(int numero) { // Testaa tässä onko numero jo arvottujen numeroiden joukossa } }
Tehtäväpohjan mukana tulee seuraava pääohjelma:
import java.util.ArrayList; public class Ohjelma { public static void main(String[] args) { LottoRivi lottoRivi = new LottoRivi(); ArrayList<Integer> lottonumerot = lottoRivi.numerot(); System.out.println("Lottonumerot:"); for (int numero : lottonumerot) { System.out.print(numero + " "); } System.out.println(""); } }
Ohjelman mahdollisia tulostuksia ovat seuraavat:
Lottonumerot: 3 5 10 14 15 27 37
Lottonumerot: 2 9 11 18 23 32 34
Huom! Sama numero saa esiintyä lottorivissä vain kerran.
Hirsipuu-pelistä pitävä kaverisi ohjelmoi hirsipuupelin joka näyttää seuraavalta.
Hänen koneestaan hajosi kovalevy, eikä hän ollut tehnyt varmuuskopiota tai käyttänyt versionhallintajärjestelmää lähdekoodien tallennukseen. Hän on kuitenkin luvannut pelin lahjaksi pikkuveljelleen, ja aikataulukiireidensä takia tarvitsee apuasi pelin toteutukseen. Tässä tehtäväsarjassa toteutat kaverisi hirsipuu-pelille arvauslogiikan ja annetun sanan salaamisen.
Kaverisi on luonut pelille käyttöliittymän sekä valmiin rungon sovelluksen logiikkaan. Kun lataat projektin TMC:ltä, näet käyttöliittymä- ja testiluokkien lisäksi allaolevan ohjelmarungon.
public class HirsipuuLogiikka { private String sana; private String arvatutKirjaimet; private int virheidenLukumaara; public HirsipuuLogiikka(String sana) { this.sana = sana.toUpperCase(); this.arvatutKirjaimet = ""; this.virheidenLukumaara = 0; } public int virheidenLukumaara() { return this.virheidenLukumaara; } public String arvatutKirjaimet() { return this.arvatutKirjaimet; } public int virheitaHavioon() { return 12; } public void arvaaKirjain(String kirjain) { // Ohjelmoi tänne toiminnallisuus kirjaimen arvaamiseksi. // Arvattua kirjainta ei saa arvata uudestaan. Jos arvattava sana // ei sisällä kirjainta, virheiden lukumäärän tulee kasvaa. // Lisää arvattu kirjain arvattuihin kirjaimiin } public String salattuSana() { // Ohjelmoi tänne toiminnallisuus salatun sanan luomiseksi // ja antamiseksi. // Luo uusi salattu sana this.sana-merkkijonoa kirjain kirjaimelta // läpikäyden. Jos kirjain on arvatuissa kirjaimissa, voit lisätä sen // sellaisenaan. Jos ei, lisää uuteen sanaan merkki "_". // Muista varmistaa ettet ohjelmoi päättymätöntä toistolauseketta! // palauta lopuksi salattua sanaa kuvaava merkkijono. return ""; } }
Tässä tehtävässä sinun tulee lisätä toiminnallisuutta vain luokan HirsipuuLogiikka
metodeihin arvaaKirjain(String kirjain)
ja salattuSana()
.
Logiikan testaaminen
Tehtäväpohja sisältää kaksi testiluokkaa. Luokka Main
käynnistää kaverisi toteuttaman graafisen version pelistä. Luokkaa Testiohjelma
taas voit käyttää HirsipuuLogiikka
-luokan testaamiseen.
Muokkaa tässä tehtävässä vain metodin arvaaKirjain(String kirjain)
toiminnallisuutta.
Kun käyttäjä arvaa jonkin kirjaimen, kutsuu hirsipuun käyttöliittymä hirsipuulogiikan metodia arvaaKirjain
jonka tulee tarkistaa onko arvattava kirjain jo arvattujen kirjainten (joita hirsipuulogiikka säilyttää oliomuuttujassa this.arvatutKirjaimet
) joukossa. Jos kirjain on jo arvattujen kirjainten joukossa, metodin ei tule tehdä mitään. Jos arvattavaa kirjainta ei ole arvattu aiemmin, on tarkastettava onko se arvattavassa sanassa (jota hirsipuulogiikka säilyttää oliomuuttujassa this.sana
). Jos ei, virheiden lukumäärää (oliomuuttuja this.virheidenLukumaara
) pitää kasvattaa yhdellä. Arvattu kirjain pitää lopuksi lisätä osaksi arvattuja kirjaimia, jos se ei ole jo siellä.
Esimerkki arvaaKirjain
-metodin toiminnasta:
HirsipuuLogiikka l = new HirsipuuLogiikka("kissa"); System.out.println("Arvataan: A, D, S, F, D"); l.arvaaKirjain("A"); // oikea l.arvaaKirjain("D"); // väärä l.arvaaKirjain("S"); // oikea l.arvaaKirjain("F"); // väärä l.arvaaKirjain("D"); // Tämän ei pitäisi kasvattaa virheiden lukumäärää koska D oli jo arvattu System.out.println("Arvatut kirjaimet: "+l.arvatutKirjaimet()); System.out.println("Virheiden lukumäärä: "+l.virheidenLukumaara());
Arvataan: A, D, S, F, D Arvatut kirjaimet: ADSF Virheiden lukumäärä: 2
Hirsipuun käyttöliittymä näyttää käyttäjälle salatun muodon arvattavana olevasta sanasta. Yllä olevassa kuvassa salattu sana on METO_I
, eli arvattavana olevasta sanasta näytetään vain arvatut kirjaimet, muut korvataan alaviivalla. Salatun sanan muodostaa hirsipuulogiikan metodi salattuSana()
jonka sisältöä tässä tehtävässä on tarkoitus muokata.
Salattu sana luodaan luomalla arvattavasta sanasta (this.sana
) uusi versio. Jokainen kirjain jota ei ole vielä arvattu tulee vaihtaa merkkiin "_". Jos kirjain on jo arvattu, eli se löytyy arvatuista kirjaimista, voidaan se lisätä sellaisenaan salattuun sanaan.
Avainsanoista while
, charAt
ja contains
lienee tästä hyötyä. Saat muutettua yksittäisen merkin merkkijonoksi seuraavasti:
char merkki = 'a'; String merkkijono = "" + merkki;
Esimerkki metodin toiminnasta:
HirsipuuLogiikka l = new HirsipuuLogiikka("kissa"); System.out.println("Sana on: "+l.salattuSana()); System.out.println("Arvataan: A, D, S, F, D"); l.arvaaKirjain("A"); l.arvaaKirjain("D"); l.arvaaKirjain("S"); l.arvaaKirjain("F"); l.arvaaKirjain("D"); System.out.println("Arvatut kirjaimet: "+l.arvatutKirjaimet()); System.out.println("Virheiden lukumäärä: "+l.virheidenLukumaara()); System.out.println("Sana on: "+l.salattuSana());
Sana on: _____ Arvataan: A, D, S, F, D Arvatut kirjaimet: ADSF Virheiden lukumäärä: 2 Sana on: __SSA
Testaa lopuksi sinun ja kaverisi yhteistyössä tekemää ohjelmaa Main
-luokan avulla! Voit muuttaa arvattavaa sanaa muuttamalla HirsipuuLogiikka-luokan konstruktorille parametrina annettavaa merkkijonoa:
HirsipuuLogiikka logiikka = new HirsipuuLogiikka("parametri"); HirsipuuIkkuna peliIkkuna = new HirsipuuIkkuna(logiikka); peliIkkuna.pelaa();
Peliä pelataan näppäimistöltä, pelin voi lopettaa painamalla peli-ikkunan vasemmassa yläkulmassa olevaa x-merkkiä.
Tässä tehtävässä osallistut kokeellisessa psykologiassa käytettyyn tehtävänvaihtotestiin, missä tutkitaan aivojen mukautumista kahden tai useamman samanaikaisen tehtävän suorittamiseen. Vaikka mahdollisesti osallistuit tähän jo viime viikolla, pyydämme että osallistut myös toistamiseen.
Käytössä olevassa testiohjelmassa on neljä osaa. Ensimmäisessä osassa mitataan reaktioaikaa, toisessa osassa parittoman (odd) ja parillisen (even) luvun tunnistamista, kolmannessa osassa konsonantin (consonant) ja vokaalin (vowel) tunnistamista, ja viimeisessä osassa yhdistetään osat kaksi ja kolme. Näet jokaisen osion jälkeen edellisen osion tulokset.
Testin tulokset tulevat vain tutkimuskäyttöön, eikä mahdollisesti raportoitavista tuloksista voida yksilöidä yksittäisiä vastaajia. Tehtävän suorittamiseen menee noin 10-15 minuuttia.
Aloita testi osoitteessa http://lukiot.it.helsinki.fi/ts-cs/. Kiitos jo nyt osallistumisesta!