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
Kerrataan ensin edellisellä viikolla tutuksi tullutta ArrayListia, jonka jälkeen tutustutaan toisenlaiseen tapaan tallentaa tietoa.
ArrayList on listarakenne, johon voidaan tallentaa lähes rajaton määrä arvoja. Uusi ArrayList-olio luodaan seuraavalla komennolla:
ArrayList<Tyyppi> lista = new ArrayList<>();
Listalle annetaan tyyppiparametri Tyyppi, joka kertoo minkätyyppisiä arvoja listalle tullaan tallentamaan. Tyyppiparametreja ovat esimerkiksi String
, Integer
(vastaa int-tyyppistä muuttujaa) sekä Double
(vastaa double-tyyppistä muuttujaa). Yllä lista
on muuttujan nimi, jolla juuri luotuun listaan viitataan ohjelmakoodissa.
ArrayList voi sisältää esimerkiksi lukuja. Alla olevalla ohjelmakoodilla luodaan uusi ArrayList-olio, ja lisätään siihen lukuja.
ArrayList<Integer> luvut = new ArrayList<>(); luvut.add(6); luvut.add(1); luvut.add(2); luvut.add(4); luvut.add(4); luvut.add(3);
Metodilla add
lisättävät luvut asetetaan aina listan loppuun, ensimmäiseen vapaaseen kohtaan. Listan kohtien numerointi ("indeksointi") alkaa aina nollasta, eli ensimmäinen arvo on kohdassa 0, toinen arvo kohdassa 1, kolmas arvo kohdassa 2 ja niin edelleen.
Jos ylläolevaan listaan lisättäisiin uusi alkio kutsumalla luvut
-listan metodia add
parametrilla 8, menisi luku 8 listan indeksiin 6 eli seitsemänneksi luvuksi.
Lista toimii vastaavasti merkkijonoilla
Luodaan lista nimet, johon lisätään kuusi nimeä. Tämän jälkeen niistä tulostetaan kolme ensimmäistä.
ArrayList<String> nimet = new ArrayList<>(); nimet.add("Ada-Maaria"); nimet.add("Antti"); nimet.add("Joonas"); nimet.add("Lalli"); nimet.add("Miika"); nimet.add("Otto-Ville"); System.out.println("Nimiä yhteensä: " + nimet.size()); System.out.println("Kohdassa 0: " + nimet.get(0)); System.out.println("Kohdassa 1: " + nimet.get(1)); System.out.println("Kohdassa 2: " + nimet.get(2));
Ohjelman tulostus on seuraava:
Nimiä yhteensä: 6 Kohdassa 0: Ada-Maaria Kohdassa 1: Antti Kohdassa 2: Joonas
Listan läpikäynti onnistuu sekä while-toistolauseen avulla että for-each -toistolauseen avulla.
ArrayList<String> nimet = new ArrayList<>(); nimet.add("Sonja"); nimet.add("Terho"); nimet.add("Tiia"); nimet.add("Timo"); nimet.add("Ville-Veikko"); // while-toistolla tapahtuva listan alkioiden tulostaminen int indeksi = 0; while (indeksi < nimet.size()) { System.out.println(nimet.get(indeksi)); indeksi++; } // for-each toistolla tapahtuva listan alkioiden tulostaminen for (String nimi: nimet) { System.out.println(nimi); }
Lyhyt yhteenveto ArrayListin komennoista:
add(Tyyppi alkio)
- lisää alkion listalle.get(int indeksi)
- palauttaa alkion listan kohdasta indeksi.size()
- palauttaa alkion listalla olevien alkioiden lukumäärän.Kerrataan listojen käyttöä ja toteutetaan kurssin kolmannen viikon alussa nähty tehtävä "Desibelimittaukset" listojen ja metodien avulla. Kun tämän tehtävän aliosat on toteutettu, seuraava pääohjelma lukee luvut listalle, valitsee niistä vain osan, ja tulostaa lukujen keskiarvon tai ilmoittaa ettei lukuja ole.
Scanner lukija = new Scanner(System.in); ArrayList<Integer> lista = new ArrayList<>(); lueLuvut(lukija, lista); lista = valitseLuvutValilta(lista, 0, Integer.MAX_VALUE); if (lista.isEmpty()) { System.out.println("Ei lukuja."); } else { System.out.println("Lukujen keskiarvo: " + keskiarvo(lista)); }
Toteuta metodi public static void lueLuvut(Scanner lukija, ArrayList<Integer> lista)
, joka käyttää sille parametrina annettua Scanner-lukijaa lukujen lukemiseen, ja tallentaa luetut luvut listalle. Lukeminen tulee lopettaa kun käyttäjä syöttää tyhjän merkkijonon, eli painaa enter.
Merkkijonon lukeminen ja sen muuntaminen kokonaisluvuksi onnistuu osissa seuraavasti:
String luettu = lukija.nextLine(); int luettuLuku = Integer.parseInt(luettu);
Kun olet toteuttanut metodin public static void lueLuvut(Scanner lukija, ArrayList<Integer> lista)
, toimii ohjelma seuraavasti:
Scanner lukija = new Scanner(System.in); ArrayList<Integer> lista = new ArrayList<>(); lueLuvut(lukija, lista); System.out.println(); for (int luku: lista) { System.out.println(luku); }
Syötä lukuja, tyhjä syöte lopettaa. -41 22 97 -41 22 97
Toteuta metodi public static ArrayList<Integer> valitseLuvutValilta(ArrayList<Integer> luvut, int pienin, int suurin)
, joka valitsee parametrina annetusta listasta ne luvut, jotka ovat suurempia tai yhtäsuuria kuin pienin
ja pienempiä tai yhtäsuuria kuin suurin
, ja palauttaa ne metodista uudessa ArrayList-listarakenteessa.
Kun olet toteuttanut metodin, voit kokeilla sen toimintaa seuraavalla ohjelmakoodilla:
ArrayList<Integer> lista = new ArrayList<>(); lista.add(10); lista.add(20); lista.add(30); lista.add(42); ArrayList<Integer> rajattu = valitseLuvutValilta(lista, 20, 50); System.out.println("Lukuja alkuperäisellä listalla: " + lista.size()); for (int luku: rajattu) { System.out.println(luku); }
Ylläolevan ohjelmakoodin tulostuksen pitäisi olla seuraava:
Lukuja alkuperäisellä listalla: 4 20 30 42
Toteuta metodi public static double keskiarvo(ArrayList<Integer> luvut)
, joka laskee sille annetussa listassa olevien lukujen keskiarvon ja palauttaa sen. Voit olettaa, että metodille parametrina annetulla listalla on aina vähintään yksi luku.
Kun olet toteuttanut metodin, voit kokeilla sen toimintaa seuraavalla ohjelmakoodilla:
ArrayList<Integer> lista = new ArrayList<>(); lista.add(10); lista.add(15); lista.add(20); lista.add(25); lista.add(30); System.out.println("Lukujen keskiarvo on " + keskiarvo(lista));
Ylläolevan ohjelmakoodin tulostuksen pitäisi olla seuraava:
Lukujen keskiarvo on 20.0
Toteutetaan seuraavaksi vieraslistan ylläpitämiseen käytettävä ohjelma. Ohjelman toiminta kokonaisuudessaan on seuraavanlainen:
Syötä nimiä vieraslistalle, tyhjä rivi lopettaa. Jack Bauer Jack Bower Jack Baur Jack Bowr Syötä nimiä, tyhjä rivi lopettaa. Chuck Norris Nimi ei ole listalla. Jack Baluer Nimi ei ole listalla. Jack Bauer Nimi on listalla. Jack Bower Nimi on listalla. Kiitos!
Pääohjelmametodi tulee kokonaisuudessaan olemaan seuraavanlainen:
Scanner lukija = new Scanner(System.in); ArrayList<String> lista = new ArrayList<>(); lueNimet(lukija, lista); System.out.println(""); tarkistaNimet(lukija, lista); System.out.println("Kiitos!");
Toteutetaan metodit lueNimet
ja tarkistaNimet
.
Toteuta metodi public static void lueNimet(Scanner lukija, ArrayList<String> lista)
, joka lukee käyttäjän kirjoittamat syötteet metodille parametrina annetulle listalle. Syötteiden lukeminen lopetetaan kun käyttäjä syöttää tyhjän rivin. Kun metodia kutsutaan pääohjelmasta, tulee tulostuksen olla esimerkiksi seuraavanlainen.
Syötä nimiä vieraslistalle, tyhjä rivi lopettaa. Jack Bauer Jack Bower Kiitos!
Esimerkki kutsusta:
public static void main(String[] args) { Scanner lukija = new Scanner(System.in); ArrayList<String> lista = new ArrayList<>(); lueNimet(lukija, lista); } // metodi ja sen toteutus
Toteuta metodi public static void tarkistaNimet(Scanner lukija, ArrayList<String> lista)
, joka kysyy käyttäjiltä nimiä, ja tarkistaa löytyykö nimiä patametrina saadulla listalla. Kun käyttäjä syöttää tyhjän merkkijonon (eli painaa vain enteriä), nimien tarkastus lopetetaan ja metodista poistutaan.
Kun parametrina annetulla listalla on nimi Coolness, metodin kutsu toimii esimerkiksi seuraavasti:
Syötä nimiä, tyhjä rivi lopettaa. Chuck Norris Nimi ei ole listalla. Jack Bauer Nimi ei ole listalla. Coolness Nimi on listalla. Testi Nimi ei ole listalla.
Jos et muista miten alkion listalla olemassaoloa voi tarkistaa helposti, katso edellisen viikon materiaalista vinkkejä.
Kun olet valmis, kokeile vielä että ohjelma toimii halutusti tehtävänannon alussa olevalla koodilla.
ArrayList on erittäin hyödyllinen kun halutaan tallentaa muuttujien arvoja myöhempää käsittelyä varten. Siinä on kuitenkin pieni rajoite. Tiedon yhdistäminen toiseen tietoon, esimerkiksi puhelinnumeroihin liittyvien nimien hakeminen, sanakirjan sanakäännösparien etsiminen tai käsitteiden ilmentymien lukumäärien laskeminen on ArrayListin kanssa -- vaikkakin mahdollista -- on hieman haastavaa. Tutustutaan seuraavaksi HashMap-nimiseen apuvälineeseen, joka on varsin näppärä väline tietoparien tallentamiseen.
HashMap on apuväline tietoparien tallentamiseen. Sitä käytetään silloin kun halutaan tallentaa avain-arvo -pareja, kuten esimerkiksi viivakoodeihin liittyviä tuotteita tai henkilöihin liittyviä saldotilanteita. HashMap-olio luodaan seuraavasti:
HashMap<Tyyppi, Tyyppi> arvoparit = new HashMap<>();
Yllä tyyppiparametrit kuvataan Tyyppi-muuttujien avulla. Tyyppiparametreilla määritellään minkätyyppisiä avaimia ja minkätyyppisiä arvoja luotavaan HashMapiin tallennetaan. Kuten ArrayListissä, tyyppiparametreja ovat esimerkiksi Integer
, String
ja Double
.
Alla olevassa esimerkissä on luotu HashMap-olio kaupunkien hakemiseen postinumeron perusteella, jonka jälkeen HashMap-olioon on lisätty neljä postinumero-kaupunki -paria. Kuten huomaat, parien lisääminen tapahtuu put(Tyyppi avain, Tyyppi arvo)
-metodin avulla.
HashMap<String, String> postinumerot = new HashMap<>(); postinumerot.put("00710", "Helsinki"); postinumerot.put("90014", "Oulu"); postinumerot.put("33720", "Tampere"); postinumerot.put("33014", "Tampere");
Toisin kuin listoilla, HashMapissa ei ole varsinaisesti käsitettä "indeksi", vaan arvoja haetaan avaimen perusteella. Ylläolevassa esimerkissä avaimeksi on määritelty postinumero (merkkijono). Voimme siis hakea arvoa merkkijonon perusteella -- HashMapin metodia get(Tyyppi avain)
käytetään arvon hakemiseen avaimen perusteella.
HashMap<String, String> postinumerot = new HashMap<>(); postinumerot.put("00710", "Helsinki"); postinumerot.put("90014", "Oulu"); postinumerot.put("33720", "Tampere"); postinumerot.put("33014", "Tampere"); System.out.println(postinumerot.get("33014")); // tulostaa "Tampere"
Jos avainta ei löydy, palauttaa get-metodi arvon null
.
HashMap<String, String> postinumerot = new HashMap<>(); postinumerot.put("00710", "Helsinki"); postinumerot.put("90014", "Oulu"); postinumerot.put("33720", "Tampere"); postinumerot.put("33014", "Tampere"); System.out.println(postinumerot.get("00550")); // tulostaa null
HashMap toimii vastaavasti myös muilla muuttujatyypeillä
Seuraavassa esimerkissä luodaan sanakirjaohjelma. Sanakirjaohjelmalle määritellään ensin ne sanaparit, jotka se tuntee, jonka jälkeen ohjelma kyselee käyttäjältä sanoja:
// luodaan HashMap-olio, johon sanat tallennetaan HashMap<String, String> sanakirja = new HashMap<>(); // lisätään sanapareja sanakirja.put("mänty", "pine"); sanakirja.put("omena", "apple"); sanakirja.put("ananas", "pineapple"); Scanner lukija = new Scanner(System.in); while(true) { System.out.print("Syötä haettava sana > "); String syote = lukija.nextLine(); if (syote.isEmpty()) { break; } if (sanakirja.containsKey(syote)) { System.out.println("Sanan " + syote + " käännös on " + sanakirja.get(syote)); } else { System.out.println("Sanaa " + syote + " ei löydy sanakirjasta."); } } System.out.println("Kiitos!");
Ohjelma voi toimia esimerkiksi seuraavasti:
Syötä haettava sana > norja Sanaa norja ei löydy sanakirjasta Syötä haettava sana > omena Sanan omena käännös on apple Syötä haettava sana > ananas Sanan ananas kännös on pineapple Syötä haettava sana > pineapple Sanaa pineapple ei löydy sanakirjasta
HashMap tallentaa tietopareja siten, että avain -- put-metodille ensin annettava muuttuja -- on se, jonka perusteella arvoja -- put-metodille annettava toinen muuttuja -- voi hakea. Yllä olevassa esimerkissä pineapple on tallennettu arvoksi, joten sitä ei löydy avaimena.
Lyhyt yhteenveto HashMapin komennoista:
put(Tyyppi1 avain, Tyyppi2 arvo)
- lisää tietoparin avain, arvo HashMapiin.get(Tyyppi1 avain)
- palauttaa avaimella löytyvät arvon. Jos arvoa ei löydy, palautetaan null.containsKey(Tyyppi1 avain)
- palauttaa totuusarvoisen muuttujan (boolean), joka kertoo onko HashMapissa haettua avainta.Toteuta edellistä sanakirjaesimerkkiä mukaillen ohjelma, joka ensin lukee ja tallentaa käyttäjän syöttämiä puhelinnumeroita ja nimiä. Lukeminen lopetetaan kun käyttäjä syöttää tyhjän merkkijonon. Tämän jälkeen käynnistetään numeropalvelu, joka kertoo soittajan nimen käyttäjän syöttämän puhelinnumeron perusteella. Jos puhelinnumeron omistaja on tiedossa, kerrotaan se, muuten kerrotaan että soittaja on tuntematon. Numeropalvelu sammutetaan kun käyttäjä syöttää tyhjän merkkijonon.
Ohjelman tulee esimerkiksi toimia seuraavasti:
Syötä puhelinnumero, tyhjä lopettaa: 123-12345 Syötä nimi: Andrew Syötä puhelinnumero, tyhjä lopettaa: 867-5309 Syötä nimi: Jenny Syötä puhelinnumero, tyhjä lopettaa: Kiitos! * Numeropalvelu * Mikä numero tarkistetaan? > 012-12345 Tuntematon numero. Mikä numero tarkistetaan? > 867-5309 Soittaja on Jenny Mikä numero tarkistetaan? > Kiitos!
HashMap on viittaustyyppinen muuttuja samalla tavalla kuin ArrayList. Tämä tarkoitta sitä, että kun HashMap-olio annetaan metodille parametrina, metodikutsun yhteydessä kopioituu vain viite, ja metodin sisällä voidaan muokata alkuperäistä HashMap-olioita.
Allaoleva esimerkkiohjelma kuvastaa tätä ominaisuutta.
import java.util.HashMap; public class Esimerkki { public static void main(String[] args) { HashMap<String, String> parit = new HashMap<>(); lisaaArvo(parit, "Esimerkki", "Toimii"); System.out.println(parit.get("Esimerkki")); } public static void lisaaArvo(HashMap<String, String> arvot, String avain, String arvo) { arvot.put(avain, arvo); } }
Kokeile mitä ylläoleva ohjelma tulostaa!
HashMapilla ei ole metodia olemassaolevan arvon päivittämiseen, vaan päivitys tapahtuu edellisen arvon ylikirjoittamisella. Jos HashMapia käytetään asioiden laskemiseen, tulee lisäyksen yhteydessä hakea vanha arvo, kasvattaa sitä yhdellä, ja lisätä kasvatettu arvo takaisin HashMap-olioon. Alla olevassa esimerkissä on toteutettu bongauslaskuri, joka pitää kirjaa erilaisista bongauksista.
import java.util.HashMap; import java.util.Scanner; public class Bongauslaskuri { public static void main(String[] args) { Scanner lukija = new Scanner(System.in); HashMap<String, Integer> bongaukset = new HashMap<>(); while (true) { String bongattu = lukija.nextLine(); if(bongattu.isEmpty()) { break; } if (!bongaukset.containsKey(bongattu)) { bongaukset.put(bongaus, 1); } else { bongaukset.put(bongaus, bongaukset.get(bongaus) + 1); } System.out.println("Bongauksia " + bongaus + " yhteensä " + bongaukset.get(bongaus)); } } }
Toteutetaan tässä tehtävässä toiminnallisuutta kahvikassan kirjanpitoon. Kahvikassa pitää kirjaa henkilöiden juomista kahveista. Juotuja kahveja voi maksaa pois tuomalla kahvipaikalle uusia kahvipaketteja. Kun ohjelma on valmis, sen tominnallisuus on seuraavanlainen.
** Kahvikassa ** > Sonja Luodaan tunnus Sonja. Juotuja kahveja 0. Kirjoita "juo" jos haluat juoda kahvin, "tuo" jos toit kahvipaketin. > juo Juotuja kahveja 1. ** Kahvikassa ** > Sonja Juotuja kahveja 1. Kirjoita "juo" jos haluat juoda kahvin, "tuo" jos toit kahvipaketin. > tuo Juotuja kahveja -9. ** Kahvikassa ** > Ada Luodaan tunnus Ada. Juotuja kahveja 0. Kirjoita "juo" jos haluat juoda kahvin, "tuo" jos toit kahvipaketin. > juo Juotuja kahveja 1. ** Kahvikassa ** ...
Kun ohjelmaan kirjautuu uusi käyttäjä, käyttäjätunnus luodaan ja käyttäjälle asetetaan saldoksi 0. Kun käyttäjä kirjoittaa "juo", juotujen kahvien määrä kasvaa yhdellä. Jos taas käyttäjä kirjoittaa "tuo", juotujen kahvien määrä vähenee kymmenellä. Lopeta ohjelman suoritus kun käyttäjä kirjoittaa tyhjän merkkijonon.
Tehdään ohjelma paloissa.
Luo metodi public static void haeTiedot(HashMap<String, Integer> kassa, String nimi)
, joka tarkistaa onko käyttäjää kassa
-nimisenä parametrina annetussa HashMapissa. Jos käyttäjää ei löydy, luodaan käyttäjä ja asetetaan käyttäjän juotujen kahvien määräksi 0. Lopulta parametrina annetusta HashMapista haetaan käyttäjän juomien kahvien määrä ja tulostetaan se.
Esimerkkejä metodin toiminnasta:
HashMap<String, Integer> data = new HashMap<>(); haeTiedot(data, "Arto");
Luodaan tunnus Arto. Juotuja kahveja 0.
HashMap<String, Integer> data = new HashMap<>(); haeTiedot(data, "Arto"); haeTiedot(data, "Arto");
Luodaan tunnus Arto. Juotuja kahveja 0. Juotuja kahveja 0.
HashMap<String, Integer> data = new HashMap<>(); haeTiedot(data, "Arto"); haeTiedot(data, "Ada"); haeTiedot(data, "Arto");
Luodaan tunnus Arto. Juotuja kahveja 0. Luodaan tunnus Ada. Juotuja kahveja 0. Juotuja kahveja 0.
Luo metodi public static void muutaSaldoa(HashMap<String, Integer> kassa, String nimi, int paljonko)
, joka muuttaa parametrina annetussa kassa
-parametrissa olevaan henkilöön nimi
liittyvää saldoa parametrina annetun paljonko
-muuttujan verran.
Esimerkkejä metodin toiminnasta yhdessä edellisessä osassa toteutetun haeTiedot-metodin kanssa:
HashMap<String, Integer> data = new HashMap<>(); haeTiedot(data, "Arto"); muutaSaldoa(data, "Arto", 1); haeTiedot(data, "Arto");
Luodaan tunnus Arto. Juotuja kahveja 0. Juotuja kahveja 1.
HashMap<String, Integer> data = new HashMap<>(); haeTiedot(data, "Arto"); muutaSaldoa(data, "Arto", -10); haeTiedot(data, "Arto");
Luodaan tunnus Arto. Juotuja kahveja 0. Juotuja kahveja -10.
Toteuta pääohjelmametodi, joka hyödyntää edellä toteutettuja metodeja. Käyttäjän tulee ensin syöttää nimi, jonka jälkeen kahvikassa tarkistaa onko käyttäjää olemassa. Jos käyttäjä on olemassa, kerrotaan käyttäjän juomien kahvien määrä. Jos käyttäjää taas ei ole olemassa, luodaan käyttäjä, jonka jälkeen kerrotaan käyttäjän juomien kahvien määrä. Tämän jälkeen käyttäjältä kysytään haluaako hän juoda vai tuoda kahvia. Jos käyttäjä haluaa juoda kahvia, hänen juomiensa kahvien määrää kasvatetaan yhdellä, jonka jälkeen juotujen kahvien määrä tulostetaan. Jos taas käyttäjä haluaa tuoda kahvia, hänen juomiensa kahvien määrää vähennetään kymmenellä, jonka jälkeen juotujen kahvien määrä tulostetaan. Ohjelman suoritus tulee lopettaa kun käyttäjä kirjoittaa nimeksi tyhjän merkkijonon, eli painaa enteriä.
Aloita metodin toteutus siten, että määrittelet Scanner-lukijan ja HashMap<String, Integer> -olion.
public static void main(String[] args) { Scanner lukija = new Scanner(System.in); HashMap<String, Integer> kahvit = new HashMap<>(); // kahvikassan toiminnallisuus } // aiemmin toteutetut metodit
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, ohjelma jaetaan pieniin osiin. Näitä pieniä osia kutsutaan olioiksi, ja jokaisella oliolla on oma yksittäinen vastuunsa. Olio 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 Javan valmiita olioita. Esimerkiksi ArrayList
:it ovat olioita. Jokainen yksittäinen lista koostuu yhteenkuuluvasta tiedosta, eli olion tilasta. Listaolioihin liittyy toiminnallisuutta, eli metodit 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<>(); ArrayList<String> maat = new ArrayList<>(); 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 myös jo luoneet olioita koodissamme. Uuden olion luominen tapahtuu komennon new
avulla. Esimerkiksi lukijan (Scanner
) ja listan (ArrayList
) luominen on tapahtunut new
-komennolla seuraavasti.
Scanner lukija = new Scanner(System.in); ArrayList<String> lista = new ArrayList<>();
Komentoa new
käytetään kun halutaan luoda uusi olio valmiiksi määritellystä Luokasta. Java-kielessä oliot luodaan aina new
-komennolla muutamaa poikkeusta lukuunottamatta.
Eräs näistä poikkeuksista ovat merkkijonot, joiden luomiseen ei aina tarvita new
-komentoa. 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ä");
Se, minkälainen oliosta tulee, riippuu olion luokasta.
On selvää että kaikki oliot eivät ole keskenään samankaltaisia. Esimerkiksi ArrayList
-oliot poikkeavat String
-olioista. Kaikilla ArrayList
-olioilla on samat metodit (add
, contains
, remove
, size
, ...) ja vastaavasti kaikilla String
-olioilla on samat metodit (substring
, length
, charAt
, ...). ArrayList
- ja String
-olioiden metodit eivät ole samat, sillä ne ovat eri tyyppisiä.
Olion tyyppiä kutsutaan luokaksi. ArrayList
on luokka, String
on luokka, Scanner
on luokka, ja niin edelleen... Oliot taas ovat luokasta tehtyjä ilmentymiä.
Kaikista luokasta tehdyillä olioilla on samat metodit sekä samankaltainen tila. Esimerkiksi 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. Edellisellä viikon alussa löytyy tähän vihjeitä.
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 { }
Luokista voi piirtää myös luokkakaavion, jonka merkintätekniikkaan tutustutaan tässä samalla. Luokka, jossa ei ole yhtäkään metodia tai muuttujaa näyttää seuraavalta:
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ä.
Nyt luokkakaavioon on merkitty luokan nimi sekä muuttujat. Muuttujille määritellään tyyppi muodossa "muuttujanNimi: muuttujanTyyppi". Miinusmerkki muuttujaa ennen kertoo, että se on määritelty private-etuliitteellä.
Olemme nyt määritelleet rakennuspiirustukset -- luokan -- henkilöoliolle. Jokaisella uudella henkilöolioilla on muuttujat nimi
ja ika
, joissa voi olla oliokohtainen arvo. Henkilöiden "tila" koostuu niiden nimeen ja ikään asetetuista arvoista.
Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Anna luokan nimeksi (Class Name) Koira
.
Luo tehtäväpohjaan luokka nimeltä Koira
ja lisää sille oliomuuttujat private String nimi
, private String rotu
ja private int ika
. Luokkakaaviossa luokka Koira näyttää seuraavalta:
Luokalla ei vielä oikeastaan tee mitään, mutta tämän askeleen harjoittelusta on hyötyä myöhempää ajatellen.
Luotavalle oliolle halutaan asettaa alkutila. Itse määritellyn olion luominen tapahtuu hyvin samaan tapaan kuin Javan valmiiden olioiden kuten ArrayList
:ien luominen. Oliot siis luodaan new
-komennolla. Olion luomisen yhteydessä on kätevää pystyä antamaan arvot luotavan olion muuttujille. Esimerkiksi uutta henkilö-oliota luotaessa olisi kätevää pystyä antamaan nimi jo luomishetkellä:
public static void main(String[] args) { Henkilo ada = new Henkilo("Ada"); // ... }
Tämä onnistuu määrittelemällä olion luova metodi, eli konstruktori. Seuraavassa on määritelty Henkilo
-luokalle konstruktori, joka luo uuden Henkilo
-olion. Alla olevassa esimerkissä olevassa Konstruktorissa henkilön iäksi asetetaan 0 ja nimeksi konstruktorin parametrina tuleva merkkijono:
public class Henkilo { private String nimi; private int ika; public Henkilo(String nimiAlussa) { this.ika = 0; this.nimi = nimiAlussa; } }
Konstruktori on 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 komennolla new Henkilo("Ada");
Oliot luodaan aina konstruktorin avulla.
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.
Nyt luokkakaavioon on merkitty luokan nimen ja muuttujien lisäksi myös konstruktori. Konstruktori saa public näkyvyysmääreen takia eteen plussan, jonka lisäksi siitä merkitään sen nimi ja parametrin tyypit (tässä + Henkilo(String)
). Konstruktorilla ei ole erikseen palautustyyppiä, joten sitä ei kaavioon merkitä.
Vielä yksi huomio: jos ohjelmoija ei tee luokalle konstruktoria, tekee Java automaattisesti luokalle oletuskonstruktorin. Oletuskonstruktori on konstruktori joka ei tee mitään. Jos konstruktoria ei jostain syystä tarvita, ei sellaista tarvitse ohjelmoida.
Uuden luokan saa lisättyä seuraavasti: Ruudun vasemmalla reunalla on projektilistaus. Paina projektin nimen kohdalla hiiren oikeaa nappia. Valitse avautuvasta valikosta New ja Java Class. Jos haluat että luokan nimi on Luokka, aseta luokan nimeksi (Class Name) Luokka
.
Luo luokka nimeltä Luokka
. Luokalla on oliomuuttujina private String koodi
, esimerkiksi "B221", ja private int istumapaikat
. Luo tämän jälkeen konstruktori public Luokka(String luokanKoodi, int istumapaikkojenMaara)
, minkä avulla oliomuuttujiin asetetaan arvot.
Tälläkään luokalla ei vielä oikeastaan tee mitään, mutta seuraavassa tehtävässä luokastamme tehdyllä oliolla voi jo tulostella :).
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 kirjoitetaan luokan sisälle konstruktorin alapuolelle. 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 tulevilla viikoilla hieman tarkemmin mistä tässä on kysymys.
Luokkakaavioon on merkitty luokan nimen, muuttujien lisäksi ja konstruktorin lisäksi nyt myös metodi tulostaHenkilo
. Koska metodilla on public-määre, tulee sille alkuun plus, jota seuraa metodin nimi. Metodille ei ole merkitty parametreja, joten niitä ei erikseen merkitä sulkujen sisälle. Metodeille merkitään myös palautustyyppi, tässä "void".
Metodin tulostaHenkilo
sisällä on yksi koodirivi joka käyttää hyvakseen oliomuuttujia nimi
ja ika
-- luokkakaavio ei kerro sisäisestä toteutuksesta. Olion sisäisiin muuttujiin viitataan 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 ada = new Henkilo("Ada"); Henkilo antti = new Henkilo("Antti"); Henkilo martin = new Henkilo("Martin"); ada.tulostaHenkilo(); antti.tulostaHenkilo(); martin.tulostaHenkilo(); } }
Tulostuu:
Ada, 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 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
Piirrä myös luokkaan liittyvä luokkakaavio itsellesi!
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 kirjoitetaan tulostaHenkilo
-metodin tapaan luokan Henkilo
sisälle. Metodissa kasvatetaan oliomuuttujan ika
arvoa yhdellä.
Myös luokkakaavio päivittyy.
Kutsutaan metodia ja katsotaan mitä tapahtuu:
public class Main { public static void main(String[] args) { Henkilo ada = new Henkilo("Ada"); Henkilo antti = new Henkilo("Antti"); ada.tulostaHenkilo(); antti.tulostaHenkilo(); System.out.println(""); ada.vanhene(); ada.vanhene(); ada.tulostaHenkilo(); antti.tulostaHenkilo(); } }
Ohjelman tulostus on seuraava:
Ada, ikä 0 vuotta Antti, ikä 0 vuotta Ada, ikä 2 vuotta Antti, ikä 0 vuotta
Eli "syntyessään" molemmat oliot ovat nollavuotiaita (konstruktorissa suoritetaan mm. rivi this.ika = 0;
). Olion ada
metodia vanhene
kutsutaan kaksi kertaa. Kuten tulostus näyttää, tämä saa aikaan sen että Adan ikä on vanhenemisen jälkeen 2 vuotta. Kutsumalla metodia Adaa vastaavalle oliolle, toisen henkilöolion ikä ei muutu.
Jokaisella oliolla on siis oma sisäinen tilansa.
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.
Oliomuuttujina voi käyttää myös listoja. Olemme aiemmin huomanneet, että listat ovat esimerkiksi näppäriä silloin, silloin kun haluamme pitää kirjaa useammasta erillisestä asiasta. Alla olevassa esimerkissä käsitteelle soittolista on luotu luokka. Soittolista sisältää kappaleita.
// importit public class Soittolista { private ArrayList<String> kappaleet; public Soittolista() { this.kappaleet = new ArrayList<>(); } public void lisaaKappale(String kappale) { this.kappaleet.add(kappale); } public void poistaKappale(String kappale) { this.kappaleet.remove(kappale); } public void tulostaKappaleet() { for (String kappale: this.kappaleet) { System.out.println(kappale); } } }
Kokeile ylläolevan soittolistan käyttöä hiekkalaatikossa!
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<>(); } // toteuta tänne tarvittavat metodit }
Ruokalistaoliolla on 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()
-- luokkakaaviona lopullinen toteutus näyttää seuraavalta.
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.
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; } }
Luokka kokonaisuudessaan:
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
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; } public int palautaIka() { return this.ika; } }
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".
Luokka kokonaisuudessaan:
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 tulostaHenkilo
-metodin.
Olioscreencastin toinen osa:
Helsingin Yliopiston opiskelijaruokaloissa eli Unicafeissa opiskelijat maksavat lounaansa käyttäen maksukorttia. Lopullinen Maksukortti tulee näyttämään luokkakaaviona seuraavalta:
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_090.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_090.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("Pekka: " + kortti); kortti.lataaRahaa(-15); System.out.println("Pekka: " + kortti); } }
Ohjelman tulisi tuottaa seuraava tulostus:
Pekka: Kortilla on rahaa 10.0 euroa Pekka: 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 -- koodi käytännössä asettaa parametrina saadulle pituus
-muuttujalle siinä jo olevan arvon:
public void setPituus(int pituus) { // ÄLÄ TEE NÄIN!!! pituus = pituus; }
public void setPituus(int pituus) { // VAAN NÄIN!!! this.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 et ole ihan varma mitä tässä pitäisi tehdä, kertaa materiaalista kohta missä puhutaan "settereistä".
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.
Huom! Tässä on 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:
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
Olemme käyttäneet ArrayList
:ejä jo monessa esimerkissä ja tehtävässä. ArrayList-olioon pystyy lisäämään esimerkiksi merkkijonoja ja listalla olevien merkkijonojen läpikäynti, etsiminen, poistaminen, järjestäminen ym. ovat vaivattomia toimenpiteitä.
ArrayList:eihin voidaan laittaa minkä tahansa tyyppisiä oliota. Luodaan seuraavassa henkilölista, eli tyyppiä ArrayList<Henkilo>
oleva ArrayList ja laitetaan sinne muutama henkilöolio:
public static void main(String[] args) { ArrayList<Henkilo> henkilot = new ArrayList<>(); // voidaan ensin ottaa henkilö muuttujaan Henkilo juhana = new Henkilo("Juhana"); // ja lisätä se sitten listalle henkilot.add(juhana); // tai voidaan myös luoda olio ja lisätä se heti: henkilot.add(new Henkilo("Matti")); henkilot.add(new Henkilo("Martin")); System.out.println("Henkilöt vastasyntyneenä: "); for (Henkilo henkilo : henkilot) { System.out.println(henkilo); } for (Henkilo henkilo : henkilot) { henkilo.vanhene(); } System.out.println("1 vuoden kuluttua: "); for (Henkilo henkilo : henkilot) { System.out.println(henkilo); } }
Ohjelman tulostus:
Henkilöt vastasyntyneenä: Juhana, ikä 0 vuotta Matti, ikä 0 vuotta Martin, ikä 0 vuotta 1 vuoden kuluttua: Juhana, ikä 1 vuotta Matti, ikä 1 vuotta Martin, ikä 1 vuotta
Listalla olevat henkilöt ovat samoja henkilöitä kuin ohjelmassa luodut henkilöt. Olion juhana
ikä muuttuu myös jos hän vanhenee listan ulkopuolella.
public static void main(String[] args) { ArrayList<Henkilo> henkilot = new ArrayList<>(); // voidaan ensin ottaa henkilö muuttujaan Henkilo juhana = new Henkilo("Juhana"); // ja lisätä se sitten listalle henkilot.add(juhana); // tai voidaan myös luoda olio ja lisätä se heti: henkilot.add(new Henkilo("Matti")); henkilot.add(new Henkilo("Martin")); System.out.println("Henkilöt vastasyntyneenä: "); for (Henkilo henkilo : henkilot) { System.out.println(henkilo); } for (Henkilo henkilo : henkilot) { henkilo.vanhene(); } System.out.println("1 vuoden kuluttua: "); for (Henkilo henkilo : henkilot) { System.out.println(henkilo); } juhana.vanhene(); juhana.vanhene(); System.out.println("1 vuoden kuluttua -- mutta -- juhana vanheni: "); for (Henkilo henkilo : henkilot) { System.out.println(henkilo); } }
Ohjelman tulostus:
Henkilöt vastasyntyneenä: Juhana, ikä 0 vuotta Matti, ikä 0 vuotta Martin, ikä 0 vuotta 1 vuoden kuluttua: Juhana, ikä 1 vuotta Matti, ikä 1 vuotta Martin, ikä 1 vuotta 1 vuoden kuluttua -- mutta -- juhana vanheni: Juhana, ikä 3 vuotta Matti, ikä 1 vuotta Martin, ikä 1 vuotta
Huom! Älä huoli jos kaiuttimista kuuluu pientä "napsuvaa" ääntä -- ongelma liittyy äänen taajuuksista toiseen hyppäämiseen (tai käytettävään kirjastoon), jota ei ole vielä tässä versiossa korjattu. Käytetty äänikirjasto ei myöskään toimi kaikilla konekokoonpanoilla -- kirjaston pitäisi kuitenkin ilmaista jos näin on: voit tehdä tehtävän vaikka ääniä ei kuuluisi.
Tee luokka Aani
, jolla on double
-tyyppinen oliomuuttuja taajuus
ja double
-tyyppinen oliomuuttuja kesto
. Tee luokalle konstruktori, joka saa ensimmäisenä parametrina taajuuden ja toisena parametrina keston. Lisää lisäksi luokalle seuraavat metodit:
getTaajuus
palauttaa olioon liittyvän taajuus-muuttujan arvongetKesto
palauttaa olioon liittyvän kesto-muuttujan arvontoString
palauttaa olioa kuvaavan merkkijonoesityksenLuokan pitäisi toimia seuraavasti:
Aani aani = new Aani(261.6, 2); System.out.println("Äänen taajuus on " + aani.getTaajuus() + " hertsiä."); System.out.println("Äänen kesto on " + aani.getKesto() + " sekuntia."); System.out.println(aani);
Äänen taajuus on 261.6 hertsiä. Äänen kesto on 2.0 sekuntia. 261.6Hz (2.0s)
Sopivat äänet ovat oikeastaan säveliä..
Toteuta luokan Paaohjelma
main
-metodiin toiminnallisuus, millä käyttäjältä kysytään soitettavat sävelet merkkijonona. Kun merkkijono on saatu, luodaan annetuista merkeistä ääni-olioita, joita lisätään listaan. Lopulta, kun lista on valmis, luo ohjelmassa valmiina tulevasta luokasta Soitin
olio ja anna lista soitin-olion soita
-metodille.
ArrayList<Aani> aanet = new ArrayList<>(); // luetaan käyttäjältä äänet ja tehdään niistä ääni-olioita, joita // lisätään aanet-listalle Soitin soitin = new Soitin(); soitin.soita(aanet);
Käytä seuraavaa taulukkoa käyttäjän syötteen muunnokseen (muunnokset Wikipediasta):
merkki | taajuus | kesto |
C | 261.626 | 0.5 |
D | 293.665 | 0.5 |
E | 329.628 | 0.5 |
F | 349.228 | 0.5 |
G | 391.995 | 0.5 |
A | 440.000 | 0.5 |
H | 493.883 | 0.5 |
Tämän lisäksi, jos merkkijonossa on tyhjä merkki, lisää listalle taukoa kuvaava ääni-olio, jonka taajuus on 0
ja kesto 0.1
. Ohjelman toiminta on esimerkiksi seuraavanlainen:
Kirjoita soitettavat äänet CDEFG G GFEDC Soitetaan!
Yllä olevalla merkkijonolla soitetaan nuotit CDEFG, pidetään tauko, soitetaan nuotti G, pidetään tauko, ja soitetaan nuotit GFEDC.
Kokeile myös soittaa merkkijono "E E F G G F E D C C D E E D D E E F G G F E D C C D E D C C"!
Lisää ohjelmaan ns. Easter egg, eli tässä tapauksessa salainen kappale, joka soitetaan kun käyttäjä kirjoittaa merkkijonon "TKO-ALY". Saat itse päättää soitettavan ääniyhdistelmän tai kappaleen.