Arto Vihavainen ja Matti Luukkainen
Huomautus lukijalle
Tämä on suoraa jatkoa Ohjelmoinnin perusteet -kurssin materiaaliin.
Tämä materiaali on tarkoitettu Helsingin Yliopiston Tietojenkäsittelytieteen laitoksen syksyn 2012 Ohjelmoinnin perusteet- ja jatkokurssille. Materiaali pohjautuu keväiden 2012, 2011 ja 2010 kurssimateriaaleihin, joiden sisältöön ovat vaikuttaneet Matti Paksula, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju, Martin Pärtel, Joel Kaasinen ja Mikael Nousiainen
Lue materiaalia siten, että teet samalla itse kaikki lukemasi esimerkit. Esimerkkeihin kannattaa tehdä pieniä muutoksia ja tarkkailla, miten muutokset vaikuttavat ohjelman toimintaan. Äkkiseltään voisi luulla, että esimerkkien tekeminen myös itse ja niiden muokkaaminen hidastaa opiskelua. Tämä ei kuitenkaan pidä ollenkaan paikkansa. Ohjelmoimaan ei ole vielä tietääksemme kukaan ihminen oppinut lukemalla (tai esim. luentoa kuuntelemalla). Oppiminen perustuu oleellisesti aktiiviseen tekemiseen ja rutiinin kasvattamiseen. Esimerkkien ja erityisesti erilaisten omien kokeilujen tekeminen on parhaita tapoja "sisäistää" luettua tekstiä.
Pyri tekemään tai ainakin yrittämään tehtäviä sitä mukaa kuin luet tekstiä. Jos et osaa heti tehdä jotain tehtävää, älä masennu, sillä saat ohjausta tehtävän tekemiseen pajassa.
Tekstiä ei ole tarkoitettu vain kertaalleen luettavaksi. Joudut varmasti myöhemmin palaamaan jo aiemmin lukemiisi kohtiin tai aiemmin tekemiisi tehtäviin. Tämä teksti ei sisällä kaikkea oleellista ohjelmointiin liittyvää. Itse asiassa ei ole olemassa mitään kirjaa josta löytyisi kaikki oleellinen. Eli joudut joka tapauksessa ohjelmoijan urallasi etsimään tietoa myös omatoimisesti. Kurssin harjoitukset sisältävät jo jonkun verran ohjeita, mistä suunnista ja miten hyödyllistä tietoa on mahdollista löytää.
Muutamiin kohtiin olemme myös liittäneet screencasteja joita katsomalla voi pelkän valmiin koodin lukemisen sijaan seurata miten ohjelma muodostuu.
Kurssi alkaa siitä mihin ohjelmoinnin perusteet loppui ja oikeastaan kaikki ohjelmoinnin perusteet -kurssilla opitut asiat oletetaan nyt osattavan. Kannattaa käydä aina silloin tällöin kertaamassa Ohjelmoinnin perusteet -kurssin materiaalia.
Huom! Käytämme tällä kurssilla NetBeans-nimistä ohjelmointiympäristöä. Ohjeet NetBeansin ja kurssilla käytettävän tehtäväautomaatin käyttöön löydät täältä.
Tässä luvussa kerrataan pikaisesti muutamia ohjelmoinnin perusteissa tutuksi tulleita asioita. Ohjelmoinnin perusteet -kurssin materiaaliin pääsee tutustumaan tarkemmin tästä.
Ruostunutta ohjelmointitaitoa voi myös verestää täältä löytyvän tehtäväsarjan avulla.
Tässä luvussa kerrataan pikaisesti muutamia aiempina viikkoina tutuksi tulleita asioita.
Tietokoneohjelma koostuu joukosta käskyjä, joita tietokone suorittaa yksi kerrallaan, ylhäältä alaspäin. Käskyillä on aina määrätty rakenne ja semantiikka. Javassa, eli kurssilla käyttämässämme ohjelmointikielessä, käskyjä luetaan ylhäältä alas vasemmalta oikealle. Ohjelmointikurssit aloitetaan usein esittelemällä ohjelma, joka tulostaa merkkijonon Hei maailma!
. Alla on Java-kielellä kirjoitettu käsky Hei maailma!
-merkkijonon tulostamiseksi.
System.out.println("Hei maailma!");
Käskyssä kutsutaan System
-luokkaan liittyvän muuttujan println
-metodia, joka tulostaa ensin parametrina annetun merkkijonon ja sen jälkeen rivinvaihdon. Metodille annetaan parametrina merkkijono Hei maailma!
, jolloin tulostus on Hei maailma!
jota seuraa rivinvaihto.
Ohjelmaan voi liittyä muuttujia joita voi käyttää osana ohjelman toimintaa. Alla on ohjelma, joka ensin esittelee kokonaislukutyyppisen muuttujan pituus
johon asetetaan seuraavalla rivillä arvo 179. Tämän jälkeen tulostetaan muuttujan pituus
arvo eli 179
.
int pituus; pituus = 179; System.out.println(pituus);
Yllä ohjelman suoritus tapahtuisi rivi kerrallaan. Ensin suoritetaan rivi int pituus;
, jossa esitellään muuttuja nimeltä pituus
. Seuraavaksi suoritetaan rivi pituus = 179;
, jossa asetetaan edellisellä rivillä esiteltyyn muuttujaan arvo 179
. Tämän jälkeen suoritetaan rivi System.out.println(pituus);
, jossa kutsutaan aiemmin näkemäämme tulostusmetodia, jolle annetaan parametrina muuttuja pituus. Metodi tulostaa muuttujan pituus
sisällön eli arvon 179
.
Yllä olevassa ohjelmassa ei oikeastaan ole tarvetta esitellä muuttujaa pituus
ja asettaa siihen arvoa erillisillä riveillä. Muuttujan esittelyn ja siihen liittyvän arvon asetuksen voi tehdä myös samalla rivillä.
int pituus = 179;
Yllä olevaa riviä suoritettaessa esitellään ensin muuttuja pituus
, johon asetetaan esittelyn yhteydessä arvo 179
.
Kaikki tieto esiintyy oikeasti tietokoneen sisällä jonona bittejä, eli lukuja nolla ja yksi. Muuttujat ovat ohjelmointikielten tarjoama abstraktio, jolla voidaan käsitellä erilaisia arvoja helpommin. Muuttujia käytetään arvojen säilyttämiseen ja ohjelman tilan ylläpitoon. Javassa käytössämme on muun muassa alkeisarvoiset muuttujatyypit int
(kokonaisluku), double
(liukuluku), boolean
(totuusarvo), char
(merkki) sekä viitetyyppiset muuttujatyypit String
(merkkijono), ArrayList
(taulukko) ja kaikki luokat. Palaamme alkeistyyppisiin ja viitetyyppisiin muuttujiin ja niiden eroihin tarkemmin myöhemmin.
Ohjelmien toiminnallisuus rakennetaan kontrollirakenteiden avulla. Kontrollirakenteet mahdollistavat erilaiset toiminnot ohjelman muuttujien arvoista riippuen. Alla esimerkki if-else if-else
-kontrollirakenteesta, jossa tehdään erilainen toiminto vertailun tuloksesta riippuen. Esimerkissä tulostetaan merkkijono Kaasua
jos muuttujan nopeus
arvo on pienempi kuin 110, merkkijono Jarrua
jos muuttujan nopeus
arvo on suurempi kuin 120 ja merkkijono Kruisaillaan
muissa tapauksissa.
int nopeus = 105; if(nopeus < 110) { System.out.println("Kaasua"); } else if (nopeus > 120) { System.out.println("Jarrua"); } else { System.out.println("Kruisaillaan"); }
Koska yllä olevassa esimerkissä muuttujan nopeus
arvo on 105, tulostaa ohjelma aina merkkijonon Kaasua
. Muistathan että merkkijonojen yhtäsuuruusvertailu tapahtuu merkkijonoon liittyvällä equals
-metodilla. Alla on esimerkki jossa käytetään Javan Scanner-luokasta luotua olioita käyttäjän kirjoittaman syötteen lukemiseen. Ohjelma tarkistaa ovatko käyttäjän syöttämät merkkijonot samat.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä ensimmäinen merkkijono: "); String ensimmainen = lukija.nextLine(); System.out.print("Syötä toinen merkkijono: "); String toinen = lukija.nextLine(); System.out.println(); if (ensimmainen.equals(toinen)) { System.out.println("Kirjoittamasi merkkijonot ovat samat!"); } else { System.out.println("Kirjoittamasi merkkijonot eivät olleet samat!"); }
Ohjelman toiminta riippuu käyttäjän syötteestä. Alla esimerkki, punaisella värillä tarkoitetaan käyttäjän kirjoittamaa syötettä.
Syötä ensimmäinen merkkijono: porkkana Syötä toinen merkkijono: salaatti Kirjoittamasi merkkijonot eivät olleet samat!
Ohjelmissa tarvitaan usein toistoa. Tehdään ensin ns. while-true-break -toistolause, jota jatketaan niin pitkään kunnes käyttäjä syöttää merkkijonon salasana
. Lause while(true)
aloittaa toistolauseen, jota jatketaan kunnes kohdataan avainsana break
.
Scanner lukija = new Scanner(System.in); while(true) { System.out.print("Syötä salasana: "); String salasana = lukija.nextLine(); if(salasana.equals("salasana")) { break; } } System.out.println("Kiitos!");
Syötä salasana: porkkana Syötä salasana: salasana Kiitos
While-toistolausekkeeseen voi asettaa totuusarvon true
sijaan myös vertailun. Alla tulostetaan käyttäjän syöttämä merkkijono siten, että sillä on ylä- ja alapuolella tähtiä.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä merkkijono: "); String merkkijono = lukija.nextLine(); int tahdenNumero = 0; while(tahdenNumero < merkkijono.length()) { System.out.print("*"); tahdenNumero = tahdenNumero + 1; } System.out.println(); System.out.println(merkkijono); tahdenNumero = 0; while(tahdenNumero < merkkijono.length()) { System.out.print("*"); tahdenNumero = tahdenNumero + 1; } System.out.println();
Syötä merkkijono: porkkana ******** porkkana ********
Yllä olevan esimerkin pitäisi nostattaa kylmiä väreitä selkäpiissäsi. Kylmät väreet toivottavasti johtuvat siitä, että huomaat esimerkin rikkovan ohjelmoinnin perusteissa opittuja käytänteitä. Esimerkissä on turhaa toistoa joka tulee poistaa metodien avulla.
While-toistolauseen lisäksi käytössämme on myös for-toistolauseen kaksi versiota. Uudempaa for-lauseketta käytetään listojen läpikäyntiin.
ArrayList<String> tervehdykset = new ArrayList<String>(); tervehdykset.add("Hei"); tervehdykset.add("Hallo"); tervehdykset.add("Hi"); for(String tervehdys: tervehdykset) { System.out.println(tervehdys); }
Hei Hallo Hi
Perinteisempää for-lausetta käytetään samankaltaisissa tilanteissa kuin while
-toistolausetta. Sitä voidaan käyttää esimerkiksi taulukoiden läpikäyntiin. Seuraavassa esimerkissä kerrotaan jokaisen taulukon luvut
alkion sisältö kahdella ja lopuksi tulostetaan luvut uudempaa for-lausetta käyttäen.
int[] luvut = new int[] {1, 2, 3, 4, 5, 6}; for (int i = 0; i < luvut.length; i++) { luvut[i] = luvut[i] * 2; } for (int luku : luvut) { System.out.println(luku); }
2 4 6 8 10 12
Perinteinen for-lauseke on erittäin hyödyllinen tapauksissa joissa käymme indeksejä yksitellen läpi. Alla oleva toistolauseke käy merkkijonon merkit yksitellen läpi, ja tulostaa merkkijono Hip!
aina kun törmäämme merkkiin a
.
String merkkijono = "saippuakauppias"; for (int i = 0; i < merkkijono.length(); i++) { if (merkkijono.charAt(i) == 'a') { System.out.println("Hip!"); } }
Hip! Hip! Hip! Hip!
Metodit ovat tapa pilkkoa ohjelman toiminnallisuutta pienempiin osakokonaisuuksiin. Kaikkien Java-ohjelmien suoritus alkaa pääohjelmametodista, joka määritellään lauseella public static void main(String[] args)
. Lause määrittelee staattisen, eli luokkaan liittyvän metodin, joka saa parametrina merkkijonotaulukon.
Ohjelmoija määrittelee metodeja ohjelman sisältävien toiminnallisuuksien abstrahoimiseksi. Ohjelmoidessa tulee tavoitella tilannetta, jossa ohjelmaa voidaan katsoa korkeammalta tasolta, jolloin pääohjelma koostuu joukosta itse määriteltyjen, hyvin nimettyjen metodien kutsuja. Metodit taas tarkentavat ohjelman toiminnallisuutta ja ehkä toteuttavat oman toiminnallisuutensa kutsumalla joitain muita metodeja.
Metodit joiden määrittelyssä on sana static
, liittyvät metodin sisältävään luokkaan ja toimivat ns. apumetodeina. Metodit joiden määrittelyssä ei ole sanaa static
liittyvät luokasta tehtyihin ilmentymiin, eli olioihin, ja voivat muokata yksittäisten olioiden sisäistä tilaa.
Metodilla on aina näkyvyysmääre (public, näkyy kaikille, private, näkyy vain luokan sisällä), paluutyyppi (void, ei palauta arvoa) sekä metodin nimi. Seuraavassa luodaan luokkaan liittyvä metodi public static void tulosta(String merkkijono, int kertaa)
, joka tulostaa merkkijonon halutun määrän kertoja. Käytetään tällä kertaa metodia System.out.print
, joka toimii muuten samoin kuin System.out.println
, mutta ei tulosta rivinvaihtoa.
public static void tulosta(String merkkijono, int kertaa) { for (int i = 0; i < kertaa; i++) { System.out.print(merkkijono); } }
Yllä oleva metodi tulostaa parametrina annetun merkkijonon niin monta kertaa kuin parametrina annettu kokonaisluku kertoo.
Toistolausekkeisiin liittyvässä luvussa huomasimme että koodi sisälsi tähtien tulostamisen osalta ikävästi "copypastea". Metodien avulla voimme siirtää tähtien tulostamisen erillisen metodiin. Luodaan metodi public static void tulostaTahtia(int kertaa)
, joka tulostaa parametrina annetun määrän tähtiä. Metodi käyttää for
-toistolausetta while
:n sijaan.
public static void tulostaTahtia(int kertaa) { for(int i = 0; i < kertaa; i++) { System.out.print("*"); } System.out.println(); }
Metodia hyödyntäessä aiemmin kauhistusta aihettanut esimerkkimme näyttää seuraavalta.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä merkkijono: "); String merkkijono = lukija.nextLine(); tulostaTahtia(merkkijono.length()); System.out.println(merkkijono); tulostaTahtia(merkkijono.length());
Metodit toimivat ohjelman abstrahoimisessa tiettyyn pisteeseen asti, mutta ohjelmien kasvaessa suuremmiksi, ohjelmia halutaan pilkkoa vielä pienempiin ja loogisempiin kokonaisuuksiin. Luokkien avulla voimme määritellä ohjelmaan korkeamman tason käsitteitä ja käsitteisiin liittyviä toiminnallisuuksia. Jokainen Java-ohjelma vaatii toimiakseen luokan, eli yllä rakennettu Hei maailma!
-esimerkki ei toimisi ilman luokkamäärittelyä. Luokka määritellään avainsanoilla public class LuokanNimi
.
public class HeiMaailma { public static void main(String[] args) { System.out.println("Hei maailma!"); } }
Luokkia siis käytetään ohjelmassa esiintyvien käsitteiden ja niihin liittyvien toiminnallisuuksien määrittelyyn. Luokista voidaan luoda olioita, jotka ovat luokan ilmentymiä. Jokaisella tiettyyn luokkaan liittyvällä oliolla on sama rakenne, mutta oliohin liittyvien muuttujien arvot voivat olla erilaiset. Olioiden metodit käsittelevät olioiden tilaa, eli sisäisiä muuttujia.
Tutkitaan alla olevaa luokkaa Kirja
, jolla on oliomuuttujat nimi (merkkijono) ja julkaisuvuosi (kokonaisluku).
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } }
Alussa oleva määritelmä public class Kirja
kertoo luokan nimen. Tätä seuraa oliomuuttujien määrittely. Oliomuuttujat ovat muuttujia, joista jokaisella luokan oliolla on oma toisista riippumaton kopionsa. On yleensä tarkoituksenmukaista piilottaa oliomuuttujat luokan käyttäjiltä, eli määritellä niille näkyvyysmääre private. Jos näkyvyysmääreeksi asetetaan public pääsee olion käyttäjä suoraan käsiksi oliomuuttujiin.
Luokasta luodaan olioita konstruktorilla. Konstruktori on metodi, joka alustaa olion (eli luo oliolle siihen liittyvät muuttujat) ja suorittaa konstruktorin sisällä olevat käskyt. Konstruktori on aina samanniminen kuin konstruktorin sisältävä luokka. Konstruktorissa public Kirja(String nimi, int julkaisuvuosi)
luodaan uusi olio luokasta Kirja
ja asetetaan siihen liittyviin muuttujiin parametrina annetut arvot.
Ylläolevalle luokalle on määritelty myös kaksi olioiden tietoja käsittelevää metodia. Metodi public String getNimi()
palauttaa käsiteltävän olion nimen. Metodi public int getJulkaisuvuosi()
palauttaa käsiteltävän olion julkaisuvuoden.
Olioita luodaan luokkaan määritellyn konstruktorin avulla. Ohjelmakoodissa konstruktoria kutsutaan new
-käskyllä, joka palauttaa viitteen uuteen olioon. Oliot ovat luokista tehtyjä ilmentymiä. Tutkitaan ohjelmaa, joka luo kaksi eri kirjaa, jonka jälkeen tulostetaan olioihin liittyvien metodien getNimi
palauttamat arvot.
Kirja jarkiJaTunteet = new Kirja("Järki ja tunteet", 1811); Kirja ylpeysJaEnnakkoluulo = new Kirja("Ylpeys ja ennakkoluulo", 1813); System.out.println(jarkiJaTunteet.getNimi()); System.out.println(ylpeysJaEnnakkoluulo.getNimi());
Järki ja tunteet Ylpeys ja ennakkoluulo
Jokaisella oliolla on siis oma sisäinen tila. Tila muodostuu olioon liittyvistä oliomuuttujista. Oliomuuttujat voivat olla sekä alkeistyyppisiä muuttujia että viitetyyppisiä muuttujia. Jos olioon liittyy viitetyyppisiä muuttujia, voi olla että muutkin oliot viittaavat samoihin olioihin! Visualisoidaan tämä pankkiesimerkillä, jossa on tilejä ja henkilöitä.
public class Tili { private String tilitunnus; private int saldoSentteina; public Tili(String tilitunnus) { this.tilitunnus = tilitunnus; this.saldoSentteina = 0; } public void pane(int summa) { this.saldoSentteina += summa; } public int getSaldoSentteina() { return this.saldoSentteina; } // .. muita tiliin liittyviä metodeja }
import java.util.ArrayList; public class Henkilo { private String nimi; private ArrayList<Tili> tilit; public Henkilo(String nimi) { this.nimi = nimi; this.tilit = new ArrayList<Tili>(); } public void lisaaTili(Tili tili) { this.tilit.add(tili); } public int rahaaYhteensa() { int yhteensa = 0; for (Tili tili: this.tilit) { yhteensa += tili.getSaldoSentteina(); } return yhteensa; } // ... muita henkilöön liittyviä metodeja }
Jokaisella Henkilo
-luokasta tehdyllä oliolla on oma nimi sekä oma lista tileistä. Luodaan seuraavaksi kaksi henkilöä ja kaksi tiliä. Toinen tileistä on vain yhden henkilön oma, toinen yhteinen.
Henkilo matti = new Henkilo("Matti"); Henkilo maija = new Henkilo("Maija"); Tili palkkatili = new Tili("NORD-LOL"); Tili kotitaloustili = new Tili("SAM-LOL"); matti.lisaaTili(palkkatili); matti.lisaaTili(kotitaloustili); maija.lisaaTili(kotitaloustili); System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa()); System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa()); System.out.println(); palkkatili.pane(150000); System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa()); System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa()); System.out.println(); kotitaloustili.pane(10000); System.out.println("Matin tileillä rahaa: " + matti.rahaaYhteensa()); System.out.println("Maijan tileillä rahaa: " + maija.rahaaYhteensa()); System.out.println();
Matin tileillä rahaa: 0 Maijan tileillä rahaa: 0 Matin tileillä rahaa: 150000 Maijan tileillä rahaa: 0 Matin tileillä rahaa: 160000 Maijan tileillä rahaa: 10000
Ensin kummankin henkilön tilit ovat tyhjiä. Kun palkkatilille, johon oliolla matti
on viite, lisätään rahaa, kasvaa Matin tileillä oleva rahamäärä. Kun kotitaloustilille lisätään rahaa, kasvaa kummankin henkilön rahamäärä. Tämä johtuu siitä että sekä Matilla että Maijalla on "oikeus" kotitaloustilille, eli kummakin omassa oliomuuttujassa tilit
on viite kotitaloustiliin. Palaamme alkeis- ja viitetyyppisten muuttujien eroon tarkemmin myöhemmin.
Ohjelmista tulee olla selkeitä niin ohjelmoijalle kuin muille ohjelmaa tarkasteleville. Selkeys saadaan aikaan sopivalla luokkarakenteella ja hyvien nimeämiskäytänteiden seuraamisella. Jokaisella luokalla tulee olla selkeä vastuu, johon liittyviä tehtäviä luokka hoitaa. Metodeja käytetään toiston vähentämiseen ja luokkien sisäisten toimintojen jäsentämiseen. Myös metodilla tulee olla selkeä vastuu eli metodien ei tule olla liian pitkiä ja liian montaa asiaa tekeviä. Liian montaa asiaa tekevät monimutkaiset metodit tuleekin pilkkoa useiksi pienemmiksi apumetodeiksi joita alkuperäinen metodi kutsuu. Hyvät ohjelmoijat ohjelmoivat koodia, jota he ja heidän työkaverinsa ymmärtävät myös viikkoja koodin kirjoittamisen jälkeenkin.
Ymmärrettävään koodiin liittyy niin kuvaava muuttujien, metodien ja luokkien nimeäminen kuin ohjelmakoodin ilmavuus ja johdonmukainen sisentäminen. Tutkitaan seuraavaa esimerkkiä käyttöliittymästä, jossa käyttäjä voi ostaa ja myydä esineitä. Vaikka esimerkissä ainoat ostettavat ja myytävät asiat ovat porkkanoita, eikä niistä pidetä kirjaa, voisi käyttöliittymää laajentaa esimerkiksi siten että sille annettaisiin myytävien tavaroiden varasto konstruktorin parametrina.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } else if (komento.equals("osta")) { String luettu = null; while(true) { System.out.print("Mitä ostetaan: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = null; while(true) { System.out.print("Mitä myydään: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Myyty!"); } } } }
Huomaamme esimerkissä heti monta ongelmakohtaa. Ensimmäinen pulma liittyy kaynnista
-metodin sisällön suuruuteen. Huomaamme että metodia voi pienentää siirtämällä muiden komentojen kuin lopeta
-komennon käsittelyn erilliseen metodiin.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } else { hoidaKomento(komento); } } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { String luettu = null; while(true) { System.out.print("Mitä ostetaan: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = null; while(true) { System.out.print("Mitä myydään: "); luettu = lukija.nextLine(); if(luettu.equals("porkkana") { break; } else { System.out.println("Ei löydy!"); } } System.out.println("Myyty!"); } } }
Metodissa hoidaKomento
on vielä toisteisuutta syötteen lukemiseen liittyen. Huomaamme että lukemisessa toistuu aina muutama merkkijono. Kun ostetaan kysytään "Mitä ostetaan: ", kun myydään kysytään "Mitä myydään: ". Molemmat lukemiskohdat odottavat merkkijonoa "porkkana" ja tulostavat "Ei löydy!" jos käyttäjä ei syötä merkkijonoa "Porkkana". Luodaan tätä varten erillinen metodi public String lueKayttajalta(String kysymys)
, joka kysyy käyttäjältä haluttua merkkijonoa. Huomaa että jos käyttöliittymässämme olisi käytössä jonkinlainen varastonhallintaolio, vertaisimme sen sisältämiä esineitä käyttäjän syötteeseen porkkanan sijasta.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } else { hoidaKomento(komento); } } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { String luettu = lueKayttajalta("Mitä ostetaan: "); System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = lueKayttajalta("Mitä myydään: "); System.out.println("Myyty!"); } } public String lueKayttajalta(String kysymys) { while(true) { System.out.print(kysymys); String luettu = lukija.nextLine(); if(luettu.equals("porkkana") { return luettu; } else { System.out.println("Ei löydy!"); } } } }
Ohjelma on nyt pilkottu sopiviksi osiksi. Huomaamme kuitenkin että kaynnista
-metodin yhteydessä on turha else
-haara. Jos ohjelman suoritus päätyy if
-haaraan, suoritetaan komento break
ja poistutaan toistolauseesta. Voimme siis poistaa else
-haaran jolloin hoidaKomento
-metodin suorittaminen on omalla rivillään. Samanlainen tilanne on myös metodissa lueKayttajalta
. Siistitään se myös samalla.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } hoidaKomento(komento); } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { String luettu = lueKayttajalta("Mitä ostetaan: "); System.out.println("Ostettu!"); } else if (komento.equals("myy")) { String luettu = lueKayttajalta("Mitä myydään: "); System.out.println("Myyty!"); } } public String lueKayttajalta(String kysymys) { while(true) { System.out.print(kysymys); String luettu = lukija.nextLine(); if(luettu.equals("porkkana") { return luettu; } System.out.println("Ei löydy!"); } } }
Yllä kuvaavaamme ohjelman pilkkomista pienempiin osiin kutsutaan refaktoroinniksi. Refaktoroinnissa ohjelman toiminta pysyy samana, mutta sisäinen rakenne muuttuu selkeämmäksi ja ylläpidettävämmäksi. Nykyinen versiomme on huomattavasti selkeämpi alkuperäiseen ohjelmaan verrattuna. Ohjelmassa on toki vieläkin parannettavaa. Esimerkiksi metodin hoidaKomento
voi pilkkoa ostamis- ja myymistoiminnallisuutta hoitaviin metodeihin.
public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { while (true) { String komento = lukija.nextLine(); if (komento.equals("lopeta")) { break; } hoidaKomento(komento); } } public void hoidaKomento(String komento) { if (komento.equals("osta")) { komentoOsta(); } else if (komento.equals("myy")) { komentoMyy(); } } public void komentoOsta() { String luettu = lueKayttajalta("Mitä ostetaan: "); System.out.println("Ostettu!"); } public void komentoMyy() { String luettu = lueKayttajalta("Mitä myydään: "); System.out.println("Myyty!"); } public String lueKayttajalta(String kysymys) { while(true) { System.out.print(kysymys); String luettu = lukija.nextLine(); if(luettu.equals("porkkana") { return luettu; } System.out.println("Ei löydy!"); } } }
Nyt ohjelma on rakenteeltaan riittävän selkeä, metodit ovat kuvaavasti nimettyjä ja jokaisella metodilla on oma pieni tehtävä, josta metodi huolehtii. Huomaa että emme lisänneet refaktoroidessa ohjelmaan uutta toiminnallisuutta, muokkasimme vain rakennetta selkeämmäksi.
Tietääksemme kukaan ei ole oppinut ohjelmoimaan luentoja kuuntelemalla. Ohjelmointitaidon kehittymisen kannalta harjoittelu ja kertaaminen on tärkeää. Ohjelmointitaitoa on verrattu niin kielten puhumiseen kuin instrumentin soittamiseen, kummassakin kehittyy vain harjoittelemalla. Oikeastaan, esimerkiksi hyvät viulistit eivät ole hyviä vain sen takia, että he ovat harjoitelleet paljon. Harjoittelua motivoi myös se, että se on hauskaa. Samaa voi sanoa ohjelmoijista.
Linus Torvaldsin sanoin "Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program.".
Tohtori Luukkainen on kirjoittanut listan jota kannattaa seurata ohjelmoidessa ja siinä kehittyessä. Seuraa listan neuvoja kunnes osaat ne unissasikin.
Olemme tähän mennessä käyttäneet kahta erilaista näkyvyyteen liittyvää avainsanaa metodien ja oliomuuttujien määrittelyssä. Avainsana public
asettaa metodit ja muuttujat kaikille näkyviksi. Esimerkiksi luokan metodit ja konstruktorit merkitään usein määreellä public, jolloin niitä voi kutsua luokan ulkopuolelta.
Avainsana private
taas piilottaa metodit ja muuttujat luokan sisälle. Metodia, jolla on määre private
, ei pysty kutsumaan luokan ulkopuolelta.
public class Kirja { private String nimi; private String sisalto; public Kirja(String nimi, String sisalto) { this.nimi = nimi; this.sisalto = sisalto; } public String getNimi() { return this.nimi; } public String getSisalto() { return this.sisalto; } // ... }
Yllä olevasta Kirja-luokasta luotujen olioiden tietoihin pääsee käsiksi vain kirjan julkisten metodien kautta. Private-määreellä merkityt oliomuuttujat ovat näkyvillä ja käsiteltävissä vain luokan sisäisessä koodissa. Jos kirjalla olisi private-määreellinen metodi, ei sitäkään voisi käyttää muualta kuin Kirja-luokan sisältä.
Sitten itse asiaan, eli harjoitteluun!
Laadi tehtavapohjan mukana tulevalle luokalle Hymiot
apumetodi private static void tulostaHymioityna(String merkkijono)
. Metodin tulee tulostaa annettu merkkijono hymiöillä ympyröitynä. Käytä hymiönä merkkijonoa :)
.
tulostaHymioityna("\\:D/");
:):):):):) :) \:D/ :) :):):):):)
Huomaa, että merkkijonoon on kirjoitettava \\ jotta saadaan tulostumaan merkki \.
Huom! Jos merkkijonon pituus on pariton, tulee ylimääräinen välilyönti lisätä annetun merkkijonon oikealle puolelle.
tulostaHymioityna("\\:D/"); tulostaHymioityna("87.");
:):):):):) :) \:D/ :) :):):):):) :):):):):) :) 87. :) :):):):):)
Kannattaa ensin miettiä montako hymiötä minkäkin pituiselle merkkijonolle tulee tulostaa. Merkkijonon pituuden saa selville siihen liittyvällä length
-metodilla. Ala- ja ylärivin hymiöiden tulostamiseen auttaa toistolause, keskimmäisellä rivillä selviät normaalilla tulostuskomennolla. Pituuden parittomuuden voit tarkistaa jakojäännöksen avulla merkkijono.length() % 2 == 1
.
Tässä tehtävässä luodaan merkkijonomuuntaja, joka koostuu kahdesta luokasta. Luokka Muunnos muuttaa yksittäiset merkit toiseksi, Muuntaja sisältää joukon Muunnoksia ja muuttaa merkkijonoja sisältämiensä Muunnos-olioiden avulla.
Luo luokka Muunnos
, jolla on seuraavat toiminnot:
public Muunnos(char muunnettava, char muunnettu)
luo olion joka tekee muunnoksia merkiltä muunnettava
merkille muunnettu
public String muunna(String merkkijono)
palauttaa muunnetun version annetusta merkkijonostaLuokkaa käytetään seuraavalla tavalla:
String sana = "porkkana"; Muunnos muunnos1 = new Muunnos('a', 'b'); sana = muunnos1.muunna(sana); System.out.println(sana); Muunnos muunnos2 = new Muunnos('k', 'x'); sana = muunnos2.muunna(sana); System.out.println(sana);
Yllä oleva esimerkki tulostaisi:
porkkbnb porxxbnb
Vihje: voit tehdä merkkien korvaamisen kahdella tavalla, joko suoraan hyödyntäen luokan String
metodia (etsi metodi itse!) tai käymällä merkkijonon läpi merkki merkiltä ja samalla muodostaen muunnetun merkkijonon.
Jos et käytä Stringin valmista metodia, niin kannattaa muistaa, että vaikka merkkijonoja vertaillaankin komennolla equals
yksittäisiä merkkejä vertaillaan operaattorin == avulla:
String sana = "porkkana"; String korvattuAat = ""; for ( int i=0; i < sana.length(); i++) { char merkki = sana.charAt(i); if ( merkki == 'a' ) { korvattuAat += '*' } else { korvattuAat += merkki; } } System.out.println(korvattuAat); // tulostu porkk*n*
Luo luokka Muuntaja
, jolla on seuraavat toiminnot:
public Muuntaja()
luo uuden muuntajanpublic void lisaaMuunnos(Muunnos muunnos)
lisää uuden muunnoksen muuntajaanpublic String muunna(String merkkijono)
suorittaa merkkijonolle kaikki lisätyt muunnokset niiden lisäysjärjestyksessä ja palauttaa muunnetun merkkijononLuokkaa käytetään seuraavalla tavalla:
Muuntaja skanditPois = new Muuntaja(); skanditPois.lisaaMuunnos(new Muunnos('ä', 'a')); skanditPois.lisaaMuunnos(new Muunnos('ö', 'o')); System.out.println(skanditPois.muunna("ääliö älä lyö, ööliä läikkyy"));
Yllä oleva esimerkki tulostaisi:
aalio ala lyo, oolia laikkyy
Vihje: Muunnokset kannattaa tallettaa Muuntajan oliomuuttujana olevalle listalle (samaan tyyliin kuin peruskurssilla talletettiin esim. pelaajat joukkueeseen, puhelinnumerot puhelinluetteloon tai kirjat kirjastoon). Muunnos suoritetaan siten, että muunnettavalle merkkijonolle suoritetaan muunnokset yksi kerrallaan seuraavan esimerkin tapaan:
ArrayList<Muunnos> muunnokset = new ArrayList<Muunnos>(); muunnokset.add( new Muunnos('a', 'b') ); muunnokset.add( new Muunnos('k', 'x') ); muunnokset.add( new Muunnos('o', 'å') ); String sana = "porkkana"; for (Muunnos muunnos : muunnokset) { sana = muunnos.muunna(sana); } System.out.println(sana); // tulostuu pårxxbnb
MUISTUTUS kun lisäät ohjelmaasi ArrayList:in, Scanner:in tai Random:in ei Java tunnista luokkaa ellet "importoi" sitä lisäämällä ohjelmatiedoston alkuun:
import java.util.ArrayList; // importoi ArrayListin import java.util.*; // importoi kaikki java.util:sissa olevat työkalut, mm. ArrayListin, Scannerin ja Randomin
Teemme tässä tehtävässä samantyylisen yksinkertaisen laskimen, joka oli jo ohjelmoinnin perusteiden viikon 1 materiaalissa. Tällä kertaa kiinnitämme kuitenkin huomiota ohjelman rakenteeseen. Erityisesti teemme main-metodista eli pääohjelmasta hyvin kevyen. Pääohjelmametodi ei tee oikeastaan mitään muuta kun käynistää ohjelman:
public class Paaohjelma { public static void main(String[] args) { Laskin laskin = new Laskin(); laskin.kaynnista(); } }
Pääohjelma siis ainoastaan luo varsinaisen sovelluslogiikan toteuttavan olion ja käynnistää sen. Tämä on oikea tyyli tehdä ohjelmia ja tulemme jatkossa usein pyrkimään tähän rakenteeseen.
Kommunikoidakseen käyttäjän kanssa laskin tarvitsee Scanner-olion. Kuten olemme huomanneet, on kokonaislukujen lukeminen scannerilla hieman työlästä. Teemme nyt erillisen luokan Lukija
joka kapseloi sisälleen Scanner-olion.
Toteuta luokka Lukija
ja lisää sille metodit
public String lueMerkkijono()
public int lueKokonaisluku()
Lukijan sisällä tulee olla oliomuuttujana Scanner-olio jota metodit käyttävät ohjelmoinnin perusteista tuttuun tyyliin. Muistathan että kokonaislukujen lukemisessa kannattaa ensin lukea koko rivi, jonka jälkeen rivi tulee muuttaa kokonaisluvuksi. Tässä on hyödyksi Integer
-luokan metodi parseInt
.
Laskin toimii seuraavan esimerkin mukaan:
komento: summa luku1: 4 luku2: 6 lukujen summa 10 komento: tulo luku1: 3 luku2: 2 lukujen tulo 6 komento: lopetus
Tee ohjelmaasi sovelluslogiikasta huolehtiva luokka Laskin
ja sille metodi public void kaynnista()
jonka sisältö on täsmälleen seuraava:
public void kaynnista() { while (true) { System.out.print("komento: "); String komento = lukija.lueMerkkijono(); if (komento.equals("lopetus")) { break; } if (komento.equals("summa")) { summa(); } else if (komento.equals("erotus")) { erotus(); } else if (komento.equals("tulo")) { tulo(); } } statistiikka(); }
Laskimellamme on operaatiot summa, erotus, tulo
.
Tee valmiiksi rungot metodeille summa
, erotus
, tulo
ja statistiikka
. Kaikkien tulee olla tyyppiä private void
, eli metodit ovat vain laskimen sisäisessä käytössä.
Lisää laskimelle oliomuuttuja jonka tyyppi on Lukija
, ja luo lukija konstruktorissa. Laskimessa ei saa olla erikseen Scanner-tyyppistä muuttujaa!
Toteuta nyt metodit summa
, erotus
ja tulo
siten, että ne toimivat yllä olevan esimerkin mukaan. Esimerkissä kysytään aina ensin komento, jonka jälkeen kysytään 2 lukua käyttäjältä, suoritetaan haluttu operaatio, ja tulostetaan operaation arvo. Huomaa, että lukujen kysyminen tapahtuu metodiensumma
, erotus
ja tulo
sisällä! Metodit käyttävät lukujen kysymiseen lukija-olioa. Metodien runko on siis seuraavanlainen:
private void summa() { System.out.print("luku1: "); int luku1 = // luetaan luku käyttäen lukija-olioa System.out.print("luku2: "); int luku2 = // luetaan luku käyttäen lukija-olioa // tulostetaan tulos ylläolevan esimerkin tapaan }
Metodissa kaynnista
olevan while
-silmukan jälkeen kutsutaan metodia statistiikka
. Metodin on tarkoitus tulostaa kuinka montaa kertaa laskin-oliolla suoritettiin joku laskutoimenpide. Esim:
komento: summa luku1: 4 luku2: 6 lukujen summa 10 komento: tulo luku1: 3 luku2: 2 lukujen tulo 6 komento: lopetus Laskuja laskettiin 2
Toteuta metodi private void statistiikka()
, ja tee statistiikan keräämiseen tarvittavat muutokset muualle Laskin-luokan koodiin.
Huom: jos ohjelmalle annetaan virheellinen komento (eli joku muu kuin summa, erotus, tulo, tai lopetus), ei laskin reagoi komentoon millään tavalla vaan jatkaa kysymällä seuraavaa komentoa. Statistiikka ei saa laskea virheellistä komentoa laskutoimenpiteeksi.
komento: integraali komento: erotus luku1: 3 luku2: 2 lukujen erotus 1 komento: lopetus Laskuja laskettiin 1
Bonustehtävä (ei testata): Syötteen lukeminen toistuu samanlaisena kaikissa kolmessa laskuoperaation suorittavassa metodissa. Posta koodistasi toisteisuus sopivan apumetodin avulla. Metodi voi palauttaa käyttäjältä luetut 2 lukua esim. taulukossa.
Java on vahvasti tyypitetty kieli, eli kaikilla sen muuttujilla on tyyppi. Muuttujien tyypit voidaan jakaa kahteen kategoriaan: alkeis- ja viitetyyppisiin muuttujiin. Kummankin kategorian tyypisillä muuttujilla on oma "lokero", joka sisältää niihin liittyvän tiedon. Alkeistyyppisillä muuttujilla lokeroon talletetaan muuttujien konkreettinen arvo, kun taas viitetyyppisten muuttujien lokero sisältää viitteen muuttujaan liittyvään konkreettiseen olioon.
Alkeistyyppisen muuttujan arvo tallennetaan muuttujaa varten luotuun lokeroon. Jokaisella alkeistyyppisellä muuttujalla on oma lokero ja oma arvo. Muuttujalle luodaan uusi lokero silloin kun se esitellään (esim. int numero;
). Lokeroon asetetaan arvo sijoitusoperaatiolla =
. Alla on esimerkki alkeistyyppisen int (kokonaisluku) -muuttujan esittelemisestä ja arvon asettamisesta samassa lausekkeessa.
int numero = 42;
Alkeistyyppisiä muuttujia ovat muun muassa int
, double
, char
, boolean
sekä harvemmin käyttämämme short
, float
, byte
ja long
. Myös void
on alkeistyyppi, mutta sillä ei ole omaa lokeroa tai arvoa. Void-tyyppiä käytetään silloin kun halutaan ilmaista että metodi ei palauta mitään arvoa.
Esitellään seuraavaksi kaksi alkeistyyppistä muuttujaa ja asetetaan niihin arvot.
int vitonen = 5; int kutonen = 6;
Yllä esiteltyjen alkeistyyppisten muuttujien nimet ovat vitonen
ja kutonen
. Muuttujaa vitonen
luodessa sitä varten luotuun lokeroon asetetaan arvo 5 (int vitonen = 5;
). Muuttujaa kutonen
luodessa sitä varten luotuun lokeroon asetetaan arvo 6 (int kutonen = 6;
). Muuttujat vitonen
ja kutonen
ovat kumpikin int
-tyyppisiä, eli kokonaislukuja.
Alkeistyyppiset muuttujat voi visualisoida laatikkoina joiden sisälle kuhunkin muuttujaan liittyvä arvo on tallennettu:
Tarkastellaan seuraavaksi alkeistyyppisten muuttujien arvojen kopioitumista.
int vitonen = 5; int kutonen = 6; vitonen = kutonen; // muuttuja vitonen sisältää nyt arvon 6, eli arvon joka oli muuttujassa kutonen kutonen = 64; // muuttuja kutonen sisältää nyt arvon 64 // muuttuja vitonen sisältää vieläkin arvon 6
Yllä esitellään muuttujat vitonen
ja kutonen
ja asetetaan niihin arvot. Tämän jälkeen muuttujan vitonen
lokeroon kopioidaan muuttujan kutonen
lokeron sisältämä arvo (vitonen = kutonen;
). Tässä vaiheessa muuttujan vitonen
lokeroon kopioituu muuttujan kutonen
sisältämä arvo. Jos muuttujan kutonen
arvoa muutetaan tämän jälkeen, ei muuttujan vitonen
sisältämä arvo muutu: muuttujan vitonen
arvo on sen omassa lokerossa eikä liity muuttujan kutonen
lokerossa olevaan arvoon millään tavalla. Lopputilanne kuvana.
Kun alkeistyyppinen muuttuja annetaan metodille parametrina, asetetaan metodin parametrille kopio annetun muuttujan lokerossa olevasta. Käytännössä myös metodin parametreillä on omat lokerot, joihin arvo kopioidaan kuten asetuslauseessa. Katsotaan seuraavaa metodia lisaaLukuun(int luku, int paljonko)
.
public int lisaaLukuun(int luku, int paljonko) { return luku + paljonko; }
Metodi lisaaLukuun
saa kaksi parametria, kokonaisluvut luku
ja paljonko
. Metodi palauttaa uuden luvun, joka on annettujen parametrien summa. Tutkitaan vielä metodin kutsumista.
int omaLuku = 10; omaLuku = lisaaLukuun(omaLuku, 15); // muuttuja omaLuku sisältää nyt arvon 25
Esimerkissä kutsutaan lisaaLukuun
-metodia muuttujalla omaLuku
ja arvolla 15
. Metodin muuttujiin luku
ja paljonko
kopioituvat arvot 10, eli muuttujan omaLuku
sisältö, ja 15. Metodi palauttaa muuttujien luku
ja paljonko
summan, eli 10 + 15 = 25
.
Huom! Edellisessä esimerkissä muuttujan omaLuku
arvo muuttuu ainoastaan koska metodin lisaaLukuun
palauttama arvo asetetaan siihen sijoituslausekkeella (omaLuku = lisaaLukuun(omaLuku, 15);
). Jos metodin lisaaLukuun
kutsu olisi seuraavanlainen, ei muuttujan omaLuku
arvo muutu.
int omaLuku = 10; lisaaLukuun(omaLuku, 15); // muuttuja omaLuku sisältää vieläkin arvon 10
Eri tietotyypeillä on omat minimi- ja maksimiarvonsa, eli arvot joita pienempiä tai suurempia ne eivät voi olla. Tämä johtuu Javan (ja useimpien ohjelmointikielten) sisäisestä tiedon esitysmuodosta, jossa tietotyyppien koot on ennalta määrätty.
Alla muutama Javan alkeistyyppi ja niiden minimi- ja maksimiarvot
Muuttujatyyppi | Selitys | Minimiarvo | Maksimiarvo |
---|---|---|---|
int | Kokonaisluku | -2 147 483 648 (Integer.MIN_VALUE ) | 2 147 483 647 (Integer.MAX_VALUE ) |
long | Iso kokonaisluku | -9 223 372 036 854 775 808 (Long.MIN_VALUE ) | 9 223 372 036 854 775 807 (Long.MAX_VALUE ) |
boolean | Totuusarvo | true tai false | |
double | Liukuluku | Double.MIN_VALUE | Double.MAX_VALUE |
Pyöristysvirheet
Liukulukuja käyttäessä kannattaa muistaa että liukuluvun arvo on aina arvio oikeasta arvosta. Koska liukuluvun, kuten kaikkien muidenkin alkeistyyppien sisältämä tietomäärä on rajoitettu, voidaan huomata yllättäviäkin pyöristysvirheitä. Esimerkiksi seuraava tilanne.
double eka = 0.39; double toka = 0.35; System.out.println(eka - toka);
Esimerkki tulostaa arvon 0.040000000000000036
. Ohjelmointikielet tarjoavat usein työkalut liukulukujen tarkempaa käsittelyä varten. Esimerkiksi Javassa on luokka BigDecimal, johon voi asettaa äärettömän pitkiä liukulukuja.
Liukulukuja vertaillessa pyöristysvirheisiin varaudutaan usein vertaamalla arvojen etäisyyttä toisistaan. Esimerkiksi edellisen esimerkin muuttujia käytettäessä vertailu eka - toka == 0.04
ei tuota toivottua tulosta pyöristysvirheen takia.
double eka = 0.39; double toka = 0.35; if((eka - toka) == 0.04) { System.out.println("Vertailu onnistui!"); } else { System.out.println("Vertailu epäonnistui!"); }
Vertailu epäonnistui!
Arvon etäisyyttä jostain luvusta voi tarkastella esimerkiksi seuraavasti. Apufunktio Math.abs
palauttaa sille annetun luvun itseisarvon.
double eka = 0.39; double toka = 0.35; double etaisyys = 0.04 - (eka - toka); if(Math.abs(etaisyys) < 0.0001) { System.out.println("Vertailu onnistui!"); } else { System.out.println("Vertailu epäonnistui!"); }
Viitetyyppiset muuttujat tallentavat niihin liittyvän tiedon viitteen taakse eli "langan päähän". Viitetyyppisten muuttujien lokerossa on viite tiedon sisältävään paikkaan. Toisin kuin alkeistyyppisillä muuttujilla, viitetyyppisillä muuttujilla ei ole rajoitettua arvoaluetta, koska niiden oikea arvo tai tieto on viitteen takana. Oleellinen ero alkeistyyppisiin muuttujiin on se, että usea viitetyyppinen muuttuja voi viitata samaan olioon.
Tutkitaan kahden viitetyyppisen muuttujan luontia. Esimerkeissä käytetään seuraavaa luokkaa Laskuri:
public class Laskuri { private int arvo; public Laskuri(int alkuarvo) { // Konstruktori this.arvo = alkuarvo; } public void kasvataArvoa() { this.arvo = this.arvo + 1; } public int annaArvo() { return arvo; } }
Pääohjelma:
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6);
Esimerkissä luodaan ensin viitetyyppinen muuttuja bonusLaskuri
. Komentoa new
kutsuessa varataan tila muuttujan tietoa varten, suoritetaan new
-kutsua seuraavan konstruktorin koodi, ja palautetaan viite juuri luotuun olioon. . Palautettu viite asetetaan sijoitusoperaattorilla =
muuttujaan bonusLaskuri
. Sama tapahtuu muuttujalle nimeltä axeLaskuri
. Kuvana viitetyyppi kannattaa ajatella siten, että muuttuja sisältää "langan" tai "nuolen", jonka päässä on olio itse. Muuttuja ei siis sisällä oliota, vaan viitteen olioon liittyviin tietoihin.
Tutkitaan seuraavaksi viitetyyppisen muuttujan kopioitumista.
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6); bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite, // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6
Viitetyyppistä muuttujaa kopioitaessa (yllä bonusLaskuri = axeLaskuri;
) muuttujan viite kopioituu. Yllä muuttujan bonusLaskuri
lokeroon kopioituu muuttujan axeLaskuri
lokerossa oleva viite. Nyt kummatkin oliot viittaavat samaan paikkaan!
Jatketaan yllä olevaa esimerkkiä ja asetetaan muuttujaan axeLaskuri
uusi viite, joka osoittaa kutsulla new Laskuri(10)
luotuun olioon.
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6); bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite, // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6 axeLaskuri = new Laskuri(10); // muuttujaan axeLaskuri asetetaan uusi viite, joka osoittaa // new Laskuri(10) - kutsulla luotuun Laskuri-olioon // muuttuja bonusLaskuri sisältää vieläkin viitteen Laskuri-olioon, joka sai konstruktorissaan arvon 6
Esimerkissä tehdään käytännössä samat operaatiot kuin alkeistyyppi-kappaleessa olevassa asetusesimerkissä. Äskeisessä esimerkissä kopioimme viitetyyppisten muuttujien viitteitä, kun taas alkeistyyppisiin muuttujiin liittyvässä esimerkissä kopioimme alkeistyyppien arvoja. Kummassakin tapauksessa siis lokeron sisältö kopioidaan, alkeistyyppisten muuttujien lokero sisältää arvon, viitetyyppisten muuttujien lokero sisältää viitteen.
Edellisen esimerkin lopussa kukaan ei viittaa Laskuriolioon, joka sai arvokseen 5 sen konstruktorissa. Javassa oleva roskienkeruumekanismi käy ajallaan poistamassa tällaiset turhat oliot. Lopputilanne kuvana:
Tarkastellaan vielä kolmatta esimerkkiä, joka näyttää viite- ja alkeistyyppisten muuttujien oleellisen eron.
Laskuri bonusLaskuri = new Laskuri(5); Laskuri axeLaskuri = new Laskuri(6); bonusLaskuri = axeLaskuri; // muuttujaan bonusLaskuri kopioidaan muuttujan axeLaskuri sisältämä viite, // eli viite Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 6 axeLaskuri.kasvataArvoa(); // kasvatetaan axeLaskuri-viitteen takana olevan olion arvoa yhdellä System.out.println(bonusLaskuri.annaArvo()); System.out.println(axeLaskuri.annaArvo());
7 7
Koska asetuksen bonusLaskuri = axeLaskuri;
jälkeen bonusLaskuri
-muuttuja viittaa samaan olioon kuin axeLaskuri
-muuttuja, on kummankin laskurin arvo 7 vaikka kasvatuksia on tehty vain yksi. Tämä johtuu siitä että kummatkin laskurit viittaavat samaan olioon.
Kuvana tilanne on ehkä selkeämpi. Kutsu axeLaskuri.kasvataArvoa()
kasvattaa muuttujan axeLaskuri
viittaaman olion sisältämää muuttujan arvo
arvoa yhdellä. Koska muuttuja bonusLaskuri
viittaa samaan olioon, palauttaa kutsu bonusLaskuri.annaArvo()
saman muuttujan arvon, jota aiempi kutsu axeLaskuri.kasvataArvoa()
kasvatti.
Seuraavassa esimerkissä on kolme viitetyyppistä muuttujaa, jotka kaikki osoittavat samaan Laskuri
-olioon.
Laskuri bonus = new Laskuri(5); Laskuri ihq = bonus; Laskuri lennon = bonus;
Esimerkissä luodaan vain yksi Laskuri
-olio, mutta kaikki kolme Laskuri
-tyyppistä muuttujaa osoittavat lopussa siihen. Tällöin kaikki metodikutsut viitteille bonus
, ihq
ja lennon
muokkaavat samaa oliota. Vielä kerran: viitetyyppisiä muuttujia kopioitaessa viitteet kopioituvat. Kuvana:
Katsotaan kopioitumista vielä esimerkillä.
Laskuri bonus = new Laskuri(5); Laskuri ihq = bonus; Laskuri lennon = bonus; lennon = new Laskuri(3);
Kun muuttujan lennon
sisältö, eli viite muuttuu, se ei vaikuta muuttujien bonus
tai ihq
sisältämiin viitteisiin. Muuttujan arvoa asetettaessa muutetaan aina vain muuttujan oman lokeron sisältöä. Kuvana:
Kun viitetyyppinen muuttuja annetaan parametrina metodille, luodaan metodin parametrille kopio annetusta muuttujan viitteestä. Parametrilla on siis oma lokero, johon viite kopioidaan. Alkeistyyppisistä muuttujista poiketen kopioimme viitteen, emmekä arvoa, eli voimme muokata viitteen takana olevaa oliota myös metodin sisällä. Oletetaan että metodimme on alla esitelty public void lisaaLaskuriin(Laskuri laskuri, int paljonko)
.
public void lisaaLaskuriin(Laskuri laskuri, int paljonko) { for (int i = 0; i < paljonko; i++) { laskuri.kasvataArvoa(); } }
Metodille lisaaLaskuriin
annetaan kaksi parametria, viitetyyppinen muuttuja ja alkeistyyppinen muuttuja. Kumpaankin muuttujaan liittyvän lokeron sisältö kopioidaan metodin parametrien omiin lokeroihin. Viitetyyppiselle parametrille laskuri
kopioituu viite ja alkeistyyppiselle parametrille paljonko
kopioituu arvo. Metodi kutsuu Laskuri
-tyyppisen parametrin metodia kasvataArvoa()
paljonko
-muuttujan sisältämän arvon määrän. Tutkitaan vielä metodin kutsumista.
int kertoja = 10; Laskuri bonus = new Laskuri(10); lisaaLaskuriin(bonus, kertoja); // muuttujan bonus sisäinen arvo on nyt 20
Esimerkissä kutsutaan lisaaLaskuriin()
-metodia muuttujilla bonus
ja kertoja
. Metodin parametriin laskuri
ja paljonko
kopioituvat siis viitetyyppisen muuttujan bonus
viite, ja alkeistyyppisen muuttujan kertoja
arvo 10
. Metodi suorittaa metodissa olevalle muuttujalle laskuri
paljonko
muuttujan määrittelemän määrän kasvataArvoa()
-metodikutsuja. Tämä kuvana:
Metodissa on siis pääohjelmasta täysin erilliset muuttujat!
Viitetyyppisestä muuttujasta kopioituu metodin sisäiseen muuttujaan viite, eli metodin sisäinen muuttuja viittaa vieläkin samaan olioon. Alkeistyyppisestä muuttujasta kopioituu arvo, eli metodin sisäisellä muuttujalla on täysin oma arvonsa.
Metodi näkee saman laskurin johon muuttuja bonus
viittaa, eli metodin tekemä muutos vaikuttaa suoraan olioon. Alkeistyyppien suhteen tilanne on toinen, eli metodille tulee ainoastaan kopio muuttujan kertoja
arvosta. Metodista käsin ei siis voi muuttaa alkeistyyppisten muuttujien arvoja.
Kun metodi palauttaa viitetyyppisen muuttujan, palauttaa se viitteen muualla sijaitsevaan olioon. Metodin palauttaman viitetyyppisen muuttujan voi asettaa muuttujalle samalla tavalla kuin normaalikin asetus tapahtuu, eli yhtäsuuruusmerkin avulla. Katsotaan metodia public Laskuri luoLaskuri(int alkuarvo)
, joka luo uuden viitetyyppisen muuttujan.
public Laskuri luoLaskuri(int alkuarvo) { return new Laskuri(alkuarvo); }
Metodi luoLaskuri palauttaa metodissa luotuun olioon viittaavan viitteen uusiLaskuri
. Uusi olio luodaan aina metodia kutsuttaessa, seuraavassa esimerkissä luomme kaksi erillistä Laskuri
-tyyppistä oliota.
Laskuri bonus = luoLaskuri(10); Laskuri lennon = luoLaskuri(10);
Metodi luoLaskuri
luo aina uuden Laskuri
-tyyppisen olion. Ensimmäisessä kutsussa, eli kutsussa Laskuri bonus = luoLaskuri(10);
asetetaan metodin palauttama viite viitetyyppiseen muuttujaan bonus
. Toisessa metodikutsussa luodaan uusi viite, joka asetetaan muuttujaan lennon
. Muuttujat bonus
ja lennon
eivät sisällä samaa viitettä, sillä metodi luo aina uuden olion ja palauttaa viitteen luotuun olioon.
Kerrataan ja täsmennetään Ohjelmoinnin perusteiden luvussa 30 käsiteltyä asiaa. Staattisuudella ja ei-staattisisuudella erotetaan se, mihin muuttuja tai metodi liittyy. Staattiset metodit liittyvät aina luokkaan, kun taas ei-staattiset metodit voivat muokata olion omia muuttujia.
Static-määreen saavat metodit eivät liity olioihin vaan luokkiin. On mahdollista määritellä myös luokkakohtaisia muuttujia lisäämällä muuttujan eteen määre static
. Esimerkiksi Integer.MAX_VALUE
, Long.MIN_VALUE
ja Double.MAX_VALUE
ovat kaikki staattisia muuttujia. Staattisia muuttujia ja metodeja käytetään luokan nimen kautta, esimerkiksi LuokanNimi.muuttuja
tai LuokanNimi.metodi()
.
Luokkakirjastoksi kutsutaan luokkaa, jossa on yleiskäyttöisiä metodeja ja muuttujia. Esimerkiksi Javan Math
-luokka on luokkakirjasto. Se tarjoaa muun muassa Math.PI
-muuttujan. Omien luokkakirjastojen toteuttaminen on usein hyödyllistä. Esimerkiksi Helsingin Seudun Liikenne (HSL) voisi pitää lippujensa hintoja luokkakirjastossa, josta ne löytyisi tarvittaessa.
public class HslHinnasto { public static final double KERTALIPPU_AIKUINEN = 2.50; public static final double RAITIOVAUNULIPPU_AIKUINEN = 2.50; }
Avainsana final
muuttujan määrittelyssä kertoo ettei muuttujaan voi asettaa uutta arvoa kun se on kerran asetettu. Final-tyyppiset muuttujat ovat vakioita, ja niiden tulee sisältää aina arvo. Esimerkiksi suurimman kokonaisluvun kertova luokkamuuttuja Integer.MAX_VALUE
on vakiotyyppinen luokkamuuttuja.
Jos käytössämme on yllä esitelty luokka HslHinnasto
, voivat kaikki ohjelmat, jotka tarvitsevat kerta- tai raitiovaunulipun hintaa, päästä niihin käsiksi HslHinnasto
-luokan kautta. Seuraavassa esimerkissä esitellään luokka Ihminen
, jolla on metodi onkoRahaaKertalippuun()
, joka käyttää HslHinnasto
-luokasta löytyvää lipun hintaa.
public class Ihminen { private String nimi; private double rahat; // muut oliomuuttujat // konstruktori public boolean onkoRahaaKertalippuun() { if(this.rahat >= HslHinnasto.KERTALIPPU_AIKUINEN) { return true; } return false; } // muut luokkaan Ihminen liittyvät metodit }
Metodi public boolean onkoRahaaKertalippuun()
vertaa luokan Ihminen
oliomuuttujaa rahat
HslHinnasto
-luokan staattiseen muuttujaan KERTALIPPU_AIKUINEN
. Metodia onkoRahaaKertalippuun()
voi kutsua vain olioviitteen kautta. Esimerkiksi:
Ihminen matti = new Ihminen(); if (matti.onkoRahaaKertalippuun()) { System.out.println("Ostetaan kertalippu."); } else { System.out.println("Mennään pummilla."); }
Huomaa nimeämiskäytäntö! Kaikki vakiot eli final-määreellä varustetut muuttujat kirjoitetaan ISOLLA_JA_ALAVIIVOILLA.
Staattiset metodit toimivat vastaavasti. Esimerkiksi Luokka HslHinnasto
saattaisi kapseloida muuttujat ja antaa vain aksessorit niihin. Aksessoriksi kutsutaan metodia, jolla voi joko lukea muuttujan arvon tai sijoittaa muuttujalle uuden arvon.
public class HslHinnasto { private static final double KERTALIPPU_AIKUINEN = 2.50; private static final double RAITIOVAUNULIPPU_AIKUINEN = 2.50; public static double annaKertalipunHinta() { // Aksessori return KERTALIPPU_AIKUINEN; } public static double annaRaitiovaunulipunHinta() { // Aksessori return RAITIOVAUNULIPPU_AIKUINEN; } }
Tällöin Ihminen
-luokan toteutuksessa tulee kutsua metodia annaKertalipunHinta()
sen sijaan että kutsuttaisiin muuttujaa suoraan.
public class Ihminen { private String nimi; private double rahat; // muut oliomuuttujat // konstruktori public boolean onkoRahaaKertalippuun() { if(this.rahat >= HslHinnasto.annaKertalipunHinta()) { return true; } return false; } // muut luokkaan Ihminen liittyvät metodit }
Vaikka Java periaatteessa mahdollistaa staattisten muuttujien käytön, ei niille useinkaan ole tarvetta. Usein staattisten muuttujien käyttö aiheuttaa ongelmia ohjelman rakenteelle, sillä staattiset muuttujat toimivat pahamaineisten globaalien muuttujien tapaan. Tällä kurssilla käytämme ainoastaan vakioarvoisia eli final määreen omaavia staattisia muuttujia!
Ei-staattiset metodit ja muuttujat liittyvät olioihin. Oliomuuttujat, eli attribuutit määritellään luokan alussa. Kun olio luodaan new
-kutsulla, kaikille oliomuuttujille varataan tila olioon liittyvän viitteen päähän. Muuttujien arvot ovat oliokohtaisia, eli jokaisella oliolla on omat muuttujien arvot. Tutkitaan taas luokkaa Ihminen
, jolla on oliomuuttujat nimi
ja rahat
.
public class Ihminen { private String nimi; private double rahat; // muut tiedot }
Kun luokasta Ihminen luodaan uusi ilmentymä, alustetaan myös siihen liittyvät muuttujat. Jos viitetyyppistä muuttujaa nimi
ei alusteta, saa se arvokseen null-viitteen. Lisätään luokan Ihminen toteutukseen vielä konstruktori ja muutama metodi.
public class Ihminen { private String nimi; private double rahat; // konstruktori public Ihminen(String nimi, double rahat) { this.nimi = nimi; this.rahat = rahat; } public String getNimi() { return this.nimi; } public double getRahat() { return this.rahat; } public void lisaaRahaa(double summa) { if(summa > 0) { this.rahat += summa; } } public boolean onkoRahaaKertalippuun() { if(this.rahat >= HslHinnasto.annaKertalipunHinta()) { return true; } return false; } }
Konstruktori Ihminen(String nimi, double rahat)
luo uuden ihmisolion ja palauttaa viitteen siihen. Metodi getNimi()
palauttaa viitteen nimi
-olioon, ja getRahat()
-metodi palauttaa alkeistyyppisen muuttujan rahat
. Metodi lisaaRahaa(double summa)
lisää oliomuuttujaan rahat
parametrina annetun summan jos parametrin arvo on suurempi kuin 0.
Oliometodeja kutsutaan olion viitteen kautta. Seuraava koodiesimerkki luo uuden Ihmis-olion, lisää sille rahaa, ja lopuksi tulostaa sen nimen. Huomaa että metodikutsut ovat muotoa olionNimi.metodinNimi()
Ihminen matti = new Ihminen("Matti", 5.0); matti.lisaaRahaa(5); if (matti.onkoRahaaKertalippuun()) { System.out.println("Ostetaan kertalippu."); } else { System.out.println("Mennään pummilla."); }
Esimerkki tulostaa "Ostetaan kertalippu
".
Luokan sisäisiä ei-staattisia metodeja voi kutsua myös ilman olio-etuliitettä metodiin liittyvissä luokissa. Esimerkiksi seuraava toString()
-metodi Ihminen
luokalle, joka kutsuu olioon liittyvää metodia getNimi()
.
public class Ihminen { // aiemmin toteutetun luokan sisältö public String toString() { return this.getNimi(); } }
Metodi toString()
kutsuu siis luokan sisäistä juuri käsiteltävään olioon liittyvää getNimi()
-metodia. Etuliite this
korostaa kutsun liittyvän juuri tähän olioon.
Ei-staattiset metodit voivat kutsua myös staattisia, eli luokkakohtaisia metodeja. Toisaalta, luokkakohtaiset metodit eivät voi kutsua oliokohtaisia metodeja ilman viitettä itse olioon, sillä ilman viitettä ei päästä käsiksi olioon liittyviin tietoihin.
Metodien sisällä määriteltävät muuttujat ovat metodien suorituksessa käytettäviä apumuuttujia, eikä niitä tule sekoittaa oliomuuttujiin. Alla esimerkki metodista, jossa luodaan metodiin paikallinen muuttuja. Muuttuja indeksi
on olemassa ja käytössä vain metodin suorituksen ajan.
public class ... { ... public static void tulostaTaulukko(String[] taulukko) { int indeksi = 0; while(indeksi < taulukko.length) { System.out.println(taulukko[indeksi]); indeksi++; } } }
Metodissa tulostaTaulukko()
luodaan apumuuttuja indeksi
jota käytetään taulukon läpikäynnissä. Muuttuja indeksi
on olemassa vain metodin suorituksen ajan.
Tässä tehtäväsarjassa tehdään luokat Tavara
, Matkalaukku
ja Lastiruuma
, joiden avulla harjoitellaan olioita, jotka sisältävät toisia olioita.
Tee luokka Tavara
, josta muodostetut oliot vastaavat erilaisia tavaroita. Tallennettavat tiedot ovat tavaran nimi ja paino (kg).
Lisää luokkaan seuraavat metodit:
public String getNimi()
, joka palauttaa tavaran nimenpublic int getPaino()
, joka palauttaa tavaran painonpublic String toString()
, joka palauttaa merkkijonon muotoa "nimi (paino kg)"Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); System.out.println("Kirjan nimi: " + kirja.getNimi()); System.out.println("Kirjan paino: " + kirja.getPaino()); System.out.println("Kirja: " + kirja); System.out.println("Puhelin: " + puhelin); } }
Ohjelman tulostuksen tulisi olla seuraava:
Kirjan nimi: Aapiskukko Kirjan paino: 2 Kirja: Aapiskukko (2 kg) Puhelin: Nokia 3210 (1 kg)
Tee luokka Matkalaukku
. Matkalaukkuun liittyy tavaroita ja maksimipaino, joka määrittelee tavaroiden suurimman mahdollisen yhteispainon.
Lisää luokkaan seuraavat metodit:
public void lisaaTavara(Tavara tavara)
, joka lisää parametrina annettavan tavaran matkalaukkuun. Metodi ei palauta mitään arvoa.public String toString()
, joka palauttaa merkkijonon muotoa "x tavaraa (y kg)"Tavarat kannattaa tallentaa ArrayList
-olioon:
ArrayList<Tavara> tavarat = new ArrayList<Tavara>();
Luokan Matkalaukku
tulee valvoa, että sen sisältämien tavaroiden yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi lisättävän tavaran vuoksi, metodi lisaaTavara
ei saa lisätä uutta tavaraa laukkuun.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matkalaukku = new Matkalaukku(5); System.out.println(matkalaukku); matkalaukku.lisaaTavara(kirja); System.out.println(matkalaukku); matkalaukku.lisaaTavara(puhelin); System.out.println(matkalaukku); matkalaukku.lisaaTavara(tiiliskivi); System.out.println(matkalaukku); } }
Ohjelman tulostuksen tulisi olla seuraava:
0 tavaraa (0 kg) 1 tavaraa (2 kg) 2 tavaraa (3 kg) 2 tavaraa (3 kg)
Ilmoitukset "0 tavaraa" ja "1 tavaraa" eivät ole kovin hyvää suomea – paremmat muodot olisivat "ei tavaroita" ja "1 tavara". Tee tämä muutos luokkaan Matkalaukku
.
Nyt edellisen ohjelman tulostuksen tulisi olla seuraava:
ei tavaroita (0 kg) 1 tavara (2 kg) 2 tavaraa (3 kg) 2 tavaraa (3 kg)
Lisää luokkaan Matkalaukku
seuraavat metodit:
tulostaTavarat
, joka tulostaa kaikki matkalaukussa olevat tavaratyhteispaino
, joka palauttaa tavaroiden yhteispainonSeuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matkalaukku = new Matkalaukku(10); matkalaukku.lisaaTavara(kirja); matkalaukku.lisaaTavara(puhelin); matkalaukku.lisaaTavara(tiiliskivi); System.out.println("Matkalaukussa on seuraavat tavarat:"); matkalaukku.tulostaTavarat(); System.out.println("Yhteispaino: " + matkalaukku.yhteispaino() + " kg"); } }
Ohjelman tulostuksen tulisi olla seuraava:
Matkalaukussa on seuraavat tavarat: Aapiskukko (2 kg) Nokia 3210 (1 kg) Tiiliskivi (4 kg) Yhteispaino: 7 kg
Muokkaa myös luokkaasi siten, että käytät vain kahta oliomuuttujaa. Toinen sisältää maksimipainon, toinen on lista laukussa olevista tavaroista.
Lisää vielä luokkaan Matkalaukku
metodi raskainTavara
, joka palauttaa painoltaan suurimman tavaran. Jos yhtä raskaita tavaroita on useita, metodi voi palauttaa minkä tahansa niistä. Metodin tulee palauttaa olioviite. Jos laukku on tyhjä, palauta arvo null.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("Tiiliskivi", 4); Matkalaukku matkalaukku = new Matkalaukku(10); matkalaukku.lisaaTavara(kirja); matkalaukku.lisaaTavara(puhelin); matkalaukku.lisaaTavara(tiiliskivi); Tavara raskain = matkalaukku.raskainTavara(); System.out.println("Raskain tavara: " + raskain); } }
Ohjelman tulostuksen tulisi olla seuraava:
Raskain tavara: Tiiliskivi (4 kg)
Tee luokka Lastiruuma
, johon liittyvät seuraavat metodit:
public void lisaaMatkalaukku(Matkalaukku laukku)
, joka lisää parametrina annetun matkalaukun lastiruumaanpublic String toString()
, joka palauttaa merkkijonon muotoa "x matkalaukkua (y kg)"Tallenna matkalaukut sopivaan ArrayList
-rakenteeseen.
Luokan Lastiruuma
tulee valvoa, että sen sisältämien matkalaukkujen yhteispaino ei ylitä maksimipainoa. Jos maksimipaino ylittyisi uuden matkalaukun vuoksi, metodi lisaaMatkalaukku
ei saa lisätä uutta matkalaukkua.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matinLaukku = new Matkalaukku(10); matinLaukku.lisaaTavara(kirja); matinLaukku.lisaaTavara(puhelin); Matkalaukku pekanLaukku = new Matkalaukku(10); pekanLaukku.lisaaTavara(tiiliskivi); Lastiruuma lastiruuma = new Lastiruuma(1000); lastiruuma.lisaaMatkalaukku(matinLaukku); lastiruuma.lisaaMatkalaukku(pekanLaukku); System.out.println(lastiruuma); } }
Ohjelman tulostuksen tulisi olla seuraava:
2 matkalaukkua (7 kg)
Lisää luokkaan Lastiruuma
metodi public void tulostaTavarat()
, joka tulostaa kaikki lastiruuman matkalaukuissa olevat tavarat.
Seuraavassa on luokan käyttöesimerkki:
public class Main { public static void main(String[] args) { Tavara kirja = new Tavara("Aapiskukko", 2); Tavara puhelin = new Tavara("Nokia 3210", 1); Tavara tiiliskivi = new Tavara("tiiliskivi", 4); Matkalaukku matinLaukku = new Matkalaukku(10); matinLaukku.lisaaTavara(kirja); matinLaukku.lisaaTavara(puhelin); Matkalaukku pekanLaukku = new Matkalaukku(10); pekanLaukku.lisaaTavara(tiiliskivi); Lastiruuma lastiruuma = new Lastiruuma(1000); lastiruuma.lisaaMatkalaukku(matinLaukku); lastiruuma.lisaaMatkalaukku(pekanLaukku); System.out.println("Ruuman matkalaukuissa on seuraavat tavarat:"); lastiruuma.tulostaTavarat(); } }
Ohjelman tulostuksen tulisi olla seuraava:
Ruuman matkalaukuissa on seuraavat tavarat: Aapiskukko (2 kg) Nokia 3210 (1 kg) tiiliskivi (4 kg)
Testataan vielä, että lastiruuman toiminta on oikea eikä maksimipaino pääse ylittymään. Tee Main-luokkaan metodi public static void lisaaMatkalaukutTiiliskivilla(Lastiruuma lastiruuma)
, joka lisää parametrina annettuun lastiruumaan 100 matkalaukkua, joissa jokaisessa on yksi tiiliskivi. Tiiliskivien painot ovat 1, 2, 3, ..., 100 kg.
Ohjelman runko on seuraava:
public class Main { public static void main(String[] args) { Lastiruuma lastiruuma = new Lastiruuma(1000); lisaaMatkalaukutTiiliskivilla(lastiruuma); System.out.println(lastiruuma); } public static void lisaaMatkalaukutTiiliskivilla(Lastiruuma lastiruuma) { // 100 matkalaukun lisääminen, jokaiseen tulee tiiliskivi } }
Ohjelman tulostus on seuraava:
44 matkalaukkua (990 kg)
Hajautustaulu on yksi Javan yleishyödyllisistä tietorakenteista. Hajautustaulun ideana on laskea olioon liittyvälle avaimelle, eli yksilöivälle arvolle (esimerkiksi henkilötunnus, opiskelijanumero, puhelinnumero), indeksi hajautustaulun sisältämästä taulukosta. Indeksin avulla Avaimen muuttamista indeksiksi kutsutaan hajautukseksi, joka tarkoittaa indeksin laskemista. Hajautus tapahtuu aina tietyn hajautusfunktion avulla, joka takaa että tietyllä avaimella saadaan aina sama indeksi.
Avaimen perusteella lisääminen ja hakeminen mahdollistaa erittäin nopean hakemisen. Sen sijaan että tutkisimme taulukon alkiot järjestyksessä (pahimmassa tapauksessa joudumme käymään kaikki alkiot läpi), tai etsisimme arvoa binäärihaulla (pahimmassa tapauksessa käymme taulukon kokoon liittyvän logaritmisen määrän alkoita läpi), voimme periaatteessa katsoa tasan yhtä taulukon indeksiä ja tarkistaa onko indeksiin tallennettu arvoa vai ei.
Hajautustaulu käyttää avaimen arvon laskemiseen Object
-luokassa määriteltyä hashCode()
-metodia, jonka jokainen toteutettu luokka perii. Emme kuitenkaan tutustu hajautustaulun toteutukseen tarkemmin tällä kurssilla. Perintään palaamme noin viikolla 410.
Javan luokka HashMap
kapseloi eli piilottaa hajautustaulun toteutuksen, ja tarjoaa valmiit metodit sen käyttöön.
Hajautustaulua luodessa tarvitaan kaksi tyyppiparametria, avainmuuttujan tyyppi ja tallennettavan olion tyyppi. Seuraava esimerkki käyttää avaimena String
-tyyppistä oliota, ja tallennettavana oliona String
-tyyppistä oliota.
HashMap<String, String> numerot = new HashMap<String, String>(); numerot.put("Yksi", "Uno"); numerot.put("Kaksi", "Dos"); String kaannos = numerot.get("Yksi"); System.out.println(kaannos); System.out.println(numerot.get("Kaksi")); System.out.println(numerot.get("Kolme")); System.out.println(numerot.get("Uno"));
Uno Dos null null
Esimerkissä luodaan hajatustaulu, jonka avaimena ja tallennettavana oliona merkkijono. Hajautustauluun lisätään tietoa put()
-metodilla, joka saa parametreikseen viitteet avaimeen ja tallennettavaan olioon. Metodi get()
-palauttaa parametrina annettuun avaimeen liittyvän viitteen tai arvon null
jos avaimella ei löydy viitettä.
Hajautustaulussa tiettyä avainta vastaa aina yksi arvo. Jos jo olemassaolevalla avaimella tallennetaan uusi arvo, katoaa vanha arvo hajautustaulusta.
HashMap<String, String> numerot = new HashMap<String, String>(); numerot.put("Yksi", "Uno"); numerot.put("Kaksi", "Dos"); numerot.put("Yksi", "Ein"); String kaannos = numerot.get("Yksi"); System.out.println(kaannos); System.out.println(numerot.get("Kaksi")); System.out.println(numerot.get("Kolme")); System.out.println(numerot.get("Uno"));
Koska avain "Yksi
" asetetaan uudestaan, on esimerkin tulostus nyt seuraavanlainen.
Ein Dos null null
Luo main
-metodissa
uusi HashMap<String,String>
-olio. Tallenna tähän
HashMappiin seuraavien henkilöiden nimet ja lempinimet niin, että
nimi on avain ja lempinimi on arvo. Käytä pelkkiä pieniä kirjaimia.
Tämän jälkeen hae HashMapistä mikaelin lempinimi ja tulosta se.
Testit edellyttävät että kirjoitat nimet pienellä alkukirjaimella.
Tutkitaan hajautustaulun toimintaa seuraavaksi kirjastoesimerkin avulla. Kirjastosta voi hakea kirjoja kirjan nimen perusteella, nimi toimii siis kirjan avaimena. Jos annetulle nimelle löytyy kirja, saadaan siihen liittyvä viite ja samalla kirjan tiedot. Luodaan ensin esimerkkiluokka Kirja
, jolla on oliomuuttujina nimi ja kirjaan liittyvä sisältö.
public class Kirja { private String nimi; private String sisalto; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi, String sisalto) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; this.sisalto = sisalto; } public String getNimi() { return this.nimi; } public void setNimi(String nimi) { this.nimi = nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } public void setJulkaisuvuosi(int julkaisuvuosi) { this.julkaisuvuosi = julkaisuvuosi; } public String getSisalto() { return this.sisalto; } public void setSisalto(String sisalto) { this.sisalto = sisalto; } public String toString() { return "Nimi: " + this.nimi + " (" + this.julkaisuvuosi + ")\n" + "Sisältö: " + this.sisalto; } }
Luodaan seuraavaksi hajautustaulu, joka käyttää avaimena kirjan nimeä eli String-tyyppistä oliota, ja tallentaa viitteitä Kirja
-olioihin.
HashMap<String, Kirja> kirjahakemisto = new HashMap<String, Kirja>();
Yllä oleva hajautustaulu käyttää avaimena String
-oliota. Laajennetaan esimerkkiä siten, että kirjahakemistoon lisätään kaksi kirjaa, "Järki ja tunteet"
ja "Ylpeys ja ennakkoluulo"
.
Kirja jarkiJaTunteet = new Kirja("Järki ja tunteet", 1811, "..."); Kirja ylpeysJaEnnakkoluulo = new Kirja("Ylpeys ja ennakkoluulo", 1813, "...."); HashMap<String, Kirja> kirjahakemisto = new HashMap<String, Kirja>(); kirjahakemisto.put(jarkiJaTunteet.getNimi(), jarkiJaTunteet); kirjahakemisto.put(ylpeysJaEnnakkoluulo.getNimi(), ylpeysJaEnnakkoluulo);
Kirjahakemistosta voi hakea kirjoja kirjan nimellä. Haku kirjalla "Viisasteleva sydän"
ei tuota osumaa, jolloin hajautustaulu palauttaa null
-viitteen. Kirja "Ylpeys ja ennakkoluulo" kuitenkin löytyy.
Kirja kirja = kirjahakemisto.get("Viisasteleva sydän"); System.out.println(kirja); System.out.println(); kirja = kirjahakemisto.get("Ylpeys ja ennakkoluulo"); System.out.println(kirja);
null Nimi: Ylpeys ja ennakkoluulo (1813) Sisältö: ...
Hajautustaulu on hyödyllinen silloin kun tiedetään avain minkä perusteella halutaan hakea. Avaimet ovat aina yksilöllisiä, joten saman avaimen taakse ei voi tallettaa montaa eri oliota. Tallennettava olio voi toki olla lista tai toinen hajautustaulukko!
Yllä olevan kirjahakemiston ongelmana on se, että kirjoja haettaessa täytyy muistaa kirjan nimi merkki merkiltä oikein. Javan valmis String
-luokka tarjoaa meille välineet tähänkin. Metodi toLowerCase()
muuttaa merkkijonon kirjaimet pieniksi, ja metodi trim()
poistaa merkkijonon alusta ja lopusta tyhjät merkit (esimerkiksi välilyönnit). Tietokoneen käyttäjät usein kirjoittavat tekstin alkuun tai loppuun vahingossa välilyöntejä.
String teksti = "Ylpeys ja ennakkoluulo "; teksti = teksti.toLowerCase(); // teksti nyt "ylpeys ja ennakkoluulo " teksti = teksti.trim() // teksti nyt "ylpeys ja ennakkoluulo"
Luodaan luokka Kirjasto
, joka kapseloi kirjat sisältävän hajautustaulun ja mahdollistaa kirjoitusasusta riippumattoman kirjojen haun. Lisätään Kirjasto
-luokalle metodit lisaaKirja(Kirja kirja)
ja poistaKirja(String kirjanNimi)
. Huomaamme jo nyt että merkkijonon siistimistä tarvitsisi useammassa metodissa, joten tehdään siitä erillinen metodi private String siistiMerkkijono(String merkkijono)
.
public class Kirjasto { private HashMap<String, Kirja> hakemisto; public Kirjasto() { this.hakemisto = new HashMap<String, Kirja>(); } public void lisaaKirja(Kirja kirja) { String nimi = siistiMerkkijono(kirja.getNimi()); if(this.hakemisto.containsKey(nimi)) { System.out.println("Kirja on jo kirjastossa!"); } else { hakemisto.put(nimi, kirja); } } public void poistaKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); if(this.hakemisto.containsKey(kirjanNimi)) { this.hakemisto.remove(kirjanNimi); } else { System.out.println("Kirjaa ei löydy, ei voida poistaa!"); } } private String siistiMerkkijono(String merkkijono) { if (merkkijono == null) { return ""; } merkkijono = merkkijono.toLowerCase(); return merkkijono.trim(); } }
Toteutetaan kirjan hakutoiminnallisuus siten, että kirjaa haetaan hajautusrakenteesta sen nimellä.
public Kirja haeKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); return this.hakemisto.get(kirjanNimi); }
Yllä oleva metodi palauttaa haetun kirjan jos sellainen löytyy, muulloin null
-arvon. Voimme myös käydä kaikki hakemiston avaimet läpi yksitellen, etsien esimerkiksi alkuosaa kirjan nimestä. Tällä tavalla etsiessä menetämme kuitenkin hajautustaulun nopeusedun, sillä huonoimmassa tapauksessa joudumme käymään kaikkien kirjojen nimet läpi. Hakeminen alkuosan perusteella onnistuisi hajautustaulun keySet()
-metodin avulla. Metodi keySet()
palauttaa avaimet joukossa, jonka voi käydä läpi for-each
-toistolauseella.
public Kirja haeKirjaNimenAlkuosalla(String kirjanAlkuosa) { kirjanAlkuosa = siistiMerkkijono(kirjanAlkuosa); for (String avain: this.hakemisto.keySet()) { if (avain.startsWith(kirjanAlkuosa)) { return this.hakemisto.get(avain); } } return null; }
Jätämme yllä olevan metodin kuitenkin pois kirjastostamme. Kirjastosta puuttuu oleellisista toiminnoista vielä kirjojen listaaminen. Luodaan metodi public ArrayList<Kirja> kirjalista()
, joka palauttaa listan kirjaston kirjoista. Metodi kirjalista
hyödyntää hajautustaulun tarjoamaa values()
-metodia. Metodi values()
palauttaa kokoelman kirjaston kirjoista, jonka voi antaa parametrina ArrayList
-luokan konstruktorille.
public class Kirjasto { private HashMap<String, Kirja> hakemisto; public Kirjasto() { this.hakemisto = new HashMap<String, Kirja>(); } public Kirja haeKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); return this.hakemisto.get(kirjanNimi); } public void lisaaKirja(Kirja kirja) { String nimi = siistiMerkkijono(kirja.getNimi()); if(this.hakemisto.containsKey(nimi)) { System.out.println("Kirja on jo kirjastossa!"); } else { this.hakemisto.put(nimi, kirja); } } public void poistaKirja(String kirjanNimi) { kirjanNimi = siistiMerkkijono(kirjanNimi); if(this.hakemisto.containsKey(kirjanNimi)) { this.hakemisto.remove(kirjanNimi); } else { System.out.println("Kirjaa ei löydy, ei voida poistaa!"); } } public ArrayList<Kirja> kirjalista() { return new ArrayList<Kirja>(this.hakemisto.values()); } private String siistiMerkkijono(String merkkijono) { if (merkkijono == null) { return ""; } merkkijono = merkkijono.toLowerCase(); return merkkijono.trim(); } }
Yksi ohjelmoinnin periaatteista on ns. DRY-periaate (Don't Repeat Yourself), jolla pyritään välttämään saman koodin olemista useassa paikassa. Merkkijonon pieneksi muuttaminen ja trimmaus, eli tyhjien merkkien poisto alusta ja lopusta, olisi toistunut useasti kirjastoluokassamme ilman metodia siistiMerkkijono
. Toistuvaa koodia ei usein huomaa ennen kuin sitä on jo kirjoittanut, jolloin sitä päätyy koodiin lähes pakosti. Tässä ei kuitenkaan ole mitään pahaa. Tärkeintä on että siistit koodiasi sitä mukaa kun huomaat siistimistä vaativia tilanteita.
Huomaa että hajautustaulun avain ja tallennettava olio ovat aina viitetyyppisiä. Jos haluat käyttää alkeistyyppisiä muuttujia avaimena tai tallennettavana arvona, on niille olemassa myös viitetyyppiset vastineet. Alla on esitelty muutama.
Alkeistyyppi | Viitetyyppinen vastine |
---|---|
int | Integer |
double | Double |
char | Character |
Java oikeastaan kapseloi alkeistyyppiset muuttujat automaattisesti viitetyyppisiksi muuttujiksi tarvittaessa. Vaikka numero 1
on alkeistyyppinen muuttuja, voit käyttää sitä suoraan Integer
-tyyppisenä avaimena seuraavasti.
HashMap<Integer, String> taulu = new HashMap<Integer, String>(); taulu.put(1, "Ole!");
Alkeistyyppisten muuttujien automaattista muunnosta viitetyyppisiksi kutsutaan Javassa auto-boxingiksi, eli automaattiseksi "laatikkoon" asettamiseksi. Vastaava onnistuu myös toisinpäin. Voimme luoda metodin, joka palauttaa hajautustaulun sisältämän kokonaisluvun. Seuraavassa esimerkissä olevassa metodissa lisaaBongaus
tapahtuu automaattinen tyyppimuunnos.
public class Rekisteribongaus { private HashMap<String, Integer> bongatut; public Rekisteribongaus() { this.bongatut = new HashMap<String, Integer>(); } public void lisaaBongaus(String nimi, int numero) { this.bongatut.put(nimi, numero); } public int viimeisinBongaus(String nimi) { this.bongatut.get(nimi); } }
Vaikka hajautustaulu sisältää Integer-tyyppisiä olioita, osaa Java myös muuntaa tietyt viitetyyppiset muuttujat myös niiden alkeistyyppisiksi vastineiksi. Esimerkiksi Integer
-oliot muuttuvat tarpeen vaatiessa int
-tyyppisiksi muuttujiksi. Tässä piilee kuitenkin vaara! Jos yritämme muuttaa null-viitettä numeroksi, näemme virheen java.lang.reflect.InvocationTargetException. Kun teemme automaattista muunnosta, tulee varmistaa että muunnettava arvo ei ole null. Yllä olevassa ohjelmassa oleva viimeisinBongaus
-metodi tulee korjata esimerkiksi seuraavasti.
public int viimeisinBongaus(String nimi) { if(this.bongatut.containsKey(nimi) { return this.bongatut.get(nimi); } return 0; }
Luo luokka Velkakirja
, jolla on seuraavat toiminnot:
public Velkakirja()
luo uuden velkakirjanpublic void asetaLaina(String kenelle, double maara)
tallettaa velkakirjaan merkinnän lainasta tietylle henkilölle.public double paljonkoVelkaa(String kuka)
palauttaa velan määrän annetun henkilön nimen perusteellaLuokkaa käytetään seuraavalla tavalla:
Velkakirja matinVelkakirja = new Velkakirja(); matinVelkakirja.asetaLaina("Arto", 51.5); matinVelkakirja.asetaLaina("Mikael", 30); System.out.println(matinVelkakirja.paljonkoVelkaa("Arto")); System.out.println(matinVelkakirja.paljonkoVelkaa("Joel"));
Yllä oleva esimerkki tulostaisi:
51.5 0
Ole tarkkana tilanteessa, jossa kysytään velattoman ihmisen velkaa. Kertaa luvun 4.3 lopun esimerkki!
Huom! Velkakirjan ei tarvitse huomioida vanhoja lainoja. Kun asetat uuden velan henkilölle jolla on vanha velka, vanha velka unohtuu.
Velkakirja matinVelkakirja = new Velkakirja(); matinVelkakirja.asetaLaina("Arto", 51.5); matinVelkakirja.asetaLaina("Arto", 10.5); System.out.println(matinVelkakirja.paljonkoVelkaa("Arto"));
10.5
Tässä tehtäväsarjassa toteutetaan sanakirja, josta voi hakea suomen kielen sanoille englanninkielisiä käännöksiä. Sanakirjan tekemisessä käytetään HashMap
-tietorakennetta.
Toteuta luokka nimeltä Sanakirja
. Luokalla on aluksi seuraavat metodit:
public String kaanna(String sana)
metodi palauttaa parametrinsa käännöksen. Jos sanaa ei tunneta, palautetaan null.public void lisaa(String sana, String kaannos)
metodi lisää sanakirjaan uuden käännöksenToteuta luokka Sanakirja siten, että sen ainoa oliomuuttuja on HashMap
-tietorakenne.
Testaa sanakirjasi toimintaa:
Sanakirja sanakirja = new Sanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); sanakirja.lisaa("cembalo", "harpsichord"); System.out.println(sanakirja.kaanna("apina")); System.out.println(sanakirja.kaanna("porkkana"));
monkey null
Lisää sanakirjaan metodi public int sanojenLukumaara()
, joka palauttaa sanakirjassa olevien sanojen lukumäärän.
Sanakirja sanakirja = new Sanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); System.out.println(sanakirja.sanojenLukumaara()); sanakirja.lisaa("cembalo", "harpsichord"); System.out.println(sanakirja.sanojenLukumaara());
2 3
Lisää sanakirjaan metodi public ArrayList<String> kaannoksetListana()
joka palauttaa sanakirjan sisällön listana avain = arvo muotoisia merkkijonoja.
Sanakirja sanakirja = new Sanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); sanakirja.lisaa("cembalo", "harpsichord"); ArrayList<String> kaannokset = sanakirja.kaannoksetListana(); for(String kaannos: kaannokset) { System.out.println(kaannos); }
banaani = banana apina = monkey cembalo = harpsichord
Vihje: saat käytyä kaikki HashMapissa olevat avaimet läpi metodin keySet
avulla seuraavasti:
HashMap<String, String> sanaparit = new HashMap<String, String>(); sanaparit.put("apina", "eläin"); sanaparit.put("etelä", "ilmansuunta"); sanaparit.put("sauerkraut", "ruoka"); for ( String avain : sanaparit.keySet() ) { System.out.print( avain + " " ); } // tulostuu apina etelä sauerkraut
Harjoitellaan tässäkin tehtävässä erillisen tekstikäyttöliittymän tekemistä. Luo luokka Tekstikayttoliittyma
, jolla on seuraavat metodit
public Tekstikayttoliittyma(Scanner lukija, Sanakirja sanakirja)
public void kaynnista()
, joka käynnistää tekstikäyttöliittymän.Tekstikäyttöliittymä tallettaa konstruktorin parametrina saamansa lukijan ja sanakirjan oliomuuttujiin. Muita oliomuuttujia ei tarvita. Käyttäjän syötteen lukeminen tulee hoitaa konstruktorin parametrina saatua lukija-olioa käyttäen! Myös kaikki käännökset on talletettava konstruktorin parametrina saatuun sanakirja-olioon. Tekstikäyttöliittymä ei saa luoda olioita itse!
HUOM: vielä uudelleen edellinen, eli Tekstikäyttöliittymä ei saa luoda itse skanneria vaan sen on käytettävä parametrina saamaansa skanneria syötteiden lukemiseen!
Tekstikäyttöliittymässä tulee aluksi olla vain komento lopeta
, joka poistuu tekstikäyttöliittymästä. Jos käyttäjä syöttää jotain muuta, käyttäjälle sanotaan "Tuntematon komento".
Scanner lukija = new Scanner(System.in); Sanakirja sanakirja = new Sanakirja(); Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja); kayttoliittyma.kaynnista();
Komennot: lopeta - poistuu käyttöliittymästä Komento: apua Tuntematon komento. Komento: lopeta Hei hei!
Lisää tekstikäyttöliittymälle komennot lisaa
ja kaanna
. Komento lisaa
lisää kysyy käyttäjältä sanaparin ja lisää sen sanakirjaan. Komento kaanna
kysyy käyttäjältä sanaa ja tulostaa sen käännöksen.
Scanner lukija = new Scanner(System.in); Sanakirja sanakirja = new Sanakirja(); Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja); kayttoliittyma.kaynnista();
Komennot: lisaa - lisää sanaparin sanakirjaan kaanna - kysyy sanan ja tulostaa sen käännöksen lopeta - poistuu käyttöliittymästä Komento: lisaa Suomeksi: porkkana Käännös: carrot Komento: kaanna Anna sana: porkkana Käännös: carrot Komento: lopeta Hei hei!
Ohjelman testaaminen käsin on toivottoman työlästä. Syötteen antaminen on kuitenkin mahdollista automatisoida esimerkiksi syöttämällä Scanner-oliolle luettava merkkijono. Alla on annettu esimerkki siitä, miten yllä olevassa tehtävässä luotua ohjelmaa voi testata automaattisesti.
String syote = "kaanna\n" + "apina\n" + "kaanna\n" + "juusto\n" + "lisaa\n" + "juusto\n" + "cheese\n" + "kaanna\n" + "juusto\n" + "lopeta\n"; Scanner lukija = new Scanner(syote); Sanakirja sanakirja = new Sanakirja(); Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja); kayttoliittyma.kaynnista();
Ohjelma tulostus näyttää vain ohjelman antaman tulostuksen, ei käyttäjän tekemiä komentoja.
Komennot: lisaa - lisää sanaparin sanakirjaan kaanna - kysyy sanan ja tulostaa sen käännöksen lopeta - poistuu käyttöliittymästä Komento: Anna sana: Tuntematon sana! Komento: Anna sana: Tuntematon sana! Komento: Suomeksi: Käännös: Komento: Anna sana: Käännös: cheese Komento: Hei hei!
Merkkijonon antaminen Scanner-luokalle korvaa näppäimistöltä luettavan syötteen merkkijonolla. Merkkijonomuuttujan syote
sisältö siis "simuloi" käyttäjän antamaa syötettä. Rivinvaihto syötteeseen merkitään \n
:llä. Jokainen yksittäinen rivinvaihtomerkkiin loppuva osa syote
-merkkijonossa siis vastaa käyttäjän yhteen nextLine()-komentoon antamaa syötettä.
Testityötettä on helppo muuttaa, esim. seuraavassa syötetään lisää uusia sanoja sanakirjaan:
String syote = "lisaa\n" + "juusto\n" + "cheese\n" + "lisaa\n" + "olut\n" + "beer\n" + "lisaa\n" + "kirja\n" + "book\n" + "lisaa\n" + "tietokone\n" + "computer\n" + "lisaa\n" + "auto\n" + "car\n" + "lopeta\n";
Kun haluat testata ohjelmasi toimintaa jälleen käsin, vaihda Scanner-olion konstruktorin parametriksi System.in
, eli järjestelmän syötevirta.
Ohjelman toiminnan oikeellisuus pitää edelleen tarkastaa itse ruudulta. Tulostus voi olla aluksi hieman hämmentävää, sillä automatisoitu syöte ei näy ruudulla ollenkaan.
Lopullinen tavoite on automatisoida myös ohjelman tulostuksen oikeellisuden tarkastaminen niin hyvin, että ohjelman testaus ja testituloksen analysointi onnistuu "nappia painamalla". Palaamme aiheeseen myöhemmin kurssin aikana.
Kurssilla käyttämämme Java-ohjelmointikieli koostuu kolmesta osasta. Ensimmäinen osa on ohjelmointikielen syntaksi ja semantiikka: muuttujien määrittelytapa, kontrollirakenteiden muoto, ja muuttujien ja luokkien rakenne ja niiden toimintatapa. Toinen osa on JVM, eli Java Virtual Machine, jota käytetään ohjelmien suorittamiseen. Java-ohjelmat käännetään tavukoodiksi, jota voidaan suorittaa missä tahansa koneessa olevan JVM:n avulla. Emme oikeastaan ole törmänneet ohjelmien kääntämiseen, sillä ohjelmointiympäristöt tekevät sen ohjelmoijien puolesta. Silloin tällöin ohjelmointiympäristö ei toimi odotetulla tavalla, ja saatamme joutua valitsemaan clean & build, joka poistaa vanhat lähdekoodit ja kääntää ohjelman uudestaan. Kolmantena osana on API (Application Programming Interface), eli ohjelmointirajapinta tai standardikirjasto.
API on ohjelmointikielen tarjoama joukko valmiita luokkia, joita ohjelmoija voi käyttää omissa projekteissaan. Esimerkiksi luokat ArrayList
, Arrays
, Collections
, ja String
ovat kaikki osa Javan valmista APIa. Javan version 7 API-kuvaus löytyy osoitteesta http://docs.oracle.com/javase/7/docs/api/. Osoitteessa olevan sivuston vasemmassa laidassa on Javan valmiille luokille luokkakuvaus. Etsiessäsi sivulta luokkaa ArrayList
, löydät sivun http://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html, joka kuvaa ArrayList
-luokan rakenteen, konstruktorit, ja metodit.
NetBeans osaa näyttää luokkaan liittyvän APIn tarvittaessa. Kun kirjoitat luokan nimen ja lisäät siihen liittyvän import-lauseen, voit klikata luokan nimeä oikealla hiirennapilla ja valita Show Javadoc. Tämä avaa luokkaan liittyvän API-kuvauksen selaimessa.
Jokaisella viikolla on yksi laajempi tehtävä, jossa pääset vapaasti suunnittelemaan ohjelman rakenteen, käyttöliittymän ulkomuoto ja vaaditut komennot on määritelty ennalta. Ohjelmoinnin jatkokurssin ensimmäinen vapaasti suunniteltava tehtävä on Lentokenttä.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Lentokenttä-tehtävässä toteutetaan lentokentän hallintasovellus. Lentokentän hallintasovelluksessa hallinnoidaan lentokoneita ja lentoja. Lentokoneista tiedetään aina tunnus ja kapasiteetti. Lennoista tiedetään lennon lentokone, lähtöpaikan tunnus (esim. HEL) ja kohdepaikan tunnus (esim. BAL).
Sekä lentokoneita että lentoja voi olla useita. Sama lentokone voi myös lentää useaa eri lentoa (useaa eri reittiä). Sovelluksen tulee toimia kahdessa vaiheessa. Ensin lentokentän työntekijä syöttää lentokoneiden ja lentojen tietoja hallintakäyttöliittymässä.
Kun käyttäjä poistuu hallintakäyttöliittymässä, avautuu käyttäjälle mahdollisuus lentopalvelun käyttöön. Lentopalvelussa on kolme toimintoa; lentokoneiden tulostaminen, lentojen tulostaminen, ja lentokoneen tietojen tulostaminen. Tämän lisäksi käyttäjä voi poistua ohjelmasta valitsemalla vaihtoehdon x
. Jos käyttäjä syöttää epäkelvon komennon, kysytään komentoa uudestaan.
Lentokentän hallinta -------------------- Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 1 Anna lentokoneen tunnus: HA-LOL Anna lentokoneen kapasiteetti: 42 Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 1 Anna lentokoneen tunnus: G-OWAC Anna lentokoneen kapasiteetti: 101 Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 2 Anna lentokoneen tunnus: HA-LOL Anna lähtöpaikan tunnus: HEL Anna kohdepaikan tunnus: BAL Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 2 Anna lentokoneen tunnus: G-OWAC Anna lähtöpaikan tunnus: JFK Anna kohdepaikan tunnus: BAL Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > 2 Anna lentokoneen tunnus: HA-LOL Anna lähtöpaikan tunnus: BAL Anna kohdepaikan tunnus: HEL Valitse toiminto: [1] Lisää lentokone [2] Lisää lento [x] Poistu hallintamoodista > x Lentopalvelu ------------ Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > 1 G-OWAC (101 henkilöä) HA-LOL (42 henkilöä) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > 2 HA-LOL (42 henkilöä) (HEL-BAL) HA-LOL (42 henkilöä) (BAL-HEL) G-OWAC (101 henkilöä) (JFK-BAL) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > 3 Mikä kone: G-OWAC G-OWAC (101 henkilöä) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > x
Huom1: Testien kannalta on oleellista että käyttöliittymä toimii täsmälleen kuten yllä kuvattu. Ohjelman tulostamat menut kannattaneekin copypasteta tästä ohjelmakoodiin. Testit eivät oleta, että ohjelmasi on varautunut epäkelpoihin syötteisiin. Tämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen.
Huom2: älä käytä luokkein nimissä skandeja, ne saattavat aiheuttaa ongelmia testeihin!
Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan.
Vielä uudelleen varoitus: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Kurssilla on jo useampaan otteeseen käytetty metodia public String toString()
olion merkkijonoesityksen muodostamiseen esim. tulostuskomennon yhteydessä. Emme ole saaneet selvyyttä miksi Java osaa käyttää kyseistä metodia. Olemattoman metodin kutsuminenhan tuottaa normaalisti virheen. Tutkitaan seuraavaa luokkaa Kirja
, jolla ei ole metodia public String toString()
, ja ohjelmaa joka yrittää tulostaa Kirja
-luokasta luodun olion System.out.println()
-komennolla.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } }
Kirja olioKirja = new Kirja("Oliokirja", 2000); System.out.println(olioKirja);
Ohjelmamme ei tulosta virheilmoitusta tai kaadu kun annamme Kirja
-luokasta tehdyn olion parametrina System.out.println
-komennolle. Näemme virheilmoituksen tai kaatumisen sijaan mielenkiintoisen tulosteen. Tuloste sisältää luokan Kirja
nimen ja epämääräisen @-merkkiä seuraavan merkkijonon. Huomaa että kutsussa System.out.println(olioKirja)
Java tekee oikeasti kutsun System.out.println(olioKirja.toString())
Selitys liittyy Javan luokkien rakenteeseen. Jokainen Javan luokka perii automaattisesti luokan Object
, joka sisältää joukon jokaiselle Javan luokalle hyödyllisiä perusmetodeja. Perintä tarkoittaa että oma luokkamme saa käyttöön perittävän luokan määrittelemiä toiminnallisuuksia ja ominaisuuksia. Luokka Object
sisältää muun muassa metodin toString
, joka periytyy luomiimme luokiin.
Object-luokassa määritelty toString
-metodin tuottama merkkijono ei yleensä ole toivomamme. Tämän takia meidän tulee korvata, eli syrjäyttää metodi omalla toteutuksellamme. Lisätään luokkaan Kirja
metodi public String toString()
, joka korvaa perityssä Object
luokassa olevan metodin toString
.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } @Override public String toString() { return this.nimi + " (" + this.julkaisuvuosi + ")"; } }
Nyt kun teemme oliosta ilmentymän ja annamme sen tulostusmetodille, näemme luokassa Kirja
olevan toString
-metodin tuottaman merkkijonon.
Kirja olioKirja = new Kirja("Oliokirja", 2000); System.out.println(olioKirja);
Oliokirja (2000)
Luokassa Kirja
olevan metodin toString
yläpuolella on annotaatio @Override
. Annotaatioilla annetaan vinkkejä sekä kääntäjälle että lukijalle siitä, miten metodeihin tulisi suhtautua. Annotaatio @Override
antaa vihjeen että annotaatiota seuraava korvaa perityssä luokassa määritellyn metodin. Jos korvattavaan metodiin ei liitetä annotaatiota,
antaa kääntäjä tilanteesa varoituksen, overriden kirjottamatta jättäminen ei kuitenkaan ole virhe.
Luokasta Object
peritään muitakin hyödyllisiä metodeja. Tutustutaan seuraavaksi metodeihin equals
ja hashCode
.
Metodia equals
käytetään kahden olion yhtäsuuruusvertailuun. Metodia on jo käytetty muun muassa String
-olioiden yhteydessä.
Scanner lukija = new Scanner(System.in); System.out.print("Kirjoita salasana: "); String salasana = lukija.nextLine(); if(salasana.equals("salasana")) { System.out.println("Oikein meni!"); } else { System.out.println("Pieleen meni!"); }
Kirjoita salasana: mahtiporkkana Pieleen meni!
Luokassa Object
määritelty equals
-metodi tarkistaa onko parametrina annetulla oliolla sama viite kuin oliolla johon verrataan, eli toisinsanoen oletusarvoisesti vertaillaan onko kyse kahdesta samasta oliosta. Jos viite on sama, palauttaa metodi arvon true
, muuten false
. Tämä selvenee seuraavalla esimerkillä. Luokassa Kirja
ei ole omaa equals
-metodin toteutusta, joten se käyttää Object
-luokassa olevaa toteutusta.
Kirja olioKirja = new Kirja("Oliokirja", 2000); Kirja toinenOlioKirja = olioKirja; if (olioKirja.equals(toinenOlioKirja)) { System.out.println("Kirjat olivat samat"); } else { System.out.println("Kirjat eivät olleet samat"); } // nyt luodaan saman sisältöinen olio joka kuitenkin on oma erillinen olionsa toinenOlioKirja = new Kirja("Oliokirja", 2000); if (olioKirja.equals(toinenOlioKirja)) { System.out.println("Kirjat olivat samat"); } else { System.out.println("Kirjat eivät olleet samat"); }
Tulostuu:
Kirjat olivat samat Kirjat eivät olleet samat
Vaikka Kirja
-olioiden sisäinen rakenne (eli oliomuuttujien arvot) ovat molemmissa tapauksissa täsmälleen samat, vain ensimmäinen vertailu tulostaa merkkijonon "Kirjat olivat samat
". Tämä johtuu siitä että vain ensimmäisessä tapauksessa myös viitteet ovat samat eli vertaillaan olioa itseensä. Toisessa vertailussa kyse on kahdesta eri oliosta, vaikka muuttujilla onkin samat arvot.
Merkkijonojen eli Stringien yhteydessä equals
toimii odotetulla tavalla, eli se ilmoittaa kaksi samansisältöistä merkkijonoa "equalseiksi" vaikka kyseessä olisikin kaksi erillistä olioa. String-luokassa onkin korvattu oletusarvoinen equals
omalla toteutuksella.
Haluamme että kirjojen vertailu onnistuu myös nimen ja vuoden perusteella. Korvataan Object
-luokassa oleva metodi equals
määrittelemällä sille toteutus luokkaan Kirja
. Metodin equals
tehtävänä on selvittää onko olio sama kuin metodin parametrina saatu olio. Metodi saa parametrina Object
-tyyppisen viitteen olion. Määritellään ensin metodi, jonka mielestä kaikki oliot ovat samoja.
public boolean equals(Object olio) { return true; }
Metodimme on varsin optimistinen, joten muutetaan sen toimintaa hieman. Määritellään että oliot eivät ole samoja jos parametrina saatu olio on null tai jos olioiden tyypit eivät ole samat. Olion tyypin saa (Object
-luokassa määritellyllä) metodilla getClass()
. Muussa tapauksessa oletetaan että oliot ovat samat.
public boolean equals(Object olio) { if (olio == null) { return false; } if (this.getClass() != olio.getClass()) { return false; } return true; }
Metodi equals
huomaa eron erityyppisten olioiden välillä, mutta ei vielä osaa erottaa samanlaisia olioita toisistaan. Jotta voisimme verrata nykyistä oliota ja parametrina saatua Object
-tyyppisellä parametrilla viitattua olioa, tulee Object-viitteen tyyppiä muuttaa. Viitteen tyyppiä voidaan muuttaa tyyppimuunnoksella jos ja vain jos olion tyyppi on oikeasti sellainen, mihin sitä yritetään muuttaa. Tyyppimuunnos tapahtuu antamalla asetuslauseen oikealla puolella haluttu luokka suluissa, esimerkiksi:
HaluttuTyyppi muuttuja = (HaluttuTyyppi) vanhaMuuttuja;
Voimme tehdä tyyppimuunnoksen koska tiedämme olioiden olevan samantyyppisiä, jos ne ovat erityyppisiä yllä oleva metodi getClass
palauttaa arvon false. Muunnetaan metodissa equals
saatu Object
-tyyppinen parametri Kirja
-tyyppiseksi, ja todetaan kirjojen olevan eri jos niiden julkaisuvuodet ovat eri. Muuten kirjat ovat vielä samat.
public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if(this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } return true; }
Nyt vertailumetodimme osaa erottaa eri vuosina julkaistut kirjat. Lisätään vielä tarkistus, että kirjojemme nimet ovat samat ja että oman kirjamme nimi ei ole null.
public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) { return false; } return true; }
Mahtavaa, viimeinkin toimiva vertailumetodi! Alla vielä tämänhetkinen Kirja
-luokkamme.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } @Override public String toString() { return this.nimi + " (" + this.julkaisuvuosi + ")"; } @Override public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) { return false; } return true; } }
Nyt kirjojen vertailu palauttaa true
jos kirjojen sisällöt ovat samat.
Kirja olioKirja = new Kirja("Oliokirja", 2000); Kirja toinenOlioKirja = new Kirja("Oliokirja", 2000); if (olioKirja.equals(toinenOlioKirja)) { System.out.println("Kirjat olivat samat"); } else { System.out.println("Kirjat eivät olleet samat"); }
Kirjat olivat samat
Useat Javan valmiit tietorakenteet käyttävät equals
-metodia osana sisäistä hakumekanismiaan. Esimerkiksi luokan ArrayList
contains
-metodi vertailee olioiden yhtäsuuruutta equals
-metodin avulla. Jatketaan aiemmin määrittelemämme Kirja
-luokan käyttöä seuraavassa esimerkissä. Jos emme toteuta omissa olioissamme equals
-metodia, emme voi käyttää esimerkiksi contains
-metodia. Kokeile alla olevaa koodia kahdella erilaisella Kirja
-luokalla. Toisessa on equals
-metodi, ja toisessa sitä ei ole.
ArrayList<Kirja> kirjat = new ArrayList<Kirja>(); Kirja olioKirja = new Kirja("Oliokirja", 2000); kirjat.add(olioKirja); if (kirjat.contains(olioKirja)) { System.out.println("Oliokirja löytyi."); } olioKirja = new Kirja("Oliokirja", 2000); if (!kirjat.contains(olioKirja)) { System.out.println("Oliokirjaa ei löytynyt."); }
Metodi hashCode
luo oliosta numeerisen arvon eli hajautusarvon. Numeerista arvoa tarvitaan esimerkiksi
jos olioa käytetään HashMap:in avaimena.
Olemme tähän mennessä käyttäneet HashMapin avaimina ainoastaan String- ja Integer-tyyppisiä olioita ja niillä on valmiit sopivasti toteutetut hashCode
-metodit. Luodaan esimerkki jossa näin ei ole: jatketaan kirjojen parissa ja ruvetaan pitämään kirjaa lainassa olevista kirjoista. Päätetään ratkaista kirjanpito HashMapin avulla. Avaimena toimii kirja ja kirjaan liitetty arvo on merkkijono, joka keroo lainaajan nimen:
HashMap<Kirja, String> lainaajat = new HashMap<Kirja, String>(); Kirja oliokirja = new Kirja("Oliokirja", 2000); lainaajat.put( oliokirja, "Pekka" ); lainaajat.put( new Kirja("Test Driven Development",1999), "Arto" ); System.out.println( lainaajat.get( oliokirja ) ); System.out.println( lainaajat.get( new Kirja("Oliokirja", 2000) ); System.out.println( lainaajat.get( new Kirja("Test Driven Development", 1999) );
Tulostuu:
Pekka null null
Löydämme lainaajan hakiessamme samalla oliolla, joka annettiin hajautustaulun put
-metodille avaimeksi. Täsmälleen samanlaisella kirjalla mutta eri oliolla haettaessa lainaajaa ei kuitenkaan löydy ja saamme null-viitteen. Syynä on taas Object
-luokassa oleva hashCode
-metodin oletustoteutus. Oletustoteutus luo indeksin viitteen perusteella eli samansisältöiset mutta eri oliot saavat eri tuloksen hashCode-metodista ja tämän takia olioa ei osata etsiä oikeasta paikasta HashMapia.
Jotta HashMap toimisi haluamallamme tavalla, eli palauttaisi lainaajan kun avaimeksi annetaan oikean sisältöinen olio (ei välttämässä siis sama olio kuin alkuperäinen avain), on avaimena toimivan luokan ylikirjoitettava metodin equals
lisäksi metodi hashCode
. Metodi on ylikirjoitettava siten, että se antaa saman numeerisen tuloksen kaikille samansisältöisille olioille. Myös jotkut erisisältöiset oliot saavat saada saman tuloksen hashCode-metodista, on kuitenkin HashMapin tehokkuuden kannalta oleellista, että erisisältöiset oliot saavat mahdollisimman harvoin saman tuloksen.
Olemme aiemmin käyttäneet String
-olioita menestyksekkäästi HashMapin avaimena, joten voimme päätellä että String
-luokassa on oma järkevästi toimiva hashCode
-toteutus. Delegoidaan, eli siirretään laskemisvastuu String
-oliolle.
public int hashCode() { return this.nimi.hashCode(); }
Yllä oleva ratkaisu on melko hyvä, mutta jos nimi
on null, näemme NullPointerException
-virheen. Korjataan tämä vielä määrittelemällä ehto: jos nimi
-muuttujan arvo on null, palautetaan arvo 7. Arvo 7 on "satunnaisesti" valittu alkuluku, yhtä hyvin olisi voitu valita esimerkiksi arvo 13.
public int hashCode() { if (this.nimi == null) { return 7; } return this.nimi.hashCode(); }
Parantelemme vielä hashCode
-metodia siten, että koodin laskennassa huomioidaan myös kirjan julkaisuvuosi:
public int hashCode() { if (this.nimi == null) { return 7; } return this.julkaisuvuosi + this.nimi.hashCode(); }
Sivuhuomautus: HashMapissa avaimina olevien olioiden hashCode-metodin tulos kertoo hajautusrakenteeseen talletettavien arvon paikan eli indeksin hajautustaulussa. Saatat tässä kohtaa miettiä "eikö tämä johda tilanteeseen jossa useampi olio päätyy samaan indeksiin hajautustaulussa?". Vastaus on kyllä ja ei. Vaikka metodi hashCode
antaisi kahdelle eri oliolle saman arvon, on hajautustaulut toteutettu sisäisesti siten että useampi olio voi olla samassa indeksissä. Jotta samassa indeksissä olevat oliot voi erottaa toisistaan, tulee hajautustaulun avaimina toimivina olioilla olla metodi equals
toteutettuna. Lisätietoa hajautustaulujen toteuttamisen periaatteista tulee kurssilla tietorakenteet ja algoritmit.
Luokka Kirja
nyt kokonaisuudessaan.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } @Override public String toString() { return this.nimi + " (" + this.julkaisuvuosi + ")"; } @Override public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) { return false; } return true; } public int hashCode() { if (this.nimi == null) { return 7; } return this.julkaisuvuosi + this.nimi.hashCode(); } }
Kerrataan vielä: jotta luokkaa voidaan käyttää HashMap:in avaimena, tulee sille määritellä
equals
siten, että kaikki samansisältöisenä ajatellut oliot tuottavat vertailussa tuloksen true ja erisisältöiset falsehashCode
siten, että kaikki samansisältöisenä ajatelluille olioille metodin kutsu tuottaa saman arvonLuokalle Kirja
määrittelemämme equals ja hashCode selvästi täyttävät nämä ehdot.
Nyt myös aiemmin kohtaamamme ongelma ratkeaa ja kirjojen lainaajat löytyvät:
HashMap<Kirja, String> lainaajat = new HashMap<Kirja, String>(); Kirja oliokirja = new Kirja("Oliokirja", 2000); lainaajat.put( oliokirja, "Pekka" ); lainaajat.put( new Kirja("Test Driven Development",1999), "Arto" ); System.out.println( lainaajat.get( oliokirja ) ); System.out.println( lainaajat.get( new Kirja("Oliokirja", 2000) ); System.out.println( lainaajat.get( new Kirja("Test Driven Development", 1999) );
Tulostuu:
Pekka Pekka Arto
NetBeans tarjoaa metodien equals
ja hashCode
automaattisen luonnin. Voit valita valikosta Source -> Insert Code, ja valita aukeavasta listasta equals() and hashCode(). Tämän jälkeen NetBeans kysyy oliomuuttujat joita metodeissa käytetään.
Eurooppalaiset rekisteritunnukset koostuvat kahdesta osasta: yksi tai kaksikirjaimisesta maatunnuksesta ja maakohtaisesti määrittyvästä rekisterinumerosta, joka taas koostuu numeroista ja merkeistä. Rekisterinumeroita esitetään seuraavanlaisen luokan avulla:
public class Rekisterinumero { // HUOM: oliomuuttujissa on määre final eli niiden arvoa ei voi muuttaa! private final String rekNro; private final String maa; public Rekisterinumero(String rekNro, String maa) { this.rekNro = rekNro; this.maa = maa; } public String toString(){ return maa+ " "+rekNro; } }
Rekisterinumeroja halutaan tallettaa esim. ArrayList:eille ja käyttää HashMap:in avaimina, eli kuten yllä mainittu, tulee niille toteuttaa
metodit equals
ja hashCode
, muuten ne eivät toimi halutulla tavalla.
Vihje: ota equals- ja hashCode-metodeihin mallia yllä olevasta Kirja-esimerkistä. Rekisterinumeron hashCode voidaan muodostaa esim. maatunnuksen ja rekNro:n hashCodejen summana.
Esimerkkiohjelma:
public static void main(String[] args) { Rekisterinumero rek1 = new Rekisterinumero("FI", "ABC-123"); Rekisterinumero rek2 = new Rekisterinumero("FI", "UXE-465"); Rekisterinumero rek3 = new Rekisterinumero("D", "B WQ-431"); ArrayList<Rekisterinumero> suomalaiset = new ArrayList<Rekisterinumero>(); suomalaiset.add(rek1); suomalaiset.add(rek2); Rekisterinumero uusi = new Rekisterinumero("FI", "ABC-123"); if (!suomalaiset.contains(uusi)) { suomalaiset.add(uusi); } System.out.println("suomalaiset: " + suomalaiset); // jos equals-metodia ei ole ylikirjoitettu, menee sama rekisterinumero toistamiseen listalle HashMap<Rekisterinumero, String> omistajat = new HashMap<Rekisterinumero, String>(); omistajat.put(rek1, "Arto"); omistajat.put(rek3, "Jürgen"); System.out.println("omistajat:"); System.out.println(omistajat.get(new Rekisterinumero("FI", "ABC-123"))); System.out.println(omistajat.get(new Rekisterinumero("D", "B WQ-431"))); // jos hashCode ei ole ylikirjoitettu, eivät omistajat löydy }
Jos equals ja hashCode on toteutettu oikein, tulostus on seuraavanlainen
suomalaiset: [FI ABC-123, FI UXE-465] omistajat: Arto Jürgen
Toteuta luokka Ajoneuvorekisteri
jolla on seuraavat metodit:
public boolean lisaa(Rekisterinumero rekkari, String omistaja)
lisää parametrina olevaa rekisterinumeroa vastaavalle autolle parametrina olevan omistajan, metodi palauttaa true jos omistajaa ei ollut ennestään, jos rekisterinumeroa vastaavalla autolla oli jo omistaja, metodi palauttaa false ja ei tee mitäänpublic String hae(Rekisterinumero rekkari)
palauttaa parametrina olevaa rekisterinumeroa vastaavan auton omistajan. Jos auto ei ole rekisterissä, palautetaan null
public boolean poista(Rekisterinumero rekkari)
poistaa parametrina olevaa rekisterinumeroa vastaavat tiedot, metodi palauttaa true jos tiedot poistetiin, ja false jos parametria vastaavia tietoja ei ollut rekisterissäHuom: Ajoneuvorekisterin täytyy tallettaa omistajatiedot HashMap<Rekisterinumero, String> omistajat
-tyyppiseen oliomuuttujaan!
Lisää Ajoneuvorekisteriin vielä seuraavat metodit:
public void tulostaRekisterinumerot()
tulostaa rekisterissä olevat rekisterinumerotpublic void tulostaOmistajat()
tulostaa rekisterissä olevien autojen omistajat, yhden omistajan nimeä ei saa tulostaa kuin kertaalleen vaikka omistajalla olisikin useampi autoRajapinta (engl. interface) on väline luokilta vaaditun käyttäytymisen määrittelyyn. Rajapinnat määritellään kuten normaalit Javan olevat luokat, mutta luokan alussa olevan määrittelyn "public class ...
" sijaan käytetään määrittelyä "public interface ...
". Rajapinnat määrittelevät käyttäytymisen metodien niminä ja palautusarvoina, mutta ne eivät sisällä metodien toteutusta. Näkyvyysmäärettä ei merkitä erikseen, sillä se on aina public
. Tutkitaan luettavuutta kuvaavaa rajapintaa Luettava.
public interface Luettava { String lue(); }
Rajapinta Luettava
määrittelee metodin lue()
, joka palauttaa String-tyyppisen olion. Rajapinnan toteuttavat luokat päättävät miten rajapinnassa määritellyt metodit lopulta toteutetaan. Luokka toteuttaa rajapinnan lisäämällä luokan nimen jälkeen avainsanalla implements, jota seuraa rajapinnan nimi. Luodaan luokka Tekstiviesti
, joka toteuttaa rajapinnan Luettava
.
public class Tekstiviesti implements Luettava { private String lahettaja; private String sisalto; public Tekstiviesti(String lahettaja, String sisalto) { this.lahettaja = lahettaja; this.sisalto = sisalto; } public String getLahettaja() { return this.lahettaja; } public String lue() { return this.sisalto; } }
Koska luokka Tekstiviesti
toteuttaa rajapinnan Luettava
(public class Tekstiviesti implements Luettava
), on luokassa Tekstiviesti
pakko olla metodin public String lue()
toteutus. Rajapinnassa määriteltyjen metodien toteutuksilla tulee aina olla näkyvyysmääre public.
Rajapinta on sopimus käyttäytymisestä. Jotta käyttäytyminen toteutuu, tulee luokan toteuttaa rajapinnan määrittelemät metodit. Rajapinnan toteuttavan luokan ohjelmoijan vastuulla on määritellä millaista käyttäytyminen on. Rajapinnan toteuttaminen tarkoittaa sopimuksen tekemistä siitä, että luokka tarjoaa kaikki rajapinnan määrittelemät toiminnot eli rajapinnan määrittelemän käyttäytymisen. Luokkaa, joka toteuttaa rajapinnan, mutta ei toteuta rajapinnan metodeja, ei voi olla olemassa.
Toteutetaan luokan Tekstiviesti
lisäksi toinen Luettava
rajapinnan toteuttava luokka. Luokka Sahkokirja
on sähköinen toteutus kirjasta, joka sisältää kirjan nimen ja sivut. Sähkökirjaa luetaan sivu kerrallaan, metodin public String lue()
kutsuminen palauttaa aina seuraavan sivun merkkijonona.
public class Sahkokirja implements Luettava { private String nimi; private ArrayList<String> sivut; private int sivunumero; public Sahkokirja(String nimi, ArrayList<String> sivut) { this.nimi = nimi; this.sivut = sivut; this.sivunumero = 0; } public String getNimi() { return this.nimi; } public int sivuja() { return this.sivut.size(); } public String lue() { String sivu = this.sivut.get(this.sivunumero); seuraavaSivu(); return sivu; } private void seuraavaSivu() { this.sivunumero = this.sivunumero + 1; if(this.sivunumero % this.sivut.size() == 0) { this.sivunumero = 0; } } }
Rajapinnan toteuttavasta luokasta voi tehdä olioita aivan kuten normaaleistakin luokista, ja niitä voidaan käyttää myös esimerkiksi ArrayList-listojen tyyppinä.
Tekstiviesti viesti = new Tekstiviesti("ope", "Huikeaa menoa!"); System.out.println(viesti.lue()); ArrayList<Tekstiviesti> tekstiviestit = new ArrayList<Tekstiviesti>(); tekstiviestit.add(new Tekstiviesti("tuntematon numero", "I hid the body.");
Huikeaa menoa!
ArrayList<String> sivut = new ArrayList<String>(); sivut.add("Pilko metodisi lyhyiksi luettaviksi kokonaisuuksiksi."); sivut.add("Erota käyttöliittymälogiikka sovelluksen logiikasta."); sivut.add("Ohjelmoi aina ensin pieni ohjelma joka ratkaisee vain osan ongelmasta."); sivut.add("Harjoittelu tekee mestarin. Keksi joku hauska oma projekti."); Sahkokirja kirja = new Sahkokirja("Vinkkejä ohjelmointiin.", sivut); for(int sivu = 0; sivu < kirja.sivuja(); sivu++) { System.out.println(kirja.lue()); }
Pilko metodisi lyhyiksi luettaviksi kokonaisuuksiksi. Erota käyttöliittymälogiikka sovelluksen logiikasta. Ohjelmoi aina ensin pieni ohjelma joka ratkaisee vain osan ongelmasta. Harjoittelu tekee mestarin. Keksi joku hauska oma projekti.
Tehtäväpohjassa on valmiina rajapinta Palvelusvelvollinen
, jossa on seuraavat toiminnot:
int getTJ()
palauttaa jäljellä olevien palveluspäivien määränvoid palvele()
vähentää yhden palveluspäivän. Palveluspäivien määrä ei saa mennä negatiiviseksi.public interface Palvelusvelvollinen { int getTJ(); void palvele(); }
Tee Palvelusvelvollinen
-rajapinnan toteuttava luokka Sivari
, jolla parametriton konstruktori. Luokalla on oliomuuttuja TJ, joka alustetaan konstruktorikutsun yhteydessä arvoon 362.
Tee Palvelusvelvollinen
-rajapinnan toteuttava luokka Asevelvollinen
, jolla on parametrillinen konstruktori, jolla määritellään palvelusaika (int tj
).
Uutta muuttujaa esitellessä esitellään aina muuttujan tyyppi. Muuttujatyyppejä on kahdenlaisia, alkeistyyppiset muuttujat (int, double, ...) ja viitetyyppiset muuttujat (kaikki oliot). Olemme tähän mennessä käyttäneet viitetyyppisten muuttujien tyyppinä olion luokkaa.
String merkkijono = "merkkijono-olio"; Tekstiviesti viesti = new Tekstiviesti("ope", "Kohta tapahtuu huikeita");
Olion tyyppi voi olla muutakin kuin sen luokka. Esimerkiksi rajapinnan Luettava
toteuttavan luokan tyyppi on lisäksi Luettava
. Esimerkiksi koska luokka Tekstiviesti
toteuttaa rajapinnan Luettava
, on sillä tyypin Tekstiviesti
lisäksi myös tyyppi Luettava
.
Tekstiviesti viesti = new Tekstiviesti("ope", "Kohta tapahtuu huikeita"); Luettava luettava = new Tekstiviesti("ope", "Tekstiviesti on Luettava!");
ArrayList<String> sivut = new ArrayList<String>(); sivut.add("Metodi voi kutsua itse itseään."); Luettava kirja = new Sahkokirja("Rekursion alkeet.", sivut); for(int sivu = 0; sivu < kirja.sivuja(); sivu++) { System.out.println(kirja.lue()); }
Koska rajapintaa voidaan käyttää tyyppinä, on mahdollista luoda rajapintaluokan tyyppisiä olioita sisältävä lista.
ArrayList<Luettava> lukulista = new ArrayList<Luettava>(); lukulista.add(new Tekstiviesti("ope", "never been programming before...")); lukulista.add(new Tekstiviesti("ope", "gonna love it i think!")); lukulista.add(new Tekstiviesti("ope", "give me something more challenging! :)")); lukulista.add(new Tekstiviesti("ope", "you think i can do it?")); lukulista.add(new Tekstiviesti("ope", "up here we send several messages each day")); for (Luettava luettava: lukulista) { System.out.println(luettava.lue()); }
Huomaa että vaikka rajapinnan Luettava
toteuttava luokka Sahkokirja
on aina rajapinnan tyyppinen, eivät kaikki Luettava
-rajapinnan toteuttavat luokat ole tyyppiä Sahkokirja
. Luokasta Sahkokirja
tehdyn olion asettaminen Luettava
-tyyppiseen muuttujaan onnistuu, mutta toiseen suuntaan asetus ei ole sallittua ilman erillistä tyyppimuunnosta.
Luettava luettava = new Tekstiviesti("ope", "Tekstiviesti on Luettava!"); // toimii Tekstiviesti viesti = luettava; // ei toimi Tekstiviesti muunnettuViesti = (Tekstiviesti) luettava; // toimii
Tyyppimuunnos onnistuu jos ja vain jos muuttuja on oikeastikin sitä tyyppiä johon sitä yritetään muuntaa. Tyyppimuunnoksen käyttöä ei yleisesti suositella, ja lähes ainut sallittu paikka sen käyttöön on equals
-metodin toteutuksessa.
Rajapintojen todelliset hyödyt tulevat esille kun niitä käytetään metodille annettavan parametrin tyyppinä. Koska rajapintaa voidaan käyttää muuttujan tyyppinä, voidaan sitä käyttää metodikutsuissa parametrin tyyppinä. Esimerkiksi seuraavan luokan Tulostin
metodi tulosta
saa parametrina Luettava
-tyyppisen muuttujan.
public class Tulostin { public void tulosta(Luettava luettava) { System.out.println(luettava.lue()); } }
Luokan Tulostin
tarjoaman metodin tulosta
huikeus piilee siinä, että sille voi antaa parametrina minkä tahansa Luettava
-rajapinnan toteuttavan luokan ilmentymän. Kutsummepa metodia millä tahansa Luettava-luokan toteuttaneen luokan oliolla, metodi osaa toimia oikein.
Tekstiviesti viesti = new Tekstiviesti("ope", "Huhhuh, tää tulostinkin osaa tulostaa näitä!"); ArrayList<String> sivut = new ArrayList<String>(); sivut.add("Lukujen {1, 3, 5} ja {2, 3, 4, 5} yhteisiä lukuja ovat {3, 5}."); Sahkokirja kirja = new Sahkokirja("Yliopistomatematiikan perusteet.", sivut); Tulostin tulostin = new Tulostin(); tulostin.tulosta(viesti); tulostin.tulosta(kirja);
Huhhuh, tää tulostinkin osaa tulostaa näitä! Lukujen {1, 3, 5} ja {2, 3, 4, 5} yhteisiä lukuja ovat {3, 5}.
Toteutetaan toinen luokka Lukulista
, johon voidaan lisätä mielenkiintoisia luettavia asioita. Luokalla on oliomuuttujana ArrayList
-luokan ilmentymä, johon luettavia asioita tallennetaan. Lukulistaan lisääminen tapahtuu lisaa
-metodilla, joka saa parametrikseen Luettava
-tyyppisen olion.
public class Lukulista { private ArrayList<Luettava> luettavat; public Lukulista() { this.luettavat = new ArrayList<Luettava>(); } public void lisaa(Luettava luettava) { this.luettavat.add(luettava); } public int luettavia() { return this.luettavat.size(); } }
Lukulistat ovat yleensä luettavia, joten toteutetaan luokalle Lukulista
rajapinta Luettava
. Lukulistan lue
-metodi lukee kaikki luettavat
-listalla olevat oliot läpi, ja lisää yksitellen niiden lue()
-metodin palauttaman merkkijonoon.
public class Lukulista implements Luettava { private ArrayList<Luettava> luettavat; public Lukulista() { this.luettavat = new ArrayList<Luettava>(); } public void lisaa(Luettava luettava) { this.luettavat.add(luettava); } public int luettavia() { return this.luettavat.size(); } public String lue() { String luettu = ""; for(Luettava luettava: this.luettavat) { luettu += luettava.lue() + "\n"; } this.luettavat.clear(); return luettu; } }
Lukulista joelinLista = new Lukulista(); joelinLista.lisaa(new Tekstiviesti("matti", "teitkö jo testit?")); joelinLista.lisaa(new Tekstiviesti("matti", "katsoitko jo palautukset?")); System.out.println("Joelilla luettavia: " + joelinLista.luettavia());
Joelilla luettavia: 2
Koska Lukulista
on tyyppiä Luettava
, voi lukulistalle lisätä Lukulista
-olioita. Alla olevassa esimerkissä Joelilla on paljon luettavaa. Onneksi Mikael tulee hätiin ja lukee viestit Joelin puolesta.
Lukulista joelinLista = new Lukulista(); for (int i = 0; i < 1000; i++) { joelinLista.lisaa(new Tekstiviesti("matti", "teitkö jo testit?")); } System.out.println("Joelilla luettavia: " + joelinLista.luettavia()); System.out.println("Delegoidaan lukeminen Mikaelille"); Lukulista mikaelinLista = new Lukulista(); mikaelinLista.lisaa(joelinLista); mikaelinLista.lue(); System.out.println(); System.out.println("Joelilla luettavia: " + joelinLista.luettavia());
Joelilla luettavia: 1000 Delegoidaan lukeminen Mikaelille Joelilla luettavia: 0
Ohjelmassa Mikaelin listalle kutsuttu lue
-metodi käy kaikki sen sisältämät Luettava
-oliot läpi, ja kutsuu niiden lue
-metodia. Kutsuttaessa lue
-metodia Mikaelin listalle käydään myös Mikaelin lukulistalla oleva Joelin lukulista läpi. Joelin lukulista käydään läpi kutsumalla sen lue
-metodia. Jokaisen lue
-metodin kutsun lopussa tyhjennetään juuri luettu lista. Eli Joelin lukulista tyhjenee kun Mikael lukee sen.
Tässä on jo hyvin paljon viitteitä, kannattaa piirtää oliot paperille ja hahmotella miten mikaelinLista
-oliolle tapahtuva metodikutsu lue
etenee!
Muuton yhteydessa tarvitaan muuttolaatikoita. Laatikoihin talletetaan erilaisia esineitä. Kaikkien laatikoihin talletettavien esineiden on toteutettava seuraava rajapinta:
public interface Talletettava { double paino(); }
Lisää rajapinta ohjelmaasi. Rajapinta lisätään melkein samalla tavalla kuin luokka, new Java class sijaan valitaan new Java interface.
Tee rajapinnan toteuttavat luokat Kirja
ja CDLevy
. Kirja saa konstruktorin parametreina kirjan kirjoittajan (String), kirjan nimen (String), ja kirjan painon (double). CD-Levyn konstruktorin parametreina annetaan artisti (String), levyn nimi (String), ja julkaisuvuosi (int). Kaikkien CD-levyjen paino on 0.1 kg.
Muista toteuttaa luokilla myös rajapinta Talletettava
. Luokkien tulee toimia seuraavasti:
public static void main(String[] args) { Kirja kirja1 = new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2); Kirja kirja2 = new Kirja("Robert Martin", "Clean Code", 1); Kirja kirja3 = new Kirja("Kent Beck", "Test Driven Development", 0.5); CDLevy cd1 = new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973); CDLevy cd2 = new CDLevy("Wigwam", "Nuclear Nightclub", 1975); CDLevy cd3 = new CDLevy("Rendezvous Park", "Closer to Being Here", 2012); System.out.println(kirja1); System.out.println(kirja2); System.out.println(kirja3); System.out.println(cd1); System.out.println(cd2); System.out.println(cd3); }
Tulostus:
Fedor Dostojevski: Rikos ja Rangaistus Robert Martin: Clean Code Kent Beck: Test Driven Development Pink Floyd: Dark Side of the Moon (1973) Wigwam: Nuclear Nightclub (1975) Rendezvous Park: Closer to Being Here (2012)
Huom! Painoa ei ilmoiteta tulostuksessa.
Tee luokka laatikko, jonka sisälle voidaan tallettaa Talletettava
-rajapinnan toteuttavia tavaroita. Laatikko saa konstruktorissaan parametrina laatikon maksimikapasiteetin kiloina. Laatikkoon ei saa lisätä enempää tavaraa kuin sen maksimikapasiteetti määrää. Laatikon sisältämien tavaroiden paino ei siis koskaan saa olla yli laatikon maksimikapasiteetin.
Seuraavassa esimerkki laatikon käytöstä:
public static void main(String[] args) { Laatikko laatikko = new Laatikko(10); laatikko.lisaa( new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2) ) ; laatikko.lisaa( new Kirja("Robert Martin", "Clean Code", 1) ); laatikko.lisaa( new Kirja("Kent Beck", "Test Driven Development", 0.7) ); laatikko.lisaa( new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973) ); laatikko.lisaa( new CDLevy("Wigwam", "Nuclear Nightclub", 1975) ); laatikko.lisaa( new CDLevy("Rendezvous Park", "Closer to Being Here", 2012) ); System.out.println( laatikko ); }
Tulostuu
Laatikko: 6 esinettä, paino yhteensä 4.0 kiloa
Huom: koska painot esitetään doubleina, saattaa laskutoimituksissa tulla pieniä pyöristysvirheitä. Tehtävässä ei tarvitse välittää niistä.
Jos teit laatikon sisälle oliomuuttujan double paino
, joka muistaa laatikossa olevien esineiden painon, korvaa se metodilla, joka laskee painon:
public class Laatikko { //... public double paino() { double paino = 0; // laske laatikkoon talletettujen tavaroiden yhteispaino return paino; } }
Kun tarvitset laatikon sisällä painoa esim. uuden tavaran lisäyksen yhteydessä, riittää siis kutsua laatikon painon laskevaa metodia.
Metodi toki voisi palauttaa myös oliomuuttujan arvon. Harjoittelemme tässä kuitenkin tilannetta, jossa oliomuuttujaa ei tarvitse eksplisiittisesti ylläpitää vaan se voidaan tarpeentullen laskea. Seuraavan tehtävän jälkeen laatikossa olevaan oliomuuttujaan talletettu painotieto ei kuitenkaan välttämättä enää toimisi. Miksi?
Rajapinnan Talletettava
toteuttaminen siis edellyttää että luokalla on metodi double paino()
. Laatikollehan lisättiin juuri tämä metodi. Laatikosta voidaan siis tehdä talletettava!
Laatikot ovat oliota joihin voidaan laittaa Talletettava
-rajapinnan toteuttavia olioita. Laatikot toteuttavat itsekin rajapinnan. Eli laatikon sisällä voi olla myös laatikoita!
Kokeile että näin varmasti on, eli tee ohjelmassasi muutama laatikko, laita laatikoihin tavaroita ja laita pienempiä laatikoita isompien laatikoiden sisään. Kokeile myös mitä tapahtuu kun laitat laatikon itsensä sisälle. Miksi näin käy?
Kuten mitä tahansa muuttujan tyyppiä, myös rajapintaa voi käyttää metodin paluuarvona. Seuraavassa Tehdas
, jota voi pyytää valmistamaan erilaisia Talletettava
-rajapinnan toteuttavia oliota. Tehdas valmistaa aluksi satunnaisesti kirjoja ja levyjä.
public class Tehdas { public Tehdas(){ // HUOM: parametritonta tyhjää konstruktoria ei ole pakko kirjoittaa, jos luokalla ei ole muita konstruktoreja // Java tekee automaattisesti tälläisissä tilanteissa luokalle oletuskonstruktorin eli parametrittoman tyhjän konstruktorin } public Talletettava valmistaUusi(){ Random arpa = new Random(); int luku = arpa.nextInt(4); if ( luku==0 ) { return new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973); } else if ( luku==1 ) { return new CDLevy("Wigwam", "Nuclear Nightclub", 1975); } else if ( luku==2 ) { return new Kirja("Robert Martin", "Clean Code", 1 ); } else { return new Kirja("Kent Beck", "Test Driven Development", 0.7); } } }
Tehdasta on mahdollista käyttää tuntematta tarkalleen mitä erityyppisiä Talletettava-rajapinnan luokkia on olemassa. Seuraavassa luokka Pakkaaja, jolta voi pyytää laatikollisen esineitä. Pakkaaja tuntee tehtaan, jota se pyytää luomaan esineet:
public class Pakkaaja { private Tehdas tehdas; public Pakkaaja(){ tehdas = new Tehdas(); } public Laatikko annaLaatikollinen() { Laatikko laatikko = new Laatikko(100); for ( int i=0; i < 10; i++ ) { Talletettava uusiTavara = tehdas.valmistaUusi(); laatikko.lisaa(uusiTavara); } return laatikko; } }
Koska pakkaaja ei tunne rajapinnan Talletettava toteuttavia luokkia, on ohjelmaan mahdollisuus lisätä uusia luokkia jotka toteuttavat rajapinnan ilman tarvetta muuttaa pakkaajaa. Seuraavassa on luotu uusi Talletettava-rajapinnan toteuttava luokka, Suklaalevy
. Tehdasta on muutettu siten, että se luo kirjojen ja cd-levyjen lisäksi suklaalevyjä. Luokka Pakkaaja
toimii muuttamatta tehtaan laajennetun version kanssa.
public class Suklaalevy implements Talletettava { // koska Javan generoima oletuskonstruktori riittää, emme tarvitse konstruktoria! public double paino(){ return 0.2; } } public class Tehdas { // koska Javan generoima oletuskonstruktori riittää, emme tarvitse konstruktoria! public Talletettava valmistaUusi(){ Random arpa = new Random(); int luku = arpa.nextInt(5); if ( luku==0 ) { return new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973); } else if ( luku==1 ) { return new CDLevy("Wigwam", "Nuclear Nightclub", 1975); } else if ( luku==2 ) { return new Kirja("Robert Martin", "Clean Code", 1 ); } else if ( luku==3 ) { return new Kirja("Kent Beck", "Test Driven Development", 0.7); } else { return new Suklaalevy(); } } }
Rajapintojen käyttö ohjelmoinnissa mahdollistaa luokkien välisten riippuvaisuuksien vähentämisen. Esimerkissämme Pakkaaja ei ole riippuvainen rajapinnan Talletettava-toteuttavista luokista vaan ainoastaan rajapinnasta. Tämä mahdollistaa rajapinnan toteuttavien luokkien lisäämisen ohjelmaan ilman tarvetta muuttaa luokkaa Pakkaaja. Myöskään pakkaajan käyttäjiin rajapinnan toteuttavien luokkien lisääminen ei vaikuta. Vähäisemmät riippuvuudet siis helpottavat ohjelman laajennettavuutta.
Javan API tarjoaa huomattavan määrän valmiita rajapintoja. Tutustutaan tässä neljään ehkä Javan eniten käytettyyn rajapintaan: List
, Map
, Set
ja Collection
.
Rajapinta List määrittelee listoihin liittyvän peruskäyttäytymisen. Koska ArrayList-luokka toteuttaa List
-rajapinnan, voi sitä käyttää myös List
-rajapinnan kautta.
List<String> merkkijonot = new ArrayList<String>(); merkkijonot.add("merkkijono-olio arraylist-oliossa!");
Kuten huomaamme List-rajapinnan Java API:sta, rajapinnan List
toteuttavia luokkia on useita. Eräs tietojenkäsittelijöille tuttu listarakenne on linkitetty lista (linked list). Linkitettyä listaa voi käyttää rajapinnan List-kautta täysin samoin kuin ArrayLististä luotua oliota.
List<String> merkkijonot = new LinkedList<String>(); merkkijonot.add("merkkijono-olio linkedlist-oliossa!");
Molemmat rajapinnan List
toteutukset toimivat käyttäjän näkökulmasta samoin. Rajapinta siis abstrahoi niiden sisäisen toiminnallisuuden. ArrayListin ja LinkedListin sisäinen rakenne on kuitenkin huomattavan erilainen. ArrayList tallentaa alkioita taulukkoon, josta tietyllä indeksillä hakeminen on nopeaa. LinkedList taas rakentaa listan, jossa jokaisessa listan alkiossa on viite seuraavan listan alkioon. Kun linkitetyssä listassa haetaan alkiota tietyllä indeksillä, tulee listaa käydä läpi alusta indeksiin asti.
Isoilla listoille voimme nähdä huomattaviakin suorituskykyeroja. Linkitetyn listan vahvuutena on se, että listaan lisääminen on aina nopeaa. ArrayListillä taas taustalla on taulukko, jota täytyy kasvattaa aina kun se täyttyy. Taulukon kasvattaminen vaatii uuden taulukon luonnin ja vanhan taulukon tietojen kopioinnin uuteen taulukkoon. Toisaalta, indeksin perusteella hakeminen on Arraylististä erittäin nopeaa, kun taas linkitetyssä listassa joudutaan käymään listan alkioita yksitellen läpi tiettyyn indeksiin pääsemiseksi. Tietorakenteiden kuten linkitetyn listan ja ArrayListin sisäisestä toteutuksesta tulee lisää tietoa kurssilla Tietorakenteet ja algoritmit.
Ohjelmointikurssilla eteen tulevissa tilanteissa kannattaa käytännössä aina valita ArrayList. Rajapintoihin ohjelmointi kuitenkin kannattaa: toteuta ohjelmasi siten, että käytät tietorakenteita rajapintojen kautta.
Rajapinta Map määrittelee hajautustauluihin liittyvän peruskäyttäytymisen. Koska HashMap-luokka toteuttaa Map
-rajapinnan, voi sitä käyttää myös Map
-rajapinnan kautta.
Map<String, String> kaannokset = new HashMap<String, String>(); kaannokset.put("gambatte", "tsemppiä"); kaannokset.put("hai", "kyllä");
Hajautustaulun avaimet saa hajautustaulusta keySet
-metodin avulla.
Map<String, String> kaannokset = new HashMap<String, String>(); kaannokset.put("gambatte", "tsemppiä"); kaannokset.put("hai", "kyllä"); for(String key: kaannokset.keySet()) { System.out.println(key + ": " + kaannokset.get(key)); }
gambatte: tsemppiä hai: kyllä
Metodi keySet
palauttaa Set
-rajapinnan toteuttavan joukon alkioita. Set
-rajapinnan toteuttavan joukon voi käydä läpi for-each -toistorakenteella. Hajautustaulusta saa talletetut arvot metodin values
-avulla. Metodi values
palauttaa Collection
rajapinnan toteuttavan joukon alkioita. Tutustutaan vielä pikaisesti Set- ja Collection-rajapintoihin.
Rajapinta Set kuvaa joukkoihin liittyvää toiminnallisuutta. Javassa joukot sisältävät aina joko 0 tai 1 kappaletta tiettyä oliota. Set-rajapinnan toteuttaa muun muassa HashSet
. Joukon alkioita pystyy käymään läpi for-each -rakenteen avulla seuraavasti.
Set<String> joukko = new HashSet<String>(); joukko.add("yksi"); joukko.add("yksi"); joukko.add("kaksi"); for (String alkio: joukko) { System.out.println(alkio); }
yksi kaksi
Huomaa että HashSet ei ota millään tavalla kantaa joukon alkioiden järjestykseen.
Rajapinta Collection kuvaa kokoelmiin liittyvää toiminnallisuutta. Javassa muun muassa listat ja joukot ovat kokoelmia -- rajapinnat List ja Set toteuttavat rajapinnan Collection. Kokoelmarajapinta tarjoaa metodit muun muassa alkioiden olemassaolon tarkistamiseen (metodi contains
) ja kokoelman koon tarkistamiseen (metodi size
). Kaikkia kokoelmarajapinnan toteuttavia luokkia voi käydä läpi for-each
-toistolausekkeella.
Luodaan vielä hajautustaulu ja käydään erikseen läpi siihen liittyvät avaimet ja arvot.
Map<String, String> kaannokset = new HashMap<String, String>(); kaannokset.put("gambatte", "tsemppiä"); kaannokset.put("hai", "kyllä"); Set<String> avaimet = kaannokset.keySet(); Collection<String> avainKokoelma = avaimet; System.out.println("Avaimet:"); for(String avain: avainKokoelma) { System.out.println(avain); } System.out.println(); System.out.println("Arvot:"); Collection<String> arvot = kaannokset.values(); for(String arvo: arvot) { System.out.println(arvo); }
Avaimet: gambatte hai Arvot: kyllä tsemppiä
Myös seuraavanlainen hajautustaulun käyttö olisi luonut saman tulostuksen.
Map<String, String> kaannokset = new HashMap<String, String>(); kaannokset.put("gambatte", "tsemppiä"); kaannokset.put("hai", "kyllä"); System.out.println("Avaimet:"); for(String avain: kaannokset.keySet()) { System.out.println(avain); } System.out.println(); System.out.println("Arvot:"); for(String arvo: kaannokset.values()) { System.out.println(arvo); }
Seuraavassa tehtävässä rakennetaan verkkokauppa ja harjoitellaan luokkien käyttämistä niiden tarjoamien rajapintojen kautta.
Teemme tehtävässä muutamia verkkokaupan hallinnointiin soveltuvia ohjelmakomponentteja.
Tee luokka Varasto jolla on seuraavat metodit:
public void lisaaTuote(String tuote, int hinta, int saldo)
lisää varastoon tuotteen jonka hinta ja varastosaldo ovat parametrina annetut luvutpublic int hinta(String tuote)
palauttaa parametrina olevan tuotteen hinnan, jos tuotetta ei ole varastossa, palauttaa metodi -99Varaston sisällä tuotteiden hinnat (ja seuraavassa kohdassa saldot) tulee tallettaa Map<String, Integer>
-tyyppiseksi määriteltyyn muuttujaan! Luotava olio voi olla tyypiltään HashMap
, muuttujan tyyppinä on kuitenkin käytettävä Map
-rajapintaa (ks. 46.4.2.)
Seuraavassa esimerkki varaston käytöstä:
Varasto varasto = new Varasto(); varasto.lisaaTuote("maito", 3, 10); varasto.lisaaTuote("kahvi", 5, 7); System.out.println("hinnat:"); System.out.println("maito: " + varasto.hinta("maito")); System.out.println("kahvi: " + varasto.hinta("kahvi")); System.out.println("sokeri: " + varasto.hinta("sokeri"));
Tulostuu:
hinnat: maito: 3 kahvi: 5 sokeri: -99
Talleta tuotteiden varastosaldot samaan tapaan Map<String, Integer>
-tyyppiseen muuttujaan kuin talletit hinnat. Täydennä varastoa seuraavilla metodeilla:
public int saldo(String tuote)
palauttaa parametrina olevan tuotteen varastosaldon.public boolean ota(String tuote)
vähentää parametrina olevan tuotteen saldoa yhdellä ja palauuttaa true jos tuotetta oli varastossa. Jos tuotetta ei ole varastossa, palauttaa metodi false, tuotteen saldo ei saa laskea alle nollan.Esimerkki varaston käytöstä:
Varasto varasto = new Varasto(); varasto.lisaaTuote("kahvi", 5, 1); System.out.println("saldot:"); System.out.println("kahvi: " + varasto.saldo("kahvi")); System.out.println("sokeri: " + varasto.saldo("sokeri")); System.out.println("otetaan kahvi " + varasto.ota("kahvi")); System.out.println("otetaan kahvi " + varasto.ota("kahvi")); System.out.println("otetaan sokeri " + varasto.ota("sokeri")); System.out.println("saldot:"); System.out.println("kahvi: " + varasto.saldo("kahvi")); System.out.println("sokeri: " + varasto.saldo("sokeri"));
Tulostuu:
saldot: kahvi: 1 sokeri: 0 otetaan kahvi true otetaan kahvi false otetaan sokeri false saldot: kahvi: 0 sokeri: 0
Listätään varastolle vielä yksi metodi:
public Set<String> tuotteet()
palauttaa joukkona varastossa olevien tuotteiden nimetMetodi on helppo toteuttaa. Saat tietoon varastossa olevat tuotteet kysymällä ne joko hinnat tai saldot muistavalta Map:iltä metodin keySet
avulla.
Esimerkki varaston käytöstä:
Varasto varasto = new Varasto(); varasto.lisaaTuote("maito", 3, 10); varasto.lisaaTuote("kahvi", 5, 6); varasto.lisaaTuote("piimä", 2, 20); varasto.lisaaTuote("jugurtti", 2, 20); System.out.println("tuotteet:"); for (String tuote : varasto.tuotteet()) { System.out.println(tuote); }
Tulostuu:
tuotteet: piimä jugurtti kahvi maito
Ostoskoriin lisätään ostoksia. Ostoksella tarkoitetaan tiettyä määrää tiettyjä tuotteita. Koriin voidaan laittaa esim. ostos joka vastaa yhtä leipää tai ostos joka vastaa 24:ää kahvia.
Tee luokka Ostos
jolla on seuraavat toiminnot:
public Ostos(String tuote, int kpl, int yksikkohinta)
konstruktori joka luo ostoksen joka vastaa parametrina annettua tuotetta. Tuotteita ostoksessa on kpl kappaletta ja yhden tuotteen hinta on kolmantena parametrina annettu yksikkohintapublic int hinta()
palauttaa ostoksen hinnan. Hinta saadaan kertomalla kappalemäärä yksikköhinnallapublic void kasvataMaaraa()
kasvattaa ostoksen kappalemäärää yhdelläpublic String toString()
palauttaa ostoksen merkkijonomuodossa, joka on alla olevan esimerkin mukainenEsimerkki ostoksen käytöstä
Ostos ostos = new Ostos("maito", 4, 2); System.out.println( "ostoksen joka sisältää 4 maitoa yhteishinta on " + ostos.hinta() ); System.out.println( ostos ); ostos.kasvataMaaraa(); System.out.println( ostos );
Tulostuu:
ostoksen joka sisältää 4 maitoa yhteishinta on 8 maito: 4 maito: 5
Huom: toString on siis muotoa tuote: kpl hintaa ei merkkijonoesitykseen tule!
Vihdoin pääsemme toteuttamaan luokan ostoskori!
Ostoskori tallettaa sisäisesti koriin lisätyt tuotteet Ostos-olioina. Ostoskorilla tulee olla oliomuuttuja jonka tyyppi on joko Map<String, Ostos>
tai List<Ostos>
. Älä laita mitään muita oliomuuttujia ostoskorille kuin ostosten talletukseen tarvittava Map tai List.
Huom: jos talletat Ostos-oliot Map-tyyppiseen apumuuttujaan, on tässä ja seuraavassa tehtävässä hyötyä Map:in metodista values(), jonka avulla on helppo käydä läpi kaikki talletetut ostos-oliot.
Tehdään aluksi ostoskorille parametriton konstruktori ja metodit:
public void lisaa(String tuote, int hinta)
lisää ostoskoriin ostoksen joka vastaa parametrina olevaa tuotetta ja jolla on parametrina annettu hinta.public int hinta()
palauttaa ostoskorin kokonaishinnanEsimerkki ostoksen käytöstä
Ostoskori kori = new Ostoskori(); kori.lisaa("maito", 3); kori.lisaa("piimä", 2); kori.lisaa("juusto", 5); System.out.println("korin hinta: " + kori.hinta()); kori.lisaa("tietokone", 899); System.out.println("korin hinta: " + kori.hinta());
Tulostuu:
korin hinta: 10 korin hinta: 909
Tehdään ostoskorille metodi public void tulosta()
joka tulostaa korin sisältämät Ostos-oliot. Tulostusjärjestyksessä ei ole merkitystä. Edellisen esimerkin ostoskori tulostetuna olisi:
piimä: 1 juusto: 1 tietokone: 1 maito: 1
Huomaa, että tulostuva numero on siis tuotteen korissa oleva kappalemäärä, ei hinta!
Täydennetään Ostoskoria siten, että jos korissa on jo tuote joka sinne lisätään, ei koriin luoda uutta Ostos-olioa vaan päivitetään jo korissa olevaa tuotetta vastaavaa ostosolioa kutsumalla sen metodia kasvataMaaraa().
Esimerkki:
Ostoskori kori = new Ostoskori(); kori.lisaa("maito", 3); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() +"\n"); kori.lisaa("piimä", 2); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() +"\n"); kori.lisaa("maito", 3); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() +"\n"); kori.lisaa("maito", 3); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() +"\n");
Tulostuu:
maito: 1 korin hinta: 3 piimä: 1 maito: 1 korin hinta: 5 piimä: 1 maito: 2 korin hinta: 8 piimä: 1 maito: 3 korin hinta: 11
Eli ensin koriin lisätään maito ja piimä ja niille omat ostos-oliot. Kun koriin lisätään lisää maitoa, ei luoda uusille maidoille omaa ostosolioa, vaan päivitetään jo korissa olevan maitoa kuvaavan ostosolion kappalemäärää.
Nyt meillä on valmiina kaikki osat "verkkokauppaa" varten. Verkkokaupassa on varasto joka sisältää kaikki tuotteet. Jokaista asiakkaan asiointia varten on oma ostoskori. Aina kun asiakas valitsee ostoksen, lisätään se asiakkaan ostoskoriin jos tuotetta on varastossa. Samalla varastosaldoa pienennetään yhdellä.
Seuraavassa on valmiina verkkokaupan koodin runko. Tee projektiin luokka Kauppa
ja kopioi alla oleva koodi luokkaan.
import java.util.Scanner; public class Kauppa { private Varasto varasto; private Scanner lukija; public Kauppa(Varasto varasto, Scanner lukija) { this.varasto = varasto; this.lukija = lukija; } // metodi jolla hoidetaan yhden asiakkaan asiointi kaupassa public void asioi(String asiakas) { Ostoskori kori = new Ostoskori(); System.out.println("Tervetuloa kauppaan " + asiakas); System.out.println("valikoimamme:"); for (String tuote : varasto.tuotteet()) { System.out.println( tuote ); } while (true) { System.out.print("mitä laitetaan ostoskoriin (pelkkä enter vie kassalle):"); String tuote = lukija.nextLine(); if (tuote.isEmpty()) { break; } // tee tänne koodi joka lisää tuotteen ostoskoriin jos sitä on varastossa // ja vähentää varastosaldoa // älä koske muuhun koodiin! } System.out.println("ostoskorissasi on:"); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta()); } }
Seuraavassa pääohjelma joka täyttää kaupan varaston ja laittaa Pekan asioimaan kaupassa:
Varasto varasto = new Varasto(); varasto.lisaaTuote("kahvi", 5, 10); varasto.lisaaTuote("maito", 3, 20); varasto.lisaaTuote("piimä", 2, 55); varasto.lisaaTuote("leipä", 7, 8); Kauppa kauppa = new Kauppa(varasto, new Scanner(System.in)); kauppa.asioi("Pekka");
Kauppa on melkein valmiina. Yhden asiakkaan asioinnin hoitavan metodin public void asioi(String asiakas)
on kommenteilla merkitty kohta jonka joudut täydentämään. Lisää kohtaan koodi joka tarkastaa onko asiakkaan haluamaa tuotetta varastossa. Jos on, vähennä tuotteen varastosaldoa ja lisää tuote ostoskoriin.
Vapise, verkkokauppa.com!
Geneerisyys (generics) liittyy olioita säilövien luokkien tapaan säilöä vapaavalintaisen tyyppisiä olioita. Vapaavalintaisuus perustuu luokkien määrittelyssä käytettyyn geneeriseen tyyppiparametriin, jonka avulla voidaan määritellä olion luontivaiheessa valittavia tyyppejä. Luokan geneerisyys määritellään antamalla luokan nimen jälkeen haluttu määrä luokan tyyppiparametreja pienempi kuin ja suurempi kuin -merkkien väliin. Toteutetaan oma geneerinen luokka Lokero
, johon voi asettaa yhden minkälaisen tahansa olion.
public class Lokero<T> { private T alkio; public void asetaArvo(T alkio) { this.alkio = alkio; } public T haeArvo() { return alkio; } }
Määrittely public class Lokero<T>
kertoo että luokalle Lokero
tulee antaa konstruktorissa tyyppiparametri. Konstruktorikutsun jälkeen kaikki olion sisäiset muuttujat tulevat olemaan kutsun yhteydessä annettua tyyppiä. Luodaan merkkijonon tallentava lokero.
Lokero<String> merkkijono = new Lokero<String>(); merkkijono.asetaArvo(":)"); System.out.println(merkkijono.haeArvo());
:)
Tyyppiparametria vaihtamalla voidaan luoda myös muuntyyppisiä olioita tallentavia Lokero
-olioita. Esimerkiksi kokonaisluvun saa tallennettua seuraavasti
Lokero<Integer> luku = new Lokero<Integer>(); luku.asetaArvo(5); System.out.println(luku.haeArvo());
5
Huomattava osa Javan tietorakenteista on ohjelmoitu geneerisiksi. Esimerkiksi ArrayList saa yhden tyyppiparametrin, HashMap kaksi.
List<String> merkkijonot = new ArrayList<String>(); Map<String, String> avainArvoParit = new HashMap<String, String>();
Jatkossa kun näet esimerkiksi tyypin ArrayList<String>
tiedät että sen sisäisessä rakenteessa on käytetty geneeristä tyyppiparametria.
Normaalien rajapintojen lisäksi Javassa on geneerisyyttä hyödyntäviä rajapintoja. Geneerisille rajapinnoille määritellään sisäisten arvojen tyypit samalla tavalla kuin geneerisille luokille. Tutkitaan Javan valmista rajapintaa Comparable
. Rajapinta Comparable
määrittelee metodin compareTo
, jonka tulee palauttaa this
-olion paikan vertailujärjestyksessä verrattuna parametrina annettuun olioon (negatiivinen luku, 0 tai positiivinen luku). Jos this
-olio on vertailujärjestyksessä ennen parametrina saatavaa olioa, tulee metodin palauttaa negatiivinen luku, jos taas parametrina saatava olio on järjestyksessä ennen, tulee metodin palauttaa positiivinen luku. Jos oliot ovat vertailujärjestykseltään samat, palautetaan 0. Vertailujärjestyksellä tarkoitetaan tässä ohjelmoijan määrittelemää olioiden "suuruusjärjestystä", eli jos oliot järjestetään sort-metodilla, mikä on niiden järjestys.
Yksi Comparable
-rajapinnan eduista on se, että se mahdollistaa Comparable-tyyppisistä alkioista koostuvan listan järjestämisen esimerkiksi standardikirjaston Collections.sort
-metodin avulla. Collections.sort
käyttää listan alkioiden compareTo
-metodia selvittääkseen, missä järjestyksessä alkoiden kuuluisi olla. Tätä compareTo
-metodin avulla johdettua järjestystä kutsutaan luonnolliseksi järjestykseksi (natural ordering).
Luodaan luokka Kerholainen
, joka kuvaa kerhossa käyvää lasta tai nuorta. Jokaisella kerholaisella on nimi ja pituus. Kerholaisten tulee mennä aina syömään pituusjärjestyksessä, joten toteutetaan kerholaisille rajapinta Comparable
. Comparable-rajapinta ottaa tyyppiparametrinaan myös luokan, johon sitä verrataan. Käytetään tyyppiparametrina luokkaa Kerholainen
-luokkaa.
public class Kerholainen implements Comparable<Kerholainen> { private String nimi; private int pituus; public Kerholainen(String nimi, int pituus) { this.nimi = nimi; this.pituus = pituus; } public String getNimi() { return this.nimi; } public int getPituus() { return this.pituus; } @Override public String toString() { return this.getNimi() + " (" + this.getPituus() + ")"; } @Override public int compareTo(Kerholainen kerholainen) { if(this.pituus == kerholainen.getPituus()) { return 0; } else if (this.pituus > kerholainen.getPituus()) { return 1; } else { return -1; } } }
Rajapinnan vaatima metodi compareTo
palauttaa kokonaisluvun, joka kertoo vertausjärjestyksestä. Koska compareTo()
-metodista riittää palauttaa negatiivinen luku, jos this
-olio on pienempi kuin parametrina annettu olio ja nolla, kun pituudet ovat samat, voidaan edellä esitelty metodi compareTo
toteuttaa myös seuraavasti:
@Override public int compareTo(Kerholainen kerholainen) { return this.pituus - kerholainen.getPituus(); }
Kerholaisten järjestäminen on nyt helppoa.
List<Kerholainen> kerholaiset = new ArrayList<Kerholainen>(); kerholaiset.add(new Kerholainen("mikael", 182)); kerholaiset.add(new Kerholainen("matti", 187)); kerholaiset.add(new Kerholainen("joel", 184)); System.out.println(kerholaiset); Collections.sort(kerholaiset); System.out.println(kerholaiset);
[mikael (182), matti (187), joel (184)] [mikael (182), joel (184), matti (187)]
Jos kerholaiset haluaa järjestää käänteiseen järjestykseen, riittää vain compareTo
-metodissa olevien muuttujien paikan vaihtaminen.
Saat valmiin luokan Ihminen. Ihmisellä on nimi- ja palkkatiedot. Muokkaa Ihminen-luokasta Comparable
-rajapinnan toteuttava niin, että compareTo
-metodi lajittelee ihmiset palkan mukaan järjestykseen - suuripalkkaiset ensin, köyhät kyykkyyn listan loppuun.
Saat valmiin luokan Opiskelija. Opiskelijalla on nimi. Muokkaa Opiskelija-luokasta Comparable
-rajapinnan toteuttava niin, että compareTo
-metodi lajittelee opiskelijat nimen mukaan aakkosjärjestykseen.
Vinkki: Opiskelijan nimi on String, ja String-luokka on itsessään Comparable
. Voit hyödyntää String-luokan compareTo
-metodia Opiskelija-luokan metodia toteuttaessasi. String.compareTo
kohtelee kirjaimia eriarvoisesti kirjainkoon mukaan, ja tätä varten String-luokalla on myös metodi compareToIgnoreCase
joka nimensä mukaisesti jättää kirjainkoon huomioimatta. Voit käyttää opiskelijoiden järjestämiseen kumpaa näistä haluat.
Tehtäväpohjan mukana on luokka, jonka oliot kuvaavat pelikortteja. Kortilla on arvo ja maa. Kortin arvo on 2, 3, ..., 10, J, Q, K tai A ja maa Risti, Ruutu, Hertta tai Pata. Arvo ja maa kuitenkin esitetään olioiden sisällä kokonaislukuina. Kortilla on myös metodi toString, jota käyttäen kortin arvo ja maa tulostuvat "ihmisystävällisesti".
Jotta korttien käyttäjän ei tarvitsisi käsitellä korttien maita numeroina, on luokkaan määritelty neljä vakioa eli public static final
-muuttujaa:
public class Kortti { public static final int RISTI = 0; public static final int RUUTU = 1; public static final int HERTTA = 2; public static final int PATA = 3; // ... }
Nyt ohjelmassa voidaan luvun 1 sijaan käyttää vakioa Kortti.RUUTU
. Seuraavassa esimerkissä luodaan kolme korttia ja tulostetaan ne:
Kortti eka = new Kortti(2, Kortti.RUUTU); Kortti toka = new Kortti(14, Kortti.PATA); Kortti kolmas = new Kortti(12, Kortti.HERTTA); System.out.println(eka); System.out.println(toka); System.out.println(kolmas);
Tulostuu:
Ruutu 2 Pata A Hertta Q
HUOM: vaikioiden käyttö ylläolevaan tapaan ei ole tyylikkäin tapa asian hoitamiseen. Myöhemmin kurssilla opimme oikeaoppisen tyylin maan esittämiseen!
Tee Kortti-luokasta Comparable. Toteuta compareTo
-metodi niin, että korttien järjestys on arvon mukaan nouseva. Jos verrattavien Korttien arvot ovat samat, verrataan niitä maan perusteella nousevassa järjestyksessä: risti ensin, ruutu toiseksi, hertta kolmanneksi, pata viimeiseksi.
Järjestyksessä pienin kortti siis olisi risti kakkonen ja suurin pataässä.
Tehdään seuraavaksi luokka Kasi
joka edustaa pelaajan kädessään pitämää korttien joukkoa. Tee kädelle seuraavat metodit:
public void lisaa(Kortti kortti)
lisää käteen kortinpublic void tulosta()
tulostaa kädessä olevat kortit alla olevan esimerkin tyylilläKasi kasi = new Kasi(); kasi.lisaa( new Kortti(2, Kortti.RUUTU) ); kasi.lisaa( new Kortti(14, Kortti.PATA) ); kasi.lisaa( new Kortti(12, Kortti.HERTTA) ); kasi.lisaa( new Kortti(2, Kortti.PATA) ); kasi.tulosta();
Tulostuu:
Ruutu 2 Pata A Hertta Q Pata 2
Talleta käden sisällä olevat kortit ArrayListiin.
Tee kädelle metodi public void jarjesta()
jota kutsumalla käden sisällä olevat kortit menevät suuruusjärjestykseen. Järjestämisen jälkeen kortit tulostuvat järjestyksessä:
Kasi kasi = new Kasi(); kasi.lisaa( new Kortti(2, Kortti.RUUTU) ); kasi.lisaa( new Kortti(14, Kortti.PATA) ); kasi.lisaa( new Kortti(12, Kortti.HERTTA) ); kasi.lisaa( new Kortti(2, Kortti.PATA) ); kasi.jarjesta(); kasi.tulosta();
Tulostuu:
Ruutu 2 Pata 2 Hertta Q Pata A
Eräässä korttipelissä kahdesta korttikädestä arvokkaampi on se, jonka sisältämien korttien arvon summa on suurempi. Tee luokasta Kasi
vertailtava tämän kriteerin mukaan, eli laita luokka toteuttamaan rajapinta Comparable<Kasi>
.
Esimerkkiohjelma, jossa vertaillaan käsiä:
Kasi kasi1 = new Kasi(); kasi1.lisaa( new Kortti(2, Kortti.RUUTU) ); kasi1.lisaa( new Kortti(14, Kortti.PATA) ); kasi1.lisaa( new Kortti(12, Kortti.HERTTA) ); kasi1.lisaa( new Kortti(2, Kortti.PATA) ); Kasi kasi2 = new Kasi(); kasi2.lisaa( new Kortti(11, Kortti.RUUTU) ); kasi2.lisaa( new Kortti(11, Kortti.PATA) ); kasi2.lisaa( new Kortti(11, Kortti.HERTTA) ); int vertailu = kasi1.compareTo(kasi2); if ( vertailu < 0 ) { System.out.println("arvokkaampi käsi sisältää kortit"); kasi2.tulosta(); } else if ( vertailu > 0 ){ System.out.println("arvokkaampi käsi sisältää kortit"); kasi1.tulosta(); } else { System.out.println("kädet yhtä arvokkaat"); }
Tulostuu
arvokkaampi käsi sisältää kortit Ruutu J Pata J Hertta J
Entä jos haluaisimme välillä järjestää kortit hieman eri tavalla, esim. kaikki saman maan kortit peräkkäin? Luokalla voi olla vain yksi compareTo-metodi, joten joudumme muunlaisia järjestyksiä saadaksemme turvautumaan muihin keinoihin.
Vaihtoehtoiset järjestämistavat toteutetaan erillisten vertailun suorittavien luokkien avulla. Korttien vaihtoehtoisten järjestyksen määräävän luokkien tulee toteuttaa Comparator<Kortti>
-rajapinta. Järjestyksen määräävän luokan olio vertailee kahta parametrina saamaansa korttia. Metodeja on ainoastaan yksi compare(Kortti k1, Kortti k2), jonka tulee palauttaa negatiivinen arvo, jos kortti k1 on järjestyksessä ennen korttia k2, positiivinen arvo jos k2 on järjestyksessä ennen k1:stä ja 0 muuten.
Periaatteena on luoda jokaista järjestämistapaa varten oma vertailuluokka, esim. saman maan kortit vierekkäin vievän järjestyksen määrittelevä luokka:
import java.util.Comparator; public class SamatMaatVierekkain implements Comparator<Kortti> { public int compare(Kortti k1, Kortti k2) { return k1.getMaa()-k2.getMaa(); } }
Maittainen järjestys on sama kuin kortin metodin compareTo
maille määrittelemä järjestys eli ristit ensin, ruudut toiseksi, hertat kolmanneksi, padat viimeiseksi.
Järjestäminen tapahtuu edelleen luokan Collections metodin sort avulla. Metodi saa nyt toiseksi parametrikseen järjestyksen määräävän luokan olion:
ArrayList<Kortti> kortit = new ArrayList<Kortti>(); kortit.add( new Kortti(3, Kortti.PATA) ); kortit.add( new Kortti(2, Kortti.RUUTU) ); kortit.add( new Kortti(14, Kortti.PATA) ); kortit.add( new Kortti(12, Kortti.HERTTA) ); kortit.add( new Kortti(2, Kortti.PATA) ); SamatMaatVierekkain samatMaatVierekkainJarjestaja = new SamatMaatVierekkain(); Collections.sort(kortit, samatMaatVierekkainJarjestaja ); for (Kortti k : kortit) { System.out.println( k ); }
Tulostuu:
Ruutu 2 Hertta Q Pata 3 Pata A Pata 2
Järjestyksen määrittelevä olio voidaan myös luoda suoraan sort-kutsun yhteydessä:
Collections.sort(kortit, new SamatMaatVierekkain() );
Tarkempia ohjeita vertailuluokkien tekemiseen täällä
Tee nyt luokka Comparator-rajapinnan toteuttava luokka SamatMaatVierekkainArvojarjestykseen
jonka avulla saat kortit muuten samanlaiseen järjestykseen kuin edellisessä esimerkissä paitsi, että saman maan kortit järjestyvät arvon mukaiseesti.
Lisää luokalle Kasi
metodi public void jarjestaMaittain()
jota kutsumalla käden sisällä olevat kortit menevät edellisen tehtävän vertailijan määrittelemään järjestykseen. Järjestämisen jälkeen kortit tulostuvat järjestyksessä:
Kasi kasi = new Kasi(); kasi.lisaa( new Kortti(12, Kortti.HERTTA) ); kasi.lisaa( new Kortti(4, Kortti.PATA) ); kasi.lisaa( new Kortti(2, Kortti.RUUTU) ); kasi.lisaa( new Kortti(14, Kortti.PATA) ); kasi.lisaa( new Kortti(7, Kortti.HERTTA) ); kasi.lisaa( new Kortti(2, Kortti.PATA) ); kasi.jarjestaMaittain(); kasi.tulosta();
Tulostuu:
Ruutu 2 Hertta 7 Hertta Q Pata 2 Pata 4 Pata A
Luokkakirjasto Collections
on Javan yleishyödyllinen kokoelmaluokkiin liittyvä kirjasto. Kuten tiedämme, Collections
tarjoaa metodit olioiden järjestämiseen joko Comparable
- tai Comparator
-rajapinnan kautta. Järjestämisen lisäksi luokkakirjaston avulla voi etsiä esimerkiksi minimi- (min
-metodi) tai maksimialkioita (max
-metodi), hakea tiettyä arvoa (binarySearch
-metodi), tai kääntää listan (reverse
-metodi).
Collections-luokkakirjasto tarjoaa valmiiksi toteutetun binäärihaun. Metodi binarySearch()
palauttaa haetun alkion indeksin listasta jos se löytyy. Jos alkiota ei löydy, hakualgoritmi palauttaa negatiivisen arvon. Metodi binarySearch()
käyttää Comparable-rajapintaa haetun olion löytämiseen. Jos olion compareTo()
-metodi palauttaa arvon 0, eli olio on sama, ajatellaan arvon löytyneen.
Kerholainen-luokkamme vertaa pituuksia compareTo()
-metodissaan, eli listasta etsiessä etsisimme samanpituista kerholaista.
List<Kerholainen> kerholaiset = new ArrayList<Kerholainen>(); kerholaiset.add(new Kerholainen("mikael", 182)); kerholaiset.add(new Kerholainen("matti", 187)); kerholaiset.add(new Kerholainen("joel", 184)); Collections.sort(kerholaiset); Kerholainen haettava = new Kerholainen("Nimi", 180); int indeksi = Collections.binarySearch(kerholaiset, haettava); if(indeksi >= 0) { System.out.println("180 senttiä pitkä löytyi indeksistä " + indeksi); System.out.println("nimi: " + kerholaiset.get(indeksi).getNimi()); } haettava = new Kerholainen("Nimi", 187); int indeksi = Collections.binarySearch(kerholaiset, haettava); if(indeksi >= 0) { System.out.println("187 senttiä pitkä löytyi indeksistä " + indeksi); System.out.println("nimi: " + kerholaiset.get(indeksi).getNimi()); }
Esimerkkimme tulostaa seuraavaa
187 senttiä pitkä löytyi indeksistä 2 nimi: matti
Huomaa että esimerkissä kutsuttiin myös metodia Collections.sort()
. Tämä tehdään sen takia, että binäärihakua ei voida tehdä jos taulukko tai lista ei ole valmiiksi järjestyksessä.
Harjoitellaan taas ohjelman rakenteen omatoimista suunnittelua. Käyttöliittymän ulkomuoto ja vaadittu toiminnallisuus on määritelty ennalta, rakenteen saat toteuttaa vapaasti.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Mäkihyppy on suomalaisille erittäin rakas laji, jossa pyritään hyppäämään hyppyrimäestä mahdollisimman pitkälle mahdollisimman tyylikkäästi. Tässä tehtävässä tulee toteuttaa simulaattori mäkihyppykilpailulle.
Simulaattori kysyy ensin käyttäjältä hyppääjien nimiä. Kun käyttäjä antaa tyhjän merkkijonon (eli painaa enteriä) hyppääjän nimeksi siirrytään hyppyvaiheeseen. Hyppyvaiheessa hyppääjät hyppäävät yksitellen käänteisessä pistejärjestyksessä. Hyppääjä jolla on vähiten pisteitä kerättynä hyppää aina kierroksen ensimmäisenä, toiseksi vähiten pisteitä omaava toisena jne, ..., eniten pisteitä kerännyt viimeisenä.
Hyppääjän yhteispisteet lasketaan yksittäisten hyppyjen pisteiden summana. Yksittäisen hypyn pisteytys lasketaan hypyn pituudesta (käytä satunnaista kokonaisluku väliltä 60-120) ja tuomariäänistä. Jokaista hyppyä kohden annetaan 5 tuomariääntä (satunnainen luku väliltä 10-20). Tuomariääniä laskettaessa otetaan huomioon vain kolme keskimmäistä ääntä: pienintä ja suurinta ääntä ei oteta huomioon. Esimerkiksi jos Mikael hyppää 61 metriä ja saa tuomariäänet 11, 12, 13, 14 ja 15, on hänen hyppynsä yhteispisteet 100.
Kierroksia hypätään niin monta kuin ohjelman käyttäjä haluaa. Kun käyttäjä haluaa lopettaa tulostetaan lopuksi kilpailun lopputulokset. Lopputuloksissa tulostetaan hyppääjät, hyppääjien yhteispisteet ja hyppääjien hyppäämien hyppyjen pituudet. Lopputulokset on järjestetty hyppääjien yhteispisteiden mukaan siten, että eniten pisteitä kerännyt on ensimmäinen.
Tehtävän tekemisessä on hyötyä muun muassa metodeista Collections.sort
ja Collections.reverse
. Kannattaa aluksi hahmotella minkälaisia luokkia ja olioita ohjelmassa voisi olla. On myös hyvä pyrkiä tilanteeseen, jossa käyttöliittymäluokka on ainut luokka joka kutsuu tulostuskomentoa.
Kumpulan mäkiviikot Syötä kilpailun osallistujat yksi kerrallaan, tyhjällä merkkijonolla siirtyy hyppyvaiheeseen. Osallistujan nimi: Mikael Osallistujan nimi: Mika Osallistujan nimi: Kilpailu alkaa! Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: hyppaa 1. kierros Hyppyjärjestys: 1. Mikael (0 pistettä) 2. Mika (0 pistettä) Kierroksen 1 tulokset Mikael pituus: 95 tuomaripisteet: [15, 11, 10, 14, 14] Mika pituus: 112 tuomaripisteet: [14, 12, 18, 18, 17] Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: hyppaa 2. kierros Hyppyjärjestys: 1. Mikael (134 pistettä) 2. Mika (161 pistettä) Kierroksen 2 tulokset Mikael pituus: 96 tuomaripisteet: [20, 19, 15, 13, 18] Mika pituus: 61 tuomaripisteet: [12, 11, 15, 17, 11] Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: hyppaa 3. kierros Hyppyjärjestys: 1. Mika (260 pistettä) 2. Mikael (282 pistettä) Kierroksen 3 tulokset Mika pituus: 88 tuomaripisteet: [11, 19, 13, 10, 15] Mikael pituus: 63 tuomaripisteet: [12, 19, 19, 12, 12] Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: lopeta Kiitos! Kilpailun lopputulokset: Sija Nimi 1 Mikael (388 pistettä) hyppyjen pituudet: 95 m, 96 m, 63 m 2 Mika (387 pistettä) hyppyjen pituudet: 112 m, 61 m, 88 m
Huom1: Testien kannalta on oleellista että käyttöliittymä toimii kuten yllä kuvattu, esim. rivien alussa olevien välilyöntien määrän on oltava oikea. Rivien alussa oleva tyhjä pitää tehdä välilyönneillä, testit eivät toimi jos tyhjä on tehty tabulaattoreilla. Ohjelman tulostamat tekstit kannattaneekin copypasteta ohjelmakoodiin joko tehtävänannosta tai testien virheilmoituksista. Tehtävä on neljän yksittäisen tehtäväpisteen arvoinen.
Huom2: älä käytä luokkein nimissä skandeja, ne saattavat aiheuttaa ongelmia testeihin!
Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan, muistutuksena vieltä, että tehtävässä saa luoda vain yhden Scanner-olion.
Screencast jossa tehdään viikon 1 ja 2 ydinasioita hyödyntävä hieman isompi sovellus:
Isompia ohjelmia suunniteltaessa pohditaan usein mille luokalle minkäkin asian toteuttaminen kuuluu. Jos kaikki ohjelmaan kuuluva toiminnallisuus asetetaan samaan luokkaan, on tuloksena väistämättä kaaos. Ohjelmistojen suunnittelun osa-alue oliosuunnittelu sisältää periaatteen Single responsibility principle, jota meidän kannattaa seurata.
Single responsibility principle sanoo että jokaisella luokalla tulee olla vain yksi vastuu ja selkeä tehtävä. Jos luokalla on yksi selkeä tehtävä, on tehtävässä tapahtuvien muutosten toteuttaminen helppoa, muutos tulee tehdä vain yhteen luokkaan. Jokaisella luokalla tulisi olla vain yksi syy muuttua.
Tutkitaan seuraavaa luokkaa Tyontekija
, jolla on metodit palkan laskemiseen ja tuntien raportointiin.
public class Tyontekija { // oliomuuttujat // työntekijään liittyvät konstruktorit ja metodit public double laskePalkka() { // palkan laskemiseen liittyvä logiikka } public String raportoiTunnit() { // työtuntien raportointiin liittyvä logiikka } }
Vaikka yllä olevasta esimerkistä puuttuvat konkreettiset toteutukset, tulisi hälytyskellojen soida. Luokalla Tyontekija
on ainakin kolme eri vastuualuetta. Se kuvaa sovelluksessa työntekijää, se toteuttaa palkanlaskennan tehtävää palkan laskemisesta, ja tuntiraportointijärjestelmän tehtävää työtuntien raportoinnista. Yllä oleva luokka tulee pilkkoa kolmeen osaan: yksi osa kuvaa työntekijää, toinen osa palkanlaskentaa ja kolmas osa tuntikirjanpitoa.
public class Tyontekija { // oliomuuttujat // työntekijään liittyvät konstruktorit ja metodit }
public class Palkanlaskenta { // oliomuuttujat // palkanlaskentaan liittyvät metodit public double laskePalkka(Henkilo henkilo) { // palkan laskemiseen liittyvä logiikka } }
public class Tuntikirjanpito { // oliomuuttujat // tuntikirjanpitoon liittyvät metodit public String luoTuntiraportti(Henkilo henkilo) { // työtuntien raportointiin liittyvä logiikka } }
Jokainen muuttuja, jokainen koodirivi, jokainen metodi, jokainen luokka, ja jokainen ohjelma pitäisi olla vain yhtä tarkoitusta varten. Usein ohjelman "parempi" rakenne on ohjelmoijalle selkeää vasta kun ohjelma on toteutettu jo kertaalleen. Tämä on täysin hyväksyttävää: vielä tärkeämpää on se, että ohjelmaa pyritään muuttamaan aina selkeämpään suuntaan. Refaktoroi eli muokkaa ohjelmaasi aina tarpeen tullen!
Suurempia ohjelmia suunniteltaessa ja toteutettaessa luokkamäärä kasvaa helposti suureksi. Luokkien määrän kasvaessa niiden tarjoamien toiminnallisuuksien ja metodien muistaminen vaikeutuu. Järkevä luokkien nimeäminen helpottaa toiminnallisuuksien muistamista. Järkevän nimennän lisäksi lähdekooditiedostot kannattaa jakaa toiminnallisuutta, käyttötarkoitusta tai jotain muuta loogista kokonaisuutta kuvaaviin pakkauksiin. Pakkaukset (package) ovat käytännössä hakemistoja, joihin lähdekooditiedostot organisoidaan. Windowsissa ja puhekielessä hakemistoja (engl. directory) kutsutaan usein kansioiksi. Me käytämme kuitenkin termiä hakemisto.
Ohjelmointiympäristöt tarjoavat valmiit työkalut pakkausten hallintaan. Olemme tähän mennessä luoneet luokkia ja rajapintoja vain projektiin liittyvän lähdekoodipakkaukset-osion (Source Packages
) oletuspakkaukseen (default package
). Uuden pakkauksen voi luoda NetBeansissa projektin pakkauksiin liittyvässä Source Packages
-osiossa oikeaa hiirennappia painamalla ja valitsemalla New -> Java Package...
. Luodun pakkauksen sisälle voidaan luoda luokkia aivan kuten oletuspakkaukseenkin (default package
).
Pakkaus, jossa luokka sijaitsee, näkyy lähdekooditiedoston alussa ennen muita komentoja olevasta lauseesta package pakkaus
. Esimerkiksi alla oleva luokka Sovellus
sijaitsee pakkauksessa kirjasto
.
package kirjasto; public class Sovellus { public static void main(String[] args) { System.out.println("Hello packageworld!"); } }
Pakkaukset voivat sisältää pakkauksia. Esimerkiksi pakkausmäärittelyssä package kirjasto.domain
pakkaus domain
on pakkauksen kirjasto
sisällä. Asettamalla pakkauksia pakkausten sisään rakennetaan sovelluksen luokille ja rajapinnoille hierarkiaa. Esimerkiksi kaikki Javan luokat sijaitsevat pakkauksen java
alla olevissa pakkauksissa. Pakkausnimeä domain
käytetään usein kuvaamaan sovellusalueen käsitteisiin liittyvien luokkien säilytyspaikkaa. Esimerkiksi luokka Kirja
voisi hyvin olla pakkauksen kirjasto.domain
sisällä sillä se kuvaa kirjastosovellukseen liittyvää käsitettä.
package kirjasto.domain; public class Kirja { private String nimi; public Kirja(String nimi) { this.nimi = nimi; } public String getNimi() { return this.nimi; } }
Pakkauksissa olevia luokkia tuodaan luokan käyttöön import
-lauseen avulla. Esimerkiksi kirjasto
-pakkauksessa oleva luokka Sovellus
saisi käyttöönsä pakkauksessa kirjasto.domain
olevan luokan määrittelyllä import kirjasto.domain.Kirja
.
package kirjasto; import kirjasto.domain.Kirja; public class Sovellus { public static void main(String[] args) { Kirja kirja = new Kirja("pakkausten ABC!"); System.out.println("Hello packageworld: " + kirja.getNimi()); } }
Hello packageworld: pakkausten ABC!
Import-lauseet asetetaan lähdekooditiedostossa pakkausmäärittelyn jälkeen mutta ennen luokkamäärittelyä. Niitä voi olla myös useita -- esimerkiksi kun haluamme käyttää useita luokkia. Javan valmiit luokat sijaitsevat yleensä ottaen pakkauksen java
alipakkauksissa. Luokkiemme alussa usein esiintyvät lauseet import java.util.ArrayList
ja import java.util.Scanner;
alkavat nyt toivottavasti vaikuttaa merkityksellisimmiltä.
Jatkossa kaikissa tehtävissämme käytetään pakkauksia. Luodaan seuraavaksi ensimmäiset pakkaukset itse.
Luo projektipohjaan pakkaus mooc
. Rakennetaan tämän pakkauksen sisälle sovelluksen toiminta. Lisää sovellukseen pakkaus ui
(tämän jälkeen pitäisi olla käytössä pakkaus mooc.ui
), ja lisää sinne rajapinta Kayttoliittyma
.
Rajapinnan Kayttoliittyma
tulee määritellä metodi void paivita()
.
Luo samaan pakkaukseen luokka Tekstikayttoliittyma
, joka toteuttaa rajapinnan Kayttoliittyma
. Toteuta luokassa Tekstikayttoliittyma
rajapinnan Kayttoliittyma
vaatima metodi public void paivita()
siten, että sen ainut tehtävä on merkkijonon "Päivitetään käyttöliittymää
"-tulostaminen System.out.println
-metodikutsulla.
Luo tämän jälkeen pakkaus mooc.logiikka
, ja lisää sinne luokka Sovelluslogiikka
. Sovelluslogiikan APIn tulee olla seuraavanlainen.
public Sovelluslogiikka(Kayttoliittyma kayttoliittyma)
import mooc.ui.Kayttoliittyma;
public void suorita(int montaKertaa)
montaKertaa
-muuttujan määrittelemän määrän merkkijonoa "Sovelluslogiikka toimii". Jokaisen "Sovelluslogiikka toimii"-tulostuksen jälkeen tulee kutsua konstruktorin parametrina saadun rajapinnan Kayttoliittyma
-toteuttaman olion määrittelemää paivita()
-metodia.Voit testata sovelluksen toimintaa seuraavalla pääohjelmaluokalla.
import mooc.logiikka.Sovelluslogiikka; import mooc.ui.Kayttoliittyma; import mooc.ui.Tekstikayttoliittyma; public class Main { public static void main(String[] args) { Kayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(); new Sovelluslogiikka(kayttoliittyma).suorita(3); } }
Ohjelman tulostuksen tulee olla seuraava:
Sovelluslogiikka toimii Päivitetään käyttöliittymää Sovelluslogiikka toimii Päivitetään käyttöliittymää Sovelluslogiikka toimii Päivitetään käyttöliittymää
Kaikki NetBeansissa näkyvät projektit ovat tietokoneesi tiedostojärjestelmässä. Jokaiselle projektille on olemassa oma hakemisto (eli kansio), jonka sisällä on projektiin liittyvät tiedostot ja hakemistot.
Projektin hakemistossa src
on ohjelmaan liittyvät lähdekoodit. Jos luokan pakkauksena on kirjasto, sijaitsee se projektin lähdekoodihakemiston src
sisällä olevassa hakemistossa kirjasto
. Jos olet kiinnostunut, NetBeansissa voi käydä katsomassa projektien konkreettista rakennetta Files-välilehdeltä joka on normaalisti Projects-välilehden vieressä. Jos et näe välilehteä Files, saa sen näkyville valitsemalla vaihtoehdon Files valikosta Window.
Sovelluskehitystä tehdään normaalisti Projects-välilehdeltä, jossa NetBeans on piilottanut projektiin liittyviä tiedostoja joista ohjelmoijan ei tarvitse välittää.
Olemme aiemmin tutustuneet kahteen näkyvyysmääreeseen. Näkyvyysmääreellä private
varustetut metodit ja muuttujat ovat näkyvissä vain sen luokan sisällä joka määrittelee ne. Niitä ei voi käyttää luokan ulkopuolelta. Näkyvyysmääreellä public
varustetut metodit ja muuttujat ovat taas kaikkien käytettävissä.
package kirjasto.ui; public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { tulostaOtsikko(); // muu toiminnallisuus } private void tulostaOtsikko() { System.out.println("************"); System.out.println("* KIRJASTO *"); System.out.println("************"); } }
Yllä olevasta Kayttoliittyma
-luokasta tehdyn olion konstruktori ja kaynnista
-metodi on kutsuttavissa mistä tahansa ohjelmasta. Metodi tulostaOtsikko
ja lukija
-muuttuja on käytössä vain luokan sisällä.
Pakkausnäkyvyyttä käytettäessä muuttujille tai metodeille ei aseteta mitään näkyvyyteen liittyvää etuliitettä. Muutetaan yllä olevaa esimerkkiä siten, että metodilla tulostaOtsikko
on pakkausnäkyvyys.
package kirjasto.ui; public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { tulostaOtsikko(); // muu toiminnallisuus } void tulostaOtsikko() { System.out.println("************"); System.out.println("* KIRJASTO *"); System.out.println("************"); } }
Nyt saman pakkauksen sisällä olevat luokat voivat käyttää metodia tulostaOtsikko
.
package kirjasto.ui; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner lukija = new Scanner(System.in); Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija); kayttoliittyma.tulostaOtsikko(); // onnistuu! } }
Jos luokka on eri pakkauksessa, ei metodia tulostaOtsikko
pysty käyttämään.
package kirjasto; import java.util.Scanner; import kirjasto.ui.Kayttoliittyma; public class Main { public static void main(String[] args) { Scanner lukija = new Scanner(System.in); Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija); kayttoliittyma.tulostaOtsikko(); // ei onnistu! } }
Viime viikolla tutustuimme rajapintoihin. Rajapinta siis määrittelee yhden tai useamman metodin, jotka sen toteuttavan luokan on pakko toteuttaa. Rajapintoja, kuten kaikkia luokkia voi asettaa pakkauksiin. Esimerkiksi seuraava Tunnistettava
-rajapinta sijaitsee pakkauksessa sovellus.domain
, ja määrittelee että Tunnistettava
-rajapinnan toteuttavien luokkien tulee toteuttaa metodi public String getTunnus()
.
package sovellus.domain; public interface Tunnistettava { String getTunnus(); }
Luokka toteuttaa rajapinnan implements
-avainsanalla. Toteutetaan luokka Henkilo
, joka toteuttaa rajapinnan tunnistettava. Henkilo-luokan metodi getTunnus
palauttaa aina henkilön henkilötunnuksen.
package sovellus.domain; public class Henkilo implements Tunnistettava { private String nimi; private String henkilotunnus; public Henkilo(String nimi, String henkilotunnus) { this.nimi = nimi; this.henkilotunnus = henkilotunnus; } public String getNimi() { return this.nimi; } public String getHenkilotunnus() { return this.henkilotunnus; } @Override public String getTunnus() { return getHenkilotunnus(); } @Override pulic toString(){ return this.nimi + " hetu: " +this.henkilotunnus; } }
Rajapintojen vahvuus on se, että rajapinta on myös tyyppi. Kaikki rajapinnan toteuttavista luokista tehdyt oliot ovat myös rajapinnan tyyppisiä. Tämä helpottaa sovellusten rakentamista huomattavasti.
Tehdään luokka Rekisteri
, josta voimme hakea henkilöitä tunnisteen perusteella. Yksittäisten henkilöiden hakemisen lisäksi Rekisteri
tarjoaa metodin kaikkien henkilöiden hakemiseen listana.
public class Rekisteri { private HashMap<String, Tunnistettava> rekisteroidyt; public Rekisteri() { this.rekisteroidyt = new HashMap<String, Tunnistettava>(); } public void lisaa(Tunnistettava lisattava) { this.rekisteroidyt.put(lisattava.getTunnus(), lisattava); } public Tunnistettava hae(String tunnus) { return this.rekisteroidyt.get(tunnus); } public List<Tunnistettava> haeKaikki() { return new ArrayList<Tunnistettava>(rekisteroidyt.values()); } }
Rekisterin käyttö on helppoa
Rekisteri henkilokunta = new Rekisteri(); henkilokunta.lisaa( new Henkilo("Pekka", "221078-123X") ); henkilokunta.lisaa( new Henkilo("Jukka", "110956-326B") ); System.out.println( henkilokunta.hae("280283-111A") ); Henkilo loydetty = (Henkilo)henkilokunta.hae("110956-326B"); System.out.println( loydetty.getNimi() );
Koska henkilöt on talletettu rekisteriin Tunnistettava
-tyyppisinä, joudumme muuntanaan ne takaisin oikeaan tyyppiin jos haluamme käsitellä henkilöitä sellaisten metodien kautta, joita ei rajapinnassa ole määritelty. Näin tapahtuu yllä olevan esimerkin kahdella viimeisellä rivillä.
Entä jos haluaisimme operaation, joka palauttaa rekisteriin talletetut henkilöt tunnisteen mukaan järjestettynä?
Yksi luokka voi toteuttaa useamman rajapinnan, eli voimme toteuttaa Henkilo
-luokalla rajapinnan Tunnistettava
lisäksi viime viikolta tutun rajapinnan Comparable
. Useamman rajapinnan toteuttaminen tapahtuu erottamalla toteutettavat rajapinnat toisistaan pilkuilla (public class ... implements RajapintaEka, RajapintaToka ...
). Toteuttaessamme useampaa rajapintaa, tulee meidän toteuttaa kaikki rajapintojen vaatimat metodit. Toteutetaan seuraavaksi luokalla Henkilo
rajapinta Comparable
.
package sovellus.domain; public class Henkilo implements Tunnistettava, Comparable<Henkilo> { private String nimi; private String henkilotunnus; public Henkilo(String nimi, String henkilotunnus) { this.nimi = nimi; this.henkilotunnus = henkilotunnus; } public String getNimi() { return this.nimi; } public String getHenkilotunnus() { return this.henkilotunnus; } @Override public String getTunnus() { return getHenkilotunnus(); } @Override public int compareTo(Henkilo toinen) { return this.getTunniste().compareTo(toinen.getTunniste()); } }
Nyt voimme lisätä rekisterille metodin haeKaikkiJarjestyksessa:
public List<Tunnistettava> haeKaikkiJarjestyksessa() { ArrayList<Tunnistettava> kaikki = new ArrayList<Tunnistettava>(rekisteroidyt.values());; Collections.sort(kaikki); return kaikki; }
Huomaamme kuitenkin, että ratkaisumme ei toimi. Koska henkilöt on talletettu rekisteriin Tunnitettava
-tyyppisinä, onkin Henkilön toteutettava rajapinta Comparable<Tunnistettava>
jotta rekisteri osaisi järjestää henkilöt metodin Collections.sort()
avulla. Eli muutamme henkilön toteuttamaa rajapintaa:
public class Henkilo implements Tunnistettava, Comparable<Tunnistettava> { // ... @Override public int compareTo(Tunnistettava toinen) { return this.getTunniste().compareTo(toinen.getTunniste()); } }
Nyt ratkaisu toimii!
Rekisteri on täysin tietämätön sinne talletettavien olioiden todellisesta tyypistä. Voimmekin käyttää luokkaa rekisteri myös muuntyyppisten olioiden kuin henkilöiden rekisteröintiin, kunhan olioiden luokka vaan toteuttaa rajapinnan Tunnistettava
. Esim. seuraavassa käytetään rekisteriä kaupassa myytävien tuotteiden hallintaan:
public class Tuote implements Tunnistettava { private String nimi; private String viivakoodi; private int varastosaldo; private int hinta; public Tuote(String nimi, String viivakoodi) { this.nimi = nimi; this.viivakoodi = viivakoodi; } public String getTunniste() { return viivakoodi; } // ... } Rekisteri tuotteet = new Rekisteri(); tuotteet.lisaa( new Tuote("maito", "11111111") ); tuotteet.lisaa( new Tuote("piimä", "11111112") ); tuotteet.lisaa( new Tuote("juusto", "11111113") ); System.out.println( tuotteet.hae("99999999") ); Tuote tuote = (Tuote)tuotteet.hae("11111112"); tuote.kasvataSaldoa(100); tuote.muutaHinta(23);
Eli olemme tehneet luokasta Rekisteri
melko yleiskäyttöisen pitämällä sen riippumattomana konkreettisista luokista. Mikä tähänsa luokka, joka toteuttaa rajapinnan Tunnistettava
on rekisterin kanssa käyttökelpoinen. Metodin haeKaikkiJarjestyksessä
toimiminen tosin edellyttää luokalta myös vertailtavuuden eli Comparable<Tunnistettava>-rajapinnan toteuttamisen.
Oletetaan että ohjelmassasi on rajapinta Rajapinta
, ja olet tekemässä rajapinnan toteuttavaa luokkaa Luokka
. Joudut näkemään hieman vaivaa kirjoittaessasi toteuttavaan luokkaan rajapinnan määrittelemien metodien esittelyrivit.
On kuitenkin mahdollista pyytää NetBeansia täydentämään automaattisesti metodirungot toteuttavalle luokalle. Kun olet määritellyt luokan toteuttavan rajapinnan, eli kirjoittanut
public class Luokka implements Rajapinta { }
NetBeans värjää luokan nimen punaisella. Mene rivin vasemmassa reunassa olevan lamppusymbolin kohdalle, klikkaa ja valitse Implement all abstract methods ja metodirungot ilmestyvät koodiin!
Tietyissä tilanteissa NetBeans saattaa mennä sekaisin ja yrittää ajaa koodista versiota johon ei ole huomioitu kaikkia koodiin kirjoitettuja muutoksia. Yleensä huomaat tilanteen siten, että jotain "outoa" vaikuttaa tapahtuvan. Ongelman korjaa usein Clean and build -operaation suorittaminen. Operaatio löytyy Run-valikosta ja sen voi suorittaa myös painamalla harja ja vasara -symbolia. Clean and build poistaa koodista olemassa olevat käännetyt versiot ja tekee uuden käännöksen.
Muuttokuormaa pakattaessa esineitä lisätään muuttolaatikoihin siten, että tarvittujen muuttolaatikoiden määrä on mahdollisimman pieni. Tässä tehtävässä simuloidaan esineiden pakkaamista muuttolaatikoihin. Jokaisella esineellä on tilavuus, ja muuttolaatikoilla on maksimitilavuus.
Muuttomiehet siirtävät tavarat myöhemmin rekka-autoon (ei toteuteta tässä), joten toteutetaan ensin kaikkia esineitä ja laatikoita kuvaava Tavara
-rajapinta.
Tavara-rajapinnan tulee määritellä metodi int getTilavuus()
, jonka avulla tavaroita käsittelevät saavat selville kyseisen tavaran tilavuuden. Toteuta rajapinta Tavara
pakkaukseen muuttaminen.domain
.
Toteuta seuraavaksi pakkaukseen muuttaminen.domain
luokka Esine
, joka saa konstruktorin parametrina esineen nimen (String) ja esineen tilavuuden (int). Luokan tulee toteuttaa rajapinta Tavara
.
Lisää luokalle Esine
myös metodit public String getNimi()
ja korvaa metodi public String toString()
siten että se tuotta merkkijonoja muotoa "nimi (tilavuus dm^3)
". Esineen pitäisi toimia nyt jotakuinkin seuraavasti
Tavara esine = new Esine("hammasharja", 2); System.out.println(esine);
hammasharja (2 dm^3)
Pakatessamme esineitä muuttolaatikkoon haluamme aloittaa pakkaamisen järjestyksessä olevista esineistä. Toteuta Esine
-luokalla rajapinta Comparable
siten, että esineiden luonnollinen järjestys on tilavuuden mukaan nouseva. Kun olet toteuttanut esineellä rajapinnan Comparable
, tulee niiden toimia Collections
-luokan sort
-metodin kanssa seuraavasti.
List<Esine> esineet = new ArrayList<Esine>(); esineet.add(new Esine("passi", 2)); esineet.add(new Esine("hammasharja", 1)); esineet.add(new Esine("sirkkeli", 100)); Collections.sort(esineet); System.out.println(esineet);
[hammasharja (1 dm^3), passi (2 dm^3), sirkkeli (100 dm^3)]
Toteuta tämän jälkeen pakkaukseen muuttaminen.domain
luokka Muuttolaatikko
. Tee aluksi muuttolaatikolle seuraavat:
public Muuttolaatikko(int maksimitilavuus)
public boolean lisaaTavara(Tavara tavara)
Tavara
-rajapinnan toteuttaman esineen. Jos laatikkoon ei mahdu, metodi palauttaa arvon false
. Jos tavara mahtuu laatikkoon, metodi palauttaa arvon true
. Muuttolaatikon tulee tallettaa tavarat listaan.Laita vielä Muuttolaatikko
toteuttamaan rajapinta Tavara
. Metodilla getTilavuus
tulee saada selville muuttolaatikossa olevien tavaroiden tämänhetkinen yhteistilavuus.
Toteuta luokka Pakkaaja
pakkaukseen muuttaminen.logiikka
. Luokan Pakkaaja
konstruktorille annetaan parametrina int laatikoidenTilavuus
, joka määrittelee minkä kokoisia muuttolaatikoita pakkaaja käyttää.
Toteuta tämän jälkeen luokalle metodi public List<Muuttolaatikko> pakkaaTavarat(List<Tavara> tavarat)
, joka pakkaa tavarat muuttolaatikoihin.
Tee metodista sellainen, että kaikki parametrina annetussa listassa olevat tavarat päätyvät palautetussa listassa oleviin muuttolaatikoihin. Sinun ei tarvitse varautua tilanteisiin, joissa tavarat ovat suurempia kuin pakkaajan käyttämä muuttolaatikon koko. Testit eivät välitä siitä kuinka täyteen pakkaaja täyttää muuttolaatikot.
Seuraavassa pakkaajan toimintaa demonstroiva esimerkki:
// tavarat jotka haluamme pakata List<Tavara> tavarat = new ArrayList<Tavara>(); tavarat.add(new Esine("passi", 2)); tavarat.add(new Esine("hammasharja", 1)); tavarat.add(new Esine("kirja", 4)); tavarat.add(new Esine("sirkkeli", 8)); // luodaan pakkaaja, joka käyttää tilavuudeltaan 10:n kokoisia muuttolaatikoita Pakkaaja pakkaaja = new Pakkaaja(10); // pyydetään pakkaajaa pakkaamaan tavarat laatikoihin List<Muuttolaatikko> laatikot = pakkaaja.pakkaaTavarat( tavarat ); System.out.println("laatikoita: "+laatikot.size()); for (Muuttolaatikko laatikko : laatikot) { System.out.println(" laatikossa tavaraa: "+laatikko.getTilavuus()+" dm^3"); }
Tulostuu:
laatikoita: 2 laatikossa tavaraa: 7 dm^3 laatikossa tavaraa: 8 dm^3
Pakkaaja on siis pakannut tavarat kahteen laatikkoon, ensimmäiseen laatikkoon on mennyt 3 ensimmäistä tavaraa, yhteistilavuudeltaan 7, ja listan viimeinen tavara eli sirkkeli jonka tilavuus on 8 on mennyt toiseen laatikkoon. Testit eivät aseta rajoitusta pakkaajan käyttävien muuttolaatioiden määrälle, tavarat olisi siis voitu pakata vaikka jokainen eri laatikkoon, eli tuloste olisi ollut:
Tulostuu:
laatikoita: 4 laatikossa tavaraa: 2 dm^3 laatikossa tavaraa: 1 dm^3 laatikossa tavaraa: 7 dm^3 laatikossa tavaraa: 8 dm^3
Huom: tehtävän testaamista helpottamaan kannatanee tehdä luokalle Muuttolaatikko
esim. toString-metodi, jonka avulla voi printata laatikon sisällön.
Poikkeustilanteet ovat tilanteita joissa ohjelman suoritus ei ole edennyt toivotusti. Ohjelma on saattanut esimerkiksi kutsua null-viitteeseen liittyvää metodia, jolloin käyttäjälle heitetään poikkeus NullPointerException
. Jos yritämme hakea taulukon ulkopuolella olevaa indeksiä, käyttäjälle heitetään poikkeus IndexOutOfBoundsException
. Kaikki poikkeukset ovat tyyppiä Exception
.
Poikkeukset käsitellään try { } catch (Exception e) { }
-lohkorakenteella. Avainsanan try
aloittaman lohkon sisällä on mahdollisesti poikkeuksen heittävä ohjelmakoodi. Avainsanan catch
aloittaman lohkon sisällä taas määritellään mitä tehdään jos try-lohkossa suoritettavassa koodissa tapahtuu poikkeus. Catch-lauseelle määritellään kiinniotettavan poikkeuksen tyyppi (catch (Exception e)
).
try { // poikkeuksen mahdollisesti heittävä ohjelmakoodi } catch (Exception e) { // lohko johon päädytään poikkeustilanteessa }
Merkkijonon numeroksi muuttava Integer
-luokan parseInt
-metodi heittää poikkeuksen NumberFormatException
jos sille parametrina annettu merkkijono ei ole muunnettavissa numeroksi. Toteutetaan ohjelma, joka yrittää muuntaa käyttäjän syöttämän merkkijonon numeroksi.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä numero: "); int numero = Integer.parseInt(lukija.nextLine());
Syötä numero: tatti Exception in thread "..." java.lang.NumberFormatException: For input string: "tatti"
Yllä oleva ohjelma heittää poikkeuksen kun käyttäjä syöttää virheellisen numeron. Ohjelman suoritus päättyy virhetilanteeseen, eikä suoritusta voi enää jatkaa. Lisätään ohjelmaan poikkeuskäsittely. Kutsu, joka saattaa heittää poikkeuksen asetetaan try
-lohkon sisään, ja virhetilanteessa tapahtuva toiminta catch
-lohkon sisään.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä numero: "); try { int numero = Integer.parseInt(lukija.nextLine()); } catch (Exception e) { System.out.println("Et syöttänyt kunnollista numeroa."); }
Syötä numero: 5
Syötä numero: enpäs! Et syöttänyt kunnollista numeroa.
Avainsanan try
määrittelemän lohkon sisältä siirrytään catch
-lohkoon heti poikkeuksen tapahtuessa. Visualisoidaan tätä lisäämällä tulostuslause try
-lohkossa metodia Integer.parseInt
kutsuvan rivin jälkeen.
Scanner lukija = new Scanner(System.in); System.out.print("Syötä numero: "); try { int numero = Integer.parseInt(lukija.nextLine()); System.out.println("Hienosti syötetty!"); } catch (Exception e) { System.out.println("Et syöttänyt kunnollista numeroa."); }
Syötä numero: 5 Hienosti syötetty!
Syötä numero: enpäs! Et syöttänyt kunnollista numeroa.
Ohjelmalle syötetty merkkijono enpäs!
annetaan parametrina Integer.parseInt
-metodille, joka heittää poikkeuksen jos parametrina saadun merkkijonon muuntaminen luvuksi epäonnistuu. Huomaa että catch
-lohkossa oleva koodi suoritetaan vain poikkeustapauksissa -- muulloin ohjelma ei pääse sinne.
Tehdään luvun muuntajasta hieman hyödyllisempi: Tehdään siitä metodi, joka kysyy numeroa yhä uudestaan kunnes käyttäjä syöttää oikean numeron. Metodista pääsee pois vain jos käyttäjä syöttää oikean luvun.
public int lueLuku(Scanner lukija) { while (true) { System.out.print("Syötä numero: "); try { int numero = Integer.parseInt(lukija.nextLine()); return numero; } catch (Exception e) { System.out.println("Et syöttänyt kunnollista numeroa."); } } }
Metodin lueLuku
kutsuminen voisi toimia esimerkiksi seuraavasti
Syötä numero: enpäs! Et syöttänyt kunnollista numeroa. Syötä numero: Matilla on ovessa tatti. Et syöttänyt kunnollista numeroa. Syötä numero: 43
Metodit ja konstruktorit voivat heittää poikkeuksia. Heitettäviä poikkeuksia on karkeasti ottaen kahdenlaisia. On poikkeuksia jotka on pakko käsitellä, ja on poikkeuksia joita ei ole pakko käsitellä. Pakosti käsiteltävät poikkeukset käsitellään joko try-catch
-lohkossa, tai heittämällä ne ulos metodista.
Ohjelmoinnin perusteiden tehtävän kellosta olio bonusversiossa kerrottiin, että ohjelma saadaan viivyttämään itseään sekunnin verran kutsumalla komentoa Thread.sleep(1000)
. Komento saattaa heittää poikkeuksen, joka on pakko käsitellä. Poikkeuksen käsittely siis tapahtuu try-catch
-lauseella, seuraavassa esimerkissä olemme välittämättä mahdollisista poikkeustilanteista ja jätimme catch
-lohkon tyhjäksi:
try { // nukutaan 1000 millisekuntia Thread.sleep(1000); } catch (Exception e) { // ei tehdä mitään poikkeustilanteessa }
Metodeissa on myös mahdollista jättää poikkeus itse käsittelemättä ja siirtää vastuu poikkeuksen käsittelystä metodin kutsujalle. Vastuun siirto tapahtuu heittämällä poikkeus metodista eteenpäin sanomalla throws Exception
.
public void nuku(int sekuntia) throws Exception { Thread.sleep(sekuntia * 1000); // nyt try-catchia ei tarvita! }
Nyt metodia nuku
-kutsuvan metodin tulee joko käsitellä poikkeus try-catch
-lohkossa, tai siirtää poikkeuksen käsittelyn vastuuta eteenpäin heittää poikkeus eteenpäin. Joskus poikkeuksen käsittelyä pakoillaan viimeiseen asti, ja main
-metodikin heittää poikkeuksen käsiteltäväksi eteenpäin:
public class Paaohjelma { public static void main(String[] args) throws Exception { // ... } }
Tällöin poikkeus päätyy Javan virtuaalikoneelle, joka keskeyttää ohjelman suorituksen poikkeukseen johtavan virheen tapahtuessa.
Osa poikkeuksista, kuten Integer.parseInt
-metodin heittämä NumberFormatException
, on sellaisia joihin ohjelmoijan ei ole pakko varautua. Poikkeukset, joihin käyttäjän ei tarvitse varautua ovat aina myös tyyppiä RuntimeException
, palaamme siihen miksi muuttujilla voi olla useita eri tyyppejä tarkemmin ensi viikolla.
Voimme itse heittää poikkeuksen lähdekoodista throw
-komennolla. Esimerkiksi NumberFormatException
-luokasta luodun poikkeuksen heittäminen tapahtuisi komennolla throw new NumberFormatException()
.
Eräs poikkeus johon käyttäjän ei ole pakko varautua on IllegalArgumentException
. Poikkeuksella IllegalArgumentException
kerrotaan että metodille tai konstruktorille annettujen parametrien arvot ovat vääränlaiset. IllegalArgumentException-poikkeusta käytetään esimerkiksi silloin kun halutaan varmistaa että parametreilla on tietyt arvot. Luodaan luokka Arvosana
, joka saa konstruktorin parametrina kokonaislukutyyppisen arvosanan.
public class Arvosana { private int arvosana; public Arvosana(int arvosana) { this.arvosana = arvosana; } public int getArvosana() { return this.arvosana; } }
Haluamme seuraavaksi validoida Arvosana-luokan konstruktorin parametrina saadun arvon. Arvosanan tulee olla aina välillä 0-5. Jos arvosana on jotain muuta, haluamme heittää poikkeuksen. Lisätään Arvosana
-luokan konstruktoriin ehtolause, joka tarkistaa onko arvosana arvovälin 0-5 ulkopuolella. Jos on, heitetään poikkeus IllegalArgumentException
sanomalla throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5");
.
public class Arvosana { private int arvosana; public Arvosana(int arvosana) { if (arvosana < 0 || arvosana > 5) { throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5"); } this.arvosana = arvosana; } public int getArvosana() { return this.arvosana; } }
Arvosana arvosana = new Arvosana(3); System.out.println(arvosana.getArvosana()); Arvosana virheellinenArvo = new Arvosana(22); // tapahtuu poikkeus, tästä ei jatketa eteenpäin
3 Exception in thread "..." java.lang.IllegalArgumentException: Arvosanan tulee olla välillä 0-5
Harjoitellaan hieman parametrien validointia IllegalArgumentException
-poikkeuksen avulla. Tehtäväpohjassa tulee kaksi luokkaa, Henkilo
ja Laskin
. Muuta luokkia seuraavasti:
Luokan Henkilo
konstruktorin tulee varmistaa että parametrina annettu nimi ei ole null, tyhjä tai yli 40 merkkiä pitkä. Myös iän tulee olla väliltä 0-120. Jos joku edelläolevista ehdoista ei päde, tulee konstruktorin heittää IllegalArgumentException
-poikkeus.
Luokan Laskin
metodeja tulee muuttaa seuraavasti: Metodin kertoma
tulee toimia vain jos parametrina annetaan ei-negatiivinen luku (0 tai suurempi). Metodin binomikerroin
tulee toimia vain jos parametrit ovat ei-negatiivisia ja osajoukon koko on pienempi kuin joukon koko. Jos jompikumpi metodeista saa epäkelpoja arvoja metodikutsujen yhteydessä, tulee metodien heittää poikkeus IllegalArgumentException
.
Kaikki sovelluksessa oleva koodi tulee sijoittaa pakkaukseen sovellus
.
Käytössämme on seuraava rajapinta:
public interface Sensori { boolean onPaalla(); // palauttaa true jos sensori on päällä void paalle(); // käynnistä sensorin void poisPaalta(); // sulkee sensorin int mittaa(); // palauttaa sensorin lukeman jos sensori on päällä // jos sensori ei päällä heittää poikkeuksen IllegalStateException }
Tee luokka Vakiosensori
joka toteuttaa rajapinnan Sensori
.
Vakiosensori on koko ajan päällä. Metodien paalle ja poisPaalta kutsuminen ei tee mitään. Vakiosensorilla tulee olla konstruktori, jonka parametrina on kokonaisluku. Metodikutsu mittaa
palauttaa aina konstruktorille parametrina annetun luvun.
Esimerkki:
public static void main(String[] args) { Vakiosensori kymppi = new Vakiosensori(10); Vakiosensori miinusViis = new Vakiosensori(-5); System.out.println( kymppi.mittaa() ); System.out.println( miinusViis.mittaa() ); System.out.println( kymppi.onPaalla() ); kymppi.poisPaalta(); System.out.println( kymppi.onPaalla() ); }
Tulostuu:
10 -5 true true
Tee luokka Lampomittari
joka toteuttaa rajapinnan Sensori
.
Aluksi lämpömittari on poissa päältä. Kutsuttaessa metodia mittaa
kun mittari on päällä mittari arpoo luvun väliltä -30...30 ja palauttaa sen kutsujalle. Jos mittari ei ole päällä, heitetään poikkeus IllegalStateException.
Tee luokka Keskiarvosensori
joka toteuttaa rajapinnan Sensori.
Keskiarvosensori sisältää useita sensoreita. Rajapinnan Sensori
määrittelemien metodien lisäksi keskiarvosensorilla on metodi
public void lisaaSensori(Sensori lisattava)
jonka avulla keskiarvosensorin hallintaan lisätään uusi sensori.
Keskiarvosensori on päällä silloin kuin kaikki sen sisältävät sensorit ovat päällä. Kun keskiarvosensori käynnistetään, täytyy kaikkien sen sisältävien sensorien käynnistyä jos ne eivät ole käynnissä. Kun keskiarvosensori suljetaan, täytyy ainakin yhden sen sisältävän sensorin mennä pois päältä. Saa myös käydä niin että kaikki sen sisältävät sensorit menevät pois päältä.
Keskiarvosensorin metodi mittaa
palauttaa sen sisältämien sensoreiden lukemien keskiarvon (koska paluuarvo on int
, pyöristyy lukema alaspäin kuten kokonaisluvuilla tehdyissä jakolaskuissa). Jos keskiarvosensorin metodia mittaa
kutsutaan sensorin ollessa poissa päältä, tai jos keskiarvosensorille ei vielä ole lisätty yhtään sensoria heitetään poikkeus IllegalStateException
.
Seuraavassa sensoreja käyttävä esimerkkiohjelma (huomaa, että sekä Lämpömittarin että Keskiarvosensorin konstruktorit ovat parametrittomia):
public static void main(String[] args) { Sensori kumpula = new Lampomittari(); kumpula.paalle(); System.out.println("lämpötila Kumpulassa "+kumpula.mittaa() + " astetta"); Sensori kaisaniemi = new Lampomittari(); Sensori helsinkiVantaa = new Lampomittari(); Keskiarvosensori paakaupunki = new Keskiarvosensori(); paakaupunki.lisaaSensori(kumpula); paakaupunki.lisaaSensori(kaisaniemi); paakaupunki.lisaaSensori(helsinkiVantaa); paakaupunki.paalle(); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); }
tulostuu (tulostetut lukuarvot riippuvat tietenkin arvotuista lämpötiloista):
lämpötila Kumpulassa -7 astetta lämpötila Pääkaupunkiseudulla -10 astetta
Huom: kannatata käyttää Vakiosensori-oliota keskiarvosensorin testaamiseen!
Lisää luokalle Keskiarvosensori metodi public List<Integer> mittaukset()
, joka palauttaa listana kaikkien keskiarvosensorin avulla suoritettujen mittausten tulokset. Seuraavassa esimerkki metodin toiminnasta:
public static void main(String[] args) { Sensori kumpula = new Lampomittari(); Sensori kaisaniemi = new Lampomittari(); Sensori helsinkiVantaa = new Lampomittari(); Keskiarvosensori paakaupunki = new Keskiarvosensori(); paakaupunki.lisaaSensori(kumpula); paakaupunki.lisaaSensori(kaisaniemi); paakaupunki.lisaaSensori(helsinkiVantaa); paakaupunki.paalle(); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); System.out.println("mittaukset: "+paakaupunki.mittaukset()); }
tulostuu (tulostetut lukuarvot riippuvat jälleen arvotuista lämpötiloista):
lämpötila Pääkaupunkiseudulla -10 astetta lämpötila Pääkaupunkiseudulla -4 astetta lämpötila Pääkaupunkiseudulla 5 astetta mittaukset: [-10, -4, 5]
Rajapintaluokilla ei ole metodirunkoa, mutta metodimäärittely on vapaasti rajapinnan suunnittelijan toteutettavissa. Rajapintaluokat voivat määritellä myös poikkeusten heiton. Esimerkiksi seuraavan rajapinnan Tiedostopalvelin
toteuttavat luokat heittävät mahdollisesti poikkeuksen lataa
- ja tallenna
-metodissa.
public interface Tiedostopalvelin { String lataa(String tiedosto) throws Exception; void tallenna(String tiedosto, String merkkijono) throws Exception; }
Jos rajapinta määrittelee metodeille throws Exception
-määreet, eli että metodit heittävät mahdollisesti poikkeuksen, tulee samat määreet olla myös rajapinnan toteuttavassa luokassa. Luokan ei kuitenkaan ole pakko heittää poikkeusta kuten alla olevasta esimerkistä näkee.
public class Tekstipalvelin implements Tiedostopalvelin { private Map<String, String> data; public Tekstipalvelin() { this.data = new HashMap<String, String>(); } @Override public String lataa(String tiedosto) throws Exception { return this.data.get(tiedosto); } @Override public void tallenna(String tiedosto, String merkkijono) throws Exception { this.data.put(tiedosto, merkkijono); } }
Poikkeusten käsittelytoiminnallisuuden sisältämä catch
-lohko määrittelee catch-osion sisällä poikkeuksen johon varaudutaan catch (Exception e)
. Poikkeuksen tiedot tallennetaan e
-muuttujaan.
try { // ohjelmakoodi, joka saattaa heittää poikkeuksen } catch (Exception e) { // poikkeuksen tiedot ovat tallessa muuttujassa e }
Luokka Exception
tarjoaa hyödyllisiä metodeja. Esimerkiksi metodi printStackTrace()
tulostaa polun, joka kertoo mistä päädyttiin poikkeukseen. Tutkitaan seuraavaa metodin printStackTrace()
tulostamaa virhettä.
Exception in thread "main" java.lang.NullPointerException at pakkaus.Luokka.tulosta(Luokka.java:43) at pakkaus.Luokka.main(Luokka.java:29)
Poikkeuspolun lukeminen tapahtuu alhaalta ylöspäin. Alimpana on ensimmäinen kutsu, eli ohjelman suoritus on alkanut luokan Luokka
metodista main()
. Luokan Luokka
main-metodin rivillä 29 on kutsuttu metodia tulosta()
. Metodin tulosta
rivillä 43 on tapahtunut poikkeus NullPointerException
. Poikkeuksen tiedot ovatkin hyvin hyödyllisiä virhekohdan selvittämisessä.
Huomattava osa ohjelmista käsittelee jollain tavalla tallennettua tietoa. Otetaan ensiaskeleet tiedostojen käsittelyyn Javassa. Javan API tarjoaa luokan File, jonka sisältö voidaan lukea kurssilla jo tutuksi tulleen Scanner-luokan avulla.
Luokan File
API-kuvausta lukiessamme huomaamme File
-luokalla on konstruktori File(String pathname)
(Creates a new File instance by converting the given pathname string into an abstract pathname). Voimme siis antaa avattavan tiedoston polun File
-luokan konstruktorille.
NetBeans-ohjelmointiympäristössä tiedostoille on oma välilehti nimeltä Files. Files-välilehdellä on määritelty kaikki projektiin liittyvät tiedostot. Jos projektin juureen, eli ei yhdenkään hakemiston sisälle, lisätään tiedosto, voidaan siihen viitata projektin sisältä suoraan tiedoston nimellä. Tiedosto-olion luominen tapahtuu antamalla sille parametrina polku tiedostoon, esimerkiksi seuraavasti
File tiedosto = new File("tiedoston-nimi.txt");
Scanner-luokan konstruktorille voi antaa myös muita lukemislähteitä kuin System.in
-syöttövirran. Lukemislähteenä voi olla näppäimistön lisäksi muun muassa tiedosto. Scanner tarjoaa tiedoston lukemiseen samat metodit kuin näppäimistöltä syötetyn syötteen lukemiseen. Seuraavassa esimerkissä avataan tiedosto ja tulostetaan kaikki tiedoston sisältämän tekstit System.out.println
-komennolla Lopuksi tiedosto suljetaan komennolla close
.
// tiedosto mistä luetaan File tiedosto = new File("tiedosto.txt"); Scanner lukija = new Scanner(tiedosto); while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); } lukija.close();
Scanner-luokan konstruktori public Scanner(File source)
(Constructs a new Scanner that produces values scanned from the specified file.) heittää FileNotFoundException
-poikkeuksen jos luettavaa tiedostoa ei löydy. Poikkeus FileNotFoundException
ei ole tyyppiä RuntimeException
, joten se tulee joko käsitellä tai heittää eteenpäin. Tässä vaiheessa riittää tietää että ohjelmointiympäristö kertoo jos sinun tulee käsitellä poikkeus erikseen. Luodaan ensin vaihtoehto, jossa poikkeus käsitellään tiedostoa avattaessa.
public void lueTiedosto(File tiedosto) { // tiedosto mistä luetaan Scanner lukija = null; try { lukija = new Scanner(tiedosto); } catch (Exception e) { System.out.println("Tiedoston lukeminen epäonnistui. Virhe: " + e.getMessage()); return; // poistutaan metodista } while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); } lukija.close(); }
Toinen vaihtoehto poikkeuksen käsittelyyn on poikkeuksen käsittelyvastuun siirtäminen metodin kutsujalle. Poikkeuksen käsittelyvastuu siirretään metodin kutsujalle lisäämällä metodiin määre throws PoikkeuksenTyyppi
, eli esimerkiksi throws Exception
sillä kaikki poikkeukset ovat tyyppiä Exception
. Kun metodilla on määre throws Exception
, tietävät kaikki sitä kutsuvat että se saattaa heittää poikkeuksen johon tulee varautua.
public void lueTiedosto(File tiedosto) throws Exception { // tiedosto mistä luetaan Scanner lukija = new Scanner(tiedosto); while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); } lukija.close(); }
Esimerkki avaa tiedoston tiedosto.txt
projektin juuripolusta ja tulostaa sen rivi riviltä käyttäjälle näkyville. Lopuksi lukija suljetaan, jolloin tiedosto myös suljetaan. Määre throws Exception
kertoo että metodi saattaa heittää poikkeuksen. Samanlaisen määreen voi laittaa kaikkiin metodeihin jotka käsittelevät tiedostoja.
Huomaa että Scanner
-olio ei liitä rivinvaihtomerkkejä osaksi nextLine
-metodin palauttamaa merkkijonoa. Yksi vaihtoehto tiedoston lukemiseen siten, että rivinvaihdot säilyvät, on lisätä jokaisen rivin jälkeen rivinvaihtomerkki:
public String lueTiedostoMerkkijonoon(File tiedosto) throws Exception { // tiedosto mistä luetaan Scanner lukija = new Scanner(tiedosto); String merkkijono = ""; while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); merkkijono += rivi; merkkijono += "\n"; } lukija.close(); return merkkijono; }
Koska käytämme tiedoston lukemiseen Scanner
-luokkaa, käytössämme on kaikki Scanner-luokan tarjoamat metodit. Esimerkiksi metodi hasNext()
palauttaa totuusarvon true
, jos luettavassa tiedostossa on vielä luettavaa jäljellä, ja metodi next()
lukee seuraavan sanan metodin palauttamaan String
-olioon.
Seuraava ohjelma luo Scanner
-olion, joka avaa tiedoston tiedosto.txt
. Sen jälkeen se tulostaa joka viidennen sanan tiedostosta.
File tiedosto = new File("tiedosto.txt"); Scanner lukija = new Scanner(tiedosto); int monesko = 0; while (lukija.hasNext()) { monesko++; String sana = lukija.next(); if (monesko % 5 == 0) { System.out.println(sana); } }
Alla on ensin luetun tiedoston sisältämä teksti ja sitten ohjelman tulostus
Poikkeukset (exceptions) ovat "poikkeuksellisia tilanteita" kesken normaalin ohjelmansuorituksen: tiedosto loppuu, merkkijono ei kelpaa kokonaisluvuksi, odotetun olion tilalla onkin null-arvo, taulukon indeksi menee ohjelmointivirheen takia sopimattomaksi, ...
tilanteita" loppuu, odotetun taulukon sopimattomaksi,
Tekstiä tiedostosta luettaessa (tai tiedostoon tallennettaessa) Java joutuu päättelemään käyttöjärjestelmän käyttämän merkistön. Merkistön tuntemusta tarvitaan sekä tekstin tallentamiseen tietokoneen kovalevylle binäärimuotoiseksi että binäärimuotoisen datan tekstiksi kääntämiseksi.
Merkistöihin on kehitetty standardeja, joista "UTF-8" on nykyään yleisin. UTF-8 -merkistö sisältää sekä jokapäiväisessä käytössä olevien aakkosten että erikoisempien merkkien kuten Japanin kanji-merkistön tai shakkipelin nappuloiden tallentamiseen ja lukemiseen tarvittavat tiedot. Ohjelmointimielessä merkistöä voi hieman yksinkertaistaen ajatella hajautustauluna merkistä numeroon ja numerosta merkkiin. Merkistä numeroon oleva hajautustaulu kuvaa minkälaisena binäärilukuna kukin merkki tallennetaan tiedostoon. Numerosta merkkiin oleva hajautustaulu taas kuvaa miten tiedostoa luettaessa saadut luvut muunnetaan merkeiksi.
Lähes jokaisella käyttöjärjestelmävalmistajalla on myös omat standardinsa. Osa tukee ja haluaa osallistua avoimien standardien käyttöön, osa ei. Mikäli sinulla on ongelmia ääkkösellisten sanojen kanssa (eritoten mac ja windows käyttäjät) voit kertoa Scanner
-oliota luodessa käytettävän merkistön. Tällä kurssilla käytämme aina merkistöä "UTF-8".
UTF-8 -merkistöä käyttävän tiedostoa lukevan Scanner-olion voi luoda seuraavasti:
File tiedosto = new File("esimerkkitiedosto.txt"); Scanner lukija = new Scanner(tiedosto, "UTF-8");
Toinen vaihtoehto merkistön asettamiseksi on ympäristömuuttujan käyttäminen. Macintosh ja Windows-käyttäjät voivat asettaa ympäristömuuttujan JAVA_TOOL_OPTIONS
arvoksi merkkijonon -Dfile.encoding=UTF8
. Tällöin Java käyttää oletuksena aina UTF-8-merkistöä.
Tee luokka Tulostaja
ja sille konstruktori public Tulostaja(String tiedostonNimi)
, joka saa parametrinaan tiedoston nimeä vastaavan merkkijonon sekä metodi public void tulostaRivitJoilla(String sana)
tulostaa tiedostosta ne rivit, joilla esiintyy parametrina oleva sana (pienet ja isot kirjaimet erotellaan tehtävässä, eli esim. "koe" ja "Koe" eivät ole sama sana), rivit tulostetaan samassa järjestyksessä missä ne ovat tiedostossa.
Jos parametri on tyhjä merkkijono, tulostuu koko tiedosto.
Jos tiedostoa ei ole olemassa, heittää konstruktori aiheutuvan poikkeuksen eteenpäin, eli try-catch-komentoa ei tarvita, riittää määritellä konstruktori seuraavasti:
public Tulostaja { public Tulostaja(String tiedostonNimi) throws Exception { // ... } // ... }
Projektisi default-pakkauksessa on testausta varten tiedosto testitiedosto.txt. Ohjelmasta avatessa tiedoston nimeksi tulee antaa src/testitiedosto.txt
. Tiedoston sisältö on seuraava:
Siinä vanha Väinämöinen katseleikse käänteleikse Niin tuli kevätkäkönen näki koivun kasvavaksi Miksipä on tuo jätetty koivahainen kaatamatta Sanoi vanha Väinämöinen
Seuraavassa esimerkki ohjelman toiminnasta testitiedostolla:
Tulostaja tulostaja = new Tulostaja("src/testitiedosto.txt"); tulostaja.tulostaRivitJoilla("Väinämöinen"); System.out.println("-----"); tulostaja.tulostaRivitJoilla("Frank Zappa"); System.out.println("-----"); tulostaja.tulostaRivitJoilla(""); System.out.println("-----");
Tulostuu:
Siinä vanha Väinämöinen Sanoi vanha Väinämöinen ----- ----- Siinä vanha Väinämöinen katseleikse käänteleikse Niin tuli kevätkäkönen näki koivun kasvavaksi Miksipä on tuo jätetty koivahainen kaatamatta Sanoi vanha Väinämöinen
Projektipohjasta löytyy myös koko Kalevala, tiedoston nimi on src/kalevala.txt
Tässä tehtävässä tehdään sovellus tiedoston rivi- ja merkkimäärän laskemiseen.
Tee pakkaukseen tiedosto
luokka Analyysi
, jolla on konstruktori public Analyysi(File tiedosto)
. Toteuta luokalle metodi public int rivimaara()
, joka palauttaa konstruktorille annetun tiedoston rivimäärän.
Metodi ei saa olla "kertakäyttöinen", eli sen pitää tuottaa oikea tulos myös usealla peräkkäisellä kutsulla. Huomaa, että kun teet tiedostoa vastaavan Scanner-olion, ja luet tiedoston koko sisällön nextLine
-komennoilla, et voi käyttää enää samaa skanneria tiedoston uudelleenlukemiseen!
Huom: jos testit sanovat timeout, et todennäköisesti muista lukea tiedostoa ollenkaan, eli nextLine
-kutsut puuttuvat!
Toteuta luokkaan Analyysi
metodi public int merkkeja()
, joka palauttaa luokan konstruktorille annetun tiedoston merkkien määrän.
Metodi ei saa olla "kertakäyttöinen", eli sen pitää tuottaa oikea tulos myös usealla peräkkäisellä kutsulla.
Voit itse päättää miten reagoidaan jos konstruktorin parametrina saatua tiedostoa ei ole olemassa.
Projektisi testipakkauksessa on testausta varten tiedosto testitiedosto.txt. Ohjelmasta avatessa tiedoston nimeksi tulee antaa test/testitiedosto.txt
. Tiedoston sisältö on seuraava:
rivejä tässä on 3 ja merkkejä koska rivinvaihdotkin ovat merkkejä
Ohjelman toiminta testaustiedostolla:
File tiedosto = new File("src/testitiedosto.txt"); Analyysi analyysi = new Analyysi(tiedosto); System.out.println("Rivejä: " + analyysi.rivimaara()); System.out.println("Merkkejä: " + analyysi.merkkeja());
Rivejä: 3 Merkkejä: 67
Tee luokka Sanatutkimus, jolla voi tehdä erilaisia tutkimuksia tiedoston sisältämille sanoille. Toteuta luokka pakkaukseen sanatutkimus
.
Kotimaisten kielten tutkimuskeskus (Kotus) on julkaissut netissä suomen kielen sanalistan. Tässä tehtävässä käytetään listan muokattua versiota, joka löytyy tehtäväpohjasta src
-hakemistosta nimellä sanalista.txt
, eli suhteellisesta polusta "src/sanalista.txt"
.
Koska sanalista on varsin pitkä, on projektissa testausta varten myös pienilista.txt
joka löytyy polusta "src/pienilista.txt"
.
Mikäli sinulla on ongelmia ääkkösellisten sanojen kanssa (mac ja windows käyttäjät) luo Scanner
-olio antaen sille parametrina merkistö "UTF-8" seuraavasti: Scanner lukija = new Scanner(tiedosto, "UTF-8");
Ongelmat liittyvät erityisesti testien suoritukseen.
Luo Sanatutkimus-luokalle konstruktori public Sanatutkimus(File tiedosto)
joka luo uuden Sanatutkimus-olion, joka tutkii parametrina annettavaa tiedostoa.
Tee luokkaan metodi public int sanojenMaara()
, joka lukee tiedostossa olevat sanat ja tulostaa niiden määrän. Tässä vaiheessa sanoilla ei tarvitse tehdä mitään, riittää laskea niiden määrä. Voit olettaa tässä tehtävässä, että tiedostossa on vain yksi sana riviä kohti.
Tee luokkaan metodi public List<String> kirjaimenZSisaltavatSanat()
, joka palauttaa tiedoston kaikki sanat, joissa on z-kirjain. Tällaisia sanoja ovat esimerkiksi jazz ja zombi.
Tee luokkaan metodi public List<String> kirjaimeenLPaattyvatSanat()
, joka palauttaa tiedoston kaikki sanat, jotka päättyvät l-kirjaimeen. Tällaisia sanoja ovat esimerkiksi kannel ja sammal.
Huom! Jos luet tiedoston uudestaan ja uudestaan jokaisessa metodissa huomaat viimeistään tässä vaiheessa copy-paste koodia. Kannattaa miettiä olisiko tiedoston lukeminen helpompi tehdä osana konstruktoria tai metodina, jota konstruktori kutsuu. Metodeissa voitaisiin käyttää tällöin jo luettua listaa ja luoda siitä aina uusi, hakuehtoihin sopiva lista. Viikolle 612 on tulossa oikeaoppinen tapa copypasten eliminointiin.
Tee luokkaan metodi public List<String> palindromit()
, joka palauttaa tiedoston kaikki sanat, jotka ovat palindromeja. Tällaisia sanoja ovat esimerkiksi ala ja enne.
Tee luokkaan metodi public List<String> kaikkiVokaalitSisaltavatSanat()
, joka palauttaa tiedoston kaikki sanat, jotka sisältävät kaikki suomen kielen vokaalit (aeiouyäö). Tällaisia sanoja ovat esimerkiksi myöhäiselokuva ja ympäristönsuojelija.
Kuten muistamme, voi HashMapiin tallettaa tiettyä avainta kohti vaan yhden arvon. Seuraavassa esimerkissä tallennamme henkilöiden puhelinnumeroita HashMap:iin.
Map<String, String> puhelinnumerot = new HashMap<String, String>(); puhelinnumerot.put("Pekka", "040-12348765"); System.out.println( "Pekan numero: "+ puhelinnumerot.get("Pekka") ); puhelinnumerot.put("Pekka", "09-111333"); System.out.println( "Pekan numero: "+ puhelinnumerot.get("Pekka") );
Kuten odotettua, tulostus kertoo, että
Pekan numero: 040-12348765 Pekan numero: 09-111333
Entä jos haluaisimmekin tallettaa yhtä avainta kohti useita arvoja, eli esim yhtä henkilöä kohti monta puhelinnumeroa? Onnistuuko se HashMap:in avulla? Kyllä, esim. tallettamalla HashMap:iin Stringien sijaan esim. ArrayList:eja arvoiksi, voidaan yhteen avaimeen "liittää" useampia oliota. Muutetaan puhelinnumeroiden talletustapaa seuraavasti:
Map<String, ArrayList<String>> puhelinnumerot = new HashMap<String, ArrayList<String>>();
Nyt siis HashMapissa jokaiseen avaimeen littyy lista. Vaikka new-komento luokin HashMapin, on mapin sisälle talletettavat listat luotava erikseen. Seuraavassa lisätään HashMapiin Pekalle kaksi numeroa ja tulostetaan ne:
Map<String, ArrayList<String>> puhelinnumerot = new HashMap<String, ArrayList<String>>(); // liitetään Pekka-nimeen ensin tyhjä ArrayList puhelinnumerot.put( "Pekka", new ArrayList<String>() ); // ja lisätään Pekkaa vastaavalle listalle puhelinnumero puhelinnumerot.get("Pekka").add("040-12348765"); // ja lisätään toinenkin puhelinnumero puhelinnumerot.get("Pekka").add("09-111333"); System.out.println( "Pekan numerot: "+ puhelinnumerot.get("Pekka") );
Tulostuu
Pekan numero: [040-12348765, 09-111333]
Määrittelimme muuttujan puhelinnumero tyypiksi Map<String, ArrayList<String>>
eli Map jonka avaimena on merkkijono ja arvona merkkijonoja sisältävä lista. Konkreettinen toteutus, eli luotu olio oli HasMap. Olisimme voineet määritellä muuttujan myös seuraavasti:
Map<String, List<String>> puhelinnumero = new HashMap<String, List<String>>();
Eli nyt muuttujan tyyppi on Map, jonka avaimena on merkkijono ja arvona merkkijonoja sisältävä List
, joka siis on rajapinta joka määrittelee listatoiminnallisuuden, esim. ArrayList toteuttaa tämän rajapinnan. Konkreettinen olio on HashMap.
HashMap:iin talletettavat arvot siis ovat List<String>
-rajapinnan toteuttavia konkreettisia olioita, esim. ArrayListeja. Eli lisäys HashMapiin tapahtuu edelleen seuraavasti:
// liitetään Pekka-nimeen ensin tyhjä ArrayList puhelinnumerot.put( "Pekka", new ArrayList<String>() ); // ...
Jatkossa pyrkimyksemme on käyttää tyyppimäärittelyissä konkreettisten luokkien, esim. HashMap
ja ArrayList
sijaan niitä vastaavia rajapintoja Map
ja List
.
Rajapinta Set
kuvaa joukon toiminnallisuutta. Toisin kuin listalla, on joukossa kutakin alkioita korkeintaan yksi kappale, eli yhtään samanlaista oliota ei ole kahdesti. Olioiden samankaltaisuuden tarkistaminen toteutetaan equals
ja hashCode
-metodeja käyttämällä.
Yksi rajapinnan Set
toteuttava luokka on HashSet
. Toteutetaan sen avulla luokka Tehtavakirjanpito
, joka tarjoaa mahdollisuuden tehtävien kirjanpitoon ja tehtyjen tehtävien tulostamiseen. Oletetaan että tehtävät ovat aina kokonaislukuja.
public class Tehtavakirjanpito { private Set<Integer> tehdytTehtavat; public Tehtavakirjanpito() { this.tehdytTehtavat = new HashSet<Integer>(); } public void lisaa(int tehtava) { this.tehdytTehtavat.add(tehtava); } public void tulosta() { for (int tehtava: this.tehdytTehtavat) { System.out.println(tehtava); } } }
Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito(); kirjanpito.lisaa(1); kirjanpito.lisaa(1); kirjanpito.lisaa(2); kirjanpito.lisaa(3); kirjanpito.tulosta();
1 2 3
Yllä oleva ratkaisu toimii tilanteessa, jossa emme tarvitse tietoa käyttäjistä eri käyttäjien tekemistä tehtävistä. Muutetaan tehtävien tallennuslogiikkaa siten, että tehtävät tallennetaan käyttäjäkohtaisesti hajautustaulua hyödyntäen. Käyttäjät tunnistetaan käyttäjän yksilöivällä merkkijonolla (esimerkiksi opiskelijanumero), ja jokaiselle käyttäjälle on oma joukko tehdyistä tehtävistä.
public class Tehtavakirjanpito { private Map<String, Set<Integer>> tehdytTehtavat; public Tehtavakirjanpito() { this.tehdytTehtavat = new HashMap<String, Set<Integer>>(); } public void lisaa(String kayttaja, int tehtava) { // huomaa miten uudelle käyttäjälle on lisättävä HashMapiin ensin tyhjä tehtäväjoukko if (!this.tehdytTehtavat.containsKey(kayttaja)) { this.tehdytTehtavat.put(kayttaja, new HashSet<Integer>()); } // haetaan ensin käyttäjän tehtävät sisältävä joukko ja tehdään siihen lisäys Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja); tehdyt.add(tehtava); // edellinen olisi onnitunut myös ilman apumuuttujaa seuraavasti // this.tehdytTehtavat.get(kayttaja).add(tehtava); } public void tulosta() { for (String kayttaja: this.tehdytTehtavat.keySet()) { System.out.println(kayttaja + ": " + this.tehdytTehtavat.get(kayttaja)); } } }
Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito(); kirjanpito.lisaa("Mikael", 3); kirjanpito.lisaa("Mikael", 4); kirjanpito.lisaa("Mikael", 3); kirjanpito.lisaa("Mikael", 3); kirjanpito.lisaa("Pekka", 4); kirjanpito.lisaa("Pekka", 4); kirjanpito.lisaa("Matti", 1); kirjanpito.lisaa("Matti", 2); kirjanpito.tulosta();
Matti: [1, 2] Pekka: [4] Mikael: [3, 4]
Huomaamme että käyttäjien nimet eivät tulostu esimerkissä järjestyksessä. Tämä selittyy sillä että HashMap
-tyyppisessä hajautustaulussa alkioiden tallennus tapahtuu hashCode
-metodin palauttaman hajautusarvon perusteella, eikä se liity millään tavalla alkioiden järjestykseen.
Tehdään hieman laajennettu versio viikolla 17 tehdystä sanakirjasta. Tehtävänäsi on toteuttaa pakkaukseen sanakirja
luokka OmaUseanKaannoksenSanakirja
, joka voi tallettaa yhden tai useamman käännöksen jokaiselle sanalle. Luokan tulee toteuttaa tehtäväpohjassa annettu rajapinta UseanKaannoksenSanakirja
, jossa on seuraavat toiminnot:
public void lisaa(String sana, String kaannos)
public Set<String> kaanna(String sana)
Set
-olion, jossa on kaikki käännökset sanalle, tai null
-viitteen, jos sanaa ei ole sanakirjassapublic void poista(String sana)
Käännökset kannattanee tallentaa yllä olevan esimerkin Tehtavakirjanpito tapaan Map<String, Set<String>>
-tyyppiseen oliomuuttujaan.
Rajapinnan koodi:
package sanakirja; import java.util.Set; public interface UseanKaannoksenSanakirja { void lisaa(String sana, String kaannos); Set<String> kaanna(String sana); void poista(String sana); }
Esimerkkiohjelma:
UseanKaannoksenSanakirja sanakirja = new OmaUseanKaannoksenSanakirja(); sanakirja.lisaa("kuusi", "six"); sanakirja.lisaa("kuusi", "spruce"); sanakirja.lisaa("pii", "silicon"); sanakirja.lisaa("pii", "pi"); System.out.println(sanakirja.kaanna("kuusi")); sanakirja.poista("pii"); System.out.println(sanakirja.kaanna("pii"));
Tulostuu:
[six, spruce] null
Tehtävänäsi on toteuttaa pakkaukseen tyokalut
luokka OmaDuplikaattienPoistaja
, joka tallettaa annetut merkkijonot siten, että annetuista merkkijonoista poistetaan samanlaiset merkkijonot (eli duplikaatit). Lisäksi luokka pitää kirjaa duplikaattien määrästä. Luokan tulee toteuttaa tehtäväpohjassa annettu rajapinta DuplikaattienPoistaja
, jossa on seuraavat toiminnot:
public void lisaa(String merkkijono)
public int getHavaittujenDuplikaattienMaara()
public Set<String> getUniikitMerkkijonot()
Set<String>
-rajapinnan toteuttavan olion, jossa on kaikki uniikit lisätyt merkkijonot (ei siis duplikaatteja!). Jos merkkijonoja ei ole, palautetaan tyhjä joukko-olio.public void tyhjenna()
Rajapinnan koodi:
package tyokalut; import java.util.Set; public interface DuplikaattienPoistaja { void lisaa(String merkkijono); int getHavaittujenDuplikaattienMaara(); Set<String> getUniikitMerkkijonot(); void tyhjenna(); }
Rajapintaa voi käyttää esimerkiksi näin:
public static void main(String[] args) { DuplikaattienPoistaja poistaja = new OmaDuplikaattienPoistaja(); poistaja.lisaa("eka"); poistaja.lisaa("toka"); poistaja.lisaa("eka"); System.out.println("Duplikaattien määrä nyt: " + poistaja.getHavaittujenDuplikaattienMaara()); poistaja.lisaa("vika"); poistaja.lisaa("vika"); poistaja.lisaa("uusi"); System.out.println("Duplikaattien määrä nyt: " + poistaja.getHavaittujenDuplikaattienMaara()); System.out.println("Uniikit merkkijonot: " + poistaja.getUniikitMerkkijonot()); poistaja.tyhjenna(); System.out.println("Duplikaattien määrä nyt: " + poistaja.getHavaittujenDuplikaattienMaara()); System.out.println("Uniikit merkkijonot: " + poistaja.getUniikitMerkkijonot()); }
Yllä oleva ohjelma tulostaisi: (merkkijonojen järjestys saa vaihdella, sillä ei ole merkitystä)
Duplikaattien määrä nyt: 1 Duplikaattien määrä nyt: 2 Uniikit merkkijonot: [eka, toka, vika, uusi] Duplikaattien määrä nyt: 0 Uniikit merkkijonot: []
Kuten muistamme, oliomuuttujat ovat viitetyyppisiä, eli muuttuja ei tallenna olioa itseään vaan viitteen olioon. Vastaavasti jos olio laitetaan esim. ArrayListiin, ei listalle talleteta olioa itseään vaan viite olioon. Mikään ei estäkään tallentamasta samaan olioon viitettä esim. useaan listaan tai HashMapiin.
Tarkastellaan esimerkkinä kirjastoa joka tallettaa kirjat hashMapeihin sekä kirjailijan että kirjan isbn-numeron perusteella. Tämän lisäksi kirjasto pitää kaikkia lainassa olevia sekä hyllyssä olevia kirjoja omalla listallaan.
public class Kirja { private String ISBN; private String kirjailija; private String nimi; private int vuosi; // ... } public class Kirjasto { private Map< String, Kirja> kirjaIsbnNumeronPerusteella; private Map< String, List<String>> kirjatKirjailijanPerusteella; private List<Kirja> lainassaOlevatKirjat; private List<Kirja> hyllyssaOlevatKirjat; public void lisaaKirjaKokoelmaan(Kirja uusiKirja){ kirjaIsbnNumeronPerusteella.put(uusiKirja.getIsbn(), uusiKirja); kirjatKirjailijanPerusteella.get(uusikirja.getKirjailija()).add(uusiKirja); hyllyssaOlevatKirjat.add(uusiKirja); } public Kirja haeKirjaIsbnNumeronPerusteella(String isbn){ return kirjaIsbnNumeronPerusteella.get(isbn); } // ... }
Jos olio on yhtäaikaa useassa kokoelmassa (listalla, joukossa tai map-rakenteessa), on kiinnitettävä erityistä huomiota, että kokoelmien tila on konsistentti. Jos esim. kirja päätetään poistaa, on se poistettava molemmista mapeista sekä lainassa/hyllyssä olevia kuvaavalta listalta.
Huomaathan että yliopistoon hakevien tulee tehdä koko tehtävä.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Tehdään sovellus jonka avulla on mahdollista hallinnoida ihmisten puhelinnumeroita ja osoitteita.
Tehtävän voi suorittaa 1-5 pisteen laajuisena. Yhden pisteen laajuuteen on toteutettava seuraavat toiminnot:
kahteen pisteeseen vaaditaan edellisten lisäksi
kolmeen pisteeseen vaaditaan edellisten lisäksi
neljään pisteeseen vaaditaan toiminto
ja täysiin pisteeseen vaaditaan vielä
Esimerkki ohjelman toiminnasta:
numerotiedustelu käytettävissä olevat komennot: 1 lisää numero 2 hae numerot 3 hae puhelinnumeroa vastaava henkilö 4 lisää osoite 5 hae henkilön tiedot 6 poista henkilön tiedot 7 filtteröity listaus x lopeta komento: 1 kenelle: pekka numero: 040-123456 komento: 2 kenen: jukka ei löytynyt komento: 2 kenen: pekka 040-123456 komento: 1 kenelle: pekka numero: 09-222333 komento: 2 kenen: pekka 040-123456 09-222333 komento: 3 numero: 02-444123 ei löytynyt komento: 3 numero: 09-222333 pekka komento: 5 kenen: pekka osoite ei tiedossa puhelinnumerot: 040-123456 09-222333 komento: 4 kenelle: pekka katu: ida ekmanintie kaupunki: helsinki komento: 5 kenen: pekka osoite: ida ekmanintie helsinki puhelinnumerot: 040-123456 09-222333 komento: 4 kenelle: jukka katu: korsontie kaupunki: vantaa komento: 5 kenen: jukka osoite: korsontie vantaa ei puhelinta komento: 7 hakusana (jos tyhjä, listataan kaikki): kk jukka osoite: korsontie vantaa ei puhelinta pekka osoite: ida ekmanintie helsinki puhelinnumerot: 040-123456 09-222333 komento: 7 hakusana (jos tyhjä, listataan kaikki): vantaa jukka osoite: korsontie vantaa ei puhelinta komento: 7 hakusana (jos tyhjä, listataan kaikki): seppo ei löytynyt komento: 6 kenet: jukka komento: 5 kenen: jukka ei löytynyt komento: x
Huomioita:
Ohjelmoinnin perusteiden kuudennellaKuudennella viikolla teimme lintubongaria varten havaintotietokannan. Jatkamme hieman samasta teemasta, tällä kertaa teemme rengastustoimistolle ohjelman, jonka avulla pidetään kirjaa tiettynä vuotena rengastettujen lintujen havaintopaikoista.
HUOM: saatat törmätä tehtävässä kummalliseen virheilmoitukseen NoSuchMethodError: Lintu.equals(LLintu;)Z
, jos näin käy suorita clean and build eli paina harja ja vasara -kuvakkeesta.
Rengastustoimisto tallettaa tiettynä vuotena rengastettettujen lintujen tiedot Lintu
-olioihin:
public class Lintu { private String nimi; private String latinankielinenNimi; private int rengastusvuosi; public Lintu(String nimi, String latinankielinenNimi, int rengastusvuosi) { this.nimi = nimi; this.latinankielinenNimi = latinankielinenNimi; this.rengastusvuosi = rengastusvuosi; } @Override public String toString() { return this.latinankielinenNimi + "(" + this.rengastusvuosi + ")"; } }
Ideana on toteuttaa rengastustoimistoon toiminnallisuus jonka avulla voidaan pitää kirjaa tiettynä vuotena rengastettujen lintujen havaintojen lukumääristä ja havaintopaikoista. Havaintomääriä ja -paikkoja ei kuitenkaan talleteta Lintu-olioihin, vaan erilliseen HashMap:iin jonka avaimena käytetään Lintu-olioita. Kuten muistamme viikolta 28, on tälläisessä tapauksessa toteutettava luokalle Lintu
metodit equals(Object t)
ja hashCode()
.
Joillain linnuilla on useita suomenkielisä nimiä (esim. punakottaraisesta käytetään edelleen joskus sen vanhaa nimitystä rusokottarainen), latinankielinen nimi on kuitenkin aina yksikäsitteinen. Tee luokalle Lintu
equals-
ja hashCode-
metodit jotka toimivat siten, että lintu-oliot tulkitaan samoiksi jos niiden latinankielinen nimi ja rengastusvuosi ovat samat.
Esimerkki:
Lintu lintu1 = new Lintu("punakottarainen", "Sturnus roseus", 2012); Lintu lintu2 = new Lintu("rusokottarainen", "Sturnus roseus", 2012); Lintu lintu3 = new Lintu("varis", "Corvus corone cornix", 2012); Lintu lintu4 = new Lintu("punakottarainen", "Sturnus roseus", 2000); System.out.println( lintu1.equals(lintu2)); // ovat sama koska sama latinankielinen nimi ja rengastusvuosi System.out.println( lintu1.equals(lintu3)); // eivät ole sama koska latinankielinen nimi eri System.out.println( lintu1.equals(lintu4)); // eivät ole sama koska eri rengastusvuosi System.out.println( lintu1.hashCode()==lintu2.hashCode() );
tulostuu:
true false false true
Rengastustoimistolla on kaksi metodia: public void havaitse(Lintu lintu, String paikka)
lisää linnulle havainnon ja havaintopaikan ja metodi public void havainnot(Lintu lintu)
tulostaa alla olevan esimerkin mukaisesti parametrina olevan linnun havaintojen määrän ja havaintopaikat. Havaintopaikkojen tulostusjärjestyksellä ei ole testien läpimenon kannalta merkitystä.
Rengastustoimisto tallettaa havaintopaikat Map<Lintu, List<String>>
-tyyppiseen oliomuuttujaan. Havaintojen lukumäärä selviää havaintopaikkojen listan pituudesta. Tarvittaessa voit ottaa tehtävään mallia luvusta 16.
Esimerkki rengastustoimiston käytöstä:
Rengastustoimisto kumpulanRengas = new Rengastustoimisto(); kumpulanRengas.havaitse( new Lintu("punakottarainen", "Sturnus roseus", 2012), "Arabia" ); kumpulanRengas.havaitse( new Lintu("rusokottarainen", "Sturnus roseus", 2012), "Vallila" ); kumpulanRengas.havaitse( new Lintu("harmaalokki", "Larus argentatus", 2008), "Kumpulanmäki" ); kumpulanRengas.havaitse( new Lintu("punakottarainen", "Sturnus roseus", 2008), "Mannerheimintie" ); kumpulanRengas.havainnot( new Lintu("rusokottarainen", "Sturnus roseus", 2012 ) ); System.out.println("--"); kumpulanRengas.havainnot( new Lintu("harmaalokki", "Larus argentatus", 2008 ) ); System.out.println("--"); kumpulanRengas.havainnot( new Lintu("harmaalokki", "Larus argentatus", 1980 ) );
tulostuu:
Sturnus roseus (2012) havaintoja: 2 Arabia Vallila -- Larus argentatus (2008) havaintoja: 1 Kumpulanmäki -- Larus argentatus (1980) havaintoja: 0
Olemme aiemmissa kappaleissa törmänneet tilanteisiin, joissa muuttujilla on oman tyyppinsä lisäksi muita tyyppejä. Esimerkiksi kappaleessa 45 huomasimme että kaikki oliot ovat tyyppiä Object
. Jos olio on jotain tiettyä tyyppiä, voidaan se myös esittää Object
-tyyppisenä muuttujana. Esimerkiksi String
on myös tyyppiä Object
, joten kaikki String
-tyyppiset muuttujat voidaan esitellä Object
tyypin avulla.
String merkkijono = "merkkijono"; Object merkkijonoString = "toinen merkkijono";
Merkkijono-olion asettaminen Object
-tyyppiseen viitteeseen onnistuu.
String merkkijono = "merkkijono"; Object merkkijonoString = merkkijono;
Toiseen suuntaan asettaminen ei onnistu. Koska Object
-tyyppiset muuttujat eivät ole tyyppiä String
, ei object-tyyppistä muuttujaa voi asettaa String
-tyyppiseen muuttujaan.
Object merkkijonoString = "toinen merkkijono"; String merkkijono = merkkijonoString; // EI ONNISTU!
Mistä tässä oikein on kyse?
Muuttujilla on oman tyyppinsä lisäksi aina perimiensä luokkien ja toteuttamiensa rajapintojen tyypit. Luokka String
perii Object
-luokan, joten String
-oliot ovat aina myös tyyppiä Object
. Luokka Object
ei peri String
-luokkaa, joten Object
-tyyppiset muuttujat eivät ole automaattisesti tyyppiä String
. Tutustutaan tarkemmin String
-luokan API-dokumentaatioon, erityisesti HTML-sivun yläosaan.
String-luokan API-dokumentaatio alkaa yleisellä otsakkeella jota seuraa luokan pakkaus (java.lang
). Pakkauksen jälkeen tulee luokan nimi (Class String
), jota seuraa luokan perintähierarkia.
java.lang.Object java.lang.String
Perintähierarkia listaa luokat, jotka luokka on perinyt. Perityt luokat listataan perimisjärjestyksessä, tarkasteltava luokka aina alimpana. String-luokan perintähierarkiasta näemme, että String
-luokka perii luokan Object
. Javassa jokainen luokka voi periä korkeintaan yhden luokan, mutta välillisesti niitä voi periä useampia.
Perintähierarkiaa voi ajatella myös listana tyypeistä, joita olio toteuttaa.
Se, että kaikki oliot ovat tyyppiä Object
helpottaa ohjelmointia. Jos tarvitsemme metodissa vain Object
-luokassa määriteltyjä toimintoja, voimme käyttää metodin parametrina tyyppiä Object
. Koska kaikki oliot ovat myös tyyppiä object, voi metodille antaa minkä tahansa olion parametrina. Luodaan metodi tulostaMonesti
, joka saa parametrinaan Object
-tyyppisen muuttujan ja tulostusten lukumäärän.
public class Tulostin { ... public void tulostaMonesti(Object object, int kertaa) { for (int i = 0; i < kertaa; i++) { System.out.println(object.toString()); } } ... }
Metodille tulostaMonesti
voi antaa parametrina minkä tahansa olion. Metodin tulostaMonesti
sisässä oliolla on käytössään vain Object
-luokassa määritellyt metodit koska olio esitellään metodissa Object
-tyyppisenä.
Tulostin tulostin = new Tulostin(); String merkkijono = " o "; List<String> sanat = new ArrayList<String>(); sanat.add("polymorfismi"); sanat.add("perintä"); sanat.add("kapselointi"); sanat.add("abstrahointi"); tulostin.tulostaMonesti(merkkijono, 2); tulostin.tulostaMonesti(sanat, 3);
o o [polymorfismi, perintä, kapselointi, abstrahointi] [polymorfismi, perintä, kapselointi, abstrahointi] [polymorfismi, perintä, kapselointi, abstrahointi]
Jatketaan String
-luokan API-kuvauksen tarkastelua. Kuvauksessa olevaa perintähierarkiaa seuraa listaus luokan toteuttamista rajapinnoista.
All Implemented Interfaces: Serializable, CharSequence, Comparable<String>
Luokka String
toteuttaa rajapinnat Serializable
, CharSequence
, ja Comparable<String>
. Myös rajapinta on tyyppi. Luokan String API-kuvauksen mukaan String-olion tyypiksi voi asettaa seuraavat rajapinnat.
Serializable serializableString = "merkkijono"; CharSequence charSequenceString = "merkkijono"; Comparable<String> comparableString = "merkkijono";
Koska metodeille voidaan määritellä metodin parametrin tyyppi, voimme määritellä metodeja jotka vastaanottavat tietyn rajapinnan toteuttavan olion. Kun metodille määritellään parametrina rajapinta, sille voidaan antaa parametrina mikä tahansa olio joka toteuttaa kyseisen rajapinnan, metodi ei välitä olion oikeasta tyypistä.
Täydennetään Tulostin
-luokkaa siten, että sillä on metodi CharSequence
-rajapinnan toteuttavien olioiden merkkien tulostamiseen. Rajapinta CharSequence
tarjoaa muunmuassa metodit int length()
, jolla saa merkkijonon pituuden, ja char charAt(int index)
, jolla saa merkin tietyssä indeksissä.
public class Tulostin { ... public void tulostaMonesti(Object object, int kertaa) { for (int i = 0; i < kertaa; i++) { System.out.println(object.toString()); } } public void tulostaMerkit(CharSequence charSequence) { for (int i = 0; i < charSequence.length(); i++) { System.out.println(charSequence.charAt(i); } } ... }
Metodille tulostaMerkit
voi antaa minkä tahansa CharSequence
-rajapinnan toteuttavan olion. Näitä on muunmuassa String
ja merkkijonojen rakentamisessa usein Stringiä tehokkaampi StringBuilder
. Metodi tulostaMerkit
tulostaa annetun olion jokaisen merkin omalle rivilleen.
Tulostin tulostin = new Tulostin(); String mjono = "toimii"; tulostin.tulostaMerkit(mjono);
t o i m i i
Tässä tehtävässä teemme eliöita ja eliöistä koostuvia laumoja jotka liikkuvat ympäriinsä. Eliöiden sijaintien ilmoittamiseen käytetään kaksiulotteista koordinaatistoa. Jokaiseen sijaintiin liittyy kaksi lukua, x
- ja y
-koordinaatti. Koordinaatti x
kertoo, kuinka pitkällä "nollapisteestä" mitattuna sijainti on vaakasuunnassa, ja koordinaatti y
vastaavasti kuinka pitkällä sijainti on pystysuunnassa. Jos koordinaatiston käsite ei ole tuttu, voit lukea siitä lisää esimerkiksi wikipediasta.
Tehtävän mukana tulee rajapinta Siirrettava
, joka kuvaa asiaa jota voidaan siirtää paikasta toiseen. Rajapinta sisältää metodin void siirra(int dx, int dy)
. Parametri dx
kertoo, paljonko asia siirtyy x-akselilla ja dy
y-akselilla.
Tehtävässä toteutat luokat Elio
ja Lauma
, jotka molemmat ovat siirrettäviä. Toteuta kaikki toiminnallisuus pakkaukseen siirrettava
.
Luo pakkaukseen siirrettava
luokka Elio
, joka toteuttaa rajapinnan Siirrettava
. Eliön tulee tietää oma sijaintinsa (x, y -koordinaatteina). Luokan Elio
APIn tulee olla seuraava:
"x: 3; y: 6"
. Huomaa että koordinaatit on erotettu puolipisteellä (;
)dx
sisältää muutoksen koordinaattiin x
, muuttuja dy
sisältää muutoksen koordinaattiin y
. Esimerkiksi jos muuttujan dx
arvo on 5, tulee oliomuuttujan x
arvoa kasvattaa viidelläKokeile luokan Elio
toimintaa seuraavalla esimerkkikoodilla.
Elio elio = new Elio(20, 30); System.out.println(elio); elio.siirra(-10, 5); System.out.println(elio); elio.siirra(50, 20); System.out.println(elio);
x: 20; y: 30 x: 10; y: 35 x: 60; y: 55
Luo seuraavaksi pakkaukseen siirrettava
luokka Lauma
, joka toteuttaa rajapinnan Siirrettava
. Lauma koostuu useasta Siirrettava
-rajapinnan toteutavasta oliosta, jotka tulee tallettaa esimerkiksi listarakenteeseen.
Luokalla Lauma
tulee olla seuraavanlainen API.
Siirrettava
-rajapinnan toteuttavan olionKokeile ohjelmasi toimintaa alla olevalla esimerkkikoodilla.
Lauma lauma = new Lauma(); lauma.lisaaLaumaan(new Elio(73, 56)); lauma.lisaaLaumaan(new Elio(57, 66)); lauma.lisaaLaumaan(new Elio(46, 52)); lauma.lisaaLaumaan(new Elio(19, 107)); System.out.println(lauma);
x: 73; y: 56 x: 57; y: 66 x: 46; y: 52 x: 19; y: 107
Luokat ovat ohjelmoijan tapa ratkaistavan ongelma-alueen käsitteiden selkeyttämiseen. Lisäämme jokaisella luomallamme luokalla uutta toiminnallisuutta ohjelmointikieleen. Toiminnallisuutta tarvitaan kohtaamiemme ongelmien ratkomiseen, ratkaisut syntyvät luokista luotujen olioiden välisen interaktion avulla. Olio-ohjelmoinnissa olio on itsenäinen kokonaisuus, jolla on olion tarjoamien metodien avulla muutettava tila. Olioita käytetään yhteistyössä; jokaisella oliolla on oma vastuualue. Esimerkiksi käyttöliittymäluokkamme ovat tähän mennessä hyödyntäneet Scanner
-olioita.
Jokainen Javan luokka perii luokan Object
, eli jokainen luomamme luokka saa käyttöönsä kaikki Object
-luokassa määritellyt metodit. Jos haluamme muuttaa Object
-luokassa määriteltyjen metodien toiminnallisuutta tulee ne korvata (Override
) määrittelemällä niille uusi toteutus luodussa luokassa.
Luokan Object
perimisen lisäksi myös muiden luokkien periminen on mahdollista. Javan ArrayList
-luokan APIa tarkasteltaessa huomaamme että ArrayList
perii luokan AbstractList
. Luokka AbstractList
perii luokan AbstractCollection
, joka perii luokan Object
.
java.lang.Object java.util.AbstractCollection<E> java.util.AbstractList<E> java.util.ArrayList<E>
Kukin luokka voi periä suoranaisesti yhden luokan. Välillisesti luokka kuitenkin perii kaikki perimänsä luokan ominaisuudet. Luokka ArrayList
perii suoranaisesti luokan AbstractList
, ja välillisesti luokat AbstractCollection
ja Object
. Luokalla ArrayList
on siis käytössään luokkien AbstractList
, AbstractCollection
ja Object
muuttujat, metodit ja rajapinnat.
Luokan ominaisuudet peritään avainsanalla extends
. Luokan perivää luokkaa kutsutaan aliluokaksi (subclass), perittävää luokkaa yliluokaksi (superclass). Tutustutaan erään autonvalmistajan järjestelmään, joka hallinnoi auton osia. Osien hallinan peruskomponentti on luokka Osa
, joka määrittelee tunnuksen, valmistajan ja kuvauksen.
public class Osa { private String tunnus; private String valmistaja; private String kuvaus; public Osa(String tunnus, String valmistaja, String kuvaus) { this.tunnus = tunnus; this.valmistaja = valmistaja; this.kuvaus = kuvaus; } public String getTunnus() { return tunnus; } public String getKuvaus() { return kuvaus; } public String getValmistaja() { return valmistaja; } }
Yksi osa autoa on moottori. Kuten kaikilla osilla, myös moottorilla on valmistaja, tunnus ja kuvaus. Näiden lisäksi moottoriin liittyy moottorityyppi: esimerkiksi polttomoottori, sähkömoottori tai hybridi. Luodaan luokan Osa
perivä luokka Moottori
: moottori on osan erikoistapaus.
public class Moottori extends Osa { private String moottorityyppi; public Moottori(String moottorityyppi, String tunnus, String valmistaja, String kuvaus) { super(tunnus, valmistaja, kuvaus); this.moottorityyppi = moottorityyppi; } public String getMoottorityyppi() { return moottorityyppi; } }
Luokkamäärittely public class Moottori extends Osa
kertoo että luokka Moottori
perii luokan Osa
toiminnallisuuden. Luokassa Moottori
määritellään oliomuuttuja moottorityyppi
.
Moottori-luokan konstruktori on mielenkiintoinen. Konstruktorin ensimmäisellä rivillä on avainsana super
, jolla kutsutaan yliluokan konstruktoria. Kutsu super(tunnus, valmistaja, kuaus)
kutsuu luokassa Osa
määriteltyä konstruktoria public Osa(String tunnus, String valmistaja, String kuvaus
, jolloin yliluokassa määritellyt oliomuuttujat saavat arvonsa. Tämän jälkeen oliomuuttujalle moottorityyppi
asetetaan siihen liittyvä arvo.
Kun luokka Moottori
perii luokan Osa
, saa se käyttöönsä kaikki luokan Osa
tarjoamat metodit. Luokasta Moottori
voi tehdä ilmentymän aivan kuten mistä tahansa muustakin luokasta.
Moottori moottori = new Moottori("polttomoottori", "hz", "volkswagen", "VW GOLF 1L 86-91"); System.out.println(moottori.getMoottorityyppi()); System.out.println(moottori.getValmistaja());
polttomoottori volkswagen
Kuten huomaat, luokalla Moottori
on käytössä luokassa Osa
määritellyt metodit.
Jos metodilla tai muuttujalla on näkyvyysmääre private
, ei se näy aliluokille eikä aliluokalla ole mitään suoraa tapaa päästä käsiksi yliluokan siihen. Edellisessä esimerkissä Moottori ei siis pääse suoraan käsiksi yliluokassa Osa määriteltyihin ominaisuuksiinsa (tunnus, valmistaja, kuvaus). Aliluokka näkee luonnollisesti kaiken yliluokan julkisen eli public
-määreellä varustetun kaluston. Jos halutaan määritellä yliluokkaan joitain muuttujia tai metodeja joiden näkeminen halutaan sallia aliluokille, mutta estää muilta voidaan käyttää näkyvyysmäärettä protected
.
Yliluokan konstruktoria kutsutaan avainsanalla super
. Kutsu super
on käytännössä samanlainen kuin this
-konstruktorikutsu. Kutsulle annetaan parametrina yliluokan konstruktorin vaatiman tyyppiset arvot.
Konstruktoria kutsuttaessa yliluokassa määritellyt muuttujat alustetaan. Konstruktorikutsussa tapahtuu käytännössä täysin samat asiat kuin normaalissa konstruktorikutsussa. Jos yliluokassa ei ole määritelty parametritonta konstruktoria, tulee aliluokan konstruktorikutsuissa olla aina mukana yliluokan konstruktorikutsu.
Huom! Kutsun super
tulee olla aina konstruktorin ensimmäisellä rivillä!
Yliluokassa määriteltyjä metodeja voi kutsua super
-etuliitteen avulla, aivan kuten tässä luokassa määriteltyjä metodeja voi kutsua this
-etuliitteellä. Esimerkiksi yliluokassa määriteltyä toString
-metodia voi hyödyntää sen korvaavassa metodissa seuraavasti:
@Override public String toString() { return super.toString() + "\n Ja oma viestini vielä!"; }
Tee pakkaus henkilot
ja sinne luokka Henkilo
, joka toimii seuraavan pääohjelman yhteydessä
public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka Mikkola", "Korsontie 1 03100 Vantaa"); Henkilo esko = new Henkilo("Esko Ukkonen", "Mannerheimintie 15 00100 Helsinki"); System.out.println(pekka); System.out.println(esko); }
siten että tulostuu
Pekka Mikkola Korsontie 1 03100 Vantaa Esko Ukkonen Mannerheimintie 15 00100 Helsinki
Tee pakkaukseen luokka Opiskelija
joka perii luokan Henkilo
.
Opiskelijalla on aluksi 0 opintopistettä. Aina kun opiskelija opiskelee, kasvaa opintopistemäärä. Toteuta luokka siten, että seuraava pääohjelma:
public static void main(String[] args) { Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); System.out.println(olli ); System.out.println("opintopisteitä " + olli.opintopisteita()); olli.opiskele(); System.out.println("opintopisteitä "+ olli.opintopisteita()); }
tuottaa tulostuksen:
Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0 opintopisteitä 1
Edellisessä tehtävässä Opiskelija
perii toString-metodin luokalta Henkilo
. Perityn metodin voi myös ylikirjoittaa, eli korvata omalla versiolla. Tee nyt Opiskelija
:lle oma versio toString:istä joka toimii seuraavan esimerkin mukaan:
public static void main(String[] args) { Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); System.out.println( olli ); olli.opiskele(); System.out.println( olli ); }
Tulostuu:
Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0 Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 1
Tee pakkaukseen luokka Henkilo
:n perivä luokka Opettaja
. Opettajalla on palkka joka tulostuu opettajan merkkijonoesityksessä.
Testaa, että seuraava pääohjelma
public static void main(String[] args) { Opettaja pekka = new Opettaja("Pekka Mikkola", "Korsontie 1 03100 Vantaa", 1200); Opettaja esko = new Opettaja("Esko Ukkonen", "Mannerheimintie 15 00100 Helsinki", 5400); System.out.println( pekka ); System.out.println( esko ); Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); for ( int i=0; i < 25; i++ ) { olli.opiskele(); } System.out.println( olli ); }
Aikaansaa tulostuksen
Pekka Mikkola Korsontie 1 03100 Vantaa palkka 1200 euroa/kk Esko Ukkonen Mannerheimintie 15 00100 Helsinki palkka 5400 euroa/kk Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 25
Toteuta oletuspakkauksessa olevaan Main
-luokkaan luokkametodi public static void tulostaLaitoksenHenkilot(List<Henkilo> henkilot)
, joka tulostaa kaikki metodille parametrina annetussa listassa olevat henkilöt. Metodin tulee toimia seuraavasti main
-metodista kutsuttaessa.
public static void tulostaLaitoksenHenkilot(List<Henkilo> henkilot) { // tulostetaan kaikki listan henkilöt } public static void main(String[] args) { List<Henkilo> henkilot = new ArrayList<Henkilo>(); henkilot.add( new Opettaja("Pekka Mikkola", "Korsontie 1 03100 Vantaa", 1200) ); henkilot.add( new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki") ); tulostaLaitoksenHenkilot(henkilot); }
Pekka Mikkola Korsontie 1 03100 Vantaa palkka 1200 euroa/kk Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0
Olion kutsuttavissa olevat metodit määrittyvät muuttujan tyypin kautta. Esimerkiksi jos Opiskelija
-tyyppisen olion viite on talletettu Henkilo
-tyyppiseen muuttujaan, on oliosta käytössä vain Henkilo
-luokassa määritellyt metodit:
Henkilo olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); olli.opintopisteita(); // EI TOIMI! olli.opiskele(); // EI TOIMI! String.out.println( olli ); // olli.toString() TOIMII
Jos oliolla on monta eri tyyppiä, on sillä käytössä jokaisen tyypin määrittelemät metodit. Esimerkiksi Opiskelija
-tyyppisellä oliolla on käytössä Henkilo
-luokassa määritellyt metodit sekä Object
-luokassa määritellyt metodit.
Edellisessä tehtävässä korvasimme Opiskelijan luokalta Henkilö perimän toString
uudella versiolla. Myös luokka Henkilö oli jo korvannut Object-luokalta perimänsä toStringin. Jos käsittelemme olioa jonkin muun kuin sen todellisen tyypin omaavan muuttujan kautta, mitä versiota olion metodista kutsutaan? Esim. seuraavassa on kaksi opiskelijaa joiden viitteet on talletettu Henkilo- ja Object-tyyppisiin muuttujiin. Molemmille kutsutaan metodia toString
. Mikä versio metodista suoritetaan, luokassa Object, Henkilo vai Opiskelija määritelty?
Henkilo olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); String.out.println( olli ); Object liisa = new Opiskelija("Liisa", "Väinö Auerin katu 20 00500 Helsinki"); String.out.println( liisa );
Tulostuu:
Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0 Liisa Väinö Auerin katu 20 00500 Helsinki opintopisteitä 0
Eli suoritettava metodi valitaan olion todellisen tyypin perusteella, ei viitteen tallettavan muuttujan tyypin perusteella!
Hieman yleisemmin: Suoritettava metodi valitaan aina olion todellisen tyypin perusteella riippumatta käytetyn muuttujan tyypistä. Oliot ovat monimuotoisia, eli olioita voi käyttää usean eri muuttujatyypin kautta. Suoritettava metodi liittyy aina olion todelliseen tyyppiin. Tätä monimuotoisuutta kutsutaan polymorfismiksi.
Kaksiulotteisessa koordinaatiostossa sijaitsevaa pistettä voisi kuvata seuraavan luokan avulla:
public class Piste { private int x; private int y; public Piste(int x, int y) { this.x = x; this.y = y; } public int manhattanEtaisyysOrigosta(){ return Math.abs(x)+Math.abs(y); } protected String sijainti(){ return x+", "+y; } @Override public String toString() { return "("+this.sijainti()+") etäisyys "+this.manhattanEtaisyysOrigosta(); } }
Metodi sijainti
ei ole tarkoitettu ulkoiseen käyttöön, joten se on näkyvyysmääreeltään protected, eli aliluokat pääsevät siihen käsiksi. Esim. reitinhakualgoritmien hyödyntämässä Manhattan-etäisyydellä tarkoitetaan pisteiden etäisyyttä, jos niiden välin voi kulkea ainoastaan koordinaattiakselien suuntaisesti.
Värillinen piste on muuten samanlainen kuin piste, mutta se sisältää merkkijonona ilmaistavan värin. Luokka voidaan siis tehdä perimällä Piste:
public class VariPiste extends Piste { private String vari; public VariPiste(int x, int y, String vari) { super(x, y); this.vari = vari; } @Override public String toString() { return super.toString()+" väri: "+vari; } }
Luokka määrittelee oliomuuttujan värin talletusta varten. Koordinaatit talletetaan yliluokkaan. Merkkijonoesityksestä halutaan muuten samanlainen kuin pisteellä, mutta väri tulee myös ilmaista. Ylikirjoitettu metodi toString
kutsuukin yliluokan toStringiä ja lisää sen tulokseen pisteen värin.
Seuraavassa esimerkki, jossa listalle laitetaan muutama piste, osa normaaleja ja osa väripisteitä ja tulostetaan listalla olevat pisteet. Polymorfismin ansioista kaikille tulee kutsutuksi olion todellisen tyypin toString-metodi vaikka lista tuntee kaikki pisteet Piste
-tyyppisinä:
public class Main { public static void main(String[] args) { Listpisteet = new ArrayList (); pisteet.add(new Piste(4, 8)); pisteet.add(new VariPiste(1, 1, "vihreä")); pisteet.add(new VariPiste(2, 5, "sininen")); pisteet.add(new Piste(0, 0)); for (Piste piste : pisteet) { System.out.println(piste); } } }
Tulostuu:
(4, 8) etäisyys 12 (1, 1) etäisyys 2 väri: vihreä (2, 5) etäisyys 7 väri: sininen (0, 0) etäisyys 0
Haluamme ohjelmaamme myös kolmiulotteisen pisteen. Koska kyseessä ei ole värillinen versio, periytetään se pisteestä:
public class Piste3D extends Piste { private int z; public Piste3D(int x, int y, int z) { super(x, y); this.z = z; } @Override protected String sijainti() { return super.sijainti()+", "+z; // tulos merkkijono muotoa "x, y, z" } @Override public int manhattanEtaisyysOrigosta() { // kysytään ensin yliluokalta x:n ja y:n perusteella laskettua etäisyyttä // ja lisätään tulokseen z-koordinaatin vaikutus return super.manhattanEtaisyysOrigosta()+Math.abs(z); } @Override public String toString() { return "("+this.sijainti()+") etäisyys "+this.manhattanEtaisyysOrigosta(); } }
Kolmiulotteinen piste siis määrittelee kolmatta koordinaattia vastaavan oliomuuttujan ja ylikirjoittaa metodit sijainti
,
manhattanEtaisyysOrigosta
ja toString
siten, että ne huomioivat kolmannen ulottuvuuden. Voimme nyt laajentaa edellistä esimerkkiä ja lisätä listalle myös kolmiulotteisia pisteitä:
public class Main { public static void main(String[] args) { Listpisteet = new ArrayList (); pisteet.add(new Piste(4, 8)); pisteet.add(new VariPiste(1, 1, "vihreä")); pisteet.add(new VariPiste(2, 5, "sininen")); pisteet.add(new Piste3D(5, 2, 8)); pisteet.add(new Piste(0, 0)); for (Piste piste : pisteet) { System.out.println(piste); } } }
Tulostus on odotusten mukainen
(4, 8) etäisyys 12 (1, 1) etäisyys 2 väri: vihreä (2, 5) etäisyys 7 väri: sininen (5, 2, 8) etäisyys 15 (0, 0) etäisyys 0
Huomamme, että kolmiulotteisen pisteen metodi toString
on täsmälleen sama kuin pisteen toString. Voisimmeko jättää toStringin ylikirjoittamatta? Vastaus on kyllä! Kolmiulotteinen piste pelkistyy seuraavanlaiseksi:
public class Piste3D extends Piste { private int z; public Piste3D(int x, int y, int z) { super(x, y); this.z = z; } @Override protected String sijainti() { return super.sijainti()+", "+z; } @Override public int manhattanEtaisyysOrigosta() { return super.manhattanEtaisyysOrigosta()+Math.abs(z); } }
Mitä tarkalleenottaen tapahtuu kuin kolmiulotteiselle pisteelle kutsutaan toString-metodia? Suoritus etenee seuraavasti:
return "("+this.sijainti()+") etäisyys "+this.manhattanEtaisyysOrigosta();
Metodikutsun aikaansaama toimintoketju siis on varsin monivaiheinen. Periaate on kuitenkin selkeä: suoritettavan metodin määrittelyä etsitään ensin olion todellisen tyypin määrittelystä ja jos sitä ei löydy edetään yliluokkaan. Ja jos yliluokastakaan ei löydy metodin toteutusta siirrytään etsimään yliluokan yliluokasta jne...
Perintä on väline käsitehierarkioiden rakentamiseen ja erikoistamiseen; aliluokka on aina yliluokan erikoistapaus. Jos luotava luokka on olemassaolevan luokan erikoistapaus, voidaan uusi luokka luoda perimällä olemassaoleva luokka. Esimerkiksi auton osiin liittyvässä esimerkissä moottori on osa, mutta moottoriin liittyy lisätoiminnallisuutta mitä jokaisella osalla ei ole.
Perittäessä aliluokka saa käyttöönsä yliluokan toiminnallisuudet. Jos aliluokka ei tarvitse tai käytä perittyä toiminnallisuutta, ei perintä ole perusteltua. Perityt luokat perivät yliluokkiensa metodit ja rajapinnat, eli aliluokkia voidaan käyttää missä tahansa missä yliluokkaa on käytetty. Perintähierarkia kannattaa pitää matalana, sillä hierarkian ylläpito ja jatkokehitys vaikeutuu perintöhierarkian kasvaessa. Yleisesti ottaen, jos perintähierarkian korkeus on yli 2 tai 3, ohjelman rakenteessa on todennäköisesti parannettavaa.
Perinnän käyttöä tulee miettiä. Esimerkiksi luokan Auto
periminen luokasta Osa
(tai Moottori
) on väärin. Auto sisältää moottorin ja osia, mutta auto ei ole moottori tai osa. Voimme yleisemmin ajatella että jos olio omistaa tai koostuu toisista olioista, ei perintää tule käyttää.
Perintää käytettäessä tulee varmistaa että Single Responsibility Principle pätee myös perittäessä. Jokaisella luokalla tulee olla vain yksi syy muuttua. Jos huomaat että perintä lisää luokan vastuita, tulee luokka pilkkoa useammaksi luokaksi.
Pohditaan postituspalveluun liittyviä luokkia Asiakas
, joka sisältää asiakkaan tiedot, ja Tilaus
, joka perii asiakkaan tiedot ja sisältää tilattavan tavaran tiedot. Luokassa Tilaus
on myös metodi postitusOsoite
, joka kertoo tilauksen postitusosoitteen.
public class Asiakas { private String nimi; private String osoite; public Asiakas(String nimi, String osoite) { this.nimi = nimi; this.osoite = osoite; } public String getNimi() { return nimi; } public String getOsoite() { return osoite; } public void setOsoite(String osoite) { this.osoite = osoite; } }
public class Tilaus extends Asiakas { private String tuote; private String lukumaara; public Tilaus(String tuote, String lukumaara, String nimi, String osoite) { super(nimi, osoite); this.tuote = tuote; this.lukumaara = lukumaara; } public String getTuote() { return tuote; } public String getLukumaara() { return lukumaara; } public String postitusOsoite() { return this.getNimi() + "\n" + this.getOsoite(); } }
Yllä perintää on käytetty väärin. Luokkaa perittäessä aliluokan tulee olla yliluokan erikoistapaus; tilaus ei ole asiakkaan erikoistapaus. Väärinkäyttö ilmenee single responsibility principlen rikkomisena: luokalla Tilaus
on vastuu sekä asiakkaan tietojen ylläpidosta, että tilauksen tietojen ylläpidosta.
Ratkaisussa piilevä ongelma tulee esiin kun mietimme mitä käy asiakkaan osoitteen muuttuessa.
Osoitteen muuttuessa joudumme muuttamaan jokaista kyseiseen asiakkaaseen liittyvää tilausoliota, mikä kertoo huonosta tilanteesta. Parempi ratkaisu olisi kapseloida Asiakas
Tilaus
-luokan oliomuuttujaksi. Jos ajattelemme tarkemmin tilauksen semantiikkaa, tämä on selvää. Tilauksella on asiakas. Muutetaan luokkaa Tilaus
siten, että se sisältää Asiakas
-viitteen.
public class Tilaus { private Asiakas asiakas; private String tuote; private String lukumaara; public Tilaus(Asiakas asiakas, String tuote, String lukumaara) { this.asiakas = asiakas; this.tuote = tuote; this.lukumaara = lukumaara; } public String getTuote() { return tuote; } public String getLukumaara() { return lukumaara; } public String postitusOsoite() { return this.asiakas.getNimi() + "\n" + this.asiakas.getOsoite(); } }
Yllä oleva luokka Tilaus
on nyt parempi. Metodi postitusosoite
käyttää asiakas-viitettä postitusosoitteen saamiseen sen sijaan että luokka perisi luokan Asiakas
. Tämä helpottaa sekä ohjelman ylläpitoa, että sen konkreettista toiminnallisuutta.
Nyt asiakkaan muuttaessa tarvitsee muuttaa vain asiakkaan tietoja, tilauksiin ei tarvitse tehdä muutoksia.
Tehtäväpohjassa tulee mukana luokka Varasto
, jonka tarjoamat konstruktorit ja metodit ovat seuraavat:
Tehtävässä rakennetaan Varasto
-luokasta useampia erilaisia varastoja. Huom! Toteuta kaikki luokat pakkaukseen varastot
.
Luokka Varasto
hallitsee tuotteen määrään liittyvät toiminnot. Nyt tuotteelle halutaan lisäksi tuotenimi ja nimen käsittelyvälineet. Ohjelmoidaan Tuotevarasto Varaston aliluokaksi! Toteutetaan ensin pelkkä yksityinen oliomuuttuja tuotenimelle, konstruktori ja getteri nimikentälle:
Muista millä tavoin konstruktori voi ensi toimenaan suorittaa yliluokan konstruktorin!
Käyttöesimerkki:
Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0); mehu.lisaaVarastoon(1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice System.out.println(mehu); // saldo = 988.7, tilaa 11.3
Juice saldo = 988.7, vielä tilaa 11.3
Kuten edellisestä esimerkistä näkee, Tuotevarasto-olion perimä toString()
ei tiedä (tietenkään!) mitään tuotteen nimestä. Asialle on tehtävä jotain! Lisätään samalla myös setteri tuotenimelle:
Uuden toString()
-metodin voisi toki ohjelmoida käyttäen yliluokalta perittyjä gettereitä, joilla perittyjen, mutta piilossa pidettyjen kenttien arvoja saa käyttöönsä. Koska yliluokkaan on kuitenkin jo ohjelmoitu tarvittava taito varastotilanteen merkkiesityksen tuottamiseen, miksi nähdä vaivaa sen uudelleen ohjelmointiin. Käytä siis hyväksesi perittyä toString
iä.
Muista miten korvattua metodia voi kutsua aliluokassa!
Käyttöesimerkki:
Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0); mehu.lisaaVarastoon(1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, tilaa 10.299999999999955
Juice Juice: saldo = 989.7, tilaa 10.299999999999955
Toisinaan saattaa olla kiinnostavaa tietää, millä tavoin jonkin tuotteen varastotilanne muuttuu: onko varasto usein hyvin vajaa, ollaanko usein ylärajalla, onko vaihelu suurta vai pientä, jne. Varustetaan siksi Tuotevarasto
-luokka taidolla muistaa tuotteen määrän muutoshistoriaa.
Aloitetaan apuvälineen laadinnalla.
Muutoshistorian muistamisen voisi toki toteuttaa suoraankin ArrayList<Double>
-oliona luokassa Tuotevarasto, mutta nyt laaditaan kuitenkin oma erikoistettu väline tähän tarkoitukseen. Väline toteutetaan kapseloimalla ArrayList<Double>
-olio.
Muutoshistoria
-luokan julkiset konstruktorit ja metodit:
Muutoshistoria
-olion.
Täydennä Muutoshistoria
-luokkaa analyysimetodein:
Täydennä Muutoshistoria
-luokkaa analyysimetodein:
Ohjeen varianssin laskemiseksi voit katsoa esimerkiksi Wikipediasta kohdasta populaatio- ja otosvarianssi. Esimerkiksi lukujen 3, 2, 7, 2 keskiarvo on 3.5, joten otosvarianssi on ((3 - 3.5)² + (2 - 3.5)² + (7 - 3.5)² + (2 - 3.5)²)/(4 - 1) ≈ 5,666667.)
Toteuta luokan Tuotevarasto
aliluokkana MuistavaTuotevarasto
.
Uusi versio tarjoaa vanhojen lisäksi varastotilanteen muutoshistoriaan liittyviä
palveluita. Historiaa hallitaan Muutoshistoria
-oliolla.
Julkiset konstruktorit ja metodit:
Huomaa että tässä esiversiossa historia ei vielä toimi kunnolla; nyt vasta vain aloitussaldo muistetaan.
Käyttöesimerkki:
// tuttuun tapaan: MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, vielä tilaa 10.3 ... // mutta vielä historia() ei toimi kunnolla: System.out.println(mehu.historia()); // [1000.0] // saadaan siis vasta konstruktorin asettama historian alkupiste... ...
Tulostus siis:
Juice Juice: saldo = 989.7, vielä tilaa 10.299999999999955 [1000.0]
On aika aloittaa historia! Ensimmäinen versio ei historiasta tiennyt kuin alkupisteen. Täydennä luokkaa metodein
Varasto
-luokan metodi, mutta muuttunut tilanne kirjataan historiaan. Huom: historiaan tulee kirjata poiston jälkeinen varastosaldo, ei poistettavaa määrää!Käyttöesimerkki:
// tuttuun tapaan: MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, vielä tilaa 10.3 ... // mutta nyt on historiaakin: System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7] ...
Tulostus siis:
Juice Juice: saldo = 989.7, vielä tilaa 10.299999999999955 [1000.0, 988.7, 989.7]
Muista miten korvaava metodi voi käyttää hyväkseen korvattua metodia!
Täydennä luokkaa metodilla
Käyttöesimerkki:
MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); mehu.lisaaVarastoon(1.0); //System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7] mehu.tulostaAnalyysi();
Metodi tulostaAnalyysi kirjoittaa ilmoituksen tyyliin:
Tuote: Juice Historia: [1000.0, 988.7, 989.7] Suurin tuotemäärä: 1000.0 Pienin tuotemäärä: 988.7 Keskiarvo: 992.8
Täydennä analyysin tulostus sellaiseksi, että mukana ovat myös muutoshistorian suurin muutos ja historian varianssi.
Perintä ei sulje pois rajapintojen käyttöä, eikä rajapintojen käyttö sulje pois perinnän käyttöä. Rajapinnat toimivat sopimuksena luokan tarjoamasta toteutuksesta, ja mahdollistavat konkreettisen toteutuksen abstrahoinnin. Rajapinnan toteuttavan luokan vaihto on hyvin helppoa.
Aivan kuten rajapintaa toteuttaessa, sitoudumme perittäessä siihen, että aliluokkamme tarjoaa kaikki yliluokan metodit. Monimuotoisuuden ja polymorfismin takia perintäkin toimii kuin rajapinnat. Voimme antaa yliluokkaa käyttävälle metodille sen aliluokan ilmentymän.
Tehdään seuraavaksi maatilasimulaattori, jossa simuloidaan maatilan elämää. Huomaa että ohjelmassa ei käytetä perintää, ja rajapintojenkin käyttö on melko vähäistä. Usein ohjelmat tehdäänkin niin että ensin toteutetaan yksi versio, jota lähdetään parantamaan myöhemmin. Tyypillistä on että ensimmäistä versiota toteutettaessa ongelma-aluetta ei vielä ymmärretä kunnolla, jolloin rajapintojen ja käsitehierarkioiden suunnittelu ennalta on hyvin vaikeaa ja saattaa jopa hidastaa työskentelyä.
Maatiloilla on lypsäviä eläimiä, jotka tuottavat maitoa. Maatilat eivät itse käsittele maitoa, vaan se kuljetetaan Maitoautoilla meijereille. Meijerit ovat yleisiä maitotuotteita tuottavia rakennuksia. Jokainen meijeri erikoistuu yhteen tuotetyyppiin, esimerkiksi Juustomeijeri tuottaa Juustoa, Voimeijeri tuottaa voita ja Maitomeijeri tuottaa maitoa.
Rakennetaan maidon elämää kuvaava simulaattori. Toteuta kaikki luokat pakkaukseen maatilasimulaattori
.
Jotta maito pysyisi tuoreena, täytyy se säilöä sille tarkoitettuun säiliöön. Säiliöitä valmistetaan sekä oletustilavuudella 2000 litraa, että asiakkaalle räätälöidyllä tilavuudella. Toteuta luokka Maitosailio jolla on seuraavat konstruktorit ja metodit.
Toteuta Maitosailio
-luokalle myös toString()
-metodi, jolla kuvaat sen tilaa.
Ilmaistessasi säiliön tilaa toString()
-metodissa, pyöristä litramäärät
ylöspäin käyttäen Math
-luokan tarjoamaa ceil()
-metodia.
Testaa maitosailiötä seuraavalla ohjelmapätkällä:
Maitosailio sailio = new Maitosailio(); sailio.otaSailiosta(100); sailio.lisaaSailioon(25); sailio.otaSailiosta(5); System.out.println(sailio); sailio = new Maitosailio(50); sailio.lisaaSailioon(100); System.out.println(sailio);
Ohjelman tulostuksen tulee olla seuraavankaltainen:
20.0/2000.0 50.0/50.0
Huomaa että kutsuttaessa System
-luokan out
-olioon liittyvää
println()
-metodia, joka saa parametrikseen Object
-tyyppisen
muuttujan, tulostus käyttää Maitosailio
-luokassa korvattua
toString()
-metodia! Tässä on kyse polymorfismista, eli
ajonaikaisesta käytettävien metodien päättelystä.
Saadaksemme maitoa tarvitsemme myös lehmiä. Lehmällä on nimi ja utareet. Utareiden tilavuus on satunnainen luku väliltä 15 ja 40, luokkaa Random
voi käyttäää satunnaislukujen arpomiseen, esimerkiksi int luku = 15 + new Random().nextInt(26);
. Luokalla Lehma
on seuraavat toiminnot:
Lehma
toteuttaa myös rajapinnat: Lypsava
, joka kuvaa lypsämiskäyttäytymistä, ja Eleleva
, joka kuvaa elelemiskäyttäytymistä.
public interface Lypsava { public double lypsa(); } public interface Eleleva { public void eleleTunti(); }
Lehmää lypsettäessä sen koko maitovarasto tyhjennetään jatkokäsittelyä varten. Lehmän elellessä sen maitovarasto täyttyy hiljalleen. Suomessa maidontuotannossa käytetyt lehmät tuottavat keskimäärin noin 25-30 litraa maitoa päivässä. Simuloidaan tätä tuotantoa tuottamalla noin 0.7 - 2 litraa tunnissa.
Jos lehmälle ei anneta nimeä, valitse sille nimi satunnaisesti seuraavasta listasta.
private static final String[] NIMIA = new String[]{ "Anu", "Arpa", "Essi", "Heluna", "Hely", "Hento", "Hilke", "Hilsu", "Hymy", "Ihq", "Ilme", "Ilo", "Jaana", "Jami", "Jatta", "Laku", "Liekki", "Mainikki", "Mella", "Mimmi", "Naatti", "Nina", "Nyytti", "Papu", "Pullukka", "Pulu", "Rima", "Soma", "Sylkki", "Valpu", "Virpi"};
Toteuta luokka Lehma ja testaa sen toimintaa seuraavan ohjelmapätkän avulla.
Lehma lehma = new Lehma(); System.out.println(lehma); Eleleva elelevaLehma = lehma; elelevaLehma.eleleTunti(); elelevaLehma.eleleTunti(); elelevaLehma.eleleTunti(); elelevaLehma.eleleTunti(); System.out.println(lehma); Lypsava lypsavaLehma = lehma; lypsavaLehma.lypsa(); System.out.println(lehma); System.out.println(""); lehma = new Lehma("Ammu"); System.out.println(lehma); lehma.eleleTunti(); lehma.eleleTunti(); System.out.println(lehma); lehma.lypsa(); System.out.println(lehma);
Ohjelman tulostus on erimerkiksi seuraavanlainen.
Liekki 0.0/23.0 Liekki 7.0/23.0 Liekki 0.0/23.0 Ammu 0.0/35.0 Ammu 9.0/35.0 Ammu 0.0/35.0
Nykyaikaisilla maatiloilla lypsyrobotit hoitavat lypsämisen. Jotta lypsyrobotti voi lypsää lypsävää otusta, tulee lypsyrobotin olla kiinnitetty maitosäiliöön:
null
-viitteen, jos säiliötä ei ole vielä kiinnitettyIllegalStateException
, jos säiliötä ei ole kiinnitetty Toteuta luokka Lypsyrobotti ja testaa sitä seuraavien ohjelmanpätkien avulla. Varmista että lypsyrobotti voi lypsää kaikkia Lypsava-rajapinnan toteuttavia olioita!
Lypsyrobotti lypsyrobotti = new Lypsyrobotti(); Lehma lehma = new Lehma(); lypsyrobotti.lypsa(lehma);
Exception in thread "main" java.lang.IllegalStateException: Maitosäiliötä ei ole asennettu at maatilasimulaattori.Lypsyrobotti.lypsa(Lypsyrobotti.java:17) at maatilasimulaattori.Main.main(Main.java:9) Java Result: 1
Lypsyrobotti lypsyrobotti = new Lypsyrobotti(); Lehma lehma = new Lehma(); System.out.println(""); Maitosailio sailio = new Maitosailio(); lypsyrobotti.setMaitosailio(sailio); System.out.println("Säiliö: " + sailio); for(int i = 0; i < 2; i++) { System.out.println(lehma); System.out.println("Elellään.."); for(int j = 0; j < 5; j++) { lehma.eleleTunti(); } System.out.println(lehma); System.out.println("Lypsetään..."); lypsyrobotti.lypsa(lehma); System.out.println("Säiliö: " + sailio); System.out.println(""); }
Ohjelman tulostus on esimerkiksi seuraavanlainen.
Säiliö: 0.0/2000.0 Mella 0.0/23.0 Elellään.. Mella 6.2/23.0 Lypsetään... Säiliö: 6.2/2000.0 Mella 0.0/23.0 Elellään.. Mella 7.8/23.0 Lypsetään... Säiliö: 14.0/2000.0
Lehmät hoidetaan (eli tässä tapauksessa lypsetään) navetassa.
Alkukantaisissa navetoissa on maitosäiliö ja tilaa yhdelle
lypsyrobotille. Huomaa että lypsyrobottia asennettaessa se kytketään
juuri kyseisen navetan maitosäiliöön. Jos navetassa ei ole
lypsyrobottia, ei siellä voida myöskään hoitaa lehmiä.
Toteuta luokka Navetta
jolla on seuraavat konstruktorit ja metodit:
IllegalStateException
, jos lypsyrobottia ei ole asennettuIllegalStateException
, jos lypsyrobottia ei ole asennettuCollection
on Javan oma rajapinta joka kuvaa kokoelmien käyttäytymistä. Esimerkiksi luokat ArrayList
ja LinkedList
toteuttavat rajapinnan Collection
. Jokaista Collection
-rajapinnan toteuttavaa ilmentymää voi myös iteroida for-each-tyyppisesti.
Testaa luokkaa Navetta
seuraavan ohjelmapätkän avulla.
Älä hermoile luokasta LinkedList
, se toimii ulkoapäin katsottuna
kuin ArrayList
, mutta sen kapseloima toteutus on hieman erilainen.
Tästä lisää tietorakenteet-kurssilla!
Navetta navetta = new Navetta(new Maitosailio()); System.out.println("Navetta: " + navetta); Lypsyrobotti robo = new Lypsyrobotti(); navetta.asennaLypsyrobotti(robo); Lehma ammu = new Lehma(); ammu.eleleTunti(); ammu.eleleTunti(); navetta.hoida(ammu); System.out.println("Navetta: " + navetta); LinkedList<Lehma> lehmaLista = new LinkedList(); lehmaLista.add(ammu); lehmaLista.add(new Lehma()); for(Lehma lehma: lehmaLista) { lehma.eleleTunti(); lehma.eleleTunti(); } navetta.hoida(lehmaLista); System.out.println("Navetta: " + navetta);
Tulostuksen tulee olla esimerkiksi seuraavanlainen:
Navetta: 0.0/2000.0 Navetta: 2.8/2000.0 Navetta: 9.6/2000.0
Maatilalla on omistaja ja siihen kuuluu navetta sekä joukko lehmiä.
Maatila toteuttaa myös aiemmin nähdyn rajapinnan Eleleva
,
jonka metodia eleleTunti()
-kutsumalla kaikki maatilaan
liittyvät lehmät elelevät tunnin. Toteuta luokka maatila siten,
että se toimii seuraavien esimerkkiohjelmien mukaisesti.
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); System.out.println(maatila); System.out.println(maatila.getOmistaja() + " on ahkera mies!");
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Ei lehmiä. Esko on ahkera mies!
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Lehmät: Naatti 0.0/19.0 Hilke 0.0/30.0 Sylkki 0.0/29.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.eleleTunti(); maatila.eleleTunti(); System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Lehmät: Heluna 2.0/17.0 Rima 3.0/32.0 Ilo 3.0/25.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); Lypsyrobotti robo = new Lypsyrobotti(); maatila.asennaNavettaanLypsyrobotti(robo); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.eleleTunti(); maatila.eleleTunti(); maatila.hoidaLehmat(); System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 18.0/2000.0 Lehmät: Hilke 0.0/30.0 Sylkki 0.0/35.0 Hento 0.0/34.0
Abstrakti luokka yhdistää rajapintoja ja perintää. Niistä ei voi tehdä ilmentymiä, vaan ilmentymät tehdään tehdään abstraktin luokan aliluokista. Abstrakti luokka voi sisältää sekä normaaleja metodeja, joissa on metodirunko, että abstrakteja metodeja, jotka sisältävät ainoastaan metodimäärittelyn. Abstraktien metodien toteutus jätetään perivän luokan vastuulle. Yleisesti ajatellen abstrakteja luokkia käytetään esimerkiksi kun abstraktin luokan kuvaama käsite ei ole selkeä itsenäinen käsite. Tällöin siitä ei tule pystyä tekemään ilmentymiä.
Sekä abstraktin luokan että abstraktien metodien määrittelyssä käytetään avainsanaa abstract
. Abstrakti luokka määritellään lauseella public abstract class LuokanNimi
, abstrakti metodi taas lauseella public abstract palautustyyppi metodinNimi
. Pohditaan seuraavaa abstraktia luokkaa Toiminto
, joka tarjoaa rungon toiminnoille ja niiden suorittamiselle.
public abstract class Toiminto { private String nimi; public Toiminto(String nimi) { this.nimi = nimi; } public String getNimi() { return this.nimi; } public abstract void suorita(Scanner lukija); }
Abstrakti luokka Toiminto
toimii runkona erilaisten toimintojen toteuttamiseen. Esimerkiksi pluslaskun voi toteuttaa perimällä luokka Toiminto
seuraavasti.
public class Pluslasku extends Toiminto { public Pluslasku() { super("Pluslasku"); } @Override public void suorita(Scanner lukija) { System.out.print("Anna ensimmäinen luku: "); int eka = Integer.parseInt(lukija.nextLine()); System.out.print("Anna toinen luku: "); int toka = Integer.parseInt(lukija.nextLine()); System.out.println("Lukujen summa on " + (eka + toka)); } }
Koska kaikki Toiminto
-luokan perivät luokat ovat myös tyyppiä toiminto, voimme rakentaa käyttöliittymän Toiminto
-tyyppisten muuttujien varaan. Seuraava luokka Kayttoliittyma
sisaltaa listan toimintoja ja lukijan. Toimintoja voi lisätä käyttöliittymään dynaamisesti.
public class Kayttoliittyma { private Scanner lukija; private List<Toiminto> toiminnot; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; this.toiminnot = new ArrayList<Toiminto>(); } public void lisaaToiminto(Toiminto toiminto) { this.toiminnot.add(toiminto); } public void kaynnista() { while (true) { tulostaToiminnot(); System.out.println("Valinta: "); String valinta = this.lukija.nextLine(); if (valinta.equals("0")) { break; } suoritaToiminto(valinta); System.out.println(); } } private void tulostaToiminnot() { System.out.println("\t0: Lopeta"); for (int i = 0; i < this.toiminnot.size(); i++) { String toiminnonNimi = this.toiminnot.get(i).getNimi(); System.out.println("\t" + (i + 1) + ": " + toiminnonNimi); } } private void suoritaToiminto(String valinta) { int toiminto = Integer.parseInt(valinta); Toiminto valittu = this.toiminnot.get(toiminto - 1); valittu.suorita(lukija); } }
Käyttöliittymä toimii seuraavasti:
Kayttoliittyma kayttolittyma = new Kayttoliittyma(new Scanner(System.in)); kayttolittyma.lisaaToiminto(new Pluslasku()); kayttolittyma.kaynnista();
Toiminnot: 0: Lopeta 1: Pluslasku Valinta: 1 Anna ensimmäinen luku: 8 Anna toinen luku: 12 Lukujen summa on 20 Toiminnot: 0: Lopeta 1: Pluslasku Valinta: 0
Rajapintojen ja abstraktien luokkien ero on siinä, että abstraktit luokat tarjoavat enemmän rakennetta ohjelmaan. Koska abstrakteihin luokkiin voidaan määritellä toiminnallisuutta, voidaan niitä käyttää esimerkiksi oletustoiminnallisuuden määrittelyyn. Yllä käyttöliittymä käytti abstraktissa luokassa määriteltyä toiminnan nimen tallentamista.
Tehtäväpohjan mukana tulee luokat Tavara
ja Laatikko
. Luokka Laatikko
on abstrakti luokka, jossa useamman tavaran lisääminen on toteutettu siten, että kutsutaan aina lisaa
-metodia. Yhden tavaran lisäämiseen tarkoitettu metodi lisaa
on abstrakti, joten jokaisen Laatikko
-luokan perivän laatikon tulee toteuttaa se. Tehtävänäsi on muokata luokkaa Tavara
ja toteuttaa muutamia erilaisia laatikoita luokan Laatikko
pohjalta.
Lisää kaikki uudet luokat pakkaukseen laatikot
.
package laatikot; import java.util.Collection; public abstract class Laatikko { public abstract void lisaa(Tavara tavara); public void lisaa(Collection<Tavara> tavarat) { for (Tavara tavara : tavarat) { lisaa(tavara); } } public abstract boolean onkoLaatikossa(Tavara tavara); }
Lisää Tavara
-luokan konstruktoriin tarkistus, jossa tarkistetaan että tavaran paino ei ole koskaan negatiivinen (paino 0 hyväksytään). Jos paino on negatiivinen, tulee konstruktorin heittää IllegalArgumentException
-poikkeus. Toteuta Tavara
-luokalle myös metodit equals
ja hashCode
, joiden avulla pääset hyödyntämään erilaisten listojen ja kokoelmien contains
-metodia. Toteuta metodit siten, että Tavara-luokan oliomuuttujan paino
arvolla ei ole väliä. Voit hyvin hyödyntää NetBeansin tarjoamaa toiminnallisuutta equalsin ja hashCoden toteuttamiseen.
Toteuta pakkaukseen laatikot
luokka MaksimipainollinenLaatikko
, joka perii luokan Laatikko
. Maksimipainollisella laatikolla on konstruktori public MaksimipainollinenLaatikko(int maksimipaino)
, joka määrittelee laatikon maksimipainon. Maksimipainolliseen laatikkoon voi lisätä tavaraa jos ja vain jos tavaran lisääminen ei ylitä laatikon maksimipainoa.
MaksimipainollinenLaatikko kahviLaatikko = new MaksimipainollinenLaatikko(10); kahviLaatikko.lisaa(new Tavara("Saludo", 5)); kahviLaatikko.lisaa(new Tavara("Pirkka", 5)); kahviLaatikko.lisaa(new Tavara("Kopi Luwak", 5)); System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Saludo"))); System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Pirkka"))); System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Kopi Luwak")));
true true false
Toteuta seuraavaksi pakkaukseen laatikot
luokka YhdenTavaranLaatikko
, joka perii luokan Laatikko
. Yhden tavaran laatikolla on konstruktori public YhdenTavaranLaatikko()
, ja siihen mahtuu tasan yksi tavara. Jos tavara on jo laatikossa sitä ei tule vaihtaa. Laatikkoon lisättävän tavaran painolla ei ole väliä.
YhdenTavaranLaatikko laatikko = new YhdenTavaranLaatikko(); laatikko.lisaa(new Tavara("Saludo", 5)); laatikko.lisaa(new Tavara("Pirkka", 5)); System.out.println(laatikko.onkoLaatikossa(new Tavara("Saludo"))); System.out.println(laatikko.onkoLaatikossa(new Tavara("Pirkka")));
true false
Toteuta seuraavaksi pakkaukseen laatikot
luokka HukkaavaLaatikko
, joka perii luokan Laatikko
. Hukkaavalla laatikolla on konstruktori public HukkaavaLaatikko()
. Hukkaavaan laatikkoon voi lisätä kaikki tavarat, mutta tavaroita ei löydy niitä etsittäessä. Laatikkoon lisäämisen tulee siis aina onnistua, mutta metodin onkoLaatikossa
kutsumisen tulee aina palauttaa false.
HukkaavaLaatikko laatikko = new HukkaavaLaatikko(); laatikko.lisaa(new Tavara("Saludo", 5)); laatikko.lisaa(new Tavara("Pirkka", 5)); System.out.println(laatikko.onkoLaatikossa(new Tavara("Saludo"))); System.out.println(laatikko.onkoLaatikossa(new Tavara("Pirkka")));
false false
Seuraavassa tehtävässä saatat joutua tilanteeseen, jossa haluat poistaa ArrayListiä läpikäydessäsi osan listan olioista:
// joissain määritelty seuraavasti: // ArrayList<Olio> lista = new ... for ( Olio olio : lista ) { if ( tarvitseePoistaa(olio) ) { lista.remove(olio); } }
Ratkaisu ei toimi ja aiheuttaa poikkeuksen ConcurrentModificationException
, sillä listaa foreach-tyylillä läpikäydessä listaa ei saa muokata. Palaamme aiheeseen hieman tarkemmin viikolla 612. Jos törmäät tilanteeseen, voit hoitaa sen esim. seuraavasti:
// joissain määritelty seuraavasti: // ArrayList<Olio> lista = new ... ArrayList<Olio> poistettavat = new ArrayList<Olio>(); for ( Olio olio : lista ) { if ( tarvitseePoistaa(olio) ) { poistattavat.add(olio); } } lista.removeAll(poistettavat);
Eli poistettavat oliot kerätään listan läpikäyntinä erilliselle listalle ja poisto-operaatio suoritetaan vasta listan läpikäynnin jälkeen.
Tämä tehtävä on neljän tehtäväpisteen arvoinen. Huom! Toteuta kaikki toiminnallisuus pakkaukseen luola
.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä käytä luokkien nimissä skandeja. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Tässä tehtävässä pääset toteuttamaan luolapelin. Pelissä pelaaja on luolassa hirviöitten kanssa. Pelaajan tehtävänä on ehtiä tallata kaikki hirviöt ennen kuin hänen lampustaan loppuu virta ja hirviöt pääsevät pimeän turvin syömään hänet. Pelaaja voi nähdä hirviöiden sijainnit välkäyttämällä lamppua, jonka jälkeen hänen on liikuttava sokkona ennen seuraavaa välkäytystä. Pelaaja voi kulkea monta askelta yhden siirron aikana.
Pelitilanne eli luola, pelaaja ja hirviöt esitetään pelaajalle tekstimuotoisesti. Tulostuksen ensimmäinen rivi kertoo jäljellä olevien siirtojen (eli lampun jäljellä olevan virran) määrän. Virran määrää seuraa pelaajan ja hirviöitten sijainnit, joiden jälkeen on pelitilanteesta piirretty kartta. Alla olevassa esimerkissä näet pelaajan (@
) ja kolme hirviötä (h
). Alla olevassa esimerkissä pelaajalla on virtaa neljääntoista siirtoon.
14 @ 1 2 h 6 1 h 7 3 h 12 2 ................. ......h.......... .@.........h..... .......h.........
Yllä olevassa esimerkissä virtaa on 14 välkäytykseen. Pelaaja @
sijatsee koordinaatissa 1 2
. Huomaa että koordinaatit lasketaan aina pelialueen vasemmasta ylälaidasta lähtien. Alla olevassa kartassa merkki X
on koordinaatissa 0 0
, Y
koordinaatissa 2 0
ja Z
koordinaatissa 0 2
.
X.Y.............. ................. Z................ .................
Käyttäjä voi liikkua antamalla sarjan komentoja ja painamalla rivinvaihtoa. Komennot ovat:
w
liiku ylöspäins
liiku alaspäina
liiku vasemmalled
liiku oikealleKun käyttäjän antamat komennot on suoritettu (niitä voi olla useampi), piirretään uusi pelitilanne. Lampun virta vähenee yhdellä aina kun uusi pelitilanne piirretään. Jos virta menee nollaan, peli loppuu ja ruudulle tulostetaan teksti HÄVISIT
Hirviöt liikkuvat pelissä satunnaisesti, yhden askeleen jokaista pelaajan askelta kohti. Jos pelaaja ja hirviö osuvat samaan ruutuun (vaikka vain tilapäisesti), hirviö tuhoutuu. Jos hirviö yrittää siirtyä pelilaudalta ulos tai ruutuun jossa on jo hirviö, jätetään siirto suorittamatta. Kun kaikki hirviöt on tuhottu, peli loppuu ja tulostetaan VOITIT
.
Testaamisen helpottamiseksi tee peliisi luokka Luola
, jolla on :
public Luola(int leveys, int korkeus, int hirvioita, int siirtoja, boolean hirviotLiikkuvat)
Luvut leveys
ja korkeus
antavat luolan koon (se on aina neliskulmainen), hirvioita
antaa hirviöiden lukumäärän alussa (hirviöiden sijainnin voi arpoa), siirtoja
antaa siirtojen lukumäärän alussa ja jos hirviotLiikkuvat
on false
, hirviöt eivät liiku.
public void run()
joka käynnistää pelinHuom! pelaajan tulee aloittaa sijainnista 0,0!
Huom! jos pelaaja tai hirviö koittaa liikkua ulos luolasta tai kaksi hirviötä koittaa liikkua samaan ruutuun, ei liikettä tule tapahtua!
Alla vielä selkeyden vuoksi vielä esimerkkipeli:
14 @ 0 0 h 1 2 h 7 8 h 7 5 h 8 0 h 2 9 @.......h. .......... .h........ .......... .......... .......h.. .......... .......... .......h.. ..h....... ssd 13 @ 1 2 h 8 8 h 7 4 h 8 3 h 1 8 .......... .......... .@........ ........h. .......h.. .......... .......... .......... .h......h. .......... ssss 12 @ 1 6 h 6 9 h 6 5 h 8 3 .......... .......... .......... ........h. .......... ......h... .@........ .......... .......... ......h... dd 11 @ 3 6 h 5 9 h 6 7 h 8 1 .......... ........h. .......... .......... .......... .......... ...@...... ......h... .......... .....h.... ddds 10 @ 6 7 h 6 6 h 5 0 .....h.... .......... .......... .......... .......... .......... ......h... ......@... .......... .......... w 9 @ 6 6 h 4 0 ....h..... .......... .......... .......... .......... .......... ......@... .......... .......... .......... www 8 @ 6 3 h 4 0 ....h..... .......... .......... ......@... .......... .......... .......... .......... .......... .......... aa 7 @ 4 3 h 4 2 .......... .......... ....h..... ....@..... .......... .......... .......... .......... .......... .......... w VOITIT
Luvussa 1546 opimme, että tekstitiedostojen lukeminen onnistuu Scanner
- ja File
-luokkien avulla.
Luokka FileWriter
tarjoaa toiminnallisuuden tiedostoon kirjoittamiseen. Luokan FileWriter
konstruktorille annetaan parametrina kohdetiedoston sijaintia kuvaava merkkijono.
FileWriter kirjoittaja = new FileWriter("tiedosto.txt"); kirjoittaja.write("Hei tiedosto!\n"); // rivinvaihto tulee myös kirjoittaa tiedostoon! kirjoittaja.write("Lisää tekstiä\n"); kirjoittaja.write("Ja vielä lisää"); kirjoittaja.close(); // sulkemiskutsu sulkee tiedoston ja varmistaa että kirjoitettu teksti menee tiedostoon
Esimerkissä kirjoitetaan tiedostoon "tiedosto.txt" merkkijono "Hei tiedosto!", jota seuraa rivinvaihto, ja vielä hieman lisää tekstiä. Huomaa että tiedostoon kirjoitettaessa metodi write
ei lisää rivinvaihtoja, vaan ne tulee lisätä itse.
Sekä FileWriter
-luokan konstruktori että write
-metodi heittää mahdollisesti poikkeuksen, joka tulee joko käsitellä tai siirtää kutsuvan metodin vastuulle. Metodi, jolle annetaan parametrina kirjoitettavan tiedoston nimi ja kirjoitettava sisältö voisi näyttää seuraavalta.
public class Tallentaja { public void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception { FileWriter kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.write(teksti); kirjoittaja.close(); } }
Yllä olevassa kirjoitaTiedostoon
-metodissa luodaan ensin FileWriter
-olio, joka kirjoittaa parametrina annetussa sijainnissa sijaitsevaan tiedostoon tiedostonNimi
. Tämän jälkeen kirjoitetaan tiedostoon write
-metodilla. Konstruktorin ja write
-metodin mahdollisesti heittämä poikkeus tulee käsitellä joko try-catch
-lohkolla tai siirtämällä poikkeuksen käsittelyvastuuta eteenpäin. Metodissa kirjoitaTiedostoon
käsittelyvastuu on siirretty eteenpäin.
Luodaan main
-metodi jossa kutsutaan Tallentaja
-olion kirjoitaTiedostoon
-metodia. Poikkeusta ei ole pakko käsitellä main
-metodissakaan, vaan se voi ilmoittaa heittävänsä mahdollisesti poikkeuksen määrittelyllä throws Exception
.
public static void main(String[] args) throws Exception { Tallentaja tallentaja = new Tallentaja(); tallentaja.kirjoitaTiedostoon("paivakirja.txt", "Rakas päiväkirja, tänään oli kiva päivä."); }
Yllä olevaa metodia kutsuttaessa luodaan tiedosto "paivakirja.txt" johon kirjoitetaan teksti "Rakas päiväkirja, tänään oli kiva päivä.". Jos tiedosto on jo olemassa, pyyhkiytyy vanhan tiedoston sisältö uutta kirjoittaessa. Metodilla append()
voidaan lisätä olemassaolevan tiedoston perään tekstiä, jolloin olemassaolevaa tekstiä ei poisteta. Lisätään Tallentaja
-luokalle metodi lisaaTiedostoon()
, joka lisää parametrina annetun tekstin tiedoston loppuun.
public class Tallentaja { public void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception { FileWriter kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.write(teksti); kirjoittaja.close(); } public void lisaaTiedostoon(String tiedostonNimi, String teksti) throws Exception { FileWriter kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.append(teksti); kirjoittaja.close(); } }
Useimmiten tiedoston perään metodilla append
kirjoittamisen sijasta on helpompi kirjoittaa koko tiedosto uudelleen.
Saat tehtävärungon mukana luokan Tiedostonkasittelija
joka sisältää metodirungot tiedoston lukemista ja tiedostoon kirjoittamista varten.
Täydennä metodi public ArrayList<String> lue(String tiedosto)
sellaiseksi, että se palauttaa parametrina annetun tekstitiedoston sisältämät rivit ArrayList:ina siten, että tiedoston jokainen rivi on omana merkkijononaan listalla.
Projektissa on testaamista varten kaksi tekstitiedostoa src/koesyote1.txt
ja src/koesyote2.txt
. Metodia on tarkoitus käyttää seuraavalla tavalla:
public static void main(String[] args) throws FileNotFoundException, IOException { TiedostonKasittelija t = new TiedostonKasittelija(); for (String rivi : t.lue("src/koesyote1.txt")) { System.out.println(rivi); } }
Tulostuksen pitäisi olla
eka toka
Täydennä projektipohjan mukana tuleva metodi public void tallenna(String tiedosto, String teksti)
sellaiseksi, että se tallentaa ensimmäisen parametrin määrittelemään tiedostoon toisena parametrina annetun merkkijonon. Jos tiedosto on jo olemassa, kirjoitetaan vanhan version päälle.
Täydennä projektipohjan mukana tuleva metodi public void tallenna(String tiedosto, ArrayList<String> tekstit)
sellaiseksi, että se tallentaa ensimmäisen parametrin määrittelemään tiedostoon toisena parametrina annetun listan siten, että jokainen merkkijono tulee omalle rivilleen. Jos tiedosto on jo olemassa, kirjoitetaan vanhan version päälle.
Tässä tehtävässä laajennetaan aiemmin toteutettua sanakirjaa siten, että sanat voidaan lukea tiedostosta ja kirjoittaa tiedostoon. Sanakirjan tulee myös osata kääntää molempiin suuntiin, suomesta vieraaseen kieleen sekä toiseen suuntaan (tehtävässä oletetaan hieman epärealistisesti, että suomen kielessä ja vieraassa kielessä ei ole yhtään samalla tavalla kirjoitettavaa sanaa). Tehtävänäsi on luoda sanakirja luokkaan MuistavaSanakirja
. Toteuta luokka pakkaukseen sanakirja
.
Tee sanakirjalle parametriton konstruktori sekä metodit:
public void lisaa(String sana, String kaannos)
public String kaanna(String sana)
Sanakirjan tulee tässä vaiheessa toimia seuraavasti:
MuistavaSanakirja sanakirja = new MuistavaSanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); sanakirja.lisaa("apina", "apfe"); System.out.println( sanakirja.kaanna("apina") ); System.out.println( sanakirja.kaanna("monkey") ); System.out.println( sanakirja.kaanna("ohjelmointi") ); System.out.println( sanakirja.kaanna("banana") );
Tulostuu
monkey apina null banaani
Kuten tulostuksesta ilmenee, käännöksen lisäämisen jälkeen sanakirja osaa tehdä käännöksen molempiin suuntiin.
Huom: metodit lisaa
ja kaanna
eivät lue tiedostoa tai kirjoita tiedostoon! Myöskään konstruktori ei koske tiedostoon.
Lisää sanakirjalle metodi public void poista(String sana)
joka poistaa annetun sanan ja sen käännöksen sanakirjasta.
HUOM: kannattaa ehkä kerrata luku 49.9Huomio olioiden poistamisesta ArrayListista.
HUOM2: metodi poista
ei kirjoita tiedostoon.
Sanakirjan tulee tässä vaiheessa toimia seuraavasti:
MuistavaSanakirja sanakirja = new MuistavaSanakirja(); sanakirja.lisaa("apina", "monkey"); sanakirja.lisaa("banaani", "banana"); sanakirja.lisaa("ohjelmointi", "programming"); sanakirja.poista("apina"); sanakirja.poista("banana"); System.out.println( sanakirja.kaanna("apina") ); System.out.println( sanakirja.kaanna("monkey") ); System.out.println( sanakirja.kaanna("banana") ); System.out.println( sanakirja.kaanna("banaani") ); System.out.println( sanakirja.kaanna("ohjelmointi") );
Tulostuu
null null null null programming
Poisto siis toimii myös molemmin puolin, alkuperäisen sanan tai sen käännöksen poistamalla, poistuu sanakirjasta tieto molempien suuntien käännöksestä.
Tee sanakirjalle konstruktori public MuistavaSanakirja(String tiedosto)
ja metodi public boolean lataa()
, joka lataa sanakirjan konstruktorin parametrina annetun nimisestä tiedostosta. Jos tiedoston avaaminen tai lukeminen ei onnistu, palauttaa metodi false ja muuten true.
Huom: parameterillinen konstruktori ainoastaan kertoo sanakirjalle käytetävän tiedoston nimen. Konstruktori ei lue tiedostoa, tiedoston lukeminen tapahtuu ainoastaan metodissa lataa
.
Sanakirjatiedostossa yksi rivi sisältää sanan ja sen käännöksen merkillä ":" erotettuna. Tehtäväpohjan mukana tuleva testaamiseen tarkoitettu sanakirjatiedosto src/sanat.txt
on sisällöltään seuraava:
apina:monkey alla oleva:below olut:beer
Lue sanakirjatiedosto rivi riviltä lukijan metodilla nextLine
. Voit pilkkoa rivin String metodilla split
seuraavasti:
Scanner tiedostonLukija = new ... while ( tiedostonLukija.hasNextLine() ){ String rivi = tiedostonLukija.nextLine(); String[] osat = rivi.split(":"); // pilkotaan rivi :-merkkien kohdalta System.out.println( osat[0] ); // ennen :-merkkiä ollut osa rivistä System.out.println( osat[1] ); // :-merkin jälkeen ollut osa rivistä }
Sanakirjaa käytetään seuraavasti:
MuistavaSanakirja sanakirja = new MuistavaSanakirja("src/sanat.txt"); boolean onnistui = sanakirja.lataa(); if ( onnistui ) { System.out.println("sanakirjan lataaminen onnistui"); } System.out.println( sanakirja.kaanna("apina") ); System.out.println( sanakirja.kaanna("ohjelmointi") ); System.out.println( sanakirja.kaanna("alla oleva") );
Tulostuu
sanakirjan lataaminen onnistui monkey null below
Tee sanakirjalle metodi public boolean tallenna()
, jota kutsuttaessa sanakirjan sisältö kirjoitetaan konstruktorin parametrina annetun nimiseen tiedostoon. Jos tallennus ei onnistu, palauttaa metodi false ja muuten true. Sanakirjatiedostot tulee tallentaa ylläesitellyssä muodossa, eli ohjelman on osattava lukea itse kirjoittamiaan tiedostoja.
Huom1: mikään muu metodi kuin tallenna
ei kirjoita tiedostoon. Jos teit edelliset kohdat oikein, sinun ei tulisi tarvita muuttaa mitään olemassaolevaa koodia.
Huom2: vaikka sanakirja osaa käännökset molempiin suuntiin, ei sanakirjatiedostoon tule kirjoittaa kuin toinen suunta. Eli jos sanakirja tietää esim. käännöksen tietokone = computer, tulee rivi:
tietokone:computer
tai rivi
computer:tietokone
mutta ei molempia!
Talletus kannattanee hoitaa siten, että koko käännöslista kirjoitetaan uudelleen vanhan tiedoston päälle, eli materiaalissa esiteltyä append
-metodia ei kannata käyttää.
Sanakirjan lopullista versiota on tarkoitus käyttää seuraavasti:
MuistavaSanakirja sanakirja = new MuistavaSanakirja("src/sanat.txt"); sanakirja.lataa(); // käytä sanakirjaa sanakirja.tallenna();
Eli käytön aluksi ladataan sanakirja tiedostosta ja lopussa tallennetaan se takaisin tiedostoon jotta sanakirjaan tehdyt muutokset pysyvät voimassa seuraavallekin käynnistyskerralle.
Huom! Osa käyttöliittymätehtävien testeistä avaa käyttöliittymän ja käyttää hiirtäsi käyttöliittymäkomponenttien klikkailuun. Kun suoritat käyttöliittymätehtävien testejä, älä käytä hiirtäsi!
Ohjelmamme ovat tähän mennessä koostuneet lähinnä sovelluslogiikasta ja sovelluslogiikkaa käyttävästä tekstikäyttöliittymästä. Muutamissa tehtävissä on ollut myös graafinen käyttöliittymä, mutta ne on yleensä luotu puolestamme. Tutustutaan seuraavaksi graafisten käyttöliittymien luomiseen Javalla.
Käyttöliittymät ovat ikkunoita, jotka sisältävät erilaisia osia kuten nappeja, tekstikenttiä ja valikkoja. Käyttöliittymien ohjelmoinnissa käytetään Javan Swing-komponenttikirjastoa, joka tarjoaa luokkia käyttöliittymäkomponenttien luomiseen ja käsittelyyn.
Käyttöliittymien peruselementti on luokka JFrame
, jonka sisältämään komponenttiosioon käyttöliittymäkomponentit luodaan. Oikeaoppisesti luodut käyttöliittymät toteuttavat rajapinnan Runnable
, ja ne käynnistetään pääohjelmasta. Käytämme kurssilla seuraavanlaista käyttöliittymärunkoa:
import java.awt.Container; import java.awt.Dimension; import javax.swing.JFrame; import javax.swing.WindowConstants; public class Kayttoliittyma implements Runnable { private JFrame frame; public Kayttoliittyma() { } @Override public void run() { frame = new JFrame("Otsikko"); frame.setPreferredSize(new Dimension(200, 100)); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); luoKomponentit(frame.getContentPane()); frame.pack(); frame.setVisible(true); } private void luoKomponentit(Container container) { } public JFrame getFrame() { return frame; } }
Tarkastellaan ylläolevan käyttöliittymäluokan koodia hieman tarkemmin.
public class Kayttoliittyma implements Runnable {
Luokka Kayttoliittyma
toteuttaa Javan rajapinnan Runnable, joka tarjoaa mahdollisuuden säikeistettyyn ohjelman suorittamiseen. Säikeistetyllä suorittamisella voidaan suorittaa useita ohjelman osia rinnakkain. Emme tutustu säikeisiin tarkemmin, lisää tietoa säikeistä tulee muunmuassa toisen vuoden kurssilla Käyttöjärjestelmät.
private JFrame frame;
Käyttöliittymä sisältää oliomuuttujana JFrame
-olion, joka on näkyvän käyttöliittymän pohjaelementti. Kaikki käyttöliittymäkomponentit lisätään JFrame
-olion sisältämään komponenttialueeseen. Huomaa että oliomuuttujia ei saa alustaa metodien ulkopuolella. Esimerkiksi oliomuuttujan JFrame
alustus luokkamäärittelyssä "private JFrame frame = new JFrame()"
kiertää käyttöliittymäsäikeiden suoritusjärjestyksen, ja voi johtaa ydintuhoon. Tai ohjelmasi kaatumiseen.
@Override public void run() { frame = new JFrame("Otsikko"); frame.setPreferredSize(new Dimension(200, 100)); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); luoKomponentit(frame.getContentPane()); frame.pack(); frame.setVisible(true); }
Rajapinta Runnable
määrittelee metodin public void run()
, joka jokaisen Runnable
-rajapinnan toteuttajan tulee toteuttaa. Metodissa public void run()
luodaan ensin uusi JFrame-ikkuna, jonka otsikoksi asetetaan "Otsikko"
. Tämän jälkeen asetetaan ikkunan toivotuksi kooksi 200, 100 eli ikkunan leveydeksi tulee 200 pikseliä, korkeudeksi 100 pikseliä. Komento frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
kertoo JFrame-oliolle, että käyttöliittymän käynnistäneen ohjelman suoritus tulee lopettaa, kun käyttäjä painaa käyttöliittymässä olevaa ruksia.
Tämän jälkeen kutsutaan luokassa myöhemmin määriteltyä metodia luoKomponentit
. Metodille annetaan parametrina JFrame
-olion Container-olio, johon voi lisätä käyttöliittymäkomponentteja.
Lopuksi kutsutaan metodia frame.pack()
, joka asettaa JFrame-olion aiemmin määritellyn kokoiseksi ja järjestää JFrame-olion sisältämän Container-olion sisällä olevat käyttöliittymäkomponentit. Lopuksi kutsutaan metodia frame.setVisible(true)
, joka näyttää käyttöliittymän käyttäjälle.
private void luoKomponentit(Container container) { }
Metodissa luoKomponentit
lisätään JFrame
-olion sisältämään komponenttialueeseen käyttöliittymäkomponentteja. Esimerkissämme ei ole yhtäkään käyttöliittymäkomponenttia JFrame-ikkunan lisäksi. Luokalla Kayttoliittyma
on myös sen käyttöä helpottava metodi getFrame
, jolla päästään käsiksi luokan kapseloimaan JFrame-olioon.
Swing-käyttöliittymät käynnistetään SwingUtilities
-luokan tarjoaman invokeLater
-metodin avulla. Metodi invokeLater
saa parametrinaan Runnable
-rajapinnan toteuttavan olion. Metodi asettaa Runnable
-olion suoritusjonoon, ja kutsuu sitä kun ehtii. Luokan SwingUtilities
avulla voimme käynnistää uusia säikeitä tarvittaessa.
import javax.swing.SwingUtilities; public class Main { public static void main(String[] args) { Kayttoliittyma kayttoliittyma = new Kayttoliittyma(); SwingUtilities.invokeLater(kayttoliittyma); } }
Kun ylläoleva pääohjelmametodi suoritetaan, näemme luokassa Kayttoliittyma
määrittellyn käyttöliittymän.
Käyttöliittymä koostuu taustaikkunan (JFrame) sisältämästä komponenttipohjasta (Container), ja siihen asetetuista käyttöliittymäkomponenteista. Käyttöliittymäkomponentteja ovat erilaiset painikkeet, tekstit ym. Jokaiselle komponentille on oma luokka. Kannattaa tutustua Oraclen kuvasarjaan erilaisista komponenteista osoitteessa http://docs.oracle.com/javase/tutorial/ui/features/components.html.
Tekstin näyttäminen tapahtuu JLabel
-luokan avulla. Luokka JLabel
tarjoaa käyttöliittymäkomponentin, jolle voi asettaa tekstiä ja jonka sisältämää tekstiä voi muokata. Teksti asetetaan joko konstruktorissa tai erillisellä setText
-metodilla.
Muokataan käyttöliittymäpohjaamme siten, että siinä näkyy tekstiä. Luodaan uusi JLabel-tekstikomponentti metodissa luoKomponentit
. Tämän jälkeen lisätään se JFrame
-oliolta saatuun Container
-olioon add
-metodia käyttäen.
private void luoKomponentit(Container container) { JLabel teksti = new JLabel("Tekstikenttä!"); container.add(teksti); }
Kuten yllä olevasta lähdekoodista näemme, JLabel-käyttöliittymäkomponentti tulee näyttämään tekstin "Tekstikenttä!"
. Kun suoritamme käyttöliittymän, näemme seuraavanlaisen ikkunan.
Toteuta käyttöliittymä, joka näyttää tekstin "Moi!". Käyttöliittymän (eli JFrame-olion) leveyden tulee olla vähintään 400 ja korkeuden vähintään 200 ja otsikkona teksti "Swing on". Tehtävä tulee toteuttaa tehtäväpohjassa tulevaan käyttöliittymärunkoon. JFrame-olion luominen ja näkyväksi asettamisen tulee tapahtua metodissa run()
, tekstikomponentti lisätään käyttöliittymälle metodissa luoKomponentit(Container container)
.
HUOM: Käyttöliittymien oliomuuttujia saa alustaa vain metodeissa tai konstruktorissa! Älä alusta oliomuuttujia suoraan määrittelyn yhteydessä.
Käyttöliittymään saa painikkeita JButton
-luokan avulla. JButton-olion lisääminen käyttöliittymään tapahtuu aivan kuin JLabel-olion lisääminen.
private void luoKomponentit(Container container) { JButton nappi = new JButton("Click!"); container.add(nappi); }
Yritetään seuraavaksi lisätä käyttöliittymään sekä tekstiä, että nappi.
private void luoKomponentit(Container container) { JButton nappi = new JButton("Click!"); container.add(nappi); JLabel teksti = new JLabel("Tekstiä."); container.add(teksti); }
Ohjelmaa suorittaessa näemme seuraavanlaisen käyttöliittymän.
Vain viimeiseksi lisätty käyttöliittymäkomponentti on näkyvillä, eikä ohjelma toimi toivotusti. Mistä tässä oikein on kyse?
Jokaisella käyttöliittymäkomponentilla on oma sijainti käyttöliittymässä. Komponentin sijainnin määrää käytössä oleva käyttöliittymän asettelija (Layout Manager). Yrittäessämme aiemmin lisätä useampia käyttöliittymäkomponentteja Container
-olioon käyttöliittymässä oli vain yksi komponentti näkyvillä. Jokaisessa Container
-oliossa on oletuksena käyttöliittymäasettelija BorderLayout
.
BorderLayout asettelee käyttöliittymäkomponentit viiteen alueeseen: käyttöliittymän keskikohdan lisäksi käytössä ovat ilmansuunnat. Voimme antaa Container-olion add
-metodille ylimääräisenä parametrina lisätoiveen kohdasta, johon haluamme asettaa käyttöliittymäkomponentin. BorderLayout-luokassa on käytössä luokkamuuttujat BorderLayout.NORTH
, BorderLayout.EAST
, BorderLayout.SOUTH
, BorderLayout.WEST
, ja BorderLayout.CENTER
.
Käytettävä käyttöliittymäasettelija asetetaan Container
-oliolle metodin setLayout
-parametrina. Metodille add
voidaan antaa käyttöliittymäkomponentin lisäksi paikka, johon komponentti lisätään. Alla on esimerkki, jossa jokaiseen BorderLayoutin tarjoamaan paikkaan asetetaan käyttöliittymäkomponentti.
private void luoKomponentit(Container container) { // seuraava rivi siis ei tässä tilanteessa pakollinen, sillä BorderLayout on JFramessa joka tapauksessa oletuksena container.setLayout(new BorderLayout()); container.add(new JButton("Pohjoinen (North)"), BorderLayout.NORTH); container.add(new JButton("Itä (East)"), BorderLayout.EAST); container.add(new JButton("Etelä (South)"), BorderLayout.SOUTH); container.add(new JButton("Länsi (West)"), BorderLayout.WEST); container.add(new JButton("Keski (Center)"), BorderLayout.CENTER); container.add(new JButton("Oletuspaikka (Center)")); }
Huomaa, että nappi "Keski (Center)"
ei tule näkymään käyttöliittymässä, sillä nappi "Oletuspaikka (Center)"
asetetaan oletuksena sen paikalle. Käyttöliittymässäpohjassa yllä oleva koodi näyttää seuraavalta.
Kuten käyttöliittymäkomponentteja, myös käyttöliittymän asettelijoita on useita. Oraclella on käyttöliittymäasettelijoihin visuaalinen opas osoitteessa http://docs.oracle.com/javase/tutorial/uiswing/layout/visual.html. Tutustutaan seuraavaksi käyttöliittymäasettelijaan BoxLayout
.
BoxLayoutia käytettäessä käyttöliittymäkomponentit asetetaan käyttöliittymään joko vaakasuunnassa tai pystysuunnassa. BoxLayoutin konstruktorille annetaan parametrina Container-olio, johon käyttöliittymäkomponentteja ollaan asettamassa, ja käyttöliittymäkomponenttien asettelusuunta. Asettelusuunta on joko BoxLayout.X_AXIS
, eli komponentit vaakasuunnassa, tai BoxLayout.Y_AXIS
, eli komponentit pystysuunnassa. Toisin kuin BorderLayout-asettelijaa käytettäessä, BoxLayoutilla ei ole rajattua määrää paikkoja. Container-olioon voi siis lisätä niin monta käyttöliittymäkomponenttia kuin haluaa.
Käyttöliittymän asettelu BoxLayout
-asettelijaa käyttäen toimii kuten BorderLayout
-asettelijan käyttö. Luomme ensin asettelijan, jonka asetamme Container
-oliolle sen metodilla setLayout
. Tämän jälkeen voimme lisätä käyttöliittymäkomponentteja Container
-olion add
-metodilla. Emme tarvitse erillistä sijaintia ilmaisevaa parametria. Alla esimerkki vaakasuunnassa asetetuista käyttöliittymäkomponenteista.
private void luoKomponentit(Container container) { BoxLayout layout = new BoxLayout(container, BoxLayout.X_AXIS); container.setLayout(layout); container.add(new JLabel("Eka!")); container.add(new JLabel("Toka!")); container.add(new JLabel("Kolmas!")); }
Käyttöliittymäkomponenttien asettelu pystysuunnassa ei vaadi suurta muutosta. Vaihdamme BoxLayout
-olion konstruktorille annettavaksi suuntaparametriksi BoxLayout.Y_AXIS
.
private void luoKomponentit(Container container) { BoxLayout layout = new BoxLayout(container, BoxLayout.Y_AXIS); container.setLayout(layout); container.add(new JLabel("Eka!")); container.add(new JLabel("Toka!")); container.add(new JLabel("Kolmas!")); }
Käyttöliittymäasettelijoita käyttämällä voimme luoda käyttöliittymiä, joissa käyttöliittymäkomponentit ovat aseteltu sopivasti. Alla on esimerkkikäyttöliittymä, jossa komponentit asetetaan pystysuuntaan. Ensin teksti, ja sitten vaihtoehtoinen valinta. Vaihtoehtoisen valinnan, eli valinnan jossa vain yksi vaihtoehto on aina voimassa, voi tehdä käyttämällä ButtonGroup
-ryhmittelijää ja JRadioButton
-painikkeita.
private void luoKomponentit(Container container) { BoxLayout layout = new BoxLayout(container, BoxLayout.Y_AXIS); container.setLayout(layout); container.add(new JLabel("Valitse ruokavalio:")); JRadioButton liha = new JRadioButton("Liha"); JRadioButton kala = new JRadioButton("Kala"); ButtonGroup buttonGroup = new ButtonGroup(); buttonGroup.add(liha); buttonGroup.add(kala); container.add(liha); container.add(kala); }
Toteuta tehtäväpohjaan käyttöliittymä, joka näyttää seuraavalta:
Käytä käyttöliittymän asettelijana luokkaa BoxLayout
, komponentteina luokkia JLabel
, JRadioButton
, JCheckBox
ja JButton
.
Käytä ButtonGroup
-luokkaa varmistamaan että vaihtoehdot "Siksi" ja "Koska se on kivaa" eivät voi olla valittuina samaan aikaan.
Varmista että käyttöliittymä on niin iso, että käyttäjä voi klikata nappeja muuttamatta sen kokoa. Voit käyttää esimerkiksi leveytenä 200 pikseliä, korkeutena 300 pikseliä.
Tähänastiset graafiset käyttöliittymämme ovat, vaikkakin hienoja, hieman tylsiä: ne eivät reagoi millään tavalla käyttöliittymässä tehtyihin tapahtumiin. Reagoimattomuus ei johdu käyttöliittymäkomponenteista, vaan siitä että emme ole lisänneet käyttöliittymäkomponentteihin tapahtumia käsitteleviä kuuntelijoita.
Tapahtumankuuntelijat kuuntelevat käyttöliittymäkomponentteja joihin ne on liitetty. Aina kun käyttöliittymäkomponentille tehdään joku toiminto, esimerkiksi napille napin painaminen, käyttöliittymäkomponentti kutsuu jokaisen siihen liitetyn tapahtumakuuntelijan tiettyä metodia. Käytännössä tapahtumankuuntelijat ovat tietyn rajapinnan toteuttavia luokkia, joiden ilmentymiä käyttöliittymäkomponentille voi lisätä. Tapahtuman tapahtuessa käyttöliittymäkomponentti käy jokaisen siihen liitetyn tapahtumankuuntelijan läpi, ja kutsuu rajapinnassa määriteltyä metodia.
Swing-käyttöliittymissä eniten käytetty tapahtumankuuntelurajapinta on ActionListener
. Rajapinta ActionListener
määrittelee metodin void actionPerformed(ActionEvent e)
, joka saa parametrinaan tapahtumasta kertovan ActionEvent
-olion.
Toteutetaan ensimmäinen oma tapahtumankuuntelija, jonka tarkoituksena on vain tulostaa viesti standarditulostusvirtaan nappia painettaessa. Luokka ViestiKuuntelija
toteuttaa rajapinnan ActionListener
ja tulostaa viestin "Viesti vastaanotettu!"
kun metodia actionPerformed
kutsutaan.
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class ViestiKuuntelija implements ActionListener { @Override public void actionPerformed(ActionEvent ae) { System.out.println("Viesti vastaanotettu!"); } }
Luodaan seuraavaksi käyttöliittymään JButton
-tyyppinen nappi, ja lisätään siihen ViestiKuuntelija
-luokan ilmentymä. Luokalle JButton
voi lisätä tapahtumankuuntelijan käyttämällä sen yläluokassa AbstractButton
määriteltyä metodia public void addActionListener(ActionListener actionListener)
.
private void luoKomponentit(Container container) { JButton nappi = new JButton("Viestitä!"); nappi.addActionListener(new ViestiKuuntelija()); container.add(nappi); }
Käyttöliittymässä olevaa nappia painettaessa näemme standarditulostusvirrassa seuraavan viestin.
Viesti vastaanotettu!
Haluamme usein että tapahtumankuuntelija muokkaa jonkun olion tilaa. Päästäksemme olioon käsiksi tapahtumankuuntelijassa, tulee meidän antaa viite käsiteltävään olioon tapahtumankuuntelijalle sen konstruktorissa. Tapahtumankuuntelijat ovat täysin samanlaisia luokkia kuin muutkin Javan luokat, eli pääsemme ohjelmoimaan kaiken haluamamme toiminnallisuuden.
Pohditaan seuraavaa käyttöliittymää jossa on kaksi JTextArea
-tyyppistä tekstikenttää, eli tekstikenttää johon käyttäjä voi syöttää tekstiä, ja JButton
-tyyppinen nappi. Käyttöliittymä käyttää GridLayout
-asettelijaa, jonka avulla käyttöliittymän voi rakentaa taulukkomaiseksi. GridLayout-luokan konstruktorille määriteltiin yksi rivi ja kolme saraketta.
private void luoKomponentit(Container container) { GridLayout layout = new GridLayout(1, 3); container.setLayout(layout); JTextArea textAreaVasen = new JTextArea("Le Kopioija"); JTextArea textAreaOikea = new JTextArea(); JButton kopioiNappi = new JButton("Kopioi!"); container.add(textAreaVasen); container.add(kopioiNappi); container.add(textAreaOikea); }
Haluamme lisätä käyttöliittymään toiminnallisuuden, jossa JButton
-nappia painettaessa vasemman tekstikentän sisältö kopioituu oikeaan tekstikenttään. Tämä onnistuu toteuttamalla tapahtumankuuntelija. Luodaan rajapinnan ActionListener
toteuttava luokka KenttienKopioija
, joka kopioi JTextArean sisällön kentästä toiseen.
import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JTextArea; public class KenttienKopioija implements ActionListener { private JTextArea lahde; private JTextArea kohde; public KenttienKopioija(JTextArea lahde, JTextArea kohde) { this.lahde = lahde; this.kohde = kohde; } @Override public void actionPerformed(ActionEvent ae) { this.kohde.setText(this.lahde.getText()); } }
Tapahtumankuuntelijan rekisteröinti JButton
-oliolle onnistuu metodilla addActionListener
.
private void luoKomponentit(Container container) { GridLayout layout = new GridLayout(1, 3); container.setLayout(layout); JTextArea textAreaVasen = new JTextArea("Le Kopioija"); JTextArea textAreaOikea = new JTextArea(); JButton kopioiNappi = new JButton("Kopioi!"); KenttienKopioija kopioija = new KenttienKopioija(textAreaVasen, textAreaOikea); kopioiNappi.addActionListener(kopioija); container.add(textAreaVasen); container.add(kopioiNappi); container.add(textAreaOikea); }
Nappia painettaessa vasemman tekstikentän sisältö kopioituu oikealla olevaan tekstikenttään.
Toteuta tehtäväpohjaan käyttöliittymä, joka näyttää seuraavalta.
Ohjelman tulee koostua seuraavista pakkauksessa ilmoitin
olevista luokista. Luokka Ilmoitin
on käyttöliittymäluokka, joka käynnistetään Main
-luokasta. Ilmoittimessa on käyttöliittymäkomponentteina JTextField
, JButton
, ja JLabel
. Voit asetella käyttöliittymäkomponentit GridLayout
-asettelijan avulla: kutsu new GridLayout(3, 1)
luo uuden asettelijan, joka asettelee kolme käyttöliittymäelementtiä pystysuunnassa.
Sovelluksessa tulee olla lisäksi luokka TapahtumanKuuntelija
, joka toteuttaa rajapinnan ActionListener
. Tapahtumankuuntelija liitetään nappiin ja sen tulee kopioida käyttöliittymässä olevan JTextField-kentän sisältö JLabel-kenttään napin painalluksen yhteydessä ja samalla tyhjentää JTextField asettamalla sen sisällöksi "".
Varmista että käyttöliittymä käynnistyy niin isona että jokaista nappulaa voi klikata.
Sovelluslogiikan (esimerkiksi tallennus- tai lukutoiminnallisuuden) ja käyttöliittymän sekoittaminen samoihin luokkiin on yleisesti ottaen huono asia. Se vaikeuttaa ohjelman testaamista ja muokkaamista huomattavasti, ja tekee koodista myös paljon vaikeammin luettavaa. Single responsibility principlen sanoin "Jokaisella luokalla pitäisi olla vain yksi selkeä vastuu". Sovelluslogiikan erottaminen käyttöliittymälogiikasta onnistuu sopivan rajapintasuunnittelun kautta. Oletetaan, että käytössämme on rajapinta HenkiloVarasto
, ja haluamme toteuttaa käyttöliittymän henkilöiden tallentamiseen.
public interface HenkiloVarasto { void talleta(Henkilo henkilo); Henkilo hae(String henkilotunnus); void poista(Henkilo henkilo); void poista(String henkilotunnus); void poistaKaikki(); Collection<Henkilo> haeKaikki(); }
Käyttöliittymää toteutettaessa hyvä aloitustapa on sopivien käyttöliittymäkomponenttien lisääminen käyttöliittymään. Henkilöiden tallennuksessa tarvitsemme kentät nimelle ja henkilötunnukselle, sekä napin jolla henkilö voidaan lisätä. Käytetään Javan JTextField
-luokkaa tekstin syöttämiseen, ja JButton
-luokkaa napin toteuttamiseen. Luodaan käyttöliittymään lisäksi selventävät JLabel
-tyyppiset selitystekstit.
Käytetään käyttöliittymän asetteluun GridLayout
-asettelijaa. Rivejä käyttöliittymässä on 3, sarakkeita 2. Lisätään tapahtumankuuntelija myöhemmin. Käyttöliittymäluokan metodi luoKomponentit
näyttää nyt seuraavalta.
private void luoKomponentit(Container container) { GridLayout layout = new GridLayout(3, 2); container.setLayout(layout); JLabel nimiTeksti = new JLabel("Nimi: "); JTextField nimiKentta = new JTextField(); JLabel hetuTeksti = new JLabel("Hetu: "); JTextField hetuKentta = new JTextField(); JButton lisaaNappi = new JButton("Lisää henkilö!"); // tapahtumankuuntelija container.add(nimiTeksti); container.add(nimiKentta); container.add(hetuTeksti); container.add(hetuKentta); container.add(new JLabel("")); container.add(lisaaNappi); }
Käyttöliittymä näyttää seuraavalta kun siihen on lisätty tietoa.
Tapahtumankuuntelijan tulee tietää tallennustoiminnallisuudesta eli HenkiloVarasto
-rajapinnasta sekä kentistä, joita se käyttää. Luodaan ActionListener
-rajapinnan toteuttava luokka HenkilonLisaysKuuntelija
. Luokka saa konstruktorissaan parametrina HenkiloVarasto
-rajapinnan toteuttavan olion sekä kaksi JTextField
-oliota, jotka ovat kentät nimelle ja henkilötunnukselle. Metodissa actionPerformed
luodaan uusi Henkilo
-olio ja tallennetaan se HenkiloVarasto
-olion tarjoamalla talleta
-metodilla.
public class HenkilonLisaysKuuntelija implements ActionListener { private HenkiloVarasto henkiloVarasto; private JTextField nimiKentta; private JTextField hetuKentta; public HenkilonLisaysKuuntelija(HenkiloVarasto henkiloVarasto, JTextField nimiKentta, JTextField hetuKentta) { this.henkiloVarasto = henkiloVarasto; this.nimiKentta = nimiKentta; this.hetuKentta = hetuKentta; } @Override public void actionPerformed(ActionEvent ae) { Henkilo henkilo = new Henkilo(nimiKentta.getText(), hetuKentta.getText()); this.henkiloVarasto.talleta(henkilo); } }
Jotta saamme HenkiloVarasto
-viitteen HenkilonLisaysKuuntelija
-oliolle, tulee sen olla käyttöliittymän tiedossa. Lisätään käyttöliittymälle oliomuuttuja private HenkiloVarasto henkiloVarasto
, joka asetetaan konstruktorissa. Luokan Kayttoliittyma
konstruktoria muokataan siten, että sille annetaan HenkiloVarasto
-rajapinnan toteuttava luokka.
public class Kayttoliittyma implements Runnable { private JFrame frame; private HenkiloVarasto henkiloVarasto; public Kayttoliittyma(HenkiloVarasto henkiloVarasto) { this.henkiloVarasto = henkiloVarasto; } // ...
Voimme nyt luoda tapahtumankuuntelijan HenkilonLisaysKuuntelija
, jolle annetaan sekä HenkiloVarasto
-viite, että kentät.
private void luoKomponentit(Container container) { GridLayout layout = new GridLayout(3, 2); container.setLayout(layout); JLabel nimiTeksti = new JLabel("Nimi: "); JTextField nimiKentta = new JTextField(); JLabel hetuTeksti = new JLabel("Hetu: "); JTextField hetuKentta = new JTextField(); JButton lisaaNappi = new JButton("Lisää henkilö!"); HenkilonLisaysKuuntelija kuuntelija = new HenkilonLisaysKuuntelija(henkiloVarasto, nimiKentta, hetuKentta); lisaaNappi.addActionListener(kuuntelija); container.add(nimiTeksti); container.add(nimiKentta); container.add(hetuTeksti); container.add(hetuKentta); container.add(new JLabel("")); container.add(lisaaNappi); }
Tässä tehtävässä toteutetaan laskuri klikkausten laskemiseen. Tehtävässä sovelluslogiikka, eli laskeminen, ja käyttöliittymälogiikka on erotettu toisistaan. Lopullisen sovelluksen tulee näyttää kutakuinkin seuraavalta.
Toteuta pakkaukseen clicker.sovelluslogiikka
rajapinnan Laskuri
toteuttava luokka OmaLaskuri
. Luokan OmaLaskuri
metodin annaArvo
palauttama luku on aluksi 0. Kun metodia kasvata
kutsutaan, kasvaa arvo aina yhdellä.
Voit halutessasi testata luokan toimintaa seuraavan ohjelman avulla.
Laskuri laskuri = new OmaLaskuri(); System.out.println("Arvo: " + laskuri.annaArvo()); laskuri.kasvata(); System.out.println("Arvo: " + laskuri.annaArvo()); laskuri.kasvata(); System.out.println("Arvo: " + laskuri.annaArvo());
Arvo: 0 Arvo: 1 Arvo: 2
Toteuta pakkaukseen clicker.kayttoliittyma
rajapinnan ActionListener
toteuttava luokka KlikkaustenKuuntelija
. Luokka KlikkaustenKuuntelija
saa konstruktorin parametrina Laskuri
-rajapinnan toteuttavan olion ja JLabel
-olion.
Toteuta actionPerformed
-metodi siten, että Laskuri
-oliota kasvatetaan aluksi yhdellä, jonka jälkeen laskurin arvo asetetaan JLabel
-olion tekstiksi. JLabel
-olion tekstiä voidaan muuttaa metodilla setText
.
Muokkaa luokkaa Kayttoliittyma
siten, että käyttöliittymä saa konstruktorin parametrina Laskuri
-olion. Tarvitset tätä varten uuden konstruktorin. Lisää käyttöliittymään tarvittavat käyttöliittymäkomponentit. Rekisteröi napille myös edellisessä osassa toteutettu tapahtumankuuntelija.
Käytä käyttöliittymäkomponenttien asetteluun BorderLayout
-luokan tarjoamia toiminnallisuuksia. Muuta myös Main
-luokkaa siten, että käyttöliittymälle annetaan OmaLaskuri
-olio. Kun käyttöliittymässä olevaa "Click!"
-nappia on painettu kahdesti, sovellus näyttää kutakuinkin seuraavalta.
Törmäämme silloin tällöin tilanteeseen, jossa JFrame
-luokan tarjoama Container
-olio ei riitä käyttöliittymän asetteluun. Saatamme tarvita erilaisia käyttöliittymänäkymiä tai mahdollisuutta käyttöliittymäkomponenttien ryhmittelyyn niiden käyttötarkoituksen mukaan. Esimerkiksi alla olevan käyttöliittymän rakentaminen ei olisi kovin helppoa vain JFrame
-luokan tarjoamalla Container
-oliolla.
Voimme asettaa Container-tyyppisiä olioita toistensa sisään. Luokka JPanel
(katso myös How to Use Panels) mahdollistaa sisäkkäiset Container
-oliot. JPanel-luokan ilmentymään voi lisätä käyttöliittymäkomponentteja samalla tavalla kuin JFrame
-luokasta saatuun Container
-ilmentymään. Tämän lisäksi JPanel
-luokan ilmentymän voi lisätä Container
-olioon. Tämä mahdollistaa useamman Container
-olion käyttämisen käyttöliittymän suunnittelussa.
Yllä olevan käyttöliittymän luominen on helpompaa JPanel
-luokan avulla.. Luodaan käyttöliittymä, jossa on kolme nappia "Suorita", "Testaa", ja "Lähetä", sekä tekstialue joka sisältää tekstiä. Napit ovat oma joukkonsa, joten tehdään niille erillinen JPanel
-olio joka asetetaan JFrame
-luokasta saadun Container
-olion eteläosaan. Tekstialue tulee keskelle.
private void luoKomponentit(Container container) { container.add(new JTextArea()); container.add(luoValikko(), BorderLayout.SOUTH); } private JPanel luoValikko() { JPanel panel = new JPanel(new GridLayout(1, 3)); panel.add(new JButton("Suorita")); panel.add(new JButton("Testaa")); panel.add(new JButton("Lähetä")); return panel; }
JPanel-luokalle annetaan konstruktorin parametrina käytettävä asettelutyyli. Jos asettelutyyli tarvitsee konstruktorissaan viitteen käytettyyn Container
-olioon (kuten BoxLayout
), on JPanel
-luokalla myös metodi setLayout
.
Jos käyttöliittymässämme on selkeät erilliset kokonaisuudet, voimme myös periä JPanel
luokan. Esimerkiksi yllä olevan valikon voisi toteuttaa myös seuraavasti.
import java.awt.GridLayout; import javax.swing.JButton; import javax.swing.JPanel; public class ValikkoPanel extends JPanel { public ValikkoPanel() { super(new GridLayout(1, 3)); luoKomponentit(); } private void luoKomponentit() { add(new JButton("Suorita")); add(new JButton("Testaa")); add(new JButton("Lähetä")); } }
Nyt käyttöliittymäluokassa voidaan luoda ValikkoPanel
-luokan ilmentymä.
private void luoKomponentit(Container container) { container.add(new JTextArea()); container.add(new ValikkoPanel(), BorderLayout.SOUTH); }
Huomaa, että tapahtumankäsittelyä tarvittaessa luokalle ValikkoPanel
tulee antaa parametrina kaikki tarvittavat oliot.
Tehtävässä on tarkoitus toteuttaa yksinkertainen laskin. Laskimen käyttöliittymän tulee olla seuraavanlainen:
Tehtäväpohjan mukana tulee käynnistyksen suorittava pääohjelma sekä graafisen käyttöliittymän sisältävä luokka GraafinenLaskin
. Käyttöliittymän on oltava täsmälleen seuraavassa osassa kuvaillulla tavalla tehty, muuten saat vapaasti suunnitella ohjelman rakenteen.
Käyttöliittymän pohjana olevassa JFrame
ssa tulee käyttää asettelijana GridLayout
ia jossa on kolme riviä ja yksi sarake. Ylimpänä on tuloskenttänä toimiva JTextField
, joka täytyy asettaa "pois päältä" metodikutsulla setEnabled(false)
. Toisena on syötekenttänä toimiva JTextField
. Tuloskentässä on aluksi teksti 0 ja syötekenttä on tyhjä.
Alimpana komponenttina sijaitsee JPanel
, jonka asettelijana on GridLayout
, jossa on yksi rivi ja kolme saraketta. JPanelissa on kolme JButton
ia, joissa tekstit "+", "-" ja "Z".
Laskimen käyttöliittymän koon on oltava vähintään 300*150.
Laskimen toimintalogiikka on seuraava. Käyttäjän kirjoittaessa syötekenttään luvun n ja painaessa +, lisätään tuloskentässä olevaan arvoon n ja päivitetään tuloskenttä uuteen arvoon. Vastaavasti käyttäjän kirjoittaessa syötekenttään luvun n ja painaessa -, vähennetään tuloskentässä olevasta arvosta n ja päivitetään tuloskenttä uuteen arvoon. Jos käyttäjä painaa Z, nollautuu tuloskenttä.
Vihje: joskus on tarkoituksenmukaista hoitaa yhdellä tapahtumankuuntelijalla usean napin painallusten käsittely. Tämä onnistuu kysymällä actionPerformed
-metodin parametrilta kuka oli tapahtuman aiheuttaja. Seuraava koodi olettaa, että tapahtumankäsittelijällä on oliomuuttujat JButton plus
ja JButton miinus
jotka viittaavat plus- ja miinus-nappeihin:
@Override public void actionPerformed(ActionEvent ae) { if (ae.getSource() == plus) { // käsittele plus-painike } else if (ae.getSource() == miinus) { // käsittele miinus-painike } else ...
Laajennetaan vielä ohjelmaa seuraavilla ominaisuuksilla:
setEnabled(false)
. Muissa tilanteissa napin tulee olla päällä.Luokkaa JPanel
käytetään Container
-toiminnallisuuden lisäksi usein piirtoalustana siten, että käyttäjä perii luokan JPanel
ja korvaa metodin protected void paintComponent(Graphics graphics)
. Käyttöliittymä kutsuu metodia paintComponent
aina kun käyttöliittymäkomponentin sisältö halutaan piirtää ruudulle. Metodi paintComponent
saa käyttöliittymältä parametrina abstraktin luokan Graphics
toteuttavan olion. Luodaan luokan JPanel
perivä luokka Piirtoalusta
, joka korvaa paintComponent
-metodin.
public class Piirtoalusta extends JPanel { public Piirtoalusta() { super.setBackground(Color.WHITE); } @Override protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); } }
Yllä oleva piirtoalusta ei sisällä konkreettista piirtämistoiminnallisuutta. Asetamme konstruktorissa piirtoalustan taustan valkoiseksi kutsumalla yläluokan metodia setBackground
. Metodi setBackGround
saa parametrina Color
-luokan ilmentymän. Luokka Color
sisältää yleisimmät värit luokkamuuttujina, esimerkiksi väri valkoinen löytyy luokkamuuttujasta Color.WHITE
.
Huom: metodin paintComponent alussa tulee olla kutsu korvattuun metodiin, eli ensimmäisen rivin tulee olla super.paintComponent(g);
muuten piirtäminen ei toimi halutulla tavalla.
Korvattu paintComponent
metodi kutsuu yläluokan paintComponent
-metodia eikä tee muuta. Lisätään piirtoalusta seuraavaksi käyttöliittymäluokan luoKomponentit
-metodiin. Käytämme kappaleen 51. Käyttöliittymät alussa määriteltyä käyttöliittymäpohjaa.
private void luoKomponentit(Container container) { container.add(new Piirtoalusta()); }
Käynnistäessämme käyttöliittymän näemme tyhjän ruudun, jonka taustaväri on valkoinen. Alla olevan käyttöliittymän toivotuksi kooksi on asetettu setPreferredSize
-metodilla 300, 300, ja sen otsikko on "Piirtoalusta"
.
Piirtoalustalle piirtäminen tapahtuu Graphics
-olion tarjoamien metodien avulla. Muokataan Piirtoalusta
-luokan metodia paintComponent
siten, että siinä piirretään kaksi suorakulmiota Graphics
-olion tarjoaman metodin fillRect
avulla.
@Override protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); graphics.fillRect(50, 80, 100, 50); graphics.fillRect(200, 20, 50, 200); }
Metodi fillRect
saa parametrina suorakulmion x
, ja y
-koordinaatit, sekä suorakulmion leveyden ja korkeuden tässä järjestyksessä. Yllä siis piirretään ensin koordinaatista (50, 80)
alkava 100 pikseliä leveä ja 50 pikseliä korkea suorakulmio. Tämän jälkeen piirretään koordinaatista (200, 20)
alkava 50 pikseliä leveä ja 100 pikseliä korkea suorakulmio.
Kuten piirtotuloksesta huomaat, koordinaatisto ei toimi aivan kuten olemme tottuneet.
Javan Graphics
-olio (ja useiden muiden ohjelmointikielten käyttöliittymäkirjastot) olettaa että y-akselin arvo kasvaa alaspäin mennessä. Koordinaatiston origo, eli piste (0, 0)
on piirrettävän alueen vasemmassa yläkulmassa: Graphics-olio tietää aina käyttöliittymäkomponentin, johon piirretään, ja osaa sen perusteella päätellä piirtotapahtuman sijainnin. Käyttöliittymän origon sijainti selkeytyy seuraavalla ohjelmalla. Piirretään ensin pisteestä (0, 0) lähtevä 10 pikseliä leveä ja 200 pikseliä korkea vihreä suorakulmio. Tämän jälkeen piirretään pisteestä (0, 0) lähtevä 200 pikseliä leveä ja 10 pikseliä korkea musta. Seuraavana piirrettävän kuvion väri määritellään Graphics
-oliolle metodilla setColor
.
@Override protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); graphics.setColor(Color.GREEN); graphics.fillRect(0, 0, 10, 200); graphics.setColor(Color.BLACK); graphics.fillRect(0, 0, 200, 10); }
Tämä koordinaatiston käänteisyys johtuu siitä, miten käyttöliittymien kokoa muokataan. Käyttöliittymän kokoa muutettaessa sitä pienennetään tai suurennetaan "oikeasta alakulmasta vetäen", jolloin ruudulla näkyvä piirros siirtyy kokoa muuttaessa. Kun koordinaatisto alkaa vasemmasta yläkulmasta, on piirroksen sijainti aina sama, mutta näkyvä osa muuttuu.
Tehtäväpohjassa on valmiina käyttöliittymä, johon on kytketty JPanel
-luokan perivä luokka Piirtoalusta
. Muuta luokan Piirtoalusta
metodin paintComponent
toteutusta siten, että se piirtää seuraavanlaisen kuvion. Saat käyttää tehtävässä vain graphics
-olion fillRect
-metodia.
Huom! Älä käytä enempää kuin viittä fillRect
-kutsua. Kuvion ei tarvitse olla täsmälleen samanlainen kuin ylläoleva, testit kertovat kun piirtämäsi kuva on tarpeeksi lähellä haluttua kuvaa.
Laajennetaan edellistä esimerkkiä siten, että piirrämme käyttöliittymässä erillisen hahmo-olion. Luodaan hahmon edustamiseen luokka Hahmo
. Hahmolla on koordinaatteina ilmaistu sijainti, ja se piirretään ympyränä jonka halkaisija on 10 pikseliä. Hahmon sijaintia voi muuttaa kutsumalla sen siirry
-metodia.
import java.awt.Graphics; public class Hahmo { private int x; private int y; public Hahmo(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } public void siirry(int xmuutos, int ymuutos) { this.x += xmuutos; this.y += ymuutos; } public void piirra(Graphics graphics) { graphics.fillOval(x, y, 10, 10); } }
Muutetaan piirtoalustaa siten, että sille annetaan Hahmo
-luokan ilmentymä konstruktorin parametrina. Luokan Piirtoalusta
metodi paintComponent
ei itse piirrä hahmoa, vaan delegoi piirtovastuun Hahmo
-luokan ilmentymälle.
import java.awt.Color; import java.awt.Graphics; import javax.swing.JPanel; public class Piirtoalusta extends JPanel { private Hahmo hahmo; public Piirtoalusta(Hahmo hahmo) { super.setBackground(Color.WHITE); this.hahmo = hahmo; } @Override protected void paintComponent(Graphics graphics) { super.paintComponent(graphics); hahmo.piirra(graphics); } }
Annetaan hahmo myös käyttöliittymälle parametrina. Hahmo on siis käyttöliittymästä erillinen olio, joka vain halutaan piirtää käyttöliittymässä. Oleelliset muutokset käyttöliittymäluokassa ovat siis konstruktorin muuttaminen siten, että se saa parametrina Hahmo
-olion. Tämän lisäksi metodissa luoKomponentit
annetaan Hahmo
-luokan ilmentymä parametrina luotavalle Piirtoalusta
-oliolle.
public class Kayttoliittyma implements Runnable { private JFrame frame; private Hahmo hahmo; public Kayttoliittyma(Hahmo hahmo) { this.hahmo = hahmo; } // ... private void luoKomponentit(Container container) { Piirtoalusta piirtoalusta = new Piirtoalusta(hahmo); container.add(piirtoalusta); } // ...
Käyttöliittymän voi nyt käynnistää antamalla sen konstruktorille Hahmo
-olion parametrina.
Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Hahmo(30, 30)); SwingUtilities.invokeLater(kayttoliittyma);
Yllä olevassa käyttöliittymässä näkyy huikea, pallonmuotoinen hahmo.
Lisätään seuraavaksi ohjelmaan hahmon siirtämistoiminnallisuus. Haluamme liikuttaa hahmoa näppäimistöllä. Kun käyttäjä painaa nuolta vasemmalle, hahmon pitäisi siirtyä vasemmalle. Oikealle osoittavaa nuolta painettaessa hahmon pitäisi siirtyä oikealle. Tarvitsemme siis tapahtumankuuntelijan, joka kuuntelee näppäimistöä. Rajapinta KeyListener
määrittelee näppäimistönkuuntelijalta vaaditut toiminnallisuudet.
Rajapinta KeyListener
vaatii metodien keyPressed
, keyReleased
, ja keyTyped
toteuttamista. Olemme kiinnostuneita vain tapahtumasta, jossa näppäintä painetaan, joten jätämme metodit keyReleased
ja keyTyped
tyhjiksi. Luodaan luokka NappaimistonKuuntelija
, joka toteuttaa rajapinnan KeyListener
. Luokka saa parametrina Hahmo
-olion, jota tapahtumankäsittelijän tulee liikuttaa.
import java.awt.event.KeyEvent; import java.awt.event.KeyListener; public class NappaimistonKuuntelija implements KeyListener { private Hahmo hahmo; public NappaimistonKuuntelija(Hahmo hahmo) { this.hahmo = hahmo; } @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_LEFT) { hahmo.siirry(-5, 0); } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { hahmo.siirry(5, 0); } } @Override public void keyReleased(KeyEvent e) { } @Override public void keyTyped(KeyEvent ke) { } }
Metodi keyPressed
saa käyttöliittymältä parametrina KeyEvent
-luokan ilmentymän. KeyEvent-oliolta saa tietoon painettuun nappiin liittyvän numeron sen getKeyCode()
-metodilla. Eri näppäimille on luokkamuuttujat KeyEvent
-luokassa, esimerkiksi nuoli vasemmalle on KeyEvent.VK_LEFT
.
Haluamme kuunnella käyttöliittymään kohdistuvia näppäimen painalluksia (emme esimerkiksi ole kirjoittamassa tekstikenttään), joten lisätään näppäimistönkuuntelija JFrame
-luokan ilmentymälle. Muokataan käyttöliittymäämme siten, että näppäimistönkuuntelija lisätään JFrame-oliolle.
private void luoKomponentit(Container container) { Piirtoalusta piirtoalusta = new Piirtoalusta(hahmo); container.add(piirtoalusta); frame.addKeyListener(new NappaimistonKuuntelija(hahmo)); }
Nyt sovelluksemme kuuntelee näppäimistöltä tulleita painalluksia, ja ohjaa ne luokan NappaimistonKuuntelija
ilmentymälle.
Kokeillessamme käyttöliittymää se ei kuitenkaan toimi: hahmo ei siirry ruudulla. Mistä tässä oikein on kyse? Voimme tarkastaa että näppäimistön painallukset ohjautuvat NappaimistonKuuntelija
-oliolle lisäämällä keyPressed
-metodin alkuun testitulostuksen.
@Override public void keyPressed(KeyEvent e) { System.out.println("Nappia " + e.getKeyCode() + " painettu."); // ...
Käynnistäessämme ohjelman ja painaessamme näppäimiä näemme konsolissa tulostuksen.
Nappia 39 painettu. Nappia 37 painettu. Nappia 40 painettu. Nappia 38 painettu.
Huomaamme että näppäimistön kuuntelija toimii, mutta piirtoalusta ei päivity.
Käyttöliittymäkomponentit sisältävät yleensä toiminnallisuuden komponentin ulkoasun uudelleenpiirtämiseen tarvittaessa. Esimerkiksi nappia painettaessa JButton
-luokan ilmentymä osaa piirtää napin "painettuna", jonka jälkeen nappi piirretään taas normaalina. Toteuttamassamme piirtoalustassa ei ole valmista päivitystoiminnallisuutta, vaan meidän tulee pyytää sitä piirtämään itsensä uudelleen tarvittaessa.
Jokaisella Component
-luokan aliluokalla on metodi public void repaint()
, jonka kutsuminen pakottaa komponentin uudelleenpiirtämisen. Haluamme että Piirtoalusta
-olio piirretään uudestaan aina kun hahmoa siirretään. Hahmon siirtäminen tapahtuu luokassa NappaimistonKuuntelija
, joten on loogista että uudelleenpiirtokutsu tapahtuu myös näppäimistönkuuntelijassa.
Uudelleenpiirtokutsua varten näppäimistönkuuntelija tarvitsee viitteen piirtoalustaan. Muutetaan luokkaa NappaimistonKuuntelija
siten, että se saa parametrinaan Hahmo
-olion lisäksi uudelleenpiirrettävän Component
-olion. Kutsutaan Component
-olion repaint
-metodia jokaisen keyPressed
tapahtuman lopussa.
import java.awt.Component; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; public class NappaimistonKuuntelija implements KeyListener { private Component component; private Hahmo hahmo; public NappaimistonKuuntelija(Hahmo hahmo, Component component) { this.hahmo = hahmo; this.component = component; } @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_LEFT) { hahmo.siirry(-5, 0); } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { hahmo.siirry(5, 0); } component.repaint(); } @Override public void keyReleased(KeyEvent e) { } @Override public void keyTyped(KeyEvent ke) { } }
Muutetaan myös Kayttoliittyma
-luokan luoKomponentit
-metodia siten, että Piirtoalusta
-luokan ilmentymä annetaan parametrina näppäimistönkuuntelijalle.
private void luoKomponentit(Container container) { Piirtoalusta piirtoalusta = new Piirtoalusta(hahmo); container.add(piirtoalusta); frame.addKeyListener(new NappaimistonKuuntelija(hahmo, piirtoalusta)); }
Nyt hahmon liikuttaminen myös näkyy käyttöliittymässä. Aina kun käyttäjä painaa näppäimistöä, käyttöliittymään liitetty näppäimistönkuuntelija käsittelee kutsun. Jokaisen kutsun lopuksi kutsutaan piirtoalustan repaint
-metodia, joka aiheuttaa piirtoalustan uudelleenpiirtämisen.
Teemme ohjelman, jossa käyttäjä voi liikutella näppäimistön avulla ruudulle piirrettyjä kuvioita. Ohjelmassa tulee mukana käyttöliittymärunko, jota pääset muokkaamaan ohjelman edetessä.
Aluksi tehdään muutama luokka, joilla kuvioita hallitaan. Pääsemme myöhemmin piirtämään kuvioita ruudulle. Tee kaikki ohjelman luokat pakkaukseen liikkuvakuvio
.
Tehtävässä käytetään perintää ja abstrakteja luokkia. Kertaa siis tarvittaessa luvut 18.1, 18.2 ja 18.5
Tee abstrakti luokka Kuvio
. Kuviolla on oliomuuttujat x
ja y
, jotka kertovat kuvion sijainnin ruudulla sekä metodi public void siirra(int dx, int dy)
, jonka avulla kuvion sijainti siirtyy parametrina olevien koordinaattisiirtymien verran. Esim. jos sijainti aluksi on (100,100), niin kutsun siirra(10,-50)
jälkeen sijainti on (110, 50). Luokan konstruktorin public Kuvio(int x, int y)
tulee asettaa kuviolle alkusijainti. Lisää luokalle myös metodit public int getX()
ja public int getY()
.
Luokalla tulee olla myös abstrakti metodi public abstract void piirra(Graphics graphics)
, jolla kuvio piirretään piirtoalustalle. Kuvion piirtämismetodi toteutetaan luokan Kuvio
perivissä metodeissa.
Tee luokka Ympyra
joka perii Kuvion. Ympyrällä on halkaisija
jonka arvon konstruktori public Ympyra(int x, int y, int halkaisija)
asettaa. Sijainti tallennetaan yläluokassa määriteltyihin oliomuuttujiin.
Ympyra määrittelee metodin piirra
siten, että oikean kokoinen ympyrä piirretään koordinaattien osoittamaan paikkaan
parametrina olevan Graphics
-olion fillOval
-metodia käyttäen, ympyrän sijaintia tulee käyttää metodin kahtena ensimmäisenä parametrina. Ota mallia Hahmo-esimerkin vastaavasta metodista.
Graphics-olion metodien toimintaa kannattaa tutkia Java API:sta.
Luo luokka Piirtoalusta
joka perii luokan JPanel
, mallia voit ottaa esimerkiksi edellisen tehtävän mukana tulleesta piirtoalustasta. Piirtoalusta saa konstruktorin parametrina Kuvio
-tyyppisen olion. Korvaa luokan JPanel
metodi protected void paintComponent(Graphics g)
siten, että siinä kutsutaan ensin yläluokan paintComponent
-metodia ja sitten piirtoalustalle asetetun kuvion piirra
-metodia.
Muokkaa luokkaa Kayttoliittyma
siten, että se saa konstruktorin parametrina Kuvio
-tyyppisen olion. Lisää käyttöliittymään Piirtoalusta luoKomponentit(Container container)
-metodissa, anna piirtoalustalle konstruktorin parametrina käyttöliittymälle annettu kuvio.
Testaa lopuksi että seuraavalla esimerkkikoodilla ruudulle piirtyy ympyrä.
Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Ympyra(50, 50, 250)); SwingUtilities.invokeLater(kayttoliittyma);
Laajennetaan piirtoalustaa siten, että kuviota voi liikutella nuolinäppäinten avulla. Luo rajapinnan KeyListener
toteuttava luokka NappaimistonKuuntelija
. Luokan NappaimistonKuuntelija
konstruktorin parametrit ovat luokan Component
ilmentymä ja luokan Kuvio
ilmentymä.
Luokan Component ilmentymä annetaan näppäimistönkuuntelijalle, jotta voimme päivittää halutun komponentin jokaisen näppäimenpainalluksen jälkeen uudestaan. Komponentin päivittäminen tapahtuu kutsumalla Component
luokasta perityvää metodia repaint
. Luokka Piirtoalusta on tyyppiä Component
koska Component
on luokan JPanel
perivän luokan yläluokka.
Toteuta rajapinnan KeyListener
määrittelemä metodi keyPressed(KeyEvent e)
siten, että käyttäjän painaessa nuolta vasemmalle kuvio siirtyy yhden pykälän vasemmalle. Oikealle painettaessa yksi oikealle. Ylös painettaessa yksi ylös, ja alas painettaessa yksi alas. Huomaa että y-akseli kasvaa ikkunan yläosasta alaspäin. Näppäinkoodit nuolinäppäimille ovat KeyEvent.VK_LEFT
, KeyEvent.VK_RIGHT
, KeyEvent.VK_UP
, ja KeyEvent.VK_DOWN
. Jätä muut rajapinnan KeyListener
vaatimat metodit tyhjiksi.
Kutsu aina Component-luokan repaint
-metodia näppäimistönkuuntelutapahtuman lopussa.
Lisää näppäimistönkuuntelija Kayttoliittyma-luokan lisaaKuuntelijat
-metodissa. Näppäimistönkuuntelija tulee liittää JFrame
-olioon.
Peri luokasta Kuvio
luokat Nelio
ja Laatikko
. Neliöllä on konstruktori public Nelio(int x, int y, int sivunPituus)
, laatikon konstruktori on muotoa public Laatikko(int x, int y, int leveys, int korkeus)
. Käytä piirtämisessä graphics-olion fillRect
-metodia.
Varmista, että neliöt ja laatikot piirtyvät ja liikkuvat oikein Piirtoalustalla.
Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Nelio(50, 50, 250)); SwingUtilities.invokeLater(kayttoliittyma);
Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Laatikko(50, 50, 100, 300)); SwingUtilities.invokeLater(kayttoliittyma);
Peri luokasta Kuvio
luokka Koostekuvio
. Koostekuvio sisältää joukon muita kuvioita jotka se tallettaa ArrayList:iin. Koostekuviolla on metodi public void liita(Kuvio k)
jonka avulla koostekuvioon voi liittää kuvio-olion. Koostekuviolla ei ole omaa sijaintia ja ei ole merkitystä mitä koostekuvio asettaa perimiensä x- ja y-koordinaatin arvoiksi. Koostekuvio piirtää itsensä pyytämällä osiaan piirtämään itsensä, koostekuvion siirtyminen tapahtuu samoin. Kuviolta peritty metodi siirra
on siis ylikirjoitettava!
Koostekuvion konstruktorilla ei ole parametreja. Koostekuvion konstruktorista on kuitenkin pakko kutsua yliluokan konstruktoria jolla taas on parametrit. Koska koostekuviolla ei ole omaa sijaintia, voit käyttää yliluokan konstruktorikutsussa mitä tahansa parametrin arvoja.
Testaa että koostekuviosi piirtyy ja siirtyy oikein, esim. seuraavan koostekuvion avulla:
Koostekuvio rekka = new Koostekuvio(); rekka.liita(new Laatikko(220, 110, 75, 100)); rekka.liita(new Laatikko(80, 120, 200, 100)); rekka.liita(new Ympyra(100, 200, 50)); rekka.liita(new Ympyra(220, 200, 50)); Kayttoliittyma kayttoliittyma = new Kayttoliittyma(rekka); SwingUtilities.invokeLater(kayttoliittyma);
Huomaa miten olioiden vastuut jakautuvat tehtävässä. Jokainen Kuvio on vastuussa itsensä piirtämisestä ja siirtämisestä. Yksinkertaiset kuviot siirtyvät kaikki samalla tavalla. Jokaisen yksinkertaisen kuvion on itse hoidettava piirtymisestään. Koostekuvio siirtää itsensä pyytämällä osiaan siirtymään, samoin hoituu koostekuvion piirtyminen. Piirtoalusta tuntee Kuvio-olion joka siis voi olla mikä tahansa yksinkertainen kuvio tai koostekuvio, kaikki piirretään ja siirretään samalla tavalla. Piirtoalusta siis toimii samalla tavalla kuvion oikeasta tyypistä huolimatta, piirtoalustan ei tarvitse tietää kuvion yksityiskohdista mitään. Kun piirtoalusta kutsuu kuvion metodia piirra
tai siirra
polymorfismin ansiosta kutsutuksi tulee kuvion todellista tyyppiä vastaava metodi.
Huomionarvoista tehtävässä on se, että Koostekuvio voi sisältää mitä tahansa Kuvio-olioita, siis myös koostekuvioita! Luokkarakenne mahdollistaakin mielivaltaisen monimutkaisen kuvion muodostamisen ja kuvion siirtely ja piirtäminen tapahtuu aina täsmälleen samalla tavalla.
Luokkarakennetta on myös helppo laajentaa, esim. perimällä Kuvio-luokasta uusia kuviotyyppejä: kolmio, piste, viiva, ym... Koostekuvio toimii ilman muutoksia myös uusien kuviotyyppien kanssa, samoin piirtoalusta ja käyttöliittymä.
Sovelluskehys on ohjelma, joka tarjoaa lähtökohdan ja joukon palveluita jonkin erityisen sovelluksen toteuttamiseen. Yksi tapa laatia sovelluskehys on laatia valmiita palveluita tarjoava luokka, jonka päälle luokan perivät luokat rakentavat erityisen sovelluksen. Sovelluskehykset ovat yleensä hyvin laajoja, ja tarkoitettu johonkin tiettyyn tarkoitukseen, esimerkiksi pelien ohjelmointiin tai web-sovelluskehitykseen. Tutustutaan seuraavasti pikaisesti valmiin sovelluskirjaston käyttöön luomalla sovelluslogiikka Game of Life -pelille.
Tässä tehtäväsarjassa toteutetaan sovelluslogiikka Game of Life-pelille perimällä valmis sovellusrunko. Sovellusrunko on projektiin erikseen lisätyssä kirjastossa, joten sen lähdekoodit eivät ole nähtävissä.
HUOM: tehtävä ei ole erityisen vaikea, mutta tehtävänanto saattaa aluksi vaikuttaa sekavalta. Lue ohje tarkasti uudelleen tai kysy apua jos et pääse alkuun. Tehtävä kannattaa ehdottomasti tehdä, sillä lopputulos on hieno!
Game of Life on matemaatikko John Conway'n kehittelemä yksinkertainen "populaatiosimulaattori", kts. http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life.
Game of Lifen säännöt ovat seuraavat:
Abstrakti luokka GameOfLifeAlusta
tarjoaa seuraavat toiminnot
Luokassa GameOfLifeAlusta
on lisäksi määritelty seuraavat abstraktit metodit, jotka sinun tulee toteuttaa.
Luo pakkaukseen game
luokka OmaAlusta
, joka perii pakkauksessa gameoflife
olevan luokan GameOfLifeAlusta
. Huomaa että pakkausta gameoflife
ei ole näkyvillä omassa projektissasi, vaan se tulee mukana luokkakirjastona. Toteuta luokalle OmaAlusta
konstruktori public OmaAlusta(int leveys, int korkeus)
, joka kutsuu yläluokan konstruktoria annetuilla parametreilla:
import gameoflife.GameOfLifeAlusta; public class OmaAlusta extends GameOfLifeAlusta { public OmaAlusta(int leveys, int korkeus) { super(leveys, korkeus); } // ..
Voit ensin korvata kaikki abstraktit metodit ei-abstrakteilla metodeilla, jotka eivät kuitenkaan vielä tee mitään järkevää. Mutta koska ne eivät ole abstrakteja, tästä luokasta voi luoda ilmentymiä, toisin kuin abstraktista luokasta GameOfLifeAlusta.
Toteuta seuraavat metodit
Vihje: Pääset yläluokassa olevaan kaksiulotteiseen taulukkoon käsiksi yläluokan tarjoaman metodin getAlusta()
avulla. Kaksiulotteisia taulukoita käytetään kuten yksiulotteisia taulukoita, mutta taulukoille annetaan kaksi indeksiä. Ensimmäinen indeksi kertoo leveyskohdan, toinen indeksi korkeuskohdan. Esimerkiksi seuraava ohjelmapätkä luo ensin 10 x 10 -kokoisen taulukon, ja tulostaa sitten taulukon indeksissä 3, 1 olevan arvon.
boolean[][] arvot = new boolean[10][10]; System.out.println(arvot[3][1]);
Vastaavasti OmaAlusta-luokassa voidaan tulostaa yläluokasta saadun taulukon arvo indeksissä x, y seuraavasti:
boolean[][] alusta = getAlusta(); System.out.println(alusta[x][y]);
Ja indeksiin x,y voidaan asettaa esim. arvo true seuraavasti:
boolean[][] alusta = getAlusta(); alusta[x][y] = true;
Tai suoraan käyttämättä apumuuttujaa:
getAlusta()[x][y] = true;
Testaa toteutustasi seuraavalla testiohjelmalla. Huom: jos projektissasi ei ole mukana luokkaa GameOfLifeTestaaja
löydät sen täältä.
package game; public class Main { public static void main(String[] args) { OmaAlusta alusta = new OmaAlusta(7, 5); alusta.muutaElavaksi(2, 0); alusta.muutaElavaksi(4, 0); alusta.muutaElavaksi(3, 3); alusta.muutaKuolleeksi(3, 3); alusta.muutaElavaksi(0, 2); alusta.muutaElavaksi(1, 3); alusta.muutaElavaksi(2, 3); alusta.muutaElavaksi(3, 3); alusta.muutaElavaksi(4, 3); alusta.muutaElavaksi(5, 3); alusta.muutaElavaksi(6, 2); GameOfLifeTestaaja gom = new GameOfLifeTestaaja(alusta); gom.pelaa(); } }
Tulostuksen pitäisi olla seuraavanlainen:
Paina enter jatkaaksesi, muut lopettaa: <enter> X X X X XXXXX Paina enter jatkaaksesi, muut lopettaa: stop Kiitos!
Toteuta metodi alustaSatunnaisetPisteet(double todennakoisyysPisteelle), joka alustaa kaikki alkiot siten, että kukin alkio on elävä todennäköisyydellä todennakoisyysPisteelle. Todennäköisyys annetaan metodille suljetulla välillä [0, 1] olevana double-tyyppisenä parametrina.
Testaa metodia. Arvolla 0.0 ei pitäisi olla yhtään elossa olevaa solua, arvolla 1.0 kaikkien solujen tulisi olla elossa (eli näkyä X-merkkisinä). Arvolla 0.5 noin puolet soluista on eläviä.
OmaAlusta alusta = new OmaAlusta(3, 3); alusta.alustaSatunnaisetPisteet(1.0); GameOfLifeTestaaja gom = new GameOfLifeTestaaja(alusta); gom.pelaa();
Paina enter jatkaaksesi, muut lopettaa: <enter> XXX XXX XXX Paina enter jatkaaksesi, muut lopettaa: stop Kiitos!
Toteuta metodi getElossaOlevienNaapurienLukumaara(int x, int y), joka laskee elossa olevien naapurien lukumäärän. Keskellä taulukkoa olevalla solulla on yhteensä kahdeksan naapuria, reunassa olevalla solulla 5, kulmassa olevalla 3.
Testaa metodia seuraavilla lauseilla (voit keksiä myös muita testitapauksia!):
OmaAlusta alusta = new OmaAlusta(7, 5); alusta.muutaElavaksi(0, 1); alusta.muutaElavaksi(1, 0); alusta.muutaElavaksi(1, 2); alusta.muutaElavaksi(2, 2); alusta.muutaElavaksi(2, 1); System.out.println("Elossa naapureita (0,0): " + alusta.getElossaOlevienNaapurienLukumaara(0, 0)); System.out.println("Elossa naapureita (1,1): " + alusta.getElossaOlevienNaapurienLukumaara(1, 1));
Tulostuksen pitäisi olla seuraavanlainen:
Elossa naapureita (0,0): 2 Elossa naapureita (1,1): 5
Jäljellä on vielä metodin hoidaSolu(int x, int y, int elossaOleviaNaapureita) toteuttaminen. GameOfLife-pelin säännöthän olivat seuraavat:
Toteuta metodi hoidaSolu(int x, int y, int elossaOleviaNaapureita)
ylläolevien sääntöjen mukaan. Kannattaa ohjelmoida ja testata yksi sääntö kerrallaan!
Kun olet saanut kaikki valmiiksi, voit testata ohjelman toimintaa seuraavalla graafisella simulaattorilla.
package game; import gameoflife.Simulaattori; public class Main { public static void main(String[] args) { OmaAlusta alusta = new OmaAlusta(100, 100); alusta.alustaSatunnaisetPisteet(0.7); Simulaattori simulaattori = new Simulaattori(alusta); simulaattori.simuloi(); } }
Kurssin lähestyessä loppua katsomme vielä muutamaa hyödyllistä Javan ominaisuutta.
Säännöllinen lauseke määrittelee tiiviissä muodossa joukon merkkijonoja. Säännöllisiä lausekkeita käytetään muunmuassa merkkijonojen oikeellisuuden tarkistamiseen. Tarkastellaan tehtävää, jossa täytyy tarkistaa, onko käyttäjän antama opiskelijanumero oikeanmuotoinen. Opiskelijanumero alkaa merkkijonolla "01", jota seuraa 7 numeroa väliltä 0–9.
Opiskelijanumeron oikeellisuuden voisi tarkistaa esimerkiksi käymällä opiskelijanumeroa esittävän merkkijonon läpi merkki merkiltä charAt
-metodin avulla. Toinen tapa olisi tarkistaa että ensimmäinen merkki on "0", ja käyttää Integer.parseInt
metodikutsua merkkijonon muuntamiseen numeroksi. Tämän jälkeen voisi tarkistaa että Integer.parseInt
-metodin palauttama luku on pienempi kuin 20000000.
Oikeellisuuden tarkistus säännöllisten lausekkeiden avulla vaatii ensin sopivan säännöllisen lausekkeen määrittelyn. Tämän jälkeen voimme käyttää String
-luokan metodia matches
, joka tarkistaa vastaako merkkijono parametrina annettua säännöllistä lauseketta. Opiskelijanumeron tapauksessa sopiva säännöllinen lauseke on "01[0-9]{7}"
, ja käyttäjän syöttämän opiskelijanumeron tarkistaminen käy seuraavasti:
System.out.print("Anna opiskelijanumero: "); String numero = lukija.nextLine(); if (numero.matches("01[0-9]{7}")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Käydään seuraavaksi läpi eniten käytettyjä säännöllisten lausekkeiden merkintöjä.
Pystyviiva tarkoittaa, että säännöllisen lausekkeen osat ovat vaihtoehtoisia. Esimerkiksi lauseke 00|111|0000
määrittelee merkkijonot 00
, 111
ja 0000
. Metodi matches
palauttaa arvon true
jos merkkijono vastaa jotain määritellyistä vaihtoehdoista.
String merkkijono = "00"; if(merkkijono.matches("00|111|0000")) { System.out.println("Merkkijonosta löytyi joku kolmesta vaihtoehdosta"); } else { System.out.println("Merkkijonosta ei löytynyt yhtäkään vaihtoehdoista"); }
Merkkijonosta löytyi joku kolmesta vaihtoehdosta
Säännöllinen lauseke 00|111|0000
vaatii että merkkijono on täsmälleen määritellyn muotoinen: se ei määrittele "contains"-toiminnallisuutta.
String merkkijono = "1111"; if(merkkijono.matches("00|111|0000")) { System.out.println("Merkkijonosta löytyi joku kolmesta vaihtoehdosta"); } else { System.out.println("Merkkijonosta ei löytynyt yhtäkään vaihtoehdoista"); }
Merkkijonosta ei löytynyt yhtäkään vaihtoehdoista
Sulkujen avulla voi määrittää, mihin säännöllisen lausekkeen osaan sulkujen sisällä olevat merkinnät vaikuttavat. Jos haluamme sallia merkkijonot 00000
ja 00001
, voimme määritellä ne pystyviivan avulla muodossa 00000|00001
. Sulkujen avulla voimme rajoittaa vaihtoehtoisuuden vain osaan merkkijonoa. Lauseke 0000(0|1)
määrittelee merkkijonot 00000
ja 00001
.
Vastaavasti säännöllinen lauseke auto(|n|a)
määrittelee sanan auto yksikön nominatiivin (auto), genetiivin (auton), partitiivin (autoa) ja akkusatiivin (auto tai auton).
System.out.print("Kirjoita joku sanan auto yksikön taivutusmuoto: "); String sana = lukija.nextLine(); if (sana.matches("auto(|n|a|ssa|sta|on|lla|lta|lle|na|ksi|tta)")) { System.out.println("Oikein meni!"); } else { System.out.println("Taivutusmuoto ei ole oikea."); }
Usein halutaan, että merkkijonossa toistuu jokin tietty alimerkkijono. Säännöllisissä lausekkeissa on käytössä seuraavat toistomerkinnät:
*
toisto 0... kertaa, esimString merkkijono = "trolololololo"; if(merkkijono.matches("trolo(lo)*")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
+
toisto 1... kertaa, esimString merkkijono = "trolololololo"; if(merkkijono.matches("tro(lo)+")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
String merkkijono = "nänänänänänänänä Bätmään!"; if(merkkijono.matches("(nä)+ Bätmään!")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
?
toisto 0 tai 1 kertaa, esimString merkkijono = "You have to accidentally the whole meme"; if(merkkijono.matches("You have to accidentally (delete )?the whole meme")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
{a}
toisto a
kertaa, esimString merkkijono = "1010"; if(merkkijono.matches("(10){2}")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
{a,b}
toisto a
... b
kertaa, esimString merkkijono = "1"; if(merkkijono.matches("1{2,4}")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto ei ole oikea.
{a,}
toisto a
... kertaa, esimString merkkijono = "11111"; if(merkkijono.matches("1{2,}")) { System.out.println("Muoto on oikea."); } else { System.out.println("Muoto ei ole oikea."); }
Muoto on oikea.
Samassa säännöllisessä lausekkeessa voi käyttää myös useampia toistomerkintöjä. Esimerkiksi säännöllinen lauseke 5{3}(1|0)*5{3}
määrittelee merkkijonot, jotka alkavat ja loppuvat kolmella vitosella. Välissä saa tulla rajaton määrä ykkösiä ja nollia.
Merkkiryhmän avulla voi määritellä lyhyesti joukon merkkejä. Merkit kirjoitetaan hakasulkujen sisään, ja merkkivälin voi määrittää viivan avulla. Esimerkiksi merkintä [145]
tarkoittaa samaa kuin (1|4|5)
ja merkintä [2-36-9]
tarkoittaa samaa kuin (2|3|6|7|8|9)
. Vastaavasti merkintä [a-c]*
määrittelee säännöllisen lausekkeen, joka vaatii että merkkijono sisältää vain merkkejä a
, b
ja c
.
Harjoitellaan hieman säännöllisten lausekkeiden käyttöä. Tehtävät tehdään oletuspakkauksessa olevaan luokkaan Paaohjelma
.
Tee säännöllisen lausekkeen avulla luokalle Paaohjelma
metodi public static boolean onViikonpaiva(String merkkijono)
, joka palauttaa true
jos sen parametrina saama merkkijono on viikonpäivän lyhenne (ma, ti, ke, to, pe, la tai su).
Esimerkkitulostuksia metodia käyttävästä ohjelmasta:
Anna merkkijono: ti Muoto on oikea.
Anna merkkijono: abc Muoto ei ole oikea.
Tee luokalle Paaohjelma
metodi public static boolean kaikkiVokaaleja(String merkkijono)
joka tarkistaa säännöllisen lausekkeen avulla ovatko parametrina olevan merkkijonon kaikki merkit vokaaleja.
Esimerkkitulostuksia metodia käyttävästä ohjelmasta:
Anna merkkijono: aie Muoto on oikea.
Anna merkkijono: ane Muoto ei ole oikea.
Säännölliset lausekkeet sopivat tietynlaisiin tilanteisiin. Joissain tapaukseesa lausekkeista tulee liian monimutkaisia, ja merkkijonon "sopivuus" kannattaa tarkastaa muulla tyylillä tai voi olla tarkoituksenmukaista käyttää säännöllisiä lausekkeita vain osaan tarkastuksesta.
Tee luokalle Paaohjelma
metodi public static boolean kellonaika(String merkkijono)
ohjelma, joka tarkistaa säännöllisen lausekkeen avulla onko parametrina oleva merkkijono muotoa tt:mm:ss
oleva kellonaika (tunnit, minuutit ja sekunnit kaksinumeroisina). Tässä metodissa saat käyttää säännöllisten lausekkeiden lisäksi mitä tahansa muutakin tekniikkaa.
Esimerkkitulostuksia metodia käyttävästä ohjelmasta:
Anna merkkijono: 17:23:05 Muoto on oikea.
Anna merkkijono: abc Muoto ei ole oikea.
Anna merkkijono: 33:33:33 Muoto ei ole oikea.
Nykyään lähes kaikista ohjelmointikielistä löytyy tuki säännöllisille lausekkeille. Säännöllisten lausekkeiden teoriaa tarkastellaan toisen vuoden kurssilla Laskennan mallit. Lisää säännöllisistä lausekkeista löydät esim. googlaamalla hakusanalla regular expressions java.
Toteutimme aiemmin pelikorttia mallintavan luokan Kortti
suunilleen seuraavasti:
public class Kortti { public static final int RUUTU = 0; public static final int PATA = 1; public static final int RISTI = 2; public static final int HERTTA = 3; private int arvo; private int maa; public Kortti(int arvo, int maa) { this.arvo = arvo; this.maa = maa; } @Override public String toString() { return maanNimi() + " "+arvo; } private String maanNimi() { if (maa == 0) { return "RUUTU"; } else if (maa == 1) { return "PATA"; } else if (maa == 2) { return "RISTI"; } return "HERTTA"; } public int getMaa() { return maa; } }
Kortin maa tallennetaan kortissa olevaan oliomuuttujaan kokonaislukuna. Maan ilmaisemiseen on määritelty luettavuutta helpottavat vakiot. Kortteja ja maita ilmaisevia vakioita käytetään seuraavasti:
public static void main(String[] args) { Kortti kortti = new Kortti(10, Kortti.HERTTA); System.out.println(kortti); if (kortti.getMaa() == Kortti.PATA) { System.out.println("on pata"); } else { System.out.println("ei ole pata"); } }
Maan esittäminen numerona on huono ratkaisu, sillä esimerkiksi seuraavat järjenvastaiset tavat käyttää korttia ovat mahdollisia:
Kortti jarjetonKortti = new Kortti(10, 55); System.out.println(jarjetonKortti); if (jarjetonKortti.getMaa() == 34) { System.out.println("kortin maa on 34"); } else { System.out.println("kortin maa on jotain muuta kun 34"); } int maaPotenssiinKaksi = jarjetonKortti.getMaa() * jarjetonKortti.getMaa(); System.out.println("kortin maa potenssiin kaksi on " + maaPotenssiinKaksi);
Jos tiedämme muuttujien mahdolliset arvot ennalta, voimme käyttää niiden esittämiseen enum
-tyyppistä luokkaa eli lueteltua tyyppiä. Luetellut tyypit ovat oma luokkatyyppinsä rajapinnan ja normaalin luokan lisäksi. Lueteltu tyyppi määritellään avainsanalla enum
. Esimerkiksi seuraava Maa
-enumluokka määrittelee neljä vakioarvoa: RUUTU
, PATA
, RISTI
ja HERTTA
.
public enum Maa { RUUTU, PATA, RISTI, HERTTA }
Yksinkertaisimmassa muodossaan enum
luettelee pilkulla erotettuina määrittelemänsä vakioarvot. Enumien vakiot on yleensä tapana kirjoittaa kokonaan isoin kirjaimin.
Enum luodaan (yleensä) omaan tiedostoon, samaan tapaan kuin luokka tai rajapinta. NetBeansissa Enumin saa luotua valitsemalla projektin kohdalla new/other/java/java enum.
Seuraavassa luokka Kortti
jossa maa esitetään enumin avulla:
public class Kortti { private int arvo; private Maa maa; public Kortti(int arvo, Maa maa) { this.arvo = arvo; this.maa = maa; } @Override public String toString() { return maa + " "+arvo; } public Maa getMaa() { return maa; } public int getArvo() { return arvo; } }
Kortin uutta versiota käytetään seuraavasti:
public class Paaohjelma { public static void main(String[] args) { Kortti eka = new Kortti(10, Maa.HERTTA); System.out.println(eka); if (eka.getMaa() == Maa.PATA) { System.out.println("on pata"); } else { System.out.println("ei ole pata"); } } }
Tulostuu:
HERTTA 10 ei ole pata
Huomaamme, että enumin tunnukset tulostuvat mukavasti! Koska kortin maat ovat nyt tyyppiä Maa
ei ylemmän esimerkin "järjenvastaiset" kummallisuudet, esim. "maan korottaminen toiseen potenssiin" onnistu. Oraclella on enum
-tyyppiin liittyvä sivusto osoitteessa http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html.
Tarkastellaan seuraavaa luokkaa Kasi
, joka mallintaa tietyssä korttipelissä pelaajan kädessä olevien korttien joukkoa:
public class Kasi { private ArrayList<Kortti> kortit; public Kasi() { kortit = new ArrayList<Kortti>(); } public void lisaa(Kortti kortti){ kortit.add(kortti); } public void tulosta(){ for (Kortti kortti : kortit) { System.out.println( kortti ); } } }
Luokan metodi tulosta
tulostaa jokaisen kädessä olevan kortin tutuksi tullutta "for each"-lausetta käyttämällä. ArrayList ja muut Collection-rajapinnan toteuttavat "oliosäiliöt" toteuttavat rajapinnan Iterable. Rajapinnan Iterable toteuttavat oliot on mahdollista käydä läpi eli "iteroida" esimerkiksi. for each -tyyppisellä komennolla.
Oliosäiliö voidaan käydä läpi myös käyttäen ns. iteraattoria, eli olioa, joka on varta vasten tarkoitettu tietyn oliokokoelman läpikäyntiin. Seuraavassa on iteraattoria käyttävä versio korttien tulostamisesta:
public void tulosta() { Iterator<Kortti> iteraattori = kortit.iterator(); while ( iteraattori.hasNext() ){ System.out.println( iteraattori.next() ); } }
Iteraattori pyydetään kortteja sisältävältä arraylistiltä kortit
. Iteraattori on ikäänkuin "sormi", joka osoittaa aina tiettyä listan sisällä olevaa olioa, ensin ensimmäistä ja sitten seuraavaa jne... kunnes "sormen" avulla on käyty jokainen olio läpi.
Iteraattori tarjoaa muutaman metodin. Metodilla hasNext()
kysytään onko läpikäytäviä olioita vielä jäljellä. Jos on, voidaan iteraattorilta pyytää seuraavana vuorossa oleva olio metodilla next()
. Metodi siis palauttaa seuraavana läpikäyntivuorossa olevan olion ja laittaa iteraattorin eli "sormen" osoittamaan seuraavana vuorossa olevaa läpikäytävää olioa.
Iteraattorin next-metodin palauttama olioviite voidaan ottaa toki talteen myös muuttujaan, eli metodi tulosta
voitaisiin muotoilla myös seuraavasti:
public void tulosta(){ Iterator<Kortti> iteraattori = kortit.iterator(); while ( iteraattori.hasNext() ){ Kortti seuraavanaVuorossa = iteraattori.next(); System.out.println( seuraavanaVuorossa ); } }
Teemme metodin jonka avulla kädestä voi poistaa tiettyä arvoa pienemmät kortit:
public class Kasi { // ... public void poistaHuonommat(int arvo) { for (Kortti kortti : kortit) { if ( kortti.getArvo() < arvo ) { kortit.remove(kortti); } } } }
Huomaamme että metodin suoritus aiheuttaa kummallisen virheen:
Exception in thread "main" java.util.ConcurrentModificationException at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372) at java.util.AbstractList$Itr.next(AbstractList.java:343) at Kasi.poistaHuonommat(Kasi.java:26) at Paaohjelma.main(Paaohjelma.java:20) Java Result: 1
Virheen syynä on se, että for-each:illa listaa läpikäydessä ei ole sallittua poistaa listalta olioita: komento for-each menee tästä "sekaisin".
Jos listalta halutaan poistaa osa olioista läpikäynnin aikana osa, tulee tämä tehdä iteraattoria käyttäen. Iteraattori-olion metodia remove
kutsuttaessa listalta poistetaan siististi se alkio jonka iteraattori palautti edellisellä metodin next
kutsulla. Toimiva versio metodista seuraavassa:
public class Kasi { // ... public void poistaHuonommat(int arvo) { Iterator<Kortti> iteraattori = kortit.iterator(); while (iteraattori.hasNext()) { if (iteraattori.next().getArvo() < arvo) { // poistetaan listalta olio jonka edellinen next-metodin kutsu palautti iteraattori.remove(); } } } }
Tehdään ohjelma pienen yrityksen henkilöstön hallintaan.
Tee pakkaukseen henkilosto
lueteltu tyyppi eli enum Koulutus
jolla on tunnukset FT
(tohtori), FM
(maisteri), LuK
(kandidaatti), FilYO
(ylioppilas).
Tee pakkaukseen henkilosto
luokka Luokka Henkilo
. Henkilölle annetaan konstruktorin parametrina annettava nimi ja koulutus. Henkilöllä on myös koulutuksen kertova metodi public Koulutus getKoulutus()
sekä alla olevan esimerkin mukaista jälkeä tekevä toString
-metodi.
Henkilo arto = new Henkilo("Arto", Koulutus.FT); System.out.println(arto);
Arto, FT
Tee pakkaukseen henkilosto
luokka Luokka Tyontekijat
. Työntekijät-olio sisältää listan Henkilo-olioita. Luokalla on parametriton konstruktori ja seuraavat metodit:
public void lisaa(Henkilo lisattava)
lisää parametrina olevan henkilön työntekijäksipublic void lisaa(List<Henkilo> lisattavat)
lisää parametrina olevan listan henkilöitä työntekijöiksipublic void tulosta()
tulostaa kaikki työntekijätpublic void tulosta(Koulutus koulutus)
tulostaa työntekijät joiden koulutus on sama kuin parametrissa määritelty koulutusHUOM: Luokan Tyontekijat
tulosta
-metodit on toteutettava iteraattoria käyttäen!
Tee luokalle Tyontekijat
metodi public void irtisano(Koulutus koulutus)
joka poistaa Työntekijöiden joukosta kaikki henkilöt joiden koulutus on sama kuin metodin parametrina annettu.
HUOM: toteuta metodi iteraattoria käyttäen!
Seuraavassa esimerkki luokan käytöstä:
Public class Paaohjelma { public static void main(String[] args) { Tyontekijat yliopisto = new Tyontekijat(); yliopisto.lisaa(new Henkilo("Matti", Koulutus.FT)); yliopisto.lisaa(new Henkilo("Pekka", Koulutus.FilYO)); yliopisto.lisaa(new Henkilo("Arto", Koulutus.FT)); yliopisto.tulosta(); yliopisto.irtisano(Koulutus.FilYO); System.out.println("=="); yliopisto.tulosta(); }
Tulostuu:
Matti, FT Pekka, FilYO Arto, FT == Matti, FT Arto, FT
Toistolauseissa on komennon break
lisäksi käytössä komento continue
, joka mahdollistaa seuraavaan toistokierrokseen hyppäämisen.
List<String> nimet = Arrays.asList("Matti", "Pekka", "Arto"); for(String nimi: nimet) { if (nimi.equals("Arto")) { continue; } System.out.println(nimi); }
Matti Pekka
Komentoa continue
käytetään esimerkiksi silloin, kun tiedetään että toistolauseessa iteroitavilla muuttujilla on arvoja, joita ei haluta käsitellä lainkaan. Klassinen lähestymistapa olisi if-lauseen käyttö, mutta komento continue
mahdollistaa sisennyksiä välttävän, ja samalla ehkä luettavamman lähestymistavan käsiteltävien arvojen välttämiseen. Alla on kaksi esimerkkiä, jossa käydään listalla olevia lukuja läpi. Jos luku on alle 5, se on jaollinen sadalla, tai se on jaollinen neljälläkymmenellä, niin sitä ei tulosteta, muulloin se tulostetaan.
List<Integer> luvut = Arrays.asList(1, 3, 11, 6, 120); for(int luku: luvut) { if (luku > 4 && luku % 100 != 0 && luku % 40 != 0) { System.out.println(luku); } } for(int luku: luvut) { if (luku < 5) { continue; } if (luku % 100 == 0) { continue; } if (luku % 40 == 0) { continue; } System.out.println(luku); }
11 6 11 6
Luetellut tyypit voivat sisältää oliomuuttujia. Oliomuuttujien arvot tulee asettaa luetellun tyypin määrittelevän luokan sisäisessä eli näkyvyysmääreen private
omaavassa konstruktorissa. Enum-tyyppisillä luokilla ei saa olla public
-konstruktoria.
Seuraavassa lueteltu tyyppi Vari
, joka sisältää vakioarvot PUNAINEN, VIHREA ja SININEN. Vakioille on määritelty värikoodin kertova oliomuuttuja:
public enum Vari { PUNAINEN("#FF0000"), // konstruktorin parametrit määritellään vakioarvoja lueteltaessa VIHREA("#00FF00"), SININEN("#0000FF"); private String koodi; // oliomuuttuja private Vari(String koodi) { // konstruktori this.koodi = koodi; } public String getKoodi() { return this.koodi; } }
Lueteltua tyyppiä Vari
voidaan käyttää esimerkiksi seuraavasti:
System.out.println(Vari.VIHREA.getKoodi());
#00FF00
Hiljattain Suomeen rantautunut Netflix lupasi lokakuussa 2006 miljoona dollaria henkilölle tai ryhmälle, joka kehittäisi ohjelman, joka on 10% parempi elokuvien suosittelussa kuin heidän oma ohjelmansa. Kilpailu ratkesi syyskuussa 2009 (http://www.netflixprize.com/).
Rakennetaan tässä tehtävässä ohjelma elokuvien suositteluun. Alla on sen toimintaesimerkki:
ArvioRekisteri arviot = new ArvioRekisteri(); Elokuva tuulenViemaa = new Elokuva("Tuulen viemää"); Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat"); Elokuva eraserhead = new Elokuva("Eraserhead"); Henkilo matti = new Henkilo("Matti"); Henkilo pekka = new Henkilo("Pekka"); Henkilo mikke = new Henkilo("Mikke"); Henkilo thomas = new Henkilo("Thomas"); arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO); arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA); arviot.lisaaArvio(matti, eraserhead, Arvio.OK); arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK); arviot.lisaaArvio(pekka, hiljaisetSillat, Arvio.HUONO); arviot.lisaaArvio(pekka, eraserhead, Arvio.VALTTAVA); arviot.lisaaArvio(mikke, eraserhead, Arvio.HUONO); Suosittelija suosittelija = new Suosittelija(arviot); System.out.println(thomas + " suositus: " + suosittelija.suositteleElokuva(thomas)); System.out.println(mikke + " suositus: " + suosittelija.suositteleElokuva(mikke));
Thomas suositus: Hiljaiset sillat Mikke suositus: Tuulen viemää
Ohjelma osaa suositella elokuvia niiden yleisen arvion perusteella, sekä henkilökohtaisten henkilön antaminen arvioiden perusteella. Lähdetään rakentamaan ohjelmaa.
Luo pakkaus suosittelija.domain
ja lisää sinne luokat Henkilo
ja Elokuva
. Kummallakin luokalla on julkinen konstruktori public Luokka(String nimi)
, sekä metodi public String getNimi()
, joka palauttaa konstruktorissa saadun nimen.
Henkilo henkilo = new Henkilo("Pekka"); Elokuva elokuva = new Elokuva("Eraserhead"); System.out.println(henkilo.getNimi() + " ja " + elokuva.getNimi());
Pekka ja Eraserhead
Lisää luokille myös public String toString()
-metodi, joka palauttaa konstruktorissa parametrina annetun nimen, sekä korvaa metodit equals
ja hashCode
.
Korvaa equals
siten että samuusvertailu tapahtuu oliomuuttujan nimi
perusteella. Katso mallia luvusta 45.1. Luvussa 45.2. on ohje metodin hashCode
korvaamiselle. Ainakin HashCode kannattaa generoida automaattisesti luvun lopussa olevan ohjeen mukaan:
NetBeans tarjoaa metodien equals ja hashCode automaattisen luonnin. Voit valita valikosta Source -> Insert Code, ja valita aukeavasta listasta equals() and hashCode(). Tämän jälkeen NetBeans kysyy oliomuuttujat joita metodeissa käytetään.
Luo pakkaukseen suosittelija.domain
lueteltu tyyppi Arvio
. Enum-luokalla Arvio
on julkinen metodi public int getArvo()
, joka palauttaa arvioon liittyvän arvon. Arviotunnusten ja niihin liittyvien arvosanojen tulee olla seuraavat:
Tunnus | Arvo |
---|---|
HUONO | -5 |
VALTTAVA | -3 |
EI_NAHNYT | 0 |
NEUTRAALI | 1 |
OK | 3 |
HYVA | 5 |
Luokkaa voi käyttää seuraavasti:
Arvio annettu = Arvio.HYVA; System.out.println("Arvio " + annettu + ", arvo " + annettu.getArvo()); annettu = Arvio.NEUTRAALI; System.out.println("Arvio " + annettu + ", arvo " + annettu.getArvo());
Arvio HYVA, arvo 5 Arvio NEUTRAALI, arvo 1
Aloitetaan arvioiden varastointiin liittyvän palvelun toteutus.
Luo pakkaukseen suosittelija
luokka ArvioRekisteri
, jolla on konstruktori public ArvioRekisteri()
sekä seuraavat metodit:
public void lisaaArvio(Elokuva elokuva, Arvio arvio)
lisää arviorekisteriin parametrina annetulle elokuvalle uuden arvion. Samalla elokuvalla voi olla useita samanlaisiakin arvioita.public List<Arvio> annaArviot(Elokuva elokuva)
palauttaa elokuvalle lisätyt arviot listana.public Map<Elokuva, List<Arvio>> elokuvienArviot()
palauttaa mapin, joka sisältää arvioidut elokuvat avaimina. Jokaiseen elokuvaan liittyy lista, joka sisältää elokuvaan lisatyt arviot.Testaa metodien toimintaa seuraavalla lähdekoodilla:
Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat"); Elokuva eraserhead = new Elokuva("Eraserhead"); ArvioRekisteri rekisteri = new ArvioRekisteri(); rekisteri.lisaaArvio(eraserhead, Arvio.HUONO); rekisteri.lisaaArvio(eraserhead, Arvio.HUONO); rekisteri.lisaaArvio(eraserhead, Arvio.HYVA); rekisteri.lisaaArvio(hiljaisetSillat, Arvio.HYVA); rekisteri.lisaaArvio(hiljaisetSillat, Arvio.OK); System.out.println("Kaikki arviot: " + rekisteri.elokuvienArviot()); System.out.println("Arviot Eraserheadille: " + rekisteri.annaArviot(eraserhead));
Kaikki arviot: {Hiljaiset sillat=[HYVA, OK], Eraserhead=[HUONO, HUONO, HYVA]} Arviot Eraserheadille: [HUONO, HUONO, HYVA]
Lisätään seuraavaksi mahdollisuus henkilökohtaisten arvioiden lisäämiseen.
Lisää luokkaan ArvioRekisteri
seuraavat metodit:
public void lisaaArvio(Henkilo henkilo, Elokuva elokuva, Arvio arvio)
lisää parametrina annetulle elokuvalle tietyn henkilön tekemän arvion. Sama henkilö voi arvioida tietyn elokuvan vain kertaalleen. Henkilön tekemä arvio tulee myös lisätä kaikkiin elokuviin liittyviin arvioihin.public Arvio haeArvio(Henkilo henkilo, Elokuva elokuva)
palauttaa parametrina annetun henkilön tekemän arvion parametrina annetulle elokuvalle. Jos henkilö ei ole arvioinut kyseistä elokuvaa, palauta arvio Arvio.EI_NAHNYT
.public Map<Elokuva, Arvio> annaHenkilonArviot(Henkilo henkilo)
palauttaa hajautustaulun, joka sisältää henkilön tekemät arviot. Hajautustaulun avaimena on arvioidut elokuvat, arvoina arvioituihin elokuviin liittyvät arviot. Jos henkilö ei ole arvioinut yhtään elokuvaa, palautetaan tyhjä hajautustaulu.public List<Henkilo> arvioijat()
palauttaa listan henkilöistä jotka ovat arvioineet elokuvia.Henkilöiden tekemät arviot kannattanee tallentaa hajautustauluun, jossa avaimena on henkilö. Arvona hajautustaulussa on toinen hajautustaulu, jossa avaimena on elokuva ja arvona arvio.
Testaa paranneltua ArvioRekisteri
-luokkaa seuraavalla lähdekoodipätkällä:
ArvioRekisteri arviot = new ArvioRekisteri(); Elokuva tuulenViemaa = new Elokuva("Tuulen viemää"); Elokuva eraserhead = new Elokuva("Eraserhead"); Henkilo matti = new Henkilo("Matti"); Henkilo pekka = new Henkilo("Pekka"); arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO); arviot.lisaaArvio(matti, eraserhead, Arvio.OK); arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK); arviot.lisaaArvio(pekka, eraserhead, Arvio.OK); System.out.println("Arviot Eraserheadille: " + arviot.annaArviot(eraserhead)); System.out.println("Matin arviot: " + arviot.annaHenkilonArviot(matti)); System.out.println("Arvioijat: " + arviot.arvioijat());
Arviot Eraserheadille: [OK, OK] Matin arviot: {Tuulen viemää=HUONO, Eraserhead=OK} Arvioijat: [Pekka, Matti]
Luodaan seuraavaksi muutama apuluokka arviointien helpottamiseksi.
Luo pakkaukseen suosittelija.comparator
luokka HenkiloComparator
. Luokan HenkiloComparator
tulee toteuttaa rajapinta Comparator<Henkilo>
, ja sillä pitää olla konstruktori public HenkiloComparator(Map<Henkilo, Integer> henkiloidenSamuudet)
. Luokkaa HenkiloComparator
käytetään myöhemmin henkilöiden järjestämiseen henkilöön liittyvän luvun perusteella.
HenkiloComparator-luokan tulee mahdollistaa henkilöiden järjestäminen henkilöön liittyvän luvun perusteella.
Testaa luokan toimintaa seuraavalla lähdekoodilla:
Henkilo matti = new Henkilo("Matti"); Henkilo pekka = new Henkilo("Pekka"); Henkilo mikke = new Henkilo("Mikke"); Henkilo thomas = new Henkilo("Thomas"); Map<Henkilo, Integer> henkiloidenSamuudet = new HashMap<Henkilo, Integer>(); henkiloidenSamuudet.put(matti, 42); henkiloidenSamuudet.put(pekka, 134); henkiloidenSamuudet.put(mikke, 8); henkiloidenSamuudet.put(thomas, 82); List<Henkilo> henkilot = Arrays.asList(matti, pekka, mikke, thomas); System.out.println("Henkilöt ennen järjestämistä: " + henkilot); Collections.sort(henkilot, new HenkiloComparator(henkiloidenSamuudet)); System.out.println("Henkilöt järjestämisen jälkeen: " + henkilot);
Henkilöt ennen järjestämistä: [Matti, Pekka, Mikke, Thomas] Henkilöt järjestämisen jälkeen: [Pekka, Thomas, Matti, Mikke]
Luo pakkaukseen suosittelija.comparator
luokka ElokuvaComparator
. Luokan ElokuvaComparator
tulee toteuttaa rajapinta Comparator<Elokuva>
, ja sillä pitää olla konstruktori public ElokuvaComparator(Map<Elokuva, List<Arvio>> arviot)
. Luokkaa ElokuvaComparator
käytetään myöhemmin elokuvien järjestämiseen niiden arvioiden perusteella.
ElokuvaComparator-luokan tulee tarjota mahdollisuus elokuvien järjestäminen niiden saamien arvosanojen keskiarvon perusteella. Korkeimman keskiarvon saanut elokuva tulee ensimmäisenä, matalimman keskiarvon saanut viimeisenä.
Testaa luokan toimintaa seuraavalla lähdekoodilla:
ArvioRekisteri arviot = new ArvioRekisteri(); Elokuva tuulenViemaa = new Elokuva("Tuulen viemää"); Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat"); Elokuva eraserhead = new Elokuva("Eraserhead"); Henkilo matti = new Henkilo("Matti"); Henkilo pekka = new Henkilo("Pekka"); Henkilo mikke = new Henkilo("Mikke"); arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO); arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA); arviot.lisaaArvio(matti, eraserhead, Arvio.OK); arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK); arviot.lisaaArvio(pekka, hiljaisetSillat, Arvio.HUONO); arviot.lisaaArvio(pekka, eraserhead, Arvio.VALTTAVA); arviot.lisaaArvio(mikke, eraserhead, Arvio.HUONO); Map<Elokuva, List<Arvio>> elokuvienArviot = arviot.elokuvienArviot(); List<Elokuva> elokuvat = Arrays.asList(tuulenViemaa, hiljaisetSillat, eraserhead); System.out.println("Elokuvat ennen järjestämistä: " + elokuvat); Collections.sort(elokuvat, new ElokuvaComparator(elokuvienArviot)); System.out.println("Elokuvat järjestämisen jälkeen: " + elokuvat);
Elokuvat ennen järjestämistä: [Tuulen viemää, Hiljaiset sillat, Eraserhead] Elokuvat järjestämisen jälkeen: [Hiljaiset sillat, Tuulen viemää, Eraserhead]
Toteuta pakkaukseen suosittelija
luokka Suosittelija
. Luokan Suosittelija
konstruktori saa parametrinaan ArvioRekisteri
-tyyppisen olion. Suosittelija käyttää arviorekisterissä olevia arvioita suositusten tekemiseen.
Toteuta luokalle metodi public Elokuva suositteleElokuva(Henkilo henkilo)
, joka suosittelee henkilölle elokuvia.
Toteuta metodi ensin siten, että se suosittelee aina elokuvaa, jonka arvioiden arvosanojen keskiarvo on suurin. Vinkki: Tarvitset parhaan elokuvan selvittämiseen ainakin aiemmin luotua ElokuvaComparator
-luokkaa, luokan ArvioRekisteri
metodia public Map<Elokuva, List<Arvio>> elokuvienArviot()
, sekä listaa olemassaolevista elokuvista.
Testaa ohjelman toimimista seuraavalla lähdekoodilla:
ArvioRekisteri arviot = new ArvioRekisteri(); Elokuva tuulenViemaa = new Elokuva("Tuulen viemää"); Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat"); Elokuva eraserhead = new Elokuva("Eraserhead"); Henkilo matti = new Henkilo("Matti"); Henkilo pekka = new Henkilo("Pekka"); Henkilo mikke = new Henkilo("Mikael"); arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO); arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA); arviot.lisaaArvio(matti, eraserhead, Arvio.OK); arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK); arviot.lisaaArvio(pekka, hiljaisetSillat, Arvio.VALTTAVA); arviot.lisaaArvio(pekka, eraserhead, Arvio.VALTTAVA); Suosittelija suosittelija = new Suosittelija(arviot); Elokuva suositeltu = suosittelija.suositteleElokuva(mikke); System.out.println("Mikaelille suositeltu elokuva oli: " + suositeltu);
Mikaelille suositeltu elokuva oli: Hiljaiset sillat
Nyt tekemämme ensimmäinen vaihe toimii oikein ainoastaan henkilöille, jotka eivät ole vielä arvostelleet yhtään elokuvaa. Heidän elokuvamaustaanhan on mahdoton sanoa mitään ja paras arvaus on suositella heille keskimäärin parhaan arvosanan saanutta elokuvaa.
Huom! Tehtävä on haastava. Kannattaa tehdä ensin muut tehtävät ja palata tähän myöhemmin. Voit palauttaa tehtäväsarjan TMC:hen vaikket saakaan tätä tehtävää tehdyksi, aivan kuten muidenkin tehtävien kohdalla.
Valitettavasti tämän osan virhediagnostiikkakaan ei ole samaa luokkaa kuin edellisissä kohdissa.
Jos henkilöt ovat lisänneet omia suosituksia suosituspalveluun, tiedämme jotain heidän elokuvamaustaan. Laajennetaan suosittelijan toiminnallisuutta siten, että se luo henkilökohtaisen suosituksen jos henkilö on jo arvioinut elokuvia. Edellisessä osassa toteutettu toiminnallisuus tulee säilyttää: Jos henkilö ei ole arvioinut yhtäkään elokuvaa, hänelle suositellaan elokuva arvosanojen perusteella.
Henkilökohtaiset suositukset perustuvat henkilön tekemien arvioiden samuuteen muiden henkilöiden tekemien arvioiden kanssa. Pohditaan seuraavaa taulukkoa, missä ylärivillä on elokuvat, ja vasemmalla on arvioita tehneet henkilöt. Taulukon solut kuvaavat annettuja arvioita.
Henkilo \ Elokuva | Tuulen viemää | Hiljaiset sillat | Eraserhead | Blues Brothers |
---|---|---|---|---|
Matti | HUONO (-5) | HYVA (5) | OK (3) | - |
Pekka | OK (3) | - | HUONO (-5) | VALTTAVA (-3) |
Mikael | - | - | HUONO (-5) | - |
Thomas | - | HYVA (5) | - | HYVA (5) |
Kun haluamme hakea Mikaelille sopivaa elokuvaa, tutkimme Mikaelin samuutta kaikkien muiden arvioijien kesken. Samuus lasketaan arvioiden perusteella: samuus on kummankin katsomien elokuvien arvioiden tulojen summa. Esimerkiksi Mikaelin ja Thomasin samuus on 0, koska Mikael ja Thomas eivät ole katsoneet yhtäkään samaa elokuvaa.
Mikaelin ja Pekan samuutta laskettaessa yhteisten elokuvien tulojen summa olisi 25. Mikael ja Pekka ovat katsoneet vain yhden yhteisen elokuvan, ja kumpikin antaneet sille arvosanan huono (-5).
-5 * -5 = 25
Mikaelin ja Matin samuus on -15. Mikael ja Matti ovat myös katsoneet vain yhden yhteisen elokuvan. Mikael antoi elokuvalle arvosanan huono (-5), Matti antoi sille arvosanan ok (3).
-5 * 3 = -15
Näiden perusteella Mikaelille suositellaan elokuvia Pekan elokuvamaun mukaan: suosituksena on elokuva Tuulen viemää.
Kun taas haluamme hakea Matille sopivaa elokuvaa, tutkimme Matin samuutta kaikkien muiden arvioijien kesken. Matti ja Pekka ovat katsoneet kaksi yhteistä elokuvaa. Matti antoi Tuulen viemälle arvosanan huono (-5), Pekka arvosanan OK (3). Elokuvalle Eraserhead Matti antoi arvosanan OK (3), Pekka arvosanan huono (-5). Matin ja Pekan samuus on siis -30.
-5 * 3 + 3 * -5 = -30
Matin ja Mikaelin samuus on edellisestä laskusta tiedetty -15. Samuudet ovat symmetrisia.
Matti ja Thomas ovat katsoneet Tuulen viemää, ja kumpikin antoi sille arvosanan hyvä (5). Matin ja Thomaksen samuus on siis 25.
5 * 5 = 25
Matille tulee siis suositella elokuvia Thomaksen elokuvamaun mukaan: suosituksena olisi Blues Brothers.
Toteuta yllä kuvattu suosittelumekanismi. Jos henkilölle ei löydy yhtään suositeltavaa elokuvaa, tai henkilö, kenen elokuvamaun mukaan elokuvia suositellaan on arvioinut elokuvat joita henkilö ei ole vielä katsonut huonoiksi, välttäviksi tai neutraaleiksi, palauta metodista suositteleElokuva
arvo null
. Edellisessä tehtävässä määritellyn lähestymistavan tulee toimia jos henkilö ei ole lisännyt yhtäkään arviota.
Älä suosittele elokuvia, jonka henkilö on jo nähnyt.
Voit testata ohjelmasi toimintaa seuraavalla lähdekoodilla:
ArvioRekisteri arviot = new ArvioRekisteri(); Elokuva tuulenViemaa = new Elokuva("Tuulen viemää"); Elokuva hiljaisetSillat = new Elokuva("Hiljaiset sillat"); Elokuva eraserhead = new Elokuva("Eraserhead"); Elokuva bluesBrothers = new Elokuva("Blues Brothers"); Henkilo matti = new Henkilo("Matti"); Henkilo pekka = new Henkilo("Pekka"); Henkilo mikke = new Henkilo("Mikael"); Henkilo thomas = new Henkilo("Thomas"); Henkilo arto = new Henkilo("Arto"); arviot.lisaaArvio(matti, tuulenViemaa, Arvio.HUONO); arviot.lisaaArvio(matti, hiljaisetSillat, Arvio.HYVA); arviot.lisaaArvio(matti, eraserhead, Arvio.OK); arviot.lisaaArvio(pekka, tuulenViemaa, Arvio.OK); arviot.lisaaArvio(pekka, eraserhead, Arvio.HUONO); arviot.lisaaArvio(pekka, bluesBrothers, Arvio.VALTTAVA); arviot.lisaaArvio(mikke, eraserhead, Arvio.HUONO); arviot.lisaaArvio(thomas, bluesBrothers, Arvio.HYVA); arviot.lisaaArvio(thomas, hiljaisetSillat, Arvio.HYVA); Suosittelija suosittelija = new Suosittelija(arviot); System.out.println(thomas + " suositus: " + suosittelija.suositteleElokuva(thomas)); System.out.println(mikke + " suositus: " + suosittelija.suositteleElokuva(mikke)); System.out.println(matti + " suositus: " + suosittelija.suositteleElokuva(matti)); System.out.println(arto + " suositus: " + suosittelija.suositteleElokuva(arto));
Thomas suositus: Eraserhead Mikael suositus: Tuulen viemää Matti suositus: Blues Brothers Arto suositus: Hiljaiset sillat
Miljoona käsissä? Ei ehkä vielä. Kursseilla Johdatus tekoälyyn ja Johdatus koneoppimiseen opitaan lisää tekniikoita oppivien järjestelmien rakentamiseen.
Olemme tähän mennessä luoneet metodimme siten, että niiden parametrien määrät ovat olleet selkeästi määritelty. Java tarjoaa tavan antaa metodille rajoittamattoman määrän määrätyntyyppisiä parametreja asettamalla metodimäärittelyssä parametrin tyypille kolme pistettä perään. Esimerkiksi metodille public int summa(int... luvut)
voi antaa summattavaksi niin monta int
-tyyppistä kokonaislukua kuin käyttäjä haluaa. Metodin sisällä parametrin arvoja voi käsitellä taulukkona.
public int summa(int... luvut) { int summa = 0; for (int i = 0; i < luvut.length; i++) { summa += luvut[i]; } return summa; }
System.out.println(summa(3, 5, 7, 9)); // luvut = {3, 5, 7, 9} System.out.println(summa(1, 2)); // luvut = {1, 2}
24 3
Huomaa yllä miten parametrimäärittely int... luvut
johtaa siihen, että metodin sisällä näkyy taulukkotyyppinen muuttuja luvut
.
Metodille voi määritellä vain yhden parametrin joka saa rajattoman määrän arvoja, ja sen tulee olla metodimäärittelyn viimeinen parametri. Esimerkiksi:
public void tulosta(String... merkkijonot, int kertaa) // ei sallittu! public void tulosta(int kertaa, String... merkkijonot) // sallittu!
Ennalta määrittelemätöntä parametrien arvojen määrää käytetään esimerkiksi silloin, kun halutaan tarjota rajapinta, joka ei rajoita sen käyttäjää tiettyyn parametrien määrään. Vaihtoehtoinen lähestymistapa on metodimäärittely, jolla on parametrina tietyn tyyppinen lista. Tällöin oliot voidaan asettaa listaan ennen metodikutsua, ja kutsua metodia antamalla lista sille parametrina.
Muutamassa tehtävässä (mm. ohpen kirjasto ja ohjan sanatutkimus) törmäsimme tilanteeseen, jossa jouduimme filtteröimään listalta jotain hakuehtoa vastaavat oliot. Esim. sanatutkimuksessa metodit zSisaltava, lLoppuiset, palindromit, kaikkiVoksSis
tekivät oleellisesti saman asian: ne kävivät läpi tiedoston sisällön sana kerrallaan ja tarkastivat jokaisen sanan kohdalla päteekö sille tietty ehto, ja jos pätee, ottivat sanan talteen. Koska kaikkien metodien ehto oli erilainen, ei toisteisuutta tehtävissä osattu poistaa vaan kaikkien koodi oli ehtoa vaille "copypastea".
Tässä tehtävässä teemme ohjelman, jonka avulla on mahdollista filtteröidä rivejä Project Gutenbergin sivuilta löytyvistä kirjoista. Seuraavassa esimerkkinä Dostojevskin Rikos ja rangaistus. Haluamme, että erilaisia filtteröintiehtoja on monelaisia ja että filtteröinti voi tapahtua myös eri ehtojen kombinaationa. Ohjelman rakenteen pitää myös mahdollistaa uusien ehtojen lisääminen myöhemmin.
Sopiva ratkaisu tilanteeseen on jokaisen filtteröintiehdon määritteleminen omana rajapinnan Ehto
toteuttavana oliona. Seuraavassa rajapinnan määritelmä:
public interface Ehto { boolean toteutuu(String rivi); }
Seuraavassa eräs rajapinnan toteuttava filtteriluokka:
public class SisaltaaSanan implements Ehto { String sana; public SisaltaaSanan(String sana) { this.sana = sana; } @Override public boolean toteutuu(String rivi) { return rivi.contains(sana); } }
Luokan oliot ovat siis hyvin yksinkertaisia, ne muistavat konstruktorin parametrina annetun sanan. Olion ainoalta metodilta voi kysyä toteutuuko ehto parametrina olevalle merkkijonolle, ja ehdon toteutuminen tarkoittaa olion tapauksessa sisältääkö merkkijono olion muistaman sanan.
Tehtäväpohjan mukana saat valmiina luokan GutenbergLukija
, jonka avulla voit tutkia kirjojen rivejä filtteröitynä parametrina annetun hakuehdon perusteella:
public class GutenbergLukija { private List<String> sanat; public GutenbergLukija(String osoite) throws IllegalArgumentException { // kirjan verkosta hakeva koodi } public List≶String> rivitJoilleVoimassa(Ehto ehto){ List≶String> ehdonTayttavat = new ArrayList<String>(); for (String rivi : sanat) { if ( ehto.toteutuu(rivi)) { ehdonTayttavat.add(rivi); } } return ehdonTayttavat; } }
Seuraavassa tulostetaan Rikoksesta ja rangaistuksesta kaikki rivit, joilla esiintyy sana "beer":
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new SisaltaaSanan("beer"); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Huom: Project Gutenbergin sivuilla olevat tekstit ovat tarkoitettu vain ihmisten luettaviksi. Jos tekstejä haluaa lukea koneellisesti eli esim. GutenbergLukijalla, on kirjan sivulta, esim. http://www.gutenberg.org/ebooks/2554 mentävä alakulmassa olevaan linkin mirror sites takaa löytyvälle sivulle, esim. bulgarialaiselle e-book Ltd:n sivulle ja luettava kirja sieltä löytyvästä osoitteesta.
Tee rajapinnan ehto
toteuttava luokka KaikkiRivit
, joka kelpuuttaa jokaisen rivin. Tämä ja muutkin tämän tehtävän luokat tulee toteuttaa pakkaukseen lukija.ehdot
.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new KaikkiRivit(); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka LoppuuHuutoTaiKysymysmerkkiin
, joka kelpuuttaa ne rivit, joiden viimeinen merkki on huuto- tai kysymysmerkki.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new LoppuuHuutoTaiKysymysmerkkiin(); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Muistutus: merkkien vertailu Javassa tapahtuu == operaattorilla:
String nimi = "pekka"; // HUOM: 'p' on merkki eli char p, "p" taas merkkojono, jonka ainoa merkki on p if ( nimi.charAt(0) == 'p' ) { System.out.println("alussa p"); } else { System.out.println("alussa jokin muu kuin p"); }
Tee rajapinnan ehto
toteuttava luokka PituusVahintaan
, jonka oliot kelpuuttavat ne rivit, joiden pituus on vähintään olion konstruktorin parametrina annettu luku.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new PituusVahintaan(40); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka Molemmat
. Luokan oliot saavat konstruktorin parametrina kaksi rajapinnan Ehto
toteuttavaa olioa. Molemmat
-olio kelpuuttavaa ne rivit, jotka sen kummatkin konstruktorissa saamansa ehdot kelpuuttavat. Seuraavassa tulostetaan kaikki huuto- tai kysymysmerkkiin loppuvat rivit, jotka sisältävät sanan "beer".
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new Molemmat( new LoppuuHuutoTaiKysymysmerkkiin(), new SisaltaaSanan("beer") ); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka Ei
. Luokan oliot saavat parametrina rajapinnan Ehto
toteuttavaavan olion. Ei
-olio kelpuuttaa ne rivit, joita sen parametrina saama ehto ei kelpuuta.
Seuraavassa tulostetaan rivit, joiden pituus vähemmän kuin 10.
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new Ei( new PituusVahintaan(10) ); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Tee rajapinnan ehto
toteuttava luokka VahintaanYksi
. Luokan oliot saavat konstruktorin parametrina mielivaltaisen määrän rajapinnan Ehto
toteuttavia olioita, konstruktorissa siis käytettävä vaihtuvanmittaista parametrilistaa. VahintaanYksi
-oliot kelpuuttavat ne rivit, jotka ainakin yksi sen konstruktoriparametrina saamista ehdoista kelpuuttaa. Seuraavassa tulostetaan rivit, jotka sisältävät jonkun sanoista "beer", "milk" tai "oil".
public static void main(String[] args) { String osoite = "http://www.gutenberg.myebook.bg/2/5/5/2554/2554-8.txt"; GutenbergLukija kirja = new GutenbergLukija(osoite); Ehto ehto = new VahintaanYksi( new SisaltaaSanan("beer"), new SisaltaaSanan("milk"), new SisaltaaSanan("oil") ); for (String rivi : kirja.rivitJoilleVoimassa(ehto)) { System.out.println(rivi); } }
Huomaa, että ehtoja voi kombinoida mielivaltaisesti. Seuraavassa ehto, joka hyväksyy rivit, joilla on vähintään yksi sanoista "beer", "milk" tai "oil" ja jotka ovat pituudeltaan 20-30 merkkiä.
Ehto sanat = new VahintaanYksi( new SisaltaaSanan("beer"), new SisaltaaSanan("milk"), new SisaltaaSanan("oil") ); Ehto oikeaPituus = new Molemmat( new PituusVahintaan(20), new Ei( new PituusVahintaan(31)) ); Ehto halutut = new Molemmat(sanat, oikeaPituus);
Olemme tottuneet rakentamaan merkkijonoja seuraavaan tapaan:
public static void main(String[] args) { int[] t = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; System.out.println(muotoile(t)); } public static String muotoile(int[] t) { String mj = "{"; for (int i = 0; i < t.length; i++) { mj += t[i]; if (i != t.length - 1) { mj += ", "; } } return mj + "}"; }
Tulostus:
{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
Tapa on toimiva mutta ei kovin tehokas. Kuten muistamme, merkkijonot ovat immutaabeleita eli olioita joita ei voi muuttaa. Merkkijono-operaatioiden tuloksena onkin aina uusi merkkijono-olio. Eli edellisessäkin esimerkissä syntyi välivaiheena 10 merkkijono-olioa. Jos syötteen koko olisi isompi, alkaisi välivaiheena olevien olioiden luominen vaikuttaa ohjelman suoritusaikaan ikävällä tavalla.
Edellisen kaltaisissa tilanteissa onkin parempi käyttää merkkijonon muodostamisessa StringBuilder
-olioita. Toisin kuin Stringit, StringBuilderit eivät ole immutaabeleita, ja yhtä StringBuilderolioa voi muokata. Tutustu StringBuilderin API-kuvaukseen (löydät sen esim googlaamalla stringbuilder java api 6) ja muuta tehtäväpohjassa oleva metodi public static String muotoile(int[] t)
toimimaan StringBuilderia käyttäen seuraavaan tapaan:
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
Eli aaltosulkeet tulevat omalle rivilleen. Taulukon alkioita tulostetaan 4 per rivi ja rivin ensimmäistä edeltää välilyönti. Pilkun jälkeen ennen seuraavaa numeroa tulee olla tasan yksi välilyönti.
Kurssi alkaa olla ohi ja on loppuhuipennuksen aika!
Tässä tehtävässä luodaan rakenteet ja osa toiminnallisuudesta seuraavannäköiseen matopeliin. Tehtävän palautettavassa versiossa tosin pelin väritys on erilainen, mato on musta, omena punainen ja pohja harmaa.
Luo pakkaukseen matopeli.domain
luokka Pala
. Luokalla Pala
on konstruktori public Pala(int x, int y)
, joka saa palan sijainnin parametrina. Lisäksi luokalla Pala
on seuraavat metodit.
public int getX()
palauttaa Palan konstruktorissa saadun x-koordinaatin.public int getY()
palauttaa Palan konstruktorissa saadun y-koordinaatin.public boolean osuu(Pala pala)
palauttaa true jos oliolla on sama x- ja y-koordinaatti kuin parametrina saadulla Pala-luokan ilmentymällä.public String toString()
palauttaa palan sijainnin muodossa (x,y)
. Esim. (5,2)
kun x-koordinaatin arvo on 5 ja y-koordinaatin arvo on 2.Toteuta pakkaukseen matopeli.domain
myös luokka Omena
. Peri luokalla Omena luokka Pala
.
Toteuta pakkaukseen matopeli.domain
luokka Mato
. Luokalla Mato
on konstruktori public Mato(int alkuX, int alkuY, Suunta alkusuunta)
, joka luo uuden madon jonka suunta on parametrina annettu alkusuunta
. Mato koostuu listasta Pala
-luokan ilmentymiä. Huom: enum Suunta
löytyy valmiina pakkauksesta Matopeli
.
Mato luodaan yhden palan pituisena, mutta madon "aikuispituus" on kolme. Madon tulee kasvaa yhdellä aina kun se liikkuu. Kun madon pituus on kolme, se kasvaa isommaksi vain syödessään.
Toteuta madolle seuraavat metodit
public Suunta getSuunta()
palauttaa madon suunnan.public void setSuunta(Suunta suunta)
asettaa madolle uuden suunnan. Mato liikkuu uuteen suuntaan kun metodia liiku
kutsutaan seuraavan kerran.public int getPituus()
palauttaa madon pituuden. Madon pituuden tulee olla sama kuin getPalat()
-metodikutsun palauttaman listan alkioiden määrä.public List<Pala> getPalat()
palauttaa listan pala-olioita, joista mato koostuu. Palat ovat listalla järjestyksessä, siten että pää sijaitsee listan lopussa.public void liiku()
liikuttaa matoa yhden palan verran eteenpäin.public void kasva()
kasvattaa madon kokoa yhdellä. Madon kasvaminen tapahtuu seuraavan liiku
-metodikutsun yhteydessä. Sitä seuraaviin liiku-kutsuihin kasvaminen ei enää vaikuta. Jos madon pituus on 1 tai 2 kun metodia kutsutaan, ei kutsulla saa olla mitään vaikutusta matoon.public boolean osuu(Pala pala)
tarkistaa osuuko mato parametrina annettuun palaan. Jos mato osuu palaan, eli joku madon pala osuu metodille parametrina annettuun palaan, tulee metodin palauttaa arvo true
. Muuten metodi palauttaa arvon false
.public boolean osuuItseensa()
tarkistaa osuuko mato itseensä. Jos mato osuu itseensä, eli joku sen pala osuu johonkin toiseen sen palaan, metodi palauttaa arvon true
. Muuten metodi palauttaa arvon false
.Metodien public void kasva()
ja public void liiku()
toiminnallisuus tulee toteuttaa siten, että mato kasvaa vasta seuraavalla liikkumiskerralla.
Liikkuminen kannattaa toteuttaa siten, että madolle luodaan liikkuessa aina uusi pala. Uuden palan sijainti riippuu madon kulkusuunnasta: vasemmalle mennessä uuden palan sijainti on edellisen pääpalan sijainnista yksi vasemmalle, eli sen x-koordinaatti on yhtä pienempi. Jos uuden palan sijainti on edellisen pääpalan alapuolella, eli madon suunta on alas, tulee uuden palan y-koordinaatin olla yhtä isompi kuin pääpalan y-koordinaatti (käytämme siis piirtämisestä tuttua koordinaattijärjestelmää, jossa y-akseli on kääntynyt).
Liikkuessa uusi pala lisätään listan loppuun, ja poistetaan listan alussa oleva alkio. Uudesta palasta siis tulee madon "uusi pää" ja jokaisen palan koordinaatteja ei tarvitse päivittää erikseen. Toteuta kasvaminen siten, että listan alussa olevaa palaa, eli "madon häntää" ei poisteta jos metodia kasva
on juuri kutsuttu.
Huom! Kasvata matoa aina sen liikkuessa jos sen pituus on pienempi kuin 3.
Mato mato = new Mato(5, 5, Suunta.OIKEA); System.out.println(mato.getPalat()); mato.liiku(); System.out.println(mato.getPalat()); mato.liiku(); System.out.println(mato.getPalat()); mato.liiku(); System.out.println(mato.getPalat()); mato.kasva(); System.out.println(mato.getPalat()); mato.liiku(); System.out.println(mato.getPalat()); mato.setSuunta(Suunta.VASEN); System.out.println(mato.osuuItseensa()); mato.liiku(); System.out.println(mato.osuuItseensa());
[(5,5)] [(5,5), (6,5)] [(5,5), (6,5), (7,5)] [(6,5), (7,5), (8,5)] [(6,5), (7,5), (8,5)] [(6,5), (7,5), (8,5), (9,5)] false true
Muokataan seuraavaksi pakkauksessa matopeli.peli
olevaa matopelin toiminnallisuutta kapseloivaa luokka Matopeli
. Matopeli-luokka perii luokan Timer
, joka tarjoaa ajastustoiminnallisuuden pelin päivittämiseen. Luokka Timer
vaatii toimiakseen ActionListener
-rajapinnan toteuttavan luokan. Olemme toteuttaneet luokalla Matopeli
rajapinnan ActionListener
.
Muokkaa matopelin konstruktorin toiminnallisuutta siten, että konstruktorissa luodaan peliin liittyvä Mato
. Luo mato siten, että sijainti riippuu Matopeli-luokan konstruktorissa saaduista parametreista. Madon x-koordinaatin tulee olla leveys / 2
, y-koordinaatin korkeus / 2
ja suunnan Suunta.ALAS
.
Luo konstruktorissa myös omena. Konstruktorissa luotavan omenan sijainnin tulee olla satunnainen, kuitenkin niin että omenan x-koordinaatti on aina välillä [0, leveys[
, ja y-koordinaatti välillä [0, korkeus[
.
Lisää matopeliin lisäksi seuraavat metodit
public Mato getMato()
palauttaa matopelin madon.public void setMato(Mato mato)
asettaa matopeliin metodin parametrina olevan madon. Jos metodia getMato
kutsutaan madon asetuksen jälkeen, tulee metodin getMato
palauttaa viite samaan matoon.public Omena getOmena
palauttaa matopelin omenan.public void setOmena(Omena omena)
asettaa matopeliin metodin parametrina olevan omenan. Jos metodia getOmena
kutsutaan omenan asetuksen jälkeen, tulee metodin getOmena
palauttaa viite samaan omenaan.Muokkaa metodin actionPerformed
-toiminnallisuutta siten, että metodissa toteutetaan seuraavat askeleet annetussa järjestyksessä.
jatkuu
arvoksi false
jatkuu
arvoksi false
Paivitettava
toteuttavan muuttujan paivitettava
metodia paivita
.setDelay
-metodia siten, että pelin nopeus kasvaa suhteessa madon pituuteen. Kutsu setDelay(1000 / mato.getPituus());
käy hyvin: kutsussa oletetaan että olet määritellyt oliomuuttujan nimeltä mato
.Aletaan seuraavaksi rakentamaan käyttöliittymäkomponentteja.
Toteuta pakkaukseen matopeli.gui
luokka Nappaimistonkuuntelija
. Luokalla on konstruktori public Nappaimistonkuuntelija(Mato mato)
, ja se toteuttaa rajapinnan KeyListener
. Korvaa metodi keyPressed
siten, että nuolinäppäintä ylös painettaessa madolle asetetaan suunta ylös. Nuolinäppäintä alas painettaessa madolle asetetaan suunta alas, vasemmalle painettaessa suunta vasen, ja oikealle painettaessa suunta oikea.
Toteuta pakkaukseen matopeli.gui
luokka Piirtoalusta
, joka perii luokan JPanel
. Piirtoalusta saa konstruktorin parametrina luokan Matopeli
ilmentymän sekä int-tyyppisen muuttujan palanSivunPituus
. Muuttuja palanSivunPituus
kertoo minkä levyinen ja korkuinen yksittäinen pala on.
Korvaa luokalta JPanel
peritty metodi paintComponent(Graphics g)
siten, että piirrät metodissa paintComponent
madon ja omenan. Käytä madon piirtämiseen Graphics-olion tarjoamaa fill3DRect
-metodia. Madon värin tulee olla musta (Color.BLACK
). Omenan piirtämisessä tulee käyttää Graphics-olion tarjoamaa fillOval
-metodia. Omenan värin tulee olla punainen (Color.RED
).
Huom: metodin paintComponent alussa tulee olla kutsu korvattuun metodiin, eli ensimmäisen rivin tulee olla super.paintComponent(g);
Toteuta luokalla Piirtoalusta
myös rajapinta Paivitettava
. Paivitettava-rajapinnan määrittelemän metodin paivita
tulee kutsua JPanel-luokalta perittyä repaint
-metodia.
Muuta luokkaa Kayttoliittyma
siten, että käyttöliittymä sisältää piirtoalustan. Metodissa luoKomponentit
tulee luoda piirtoalustan ilmentymä ja lisätä se container-olioon. Luo metodin luoKomponentit
lopussa luokan Nappaimistokuuntelija
ilmentymä, ja lisää se frame-olioon.
Lisää luokalle Kayttoliittyma
myös metodi public Paivitettava getPaivitettava()
, joka palauttaa metodissa luoKomponentit
luotavan piirtoalustan.
Voit käynnistää käyttöliittymän Main
-luokassa seuraavasti. Ennen pelin käynnistystä odotamme että käyttöliittymä luodaan. Kun käyttöliittymä on luotu, se kytketään matopeliin ja matopeli käynnistetään.
Matopeli matopeli = new Matopeli(20, 20); Kayttoliittyma kali = new Kayttoliittyma(matopeli, 20); SwingUtilities.invokeLater(kali); while (kali.getPaivitettava() == null) { try { Thread.sleep(100); } catch (InterruptedException ex) { System.out.println("Piirtoalustaa ei ole vielä luotu."); } } matopeli.setPaivitettava(kali.getPaivitettava()); matopeli.start();
Peliin jää vielä paljon paranneltavaa. Palauta nyt tehtävä ja voit sen jälkeen laajentaa ja parannella peliä haluamallasi tavalla!
Olemme saaneet paljon arvokasta palautetta TMC:n kautta. Näin kurssin viimeisenä kysymyksenä haluaisimme koko kurssin sisältöä koskevan palautteen. Anna palaute täyttämällä täältä löytyvä lomake. Palaute on anonyymi.
Jotta saat merkatuksi tämän tehtävän, aja tehtävän TMC-testit ja lähetä tehtävä palvelimelle.
Vaikka olemme saaneet paljon arvokasta palautetta TMC:n kautta yksittäisistä tehtävistä, palauttaessasi tätä, kirjoitathan kommenttikenttään palautteen koko MOOC-kurssista. Mainitsethan siinä, mikäli et halua palautettasi julkaistavan anonyymisti sivustoillamme. Vain osa palautteesta tullaan julkaisemaan.