Huomautus lukijalle
Tämä on suoraa jatkoa Ohjelmoinnin perusteet -kurssin materiaaliin.
Tämä materiaali on tarkoitettu Helsingin Yliopiston Tietojenkäsittelytieteen laitoksen kevään 2012 Ohjelmoinnin perusteet- ja jatkokurssille sekä koko maailmalle avoimelle MOOC-ohjelmointikurssille. Materiaali pohjautuu kevään 2011 ja 2010 kurssimateriaaleihin, joiden sisältöön ovat vaikuttaneet Matti Paksula, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju ja Martin Pärtel.
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 (tai MOOC-kurssilaisena verkossa).
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ä kappaleessa kerrataan pikaisesti muutamia ohjelmoinnin perusteissa tutuksi tulleita asioita. Ohjelmoinnin perusteet -kurssin materiaaliin pääsee tutustumaan tarkemmin tästä.
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ä viittaustyyppiset muuttujatyypit String
(merkkijono), ArrayList
(taulukko) ja kaikki luokat. Palaamme alkeistyyppisiin ja viittaustyyppisiin muuttujiin ja niiden eroihin tarkemmin myöhemmin.
Ohjelmien toiminnallisuus rakennetaan kontrollirakenteiden avulla. Kontrollirakenteet mahdollistavat erilaiset toiminnot ohjelman muuttujien arvoista riippuen. Alla esimerkki if-elseif-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. Allaoleva 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. Java-ohjelmat käynnistetään pääohjelmametodista.
public static void main(String[] args) { System.out.println("Hei maailma!"); }
Ohjelmoija määrittelee metodeja toiminnallisuuksien abstrahoimiseksi. Ohjelmoidessa tulee tavoitella tilannetta, jossa ohjelmaa voidaan katsoa ns. korkeammalta tasolta, jolloin ohjelma sisältää joukosta itse määriteltyjä metodikutsuja, jotka sisältävät ohjelman toiminnallisuudet. 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. 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 on kuin sen kaveri 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ä kappaleessa huomasimme tilanteen, joka nostatti kylmiä väreitä selkäpiissämme. Metodien avulla voimme siirtää tähtien tulostamisen erillisen metodiin. Luodaan metodi public static void tulostaTahtia(int kertaa)
, joka tulostaa parametrina annetun muuttujan määräämän määrän tähtiä. Metodi käyttää for
-toistolausetta while
-toistolauseen 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 pienempiin metodit sisältäviin kokonaisuuksiin. Luokkien avulla voimme määrittelemme 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 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, jota seuraa oliomuuttujat. Oliomuuttujat ovat muuttujia jotka ovat omat jokaiselle luokasta luodulle oliolle. 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.
Tämän lisäksi luokalle on määritelty 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ä viittaustyyppisiä muuttujia. Jos olioon liittyy viittaustyyppisiä 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 viittaustyyppisten muuttujien eroon tarkemmin myöhemmin.
Ohjelmien tulee olla selkeitä paitsi itselle, myös toisille. Käytämme luokkia ja metodeja selventääksemme ohjelmissa esiintyviä käsitteitä kaikille. Jokaisella luokalla on vastuu, johon liittyviä tehtäviä se hoitaa. Metodeja käytetään toiston vähentämiseen ja luokkien sisäisten toimintojen selventämiseksi. Metodien avulla voidaan myös selkeyttää ohjelmakoodia. Käytännössä metodi ei saa olla koskaan niin pitkä että sen luettavuus vaikeutuisi. Jos huomaat että metodisi on niin pitkä, ettet ymmärrä tai muista mitä se tekee, kannattaa pilkkoa se heti pienempiin metodeihin. 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 nimentä kuin ohjelmakoodin ilmavuus ja seurattavien sisennysten määrä. 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 varasto konstruktorin parametrina -- tähänkin palataan myöhemmin kurssin aikana.
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 sopivan pieni. Huomaa että jokaisella metodilla on oma pieni tehtävä jota se hoitaa. 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 aksessorit 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, kannattaa lisätä ylimääräinen välilyönti 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:
Muunnos muunnos = new Muunnos('a', 'b'); System.out.println(muunnos.muunna("porkkana"));
Yllä oleva esimerkki tulostaisi:
porkkbnb
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
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 sovelluslogiigasta 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ä koska metodit ovat luokan Laskin
sisällä, on oliomuuttujana oleva Lukija
käytettävissä metodien sisällä.
Metodissa kaynnista
olevan while
-silmukan jälkeen kutsutaan metodia statistiikka
. Metodin on tarkoitus tulostaa kuinka montaa kertaa laskinoliolla 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
Java on vahvasti tyypitetty kieli, eli kaikilla sen muuttujilla on tyyppi. Muuttujatyypit voidaan jakaa kahteen kategoriaan; alkeis- ja viittaustyyppisiin muuttujiin. Kummankin kategorian muuttujatyypeillä on oma "lokero", joka sisältää niihin liittyvän arvon. Alkeistyyppisillä muuttujilla muuttujien konkreettinen arvo tallennetaan lokeroon, kun taas viittaustyyppisten muuttujien lokero sisältää viitteen muuttujaan liittyvään konkreettiseen olioon.
Oliot eivät mahdu lokeroihin, vaan niihin viitataan aina.
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 parametrimuuttujaan kopio annetun muuttujan lokerossa olevasta. Käytännössä myös metodin parametrimuuttujilla 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.4
ei tuota toivottua tulosta pyöristysvirheen takia.
double eka = 0.39; double toka = 0.35; if((eka - toka) == 0.4) { 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.4 - (eka - toka); if(Math.abs(etaisyys) < 0.0001) ) { System.out.println("Vertailu onnistui!"); } else { System.out.println("Vertailu epäonnistui!"); }
Viittaustyyppiset muuttujat tallentavat niihin liittyvän tiedon viitteen taakse eli "langan päähän". Viittaustyyppisten muuttujien lokerossa on viite tiedon sisältävään paikkaan. Toisin kuin alkeistyyppisillä muuttujilla, viittaustyyppisillä muuttujilla ei ole rajoitettua arvoaluetta, koska niiden oikea arvo tai tieto on viitteen takana. Oleellinen ero alkeistyyppisiin muuttujiin on se, että eri viittaustyyppiset muuttujat voivat viitata samaan paikkaan.
Viittaustyyppisistä muuttujista puhutaan olioina, joita luodaan new
-kutsulla. Muuttujan arvo asetetaan vieläkin sijoitusoperaattorilla =
, mutta komento new
luo olion ja palauttaa viitteen olioon. Viite asetetaan muuttujaan liittyvään lokeroon eli sen arvoksi. Tutkitaan kahden viittaustyyppisen 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 viittaustyyppinen muuttuja bonusLaskuri
(Laskuri bonusLaskuri = new Laskuri(5);
). Komentoa new
kutsuessa varataan tila muuttujan tietoa varten, suoritetaan new
-kutsua seuraavan konstruktorin koodi, ja palautetaan viite juuri luotuun olioon. Konstruktori suorittaa mahdolliset muutokset olioon liittyvään tilaan. Esimerkissä luodaan Laskuri-tyyppinen olio, ja palautetaan viite siihen. Palautettu viite asetetaan sijoitusoperaattorilla =
muuttujaan bonusLaskuri
. Sama tapahtuu muuttujalle nimeltä axeLaskuri
. Kuvana viittaustyyppi 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 viittaustyyppisen 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
Viittaustyyppistä 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 viittaustyyppisten muuttujien viitteitä, kun taas alkeistyyppisiin muuttujiin liittyvässä esimerkissä kopioimme alkeistyyppien arvoja. Kummassakin tapausessa siis lokeron sisältö kopioidaan, alkeistyyppisten muuttujien lokero sisältää arvon, viittaustyyppisten 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.
Viittaustyyppiset muuttujat viittaavat aina toisaalla oleviin olioihin. Useat viittaustyyppiset muuttujat voivat sisältää saman viitteen, jolloin kaikki muuttujat osoittavat samaan olioon. Seuraavassa esimerkissä on kolme viittaustyyppistä 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: viittaustyyppisiä 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 viittaustyyppinen muuttuja annetaan parametrina metodille, luodaan metodin parametrimuuttujalle kopio annetusta muuttujan viitteestä. Parametrimuuttujalla 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, viittaustyyppinen muuttuja ja alkeistyyppinen muuttuja. Kumpaankin muuttujaan liittyvän lokeron sisältö kopioidaan metodin parametrimuuttujien omiin lokeroihin. Viittaustyyppiselle parametrimuuttujalle laskuri
kopioituu viite ja alkeistyyppiselle parametrimuuttujalle 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 parametrimuuttujiin laskuri
ja paljonko
kopioituvat siis viittaustyyppisen 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!
Viittaustyyppisestä 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 viittaustyyppisen muuttujan, palauttaa se viitteen muualla sijaitsevaan olioon. Metodin palauttaman viittaustyyppisen muuttujan voi asettaa muuttujalle samalla tavalla kuin normaalikin asetus tapahtuu, eli yhtäsuuruusmerkin avulla. Katsotaan metodia public Laskuri luoLaskuri(int alkuarvo)
, joka luo uuden viittaustyyppisen 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 viittaustyyppiseen 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. Staattisilla ja ei-staattisilla metodeilla 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 luokkamuuttuja Integer.MAX_VALUE
on vakiotyyppinen luokkamuuttuja. Sillä on final-määre, joten sitä ei voi muuttaa.
Jos käytössämme on yllä esitelty luokka HslHinnasto
, voivat kaikki ohjelmat, jotka käyttävät kerta- tai raitiovaunulipun hintaa käyttää niitä 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 staattiset alkeistyyppiset 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 metodiaannaKertalipunHinta()
sen sijaan että kutsuttaisiin muuttujaa suoraan. Palaamme näkyvyysmääreiden public
ja private
tarkempaan merkitykseen kohta.
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 }
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 viittaustyyppistä 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. Aksessori getNimi()
palauttaa viitteen nimi
-olioon, ja getRahat()
-metodi palauttaa alkeistyyppisen muuttujan rahat
. Metodi lisaaRahaa(double summa)
lisaa 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); // lottovoitto! 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 4.
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.
Hajautustaulun toiminnasta ja käytöstä on lyhyt yhteenveto MOOC-kurssin Cheatsheetissä.
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 avain osoittaa aina tiettyyn paikkaan. Sama avain ei voi osoittaa kahteen eri olioon. Jos jo olemassaolevalla avaimella tallennetaan uusi olio, katoaa vanhan olion viite 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.
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() { } 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() { String palautus = "Nimi: " + this.nimi + " (" + this.julkaisuvuosi + ")\n" + "Sisältö: " + this.sisalto; return palautus; } }
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 viittaustyyppisiä. Jos haluat käyttää alkeistyyppisiä muuttujia avaimena tai tallennettavana arvona, on niille olemassa myös viittaustyyppiset vastineet. Alla on esitelty muutama.
Alkeistyyppi | Viittaustyyppinen vastine |
---|---|
int | Integer |
double | Double |
char | Character |
Java oikeastaan kapseloi alkeistyyppiset muuttujat automaattisesti viittaustyyppisiksi 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 viittaustyyppisiksi 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 Numerokirjanpito() { 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 viittaustyyppiset 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
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 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
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
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ä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 semantiikka ja syntaksi: muuttujien määrittelytapa, kontrollirakenteiden muoto, ja muuttujien ja luokkien rakenne ja niin edelleen. 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.
Merkkijonojen rakentaminen +
-operaattorilla on hieman vaivalloista ja lisäksi erittäin tehotonta. Tätä varten Javan standardikirjasto tarjoaa luokan StringBuilder
.
Tutustu ensin StringBuilder
-luokan apidokumentaatioon.
Tehtävänäsi on toteuttaa luokkametodi public static void pyramidi(int koko, StringBuilder rakentaja)
, joka toimii seuraavalla tavalla:
StringBuilder rakentaja = new StringBuilder(); pyramidi(3, rakentaja); System.out.print(rakentaja.toString());
1 1 2 1 2 3 -----
StringBuilder rakentaja = new StringBuilder(); pyramidi(5, rakentaja); System.out.print(rakentaja.toString());
1 1 2 1 2 3 1 2 3 4 1 2 3 4 5 ---------
Huom! Käytä vain metodin pyramidi
parametrina saamaa StringBuilder
-oliota pyramidin rakentamiseen. Älä esimerkiksi käytä merkkijonojen katenaatiota tai luo omaa erillistä StringBuilder
-oliota metodissa pyramidi
.
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ä.
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 > porkkana 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 > 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) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > 3 Anna lentokoneen tunnus: G-OWAC G-OWAC (101 henkilöä) Valitse toiminto: [1] Tulosta lentokoneet [2] Tulosta lennot [3] Tulosta lentokoneen tiedot [x] Lopeta > x
Huom! Testien kannalta on oleellista että käyttöliittymä toimii kuten yllä kuvattu. Jos käyttäjä syöttää epäkelvon komennon, pyydetään komentoa uudestaan. Tämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen.
Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan, tehtävässä saa luoda vain yhden Scanner-olion.
Kurssilla on jo useampaan otteeseen käytetty metodia public String toString()
olion merkkijonoesityksen tulostamiseen. 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())
-- emme kuitenkaan kohtaa virhettä.
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
tarkoittaa että annotaatiota seuraava korvaa perityssä luokassa määritellyn metodin. Annotaatio @Override
auttaa ohjelmoijaa, sillä kääntäjä luo virheilmoituksen jos @Override
on lisätty ei-perityn metodin yläpuolelle.
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 kanssa.
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. 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"); } 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 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. Toisessa vertailussa viitteet ovat eri vaikka olioiden sisäiset muuttujat ovat samat.
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 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 (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
-oliota, tulee Object-olion tyyppiä muuttaa. Olion tyyppiä voi 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 allaolevaa 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 hajautustauluissa olion paikan päättelemistä varten. Oikeastaan kaikilla luokilla joita olemme tähän mennessä käyttäneet hajautustaulun avaimina on ollut oma hashCode
-metodin toteutus. Luodaan esimerkki jossa näin ei ole: jatketaan kirjojen parissa ja mietitään kirjojen sijoittamista hyllyihin. Luodaan esimerkkiä varten luokka Hylly
, joka kuvaa kirjojen sijoituspaikkaa.
public class Hylly { private String nimi; public Hylly(String nimi) { this.nimi = nimi; } @Override public String toString() { return this.nimi; } }
Esimerkissä luodaan hahmotelmaa järjestelmälle joka kertoo mihin hyllyyn mikäkin kirja kuuluu. Kirja-olioita käytetään hajautustaulun avaimena ja Hylly-olioita talletettavana arvona. Tallennamme hajautustauluun siis kirjojen säilytyshyllyjä. Alla olevassa esimerkissä luodaan tuttu "Oliokirja
" ja asetetaan se hyllyyn "Ohjelmointikirjat
". Tämän jälkeen hyllystä haetaan ensin samalla oliolla, jolla on sama viite. Tämän jälkeen luodaan uusi täsmälleen samanlainen "Oliokirja
"oliokirja, ja yritetään etsiä sille sopivaa sijoituspaikkaa.
HashMap<Kirja, Hylly> kirjojenHyllyt = new HashMap<Kirja, Hylly>(); Kirja olioKirja = new Kirja("Oliokirja", 2000); Hylly hylly = new Hylly("Ohjelmointikirjat"); kirjojenHyllyt.put(olioKirja, hylly); System.out.println(kirjojenHyllyt.get(olioKirja)); Kirja toinenOlioKirja = new Kirja("Oliokirja", 2000); System.out.println(kirjojenHyllyt.get(toinenOlioKirja));
Ohjelmointikirjat null
Löydämme halutun hyllyn hakiessamme viitteellä joka annettiin hajautustaulun put
-metodille avaimeksi. Täsmälleen samanlaisella kirjalla mutta eri viitteellä haettaessa hyllyä ei löydy ja saamme null-viitteen. Syynä on taas Object
-luokassa oleva hashCode
-metodin oletustoteutus. Oletustoteutus luo indeksin viitteen perusteella.
Haluamme kirjoille sijoitushyllyn kirjan nimen perusteella, joten korvataan Object
-luokassa oleva hashCode
-metodin toteutus omalla hashCode
-metodilla. Olemme aiemmin käyttäneet String
-olioita menestyksekkäästi hajautustaulun avaimena, joten voimme päätellä että String
-luokassa on oma 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 tätä luokkaa varten satunnaisesti valittu alkuluku. Jos toteutat toisen luokan, voit hyvin valita esimerkiksi arvon 13.
public int hashCode() { if (this.nimi == null) { return 7; } return this.nimi.hashCode(); }
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 olioilla olla metodi equals
toteutettuna. Hajautustaulujen syvemmästä sielunelämästä tulee lisätietoa muun muassa 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; } @Override public int hashCode() { if (this.nimi == null) { return 7; } return this.nimi.hashCode(); } }
Nyt myös aiemmin kohtaamamme hyllytysongelma ratkeaa. Uudelle "Oliokirja
"-kirjalle löytyy oikea hylly.
HashMap<Kirja, Hylly> kirjojenHyllyt = new HashMap<Kirja, Hylly>(); Kirja olioKirja = new Kirja("Oliokirja", 2000); Hylly hylly = new Hylly("Ohjelmointikirjat"); kirjojenHyllyt.put(olioKirja, hylly); System.out.println(kirjojenHyllyt.get(olioKirja)); Kirja toinenOlioKirja = new Kirja("Oliokirja", 2000); System.out.println(kirjojenHyllyt.get(toinenOlioKirja));
Ohjelmointikirjat Ohjelmointikirjat
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.
Kurt Koodari toteutti eräälle lounasravintolalle järjestelmän asiakkaiden ja myyntien hallintaan. Järjestelmä hallinnoi asiakkaiden tilejä sekä ravintolan vip-listaa. Vip-asiakkaat syövät ravintolassa edullisemmin kuin normaalit asiakkaat.
Kurt suunnitteli ohjelman rakenteen fiksusti siten, että ohjelmassa on eriytetty käyttöliittymälogiikka ja sovelluslogiikka. Sovelluslogiikassa Asiakas-olioita tallennetaan muun muassa ArrayList
- ja HashMap
-tietorakenteisiin. Koodausinnossaan Kurt kuitenkin unohti Object
-luokan metodien ylikirjoittamisen, joten ohjelma ei toimi toivotusti. Tällä hetkellä esimerkiksi sisäänkirjautuminen ei onnistu.
Tervetuloa ravintolapalveluun. Kirjoita nimesi: Arto Et ole asiakkaamme. Kirjoita nimesi: Bonus Et ole asiakkaamme. Kirjoita nimesi:
Hätä ei ole tämän näköinen! Tehtävänäsi on pelastaa Kurtin projekti: toteuta tehtäväpohjassa tulevaan luokkaan Asiakas
metodit equals
ja hashCode
. Hyödynnä asiakkaan nimeä sekä equals
-metodissa että hashCode
-metodissa. Muutoksen jälkeen ohjelman pitäisi toimia seuraavasti.
Tervetuloa ravintolapalveluun. Kirjoita nimesi: Arto Et ole asiakkaamme. Kirjoita nimesi: Lennon Hei Lennon, tililläsi on 8 euroa. Komennot: [1] osta lounas [2] rekisteröidy vip-käyttäjäksi [x] lopeta Valitse komento: 1 Hei Lennon, tililläsi on 4 euroa. Komennot: [1] osta lounas [2] rekisteröidy vip-käyttäjäksi [x] lopeta Valitse komento: 2 Hei Lennon, tililläsi on 4 euroa. Komennot: [1] osta lounas [x] lopeta Valitse komento: 1 Hei Lennon, tililläsi on 1 euroa. Komennot: [1] osta lounas [x] lopeta Valitse komento: x Hei hei. Tervetuloa ravintolapalveluun. Kirjoita nimesi: Bonus Hei Bonus, tililläsi on 400000 euroa. Komennot: [1] osta lounas [x] lopeta Valitse komento: x Hei hei. Tervetuloa ravintolapalveluun. Kirjoita nimesi:
Huom! Muuta vain Asiakas-luokan toteutusta. Neuvoja equals
ja hashCode
-metodien toteutukseen saat edellisestä kappaleesta.
Rajapinta (engl. interface) on väline luokilta vaaditun käyttäytymisen määrittelyyn. Rajapinnat ovat luokkia kuten normaalit Javan olevat luokat, mutta luokan alussa olevan määrittelyn "public class ...
" sijaan käytetään määrittelyä "public interface ...
". Rajapintaluokat 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 -- tai oikeastaan luokat toteuttavat ohjelmoijat -- 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 viittaustyyppiset muuttujat (kaikki oliot). Olemme tähän mennessä käyttäneet viittaustyyppisten 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 merkkijonon StringBuilder-olioon. Tämän jälkeen lukulista tyhjennetään ja palautetaan StringBuilder-olioon luettu data.
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() { StringBuilder luettu = new StringBuilder(); for(Luettava luettava: this.luettavat) { luettu.append(luettava.lue()).append("\n"); } this.luettavat.clear(); return luettu.toString(); } }
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.5) ); 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ä 3.8 kiloa
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. Toteutetaan luokat Uutinen
, joka kuvaa yksittäistä luettavaa uutista, ja Uutispalvelu
, jonka tehtävänä on luoda luettavia uutisia. Uutispalvelua käyttäville sovelluksille ei ole tärkeää tai edes mielekästä tietää uutisten todellisesta toteutuksesta, oleellista on vain niiden lukeminen. Uutispalvelu voi siis hyvin tarjota uutisensa Luettava
-rajapinnan kautta.
public class Uutinen implements Luettava { private String teksti = teksti; public Uutinen(String teksti) { this.teksti = teksti; } public String lue() { return this.teksti; } }
public class Uutispalvelu { public Luettava haeViimeisinUutinen() { return new Uutinen("uusinta hottia!"); } }
Uutispalvelu tuottaa uutisensa aina Luettava
-rajapinnan tyyppisenä. Tässä kohtaa nousee usein esille erinomainen kysymys "Miksi emme käyttäisi vain luokkaa Uutinen?
". Vastaus on pitkähkö, mutta toivottavasti selvittää taustaidean.
Pohditaan tilannetta, jossa meillä on uutisia julkaiseva Julkaisupalvelu
. Julkaisupalvelun tehtävänä on lukea uutisia tasaisin väliajoin uutispalvelulta ja tulostaa viestit näkyville (julkaisupalvelu voisi lähettää hyvin viestin esimerkiksi eri medioille, mutta pidättäydytään pienemmässä esimerkissä). Oletetaan että Uutispalvelu palauttaa Uutinen-olioita.
public class Uutispalvelu { public Uutinen haeViimeisinUutinen() { return new Uutinen("uusinta hottia!"); } }
Julkaisupalvelun oleellinen toiminnallisuus on toistolauseke, joka kutsuu tasaisin väliajoin uutispalvelun haeViimeisinUutinen-metodia
.
public class Julkaisupalvelu { private Uutispalvelu uutispalvelu; public Julkaisupalvelu() { this.uutispalvelu = new Uutispalvelu(); } public void kaynnista() { while (true) { Uutinen uutinen = uutispalvelu.haeViimeisinUutinen(); System.out.println(uutinen.lue()); try { Thread.sleep(10000); } catch (Exception e) { } } } }
Tässä vaiheessa kaikki toimii hyvin. Oletetaan että Uutispalvelun toimitusjohtaja huomaa, että he tarvitsevat uuden formaatin kuvallisille uutisille. Kuvallisia uutisia varten toteutetaan erillinen luokka KuvaUutinen
. KuvaUutinen on luettava, joten Uutispalvelun ohjelmoijat toteuttavat myös sille rajapinnan Luettava-rajapinta.
public class KuvaUutinen implements Luettava { private String kuvaOsoite; private String teksti; public KuvaUutinen(String teksti, String kuvaOsoite) { this.teksti = teksti; this.kuvaOsoite = kuvaOsoite; } public String lue() { return this.teksti + " (kuvan osoite: " + this.kuvaOsoite + ")"; } }
Samalla he joutuvat myös muuttamaan UutisPalvelu-luokan toteutusta, sillä se ei tue uutta uutisformaattia:
public class Uutispalvelu { public KuvaUutinen haeViimeisinUutinen() { return new KuvaUutinen("uusinta hottia!", "kuvan osoite"); } }
Nyt julkaisupalvelun toteutusta on pakko muuttaa, sillä se ei enää toimi koska Uutispalvelu palauttaa KuvaUutinen
-luokan ilmentymän. Kuinka montaa luokkaa pitäisi muuttaa jos uutispalvelua olisi käyttänyt kymmenen palvelua, entä jos tuhat? Tässä vaiheessa jokaisen uutispalvelua käyttävän sovelluksen tulee muuttaa omaa toimintaansa.
Entä jos kaikissa uutisissa ei ole kuvia, ja haluaisimme silloin tällöin kuitenkin palauttaa Uutinen
-luokan ilmentymän? Yllä oleva metodi haeViimeisinUutinen
ei taida riittää..
Pohditaan seuraavaksi yllä tehtyä uutisformaatin muutosta tilanteessa, jossa Uutispalvelun haeViimeisinUutinen
-metodin palautustyyppi on Luettava
.
public class Uutispalvelu { public Luettava haeViimeisinUutinen() { return new Uutinen("uusinta hottia!"); } }
Kun Uutispalvelun toimitusjohtaja haluaa uuden kuvaformaatin, ei Uutispalvelun haeViimeisinUutinen
-metodin palautustyypille tarvitse tehdä mitään.
public class Uutispalvelu { public Luettava haeViimeisinUutinen() { return new KuvaUutinen("uusinta hottia!", "kuvan osoite"); } }
Julkaisupalveluunkaan ei tarvitse tehdä muutoksia.
public class Julkaisupalvelu { private Uutispalvelu uutispalvelu; public Julkaisupalvelu() { this.uutispalvelu = new Uutispalvelu(); } public void kaynnista() { while (true) { Luettava luettava = uutispalvelu.haeViimeisinUutinen(); System.out.println(luettava.lue()); try { Thread.sleep(10000); } catch (Exception e) { } } } }
Kuinka montaa luokkaa olisi pitänyt muuttaa jos uutispalvelua olisi käyttänyt kymmenen palvelua, entä jos tuhat? Nollaa. Entä jos uutispalvelu haluaa välillä lähettää normaaleja uutisia, välillä kuvallisia uutisia? Helppo homma, muutos tarvitaan vain uutispalvelun puolelle.
public class Uutispalvelu { public Luettava haeViimeisinUutinen() { Random random = new Random(); if(random.nextDouble() > 0.5) { return new Uutinen("uusinta hottia!"); } return new KuvaUutinen("uusinta hottia!", "kuvan osoite"); } }
Emme joudu tässäkään tapauksessa muuttamaan Uutispalvelua käyttäviä sovelluksia.
Rajapintojen käyttö ohjelmoinnissa mahdollistaa riippuvaisuuksien vähentämisen. Jos kaikki uutispalvelua käyttävät palvelut käyttävät rajapintaa Luettava, eivät ne ole suoraan riippuvaisia jostain tietystä Luettava-rajapinnan toteuttavasta luokasta. Yllä olevassa esimerkissä uutispalvelun sisäistä toteutusta pystyi muuttamaan siten, että siinä tehdyt muutokset eivät vaikuttaneet uutispalvelua käyttäneisiin olioihin millään tavalla.
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 sielunelämästä 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 oliomuuttja 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 allaoleva 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 asiointi(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 ArrayList<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.
Ohjelmoinnin perusteet-kurssilla toteutettiin Korttipakka-, Kasi- ja Kortti-luokat. Nyt kun osaamme tehdä Comparable-luokkia, voimme parannella tuolloin tekemiämme ratkaisuja huomattavasti.
Saat valmiiksi toteutettuina Korttipakka-, Kasi- ja Kortti-luokat. 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.
Kun Kortti-luokasta on tehty Comparable, toteuta Korttipakka-luokkaan jarjesta
-metodi, joka nimensä mukaisesti järjestää Korttipakan.
Käsien käsittely voi olla helpompaa, jos Kasi-luokan sisältämä korttilista on järjestyksessä. Tällöin voisimme halutessamme etsiä Kortteja kädestä vaikka puolitushaulla. Tee siis Kasi-luokan konstruktorista sellainen, että jokainen luotu Kasi on järjestyksessä.
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ä.
NHL:ssä pidetään pelaajista yllä monenlaisia tilastotietoja. Teemme nyt oman ohjelman NHL-pelaajien tilastojen hallintaan.
Tee luokka Pelaaja
, johon voidaan tallettaa pelaajan nimi, joukkue, pelatut ottelut, maalimäärä, ja syöttömäärä. Luokalla tulee olla konstruktori, joka saa edellämainitut tiedot edellä annetussa järjestyksessä.
Tee kaikille edelläminituille arvoille myös ns. getterimetodit, jotka palauttavat arvot:
String getNimi
String getJoukkue
int getOttelut
int getMaalit
int getSyotot
int getPisteet
- laskee kokonaispistemäärän eli maalien ja syöttöjen summanTalleta seuraavat pelaajat ArrayList:iin ja tulosta listan sisältö:
public static void main(String[] args) { ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>(); pelaajat.add(new Pelaaja("Alex Ovechkin", "WSH", 71, 28, 46)); pelaajat.add(new Pelaaja("Dustin Byfuglien", "ATL", 69, 19, 31)); pelaajat.add(new Pelaaja("Phil Kessel", "TOR", 70, 28, 24)); pelaajat.add(new Pelaaja("Brendan Mikkelson", "ANA, CGY", 23, 0, 2)); pelaajat.add(new Pelaaja("Matti Luukkainen", "SaPKo", 1, 0, 0 )); for (Pelaaja pelaaja : pelaajat) { System.out.println(pelaaja); } }
Pelaajan toString()
-metodin muodostaman tulostuksen tulee olla seuraavassa muodossa:
Alex Ovechkin WSH 71 28 + 46 = 74 Dustin Byfuglien ATL 69 19 + 31 = 50 Phil Kessel TOR 70 28 + 24 = 52 Brendan Mikkelson ANA, CGY 23 0 + 2 = 2 Matti Luukkainen SaPKo 1 0 + 0 = 0
Ensin siis nimi, sitten joukkue, jonka jälkeen ottelut, maalit, plusmerkki, syötöt, yhtäsuuruusmerkki ja kokonaispisteet eli maalien ja syöttöjen summa.
Tee Pelaaja
-luokkaan metodi toSiistiMerkkijono()
, joka palauttaa samat tiedot siististi aseteltuna siten, että jokaiselle muuttujalle on varattu tietty määrä tilaa tulostuksessa.
Tulostuksen tulee näyttää seuraavalta:
Alex Ovechkin WSH 71 28 + 46 = 74 Dustin Byfuglien ATL 69 19 + 31 = 50 Phil Kessel TOR 70 28 + 24 = 52 Brendan Mikkelson ANA, CGY 23 0 + 2 = 2 Matti Luukkainen SaPKo 1 0 + 0 = 0
Nimen jälkeen joukkueen nimien täytyy alkaa samasta kohdasta. Saat tämän aikaan esim. muotoilemalla nimen tulostuksen yhteydessä seuraavasti:
String nimiJaTyhjaa = String.format("%-25s", nimi);
Komento tekee merkkijonon nimiJaTyhjaa
joka alkaa merkkijonon nimi
sisällöllä ja se jälkeen tulee välilyöntejä niin paljon että merkkijonon pituudeksi tulee 25. Joukkueen nimi tulee vastaavalla tavalla tulostaa 14 merkin pituisena merkkijonona. Tämän jälkeen on otteluiden määrä (2 merkkiä), jota seuraa 2 välilyöntiä. Tämän jälkeen on maalien määrä (2 merkkiä), jota seuraa merkkijono " + ". Tätä seuraa syöttöjen määrä (2 merkkiä), merkkijono " = ", ja lopuksi yhteispisteet (2 merkkiä).
Lukuarvot eli ottelu-, maali-, syöttö- ja pistemäärä muotoillaan kahden merkin mittaisena, eli lukeman 0 sijaan tulee tulostua välilyönti ja nolla. Seuraava komento auttaa tässä:
String maalitMerkkeina = String.format("%2d", maalit);
Lisää luokalle Pelaaja rajapinta Comparable<Pelaaja>
, jonka avulla pelaajat voidaan järjestää kokonaispistemäärän mukaiseen laskevaan järjestykseen. Järjestä pelaajat Collections-luokan avulla ja tulosta pistepörssi:
Collections.sort(pelaajat); System.out.println("NHL pistepörssi:\n"); for (Pelaaja pelaaja : pelaajat) { System.out.println(pelaaja); }
Tulostuu:
NHL pistepörssi: Alex Ovechkin WSH 71 28 + 46 = 74 Phil Kessel TOR 70 28 + 24 = 52 Dustin Byfuglien ATL 69 19 + 31 = 50
Ohjeita tähän tehtävään materiaalissa.
Tilastomme on vielä hieman vajavainen, siinä on vaan muutaman pelaajan tiedot (ja nekin vastaavat 16.3. tilannetta). Kaikkien tietojen syöttäminen käsin olisi kovin vaivalloista. Onneksemme internetistä osoitteesta http://nhlstatistics.herokuapp.com/players.txt
löytyy päivittyvä, koneen luettavaksi tarkoitettu lista pelaajatiedoista.
Huom: kun menet osoitteeseen ensimmäistä kertaa, sivun latautuminen kestää muutaman sekunnin (sivu pyörii virtuaalipalvelimella joka sammutetaan jos sivua ei ole hetkeen käytetty). Sen jälkeen sivu toimii nopeasti.
Datan lukeminen internetistä on helppoa. Projektissasi on valmiina luokka Tilasto
, joka lataa annetun verkkosivun.
import java.io.InputStream; import java.net.URL; import java.util.Scanner; public class Tilasto { private static final String OSOITE = "http://nhlstatistics.herokuapp.com/players.txt"; private Scanner lukija; public Tilasto() { this(OSOITE); } public Tilasto(String osoite) { try { URL url = new URL(osoite); lukija = new Scanner(url.openStream()); } catch (Exception ex) { } } public Tilasto(InputStream in) { try { lukija = new Scanner(in); } catch (Exception ex) { } } public boolean onkoRivejaJaljella() { return lukija.hasNextLine(); } public String annaSeuraavaRivi() { String rivi = lukija.nextLine(); return rivi.trim(); } }
Tilasto
-luokka lukee pelaajien tilastotiedot internetistä. Metodilla annaSeuraavaRivi()
saadaan selville yhden pelaajan tiedot. Tietoja on tarkoitus lukea niin kauan kuin pelaajia riittää, tämä voidaan tarkastaa metodilla onkoRivejaJaljella()
Kokeile että ohjelmasi onnistuu tulostamaan Tilasto
-luokan hakemat tiedot:
public static void main(String[] args) { Tilasto tilasto = new Tilasto(); while (tilasto.onkoRivejaJaljella()) { String pelaajaRivina = tilasto.annaSeuraavaRivi(); System.out.println(pelaajaRivina); } }
Tulostus on seuraavan muodoinen:
Evgeni Malkin;PIT;62;39;46;54
Steven Stamkos;TBL;70;50;34;64
Claude Giroux;PHI;66;26;56;27
Jason Spezza;OTT;72;29;46;30
// ... ja yli 800:n muun pelaajan tiedot
Huom: tulostuksen alussa ja lopussa ja jokaisen pelaajan välissä on html-tägejä, esim. <br/> joka aiheuttaa www-sivulle rivin vaihtumisen.
Tulostuksessa pelaajan tiedot on erotettu toisistaan puolipisteellä. Ensin nimi, sitten joukkue, ottelut, maalit, syötöt ja laukaukset.
Pelaajaa vastaava merkkijono on siis yksittäinen merkkijono. Saat pilkottua sen osiin split
-komennolla seuraavasti:
while (tilasto.onkoRivejaJaljella()) { String pelaajaRivina = tilasto.annaSeuraavaRivi(); String[] pelaajaOsina = pelaajaRivina.split(";"); for (int j = 0; j < pelaajaOsina.length; j++) { System.out.print(pelaajaOsina[j] + " "); } System.out.println(""); }
Kokeile että tämä toimii. Saat tästä tehtävästä pisteet seuraavan tehtävän yhteydessä.
Tee kaikista Tilasto
-luokan hakemien pelaajien tiedoista Pelaaja-olioita ja lisää ne ArrayListiin. Lisää tehtävään luokka PelaajatTilastosta
. Käytä allaolevaa koodia luokan runkona.
import java.util.ArrayList; public class PelaajatTilastosta { public ArrayList<Pelaaja> haePelaajat(Tilasto tilasto) { ArrayList<Pelaaja> pelaajat = new ArrayList<Pelaaja>(); while (tilasto.onkoRivejaJaljella()) { String pelaajaRivina = tilasto.annaSeuraavaRivi(); String[] pelaajaOsina = pelaajaRivina.split(";"); // Lisätään uusi pelaaja vain, jos syötteessä on kenttiä riittävästi if (pelaajaOsina.length > 4) { int ottelut = Integer.parseInt(pelaajaOsina[2].trim()); // Täydennä koodia lukemalla kaikki pelaajaOsina-taulukon kentät uuteen Pelaaja-olioon // ... // pelaajat.add(new Pelaaja( ... )); } } return pelaajat; } }
Tehtävänäsi on täydentää runkoa siten, että jokaisesta luetusta rivistä luodaan pelaaja, joka lisätään pelaajat-listaan. Huom! Tilasto
-luokka palauttaa merkkijonoja, joten joudut muuntamaan merkkijonoja myös numeroiksi. Esimerkiksi numeromuotoinen ottelut
on muutettava int
:iksi Integer.parseInt
-metodilla.
Jos merkkijonon metodi split
ei ole tuttu, se jakaa merkkijonon useampaan osaan annetun merkin kohdalta. Esimerkiksi komento merkkijono.split(";");
palauttaa merkkijonosta taulukon, jossa alkuperäisen merkkijonon puolipisteellä erotetut osat ovat kukin omassa taulukon indeksissä.
Voit käyttää testauksen apuna seuraavaa pääohjelmaa:
Tilasto tilasto = new Tilasto(); PelaajatTilastosta pelaajienHakija = new PelaajatTilastosta(); ArrayList<Pelaaja> pelaajat = pelaajienHakija.haePelaajat(tilasto); for (Pelaaja pelaaja : pelaajat) { System.out.println( pelaaja ); }
Haluamme tulostaa myös maalintekijäpörssin eli pelaajien tiedot maalimäärän mukaan järjestettynä sekä syöttöpörssin. NHL:n kotisivu tarjoaa tämänkaltaisen toiminnallisuuden, eli selaimessa näytettävä lista on mahdollista saada järjestettyä halutun kriteerin mukaan.
Edellinen tehtävä määritteli pelaajien suuruusjärjestyksen perustuvan kokonaispistemäärään. Luokalla voi olla vain yksi compareTo
-metodi, joten joudumme muunlaisia järjestyksiä saadaksemme turvautumaan muihin keinoihin.
Vaihtoehtoiset järjestämistavat toteutetaan erillisten luokkien avulla. Pelaajien vaihtoehtoisten järjestyksen määräävän luokkien tulee toteuttaa Comparator<Pelaaja>
-rajapinta. Järjestyksen määräävän luokan olio vertailee kahta parametrina saamaansa pelaajaa. Metodeja on ainoastaan yksi compare(Pelaaja p1, Pelaaja p2)
, jonka tulee palauttaa negatiivinen arvo, jos pelaaja p1
on järjestyksessä ennen pelaajaa p2
, positiivinen arvo jos p2
on järjestyksessä ennen
p1
:stä ja 0 muuten.
Periaatteena on luoda jokaista järjestämistapaa varten oma vertailuluokka, esim. maalipörssin järjestyksen määrittelevä luokka:
import java.util.Comparator; public class Maali implements Comparator<Pelaaja> { public int compare(Pelaaja p1, Pelaaja p2) { // maalien perusteella tapahtuvan vertailun koodi tänne } }
Tee Comparator
-rajapinnan toteuttavat luokat Maali
ja Syotto
, ja niille vastaavat maali- ja syöttöpörssien generoimiseen sopivat sopivat vertailufunktiot.
Järjestäminen tapahtuu edelleen luokan Collections
metodin sort
avulla. Metodi saa nyt toiseksi parametrikseen järjestyksen määräävän luokan olion:
Maali maalintekijat = new Maali(); Collections.sort(pelaajat, maalintekijat); System.out.println("NHL parhaat maalintekijät\n"); // tulostetaan maalipörssi
Järjestyksen määrittelevä olio voidaan myös luoda suoraan sort-kutsun yhteydessä:
Collections.sort(pelaajat, new Maali()); System.out.println("NHL parhaat maalintekijät\n"); // tulostetaan maalipörssi
Kun sort-metodi saa järjestyksen määrittelevän olion parametrina, se käyttää olion compareTo()
-metodia pelaajia järjestäessään.
Tarkempia ohjeita vertailuluokkien tekemiseen täällä
Harjoitellaan taas ohjelman rakenteen omatoimista suunnittelua. Käyttöliittymän ulkomuoto ja vaadittu toiminnallisuus on määritelty ennalta -- rakenteen saat toteuttaa vapaasti.
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
Huom! Testien kannalta on oleellista että käyttöliittymä toimii kuten yllä kuvattu. Tehtävä on neljän yksittäisen tehtäväpisteen arvoinen.
Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan, 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ä puuttuu 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 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 nimentä helpottaa toiminnallisuuksien muistamista. Järkevän nimennän lisäksi lähdekooditiedostot kannattaa jakaa toiminnallisuutta, käyttötarkoitusta ja/tai tahoa 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 fi.mooc.observer
. 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 fi.mooc.observer.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 fi.mooc.observer.logiikka
, ja lisää sinne luokka Sovelluslogiikka
. Sovelluslogiikan APIn tulee olla seuraavanlainen.
public Sovelluslogiikka(Kayttoliittyma 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 fi.mooc.observer.logiikka.Sovelluslogiikka; import fi.mooc.observer.ui.Kayttoliittyma; import fi.mooc.observer.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ähdekoodihakemistoon 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! } }
Rajapintaluokan alussa on sanat public interface RajapinnanNimi
, missä RajapinnanNimi on rajapintaluokan nimi. Rajapintaluokka sisältää 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(); } }
Rajapintojen vahvuus on se, että rajapinta on myös muuttujatyyppi. Kaikki rajapinnan toteuttavista luokista tehdyt oliot ovat myös rajapinnan tyyppisiä. Tämä helpottaa sovellusten rakentamista huomattavasti.
Tehdään luokka Henkilorekisteri
, josta voimme hakea henkilöitä Tunnistettava
-rajapinnan avulla. Yksittäisten henkilöiden hakemisen lisäksi Henkilorekisteri
tarjoaa metodin kaikkien henkilöiden hakemiseen listana.
public class Henkilorekisteri { private HashMap<String, Henkilo> henkilot; public Henkilorekisteri(HashMap<String, Henkilo> henkilot) { this.henkilot = henkilot; } public void lisaaHenkilo(Henkilo henkilo) { this.henkilot.put(henkilo.getTunnus(), henkilo); } public void haeHenkiloTunnuksella(Tunnistettava tunnistettava) { this.henkilot.get(tunnistettava.getTunnus()); } public List<Henkilo> getHenkilot() { return new ArrayList<Henkilo>(henkilot.values()); } }
Entä jos haluaisimme järjestää henkilölistan aakkosjärjestykseen?
Yksi ratkaisu on Tilastot kuntoon -tehtävässä esitelty Comparator
-rajapinnan käyttö. Comparator-rajapinnan avulla tapahtuvan nimen vertailun voi toteuttaa esimerkiksi seuraavasti. Toteutetaan ensiksi Comparator
-rajapinnan toteuttava luokka NimiJarjestys
.
import java.util.Comparator; public class NimiJarjestys implements Comparator<Henkilo> { @Override public int compare(Henkilo h1, Henkilo h2) { return h1.getNimi().compareTo(h2.getNimi()); } }
Luokan NimiJarjestys
metodi compare
vertaa kahden parametrina annetun henkilön nimiä toisiinsa. Luokan avulla voimme toteuttaa henkilörekisterin metodia getHenkilot
palauttaman listan järjestämisen seuraavasti.
public List<Henkilo> getHenkilot() { ArrayList<Henkilo> lista = new ArrayList<Henkilo>(this.henkilot.values()); Collections.sort(lista, new NimiJarjestys()); return lista; }
Luomme ensin metodissa henkilot
-listan, jonka annamme Collections
-luokan sort
-metodille. Metodi sort
saa toisena parametrinaan NimiJarjestys
-luokan ilmentymän, joka kertoo miten henkilot
-listalla olevat oliot tulee järjestää.
Comparator-rajapinnan toteuttamaa luokkaa ei kuitenkaan tarvitse toteuttaa tässä tapauksessa. Yksi luokka voi toteuttaa useamman rajapinnan, eli voimme toteuttaa Henkilo
-luokalla lisäksi Comparable
-rajapinnan. 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.nimi.compareTo(toinen.getNimi()); } }
Nyt henkilöstörekisterin listaa-metodi on seuraavanlainen ja pääsemme eroon turhasta NimiJarjesta
-luokasta.
public List<Henkilo> getHenkilot() { ArrayList<Henkilo> henkilot = new ArrayList<Henkilo>(henkilot.values()); Collections.sort(henkilot); return henkilot; }
Pohditaan seuraavaksi kauppojen kassalaitteessa olevaan lukijalaitteeseen liittyvää tuotteen tunnistamistoimintoa. Oletetaan että tuotteet sisältävät niihin liittyvän viivakoodin, nimen ja hinnan. Tuotteita kuvastava Tuote
-olio toteuttaa aiemmin määritellyn rajapinnan Tunnistettava
.
package sovellus.domain; public class Tuote implements Tunnistettava { private String viivakoodi; private String nimi; private int hinta; public Tuote(String viivakoodi, String nimi, int hinta) { this.viivakoodi = viivakoodi; this.nimi = nimi; this.hinta = hinta; } public String getNimi() { return this.nimi; } public int getHinta() { return this.hinta; } @Override public String getTunnus() { return this.viivakoodi; } }
Toteutetaan seuraavaksi Lukijalaite
, joka osaa muuntaa tunnistettavat oliot tuotteiksi. Lukijalaitteen tulee lukea ja tunnistaa tuotteet rajapinnan Tunnistettava
-avulla. Toteutetaan luokka Lukijalaite
siten, että se sisältää hajautustaulun, josta tuotteet löytyvät tunnisteen perusteella. Lukijalaite on osa sovelluksen logiikkaa, joten lisätään se pakkaukseen sovellus.logiikka
.
package sovellus.logiikka; public class Lukijalaite { private HashMap<String, Tuote> tuotteet; public Lukijalaite(HashMap<String, Tuote> tuotteet) { this.tuotteet = tuotteet; } public Tuote tunnista(Tunnistettava tunnistettava) { return this.tuotteet.get(tunnistettava.getTunnus()); } }
Lukijalaite-olio palauttaa Tuote
-olion jos sen tunnus löytyy lukijalaitteen sisältämästä hajautustaulusta. Luodaan seuraavaksi kassa, joka käyttää lukijalaitetta tuotteiden lisäämiseen ostettujen tuotteiden listalle. Kassalla on metodi public void osta
, jolle annetaan luokan Tunnistettava
-ilmentymä parametrina. Ostaminen lisää lukijalaitteella tunnistetun tuotteen ostettujen listalle. Jos tuotetta ei tunnisteta, ei tehdä mitään. Metodi tulostaOstokset
tulostaa ostettujen tuotteiden nimet.
package sovellus.domain; public class Kassa { private Lukijalaite laite; private List<Tuote> tuotteet; public Kassa(Lukijalaite lukijalaite) { this.laite = lukijalaite; this.tuotteet = new ArrayList<Tuote>(); } public void osta(Tunnistettava tunnistettava) { Tuote tunnistettu = this.laite.tunnista(tunnistettava); if (tunnistettu == null) { return; } this.tuotteet.add(tunnistettu); } public void tulostaOstokset() { for(Tuote tuote: this.tuotteet) { System.out.println(tuote.getNimi()); } } }
Huomaamme tässä vaiheessa että tuotteiden lukeminen listalta, joka on tulostettu tuotteiden lisäysjärjestyksessä, on hyvin kuormittavaa asiakkaalle. Muokataan tulostusta siten, että tuotteet listataan aakkosjärjestyksessä. Lisätään Tuote
-luokalle rajapinta Comparable
, jonka avulla tuotteet voidaan järjestää aakkosjärjestyksessä.
package sovellus.domain; public class Tuote implements Tunnistettava, Comparable<Tuote> { private String viivakoodi; private String nimi; private int hinta; public Tuote(String viivakoodi, String nimi, int hinta) { this.viivakoodi = viivakoodi; this.nimi = nimi; this.hinta = hinta; } public String getNimi() { return this.nimi; } public int getHinta() { return this.hinta; } @Override public String getTunnus() { return this.viivakoodi; } @Override public int compareTo(Tuote tuote) { return this.nimi.compareTo(tuote.getNimi()); } }
Muokataan vielä kassaan liittyvää toiminnallisuutta siten, että tuotteet järjestetään tarvittaessa. Huomaa että tuotteita tarvitsee järjestää vain silloin kun tuotteita ostetaan. Ostaminen on ainut tilanne, jossa tuotteita sisältävän listan järjestys mahdollisesti muuttuu. Tällöin ostokset ovat aina järjestyksessä metodia tulostaOstokset
kutsuttaessa.
package sovellus.domain; public class Kassa { private Lukijalaite laite; private List<Tuote> tuotteet; public Kassa(Lukijalaite lukijalaite) { this.laite = lukijalaite; this.tuotteet = new ArrayList<Tuote>(); } public void osta(Tunnistettava tunnistettava) { Tuote tunnistettu = this.laite.tunnista(tunnistettava); if (tunnistettu == null) { return; } this.tuotteet.add(tunnistettu); Collections.sort(this.tuotteet); } public void tulostaOstokset() { for(Tuote tuote: this.tuotteet) { System.out.println(tuote.getNimi()); } } }
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.
Huom! Tässä ei tarvitse ottaa esineiden ulkomuotoa huomioon!
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). Muuta luokkaa Esine
siten, että se toteuttaa rajapinnan Tavara
.
Lisää luokalle Esine
myös metodit public String getNimi()
ja korvaa metodi public String toString()
versiolla, joka tulostaa "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
, jonka tulee toteuttaa rajapinta Tavara
. Metodilla getTilavuus
saa selville muuttolaatikon tämänhetkisen tilavuuden. Rajapinnan Tavara
lisäksi Muuttolaatikolla on seuraavanlainen API
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.Toteuta vielä luokalle Muuttolaatikko
rajapinta Tavara
. Metodilla getTilavuus
tulee saada selville muuttolaatikossa olevien tavaroiden tämänhetkisen yhteistilavuuden.
Seuraavaksi toteutetaan esineiden pakkaustoiminnallisuus.
Toteuta luokka Pakkaaja
pakkaukseen muuttaminen.logiikka
. Luokan Pakkaaja
konstruktorille annetaan parametrina int laatikoidenTilavuus
. Kokonaisluku laatikoidenTilavuus
määrittelee minkä kokoisia muuttolaatikoita pakkaaja käyttää.
Toteuta tämän jälkeen luokalle metodi public List<Muuttolaatikko> pakkaaEsineet(List<Esine> esineet)
, joka pakkaa esineet muuttolaatikoihin.
Tee metodista sellainen että kaikki parametrina annetussa listassa olevat esineet päätyvät palautetussa listassa oleviin muuttolaatikoihin. Sinun ei tarvitse varautua tilanteisiin joissa esineet ovat suurempia kuin muuttolaatikko.
Alla on kuvattu eräs hieman tehokkaampi pakkaustapa pseudokoodina, eli ohjelmointikielen tapaisena koodina. Pseudokoodia käytetään muun muassa ohjelmointikieliriippumattomaan algoritmien eli ohjelmien kuvaamiseen.
pakkaaEsineet( esineet ): jarjesta( esineet ) laatikot = [] // huom, kannattaa käyttää ArrayListiä while esineet is not empty: Muuttolaatikko pakattu = pakkaaLaatikko( esineet ) laatikot.add( pakattu ) return laatikot pakkaaLaatikko( esineet ): Muuttolaatikko laatikko = new Muuttolaatikko lisaaSuuria( esineet, laatikko ) lisaaPienia( esineet, laatikko ) return laatikko lisaaSuuria( esineet, laatikko ): while esineet is not empty: esine = suurin( esineet ) if lisaa ( laatikko, esine ) == false: return lisaaPienia(List esineet, Muuttolaatikko laatikko): while esineet is not empty: esine = pienin( esineet ) if lisaa ( laatikko, esine ) == false: return
Muokkaa luokkaa Pakkaaja
siten, että se toimii samoin tai paremmin kuin yllä kuvattu lähestymistapa.
Metodissa pakkaaEsineet
pakataan muuttolaatikoita niin pitkään kun esineet
-listalla on esineitä. Muuttolaatikkoa pakattaessa algoritmi lisää laukkuun ensiksi niin paljon suurimpia esineitä kuin laatikkoon mahtuu. Kun laatikkoon ei enää mahdu suurimpia esineitä, aletaan täyttämään sitä pienimmillä esineillä.
Toteutuksesta: Kun esineesi ovat järjestyksessä, suurin tavara löytyy indeksistä esinelistan koko - 1
, pienin tavara löytyy indeksistä 0
. Älä käytä tässä Collections.min
ja Collections.max
-metodeja, sillä ne eivät osaa arvata että ArrayList
-lista on jo järjestyksessä.
Poista esineitä esineet
-listalta sitä mukaa kun niitä on lisätty muuttolaatikoihin. Sinun ei tarvitse varautua tilanteisiin joissa esineet ovat suurempia kuin muuttolaatikko.
Huom! Saadaksesi pisteen tästä viimeisestä tehtävästä algoritmisi tulee toimia vähintään yhtä hyvin kuin yllä kuvattu algoritmi. Hyvyydellä tarkoitetaan sitä, että pakkaukseen kulunut aika tulee olla vähintään yhtä pieni kuin pseudokoodiratkaisun. Muuttolaatikkojen määrän tulee myös olla vähintään yhtä pieni.
Voit käyttää seuraavaa metodia satunnaisten esineiden luomiseen.
public static List<Esine> luoEsineet(int kpl, int maxTilavuus) { Random rand = new Random(); List<Esine> esineet = new ArrayList<Esine>(); for (int i = 0; i < kpl; i++) { esineet.add(new Esine("hammasharja", 1 + rand.nextInt(maxTilavuus))); } return esineet; }
Yllä kuvatulla algoritmilla pakkaamisen pitäisi toimia nopeasti jopa 100000 esinettä sisältävillä listoilla. Voit testata pakkaajasi nopeutta esimerkiksi seuraavasti:
List<Esine> esineet = luoEsineet(100000, 10); Pakkaaja pakkaaja = new Pakkaaja(50); long start = System.nanoTime(); List<Muuttolaatikko> laatikot = pakkaaja.pakkaaEsineet(esineet); long kulunutAika = ((System.nanoTime() - start) / 1000000); System.out.println("Pakkaukseen kului " + kulunutAika + " ms."); System.out.println("Tarvittiin " + laatikot.size() + " laatikkoa.");
Tehtäväsarjassa tehdään laajennettava tekstiseikkailupelin runko. Seikkailu koostuu kohdista, joissa jokaisessa ruudulle tulee tekstiä. Kohdat voivat olla joko välivaiheita, kysymyksiä, tai monivalintakohtia. Monivalinta-tyyppisen kohdan näyttämä teksti voi olla esimerkiksi seuraavanlainen:
Huoneessa on kaksi ovea. Kumman avaat? 1. Vasemmanpuoleisen. 2. Oikeanpuoleisen. 3. Juoksen pakoon.
Käyttäjä vastaa kohdassa esitettävään tekstiin. Yllä olevaan tekstiin voi vastata 1
, 2
tai 3
, ja vastauksesta riippuu, minne käyttäjä siirtyy seuraavaksi.
Peliin tullaan toteuttamaan kohtia kuvaava rajapinta ja tekstikäyttöliittymä, jonka kautta peliä pelataan.
Huom! Toteuta kaikki tehtävän vaiheet pakkaukseen "seikkailu"
Pelissä voi olla hyvinkin erilaisia kohtia, ja edellä olleessa esimerkissä ollut monivalinta on vain eräs vaihtoehto.
Toteuta kohdan käyttäytymistä kuvaava rajapinta Kohta
. Rajapinnalla Kohta
tulee olla metodi String teksti()
, joka palauttaa kohdassa tulostettavan tekstin. Metodin teksti
lisäksi kohdalla tulee olla metodi Kohta seuraavaKohta(String vastaus)
, jonka toteuttavat luokat palauttavat seuraavan kohdan vastauksen perusteella.
Toteuta tämän jälkeen yksinkertaisin tekstiseikkailun kohta, eli ei-interaktiivinen tekstiruutu, josta pääsee etenemään millä tahansa syötteellä. Toteuta ei-interaktiivista tekstiruutua varten luokka Valivaihe
, jolla on seuraavanlainen API.
Kohta
. public String teksti()
-metodi palauttaa konstruktorissa annetun tekstin sekä rivin "(jatka painamalla enteriä)"
. (Rivinvaihto saadaan aikaan merkillä "\n".)public void asetaSeuraava(Kohta seuraava)
-metodilla voidaan asettaa Kohta
-olio, jonka seuraavaKohta(String vastaus)
aina palauttaa (vastauksesta riippumatta). Testaa ohjelmaasi seuraavalla esimerkillä:
Scanner lukija = new Scanner(System.in); Valivaihe alkuteksti = new Valivaihe("Olipa kerran ohjelmoija."); Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla."); alkuteksti.asetaSeuraava(johdanto); Kohta nykyinen = alkuteksti; System.out.println(nykyinen.teksti()); nykyinen = nykyinen.seuraavaKohta(lukija.nextLine()); if (nykyinen == null) { System.out.println("Virhe ohjelmassa!"); } System.out.println(nykyinen.teksti()); nykyinen = nykyinen.seuraavaKohta(lukija.nextLine()); if (nykyinen != null) { System.out.println("Virhe ohjelmassa!"); }
Olipa kerran ohjelmoija. (jatka painamalla enteriä) Joka alkoi ohjelmoimaan Javalla. (jatka painamalla enteriä)
Pelin käyttöliittymä (luokka Kayttoliittyma
) saa konstruktorin parametrina Scanner
-olion ja Kohta
-rajapinnan toteuttavan pelin aloittavan olion. Luokka tarjoaa metodin public void kaynnista()
, joka käynnistää pelin suorituksen.
Käyttöliittymä käsittelee kaikkia kohtia Kohta
-rajapinnan kautta. Käyttöliittymän tulee jokaisessa kohdassa kysyä kohtaan liittyvältä metodilta teksti
tekstiä, joka käyttäjälle näytetään. Tämän jälkeen käyttöliittymä kysyy käyttäjältä vastauksen, ja antaa sen parametrina kohta-olion metodille seuraavaKohta
. Metodi seuraavaKohta
palauttaa vastauksen perusteella seuraavan kohdan, johon pelin on määrä siirtyä. Peli loppuu, kun metodi seuraavaKohta
palauttaa arvon null
.
Koska pääohjelma tulee käyttämään kohtia vain Kohta
-rajapinnan kautta, voidaan peliin lisätä vaikka minkälaisia kohtia pääohjelmaa muuttamatta. Riittää tehdä uusia Kohta
-rajapinnan toteuttavia luokkia.
Toteuta luokka Kayttoliittyma
, ja testaa sen toimintaa seuraavalla esimerkillä
Scanner lukija = new Scanner(System.in); Valivaihe alku = new Valivaihe("Olipa kerran ohjelmoija."); Valivaihe johdanto = new Valivaihe("Joka alkoi ohjelmoimaan Javalla."); Valivaihe loppu = new Valivaihe("Ja päätti muuttaa Helsinkiin."); alku.asetaSeuraava(johdanto); johdanto.asetaSeuraava(loppu); new Kayttoliittyma(lukija, alku).kaynnista();
Olipa kerran ohjelmoija. (jatka painamalla enteriä) > Joka alkoi ohjelmoimaan Javalla. (jatka painamalla enteriä) > Ja päätti muuttaa Helsinkiin. (jatka painamalla enteriä) >
Käytä seuraavaa metodia käyttöliittymän kaynnista
-metodina. Yritä piirtää paperille mitä käy kun käyttöliittymä käynnistetään.
public void kaynnista() { Kohta nykyinen = alkukohta; while (nykyinen != null) { System.out.println(nykyinen.teksti()); System.out.print("> "); String vastaus = lukija.nextLine(); nykyinen = nykyinen.seuraavaKohta(vastaus); System.out.println(""); } }
Käyttöliittymän kaynnista
-metodi sisältää siis toistolauseen, jossa ensin tulostetaan käsiteltävän kohdan teksti. Tämän jälkeen kysytään käyttäjältä syötettä. Käyttäjän syöte annetaan vastauksena käsiteltävän kohdan seuraavaKohta
-metodille. Metodi seuraavaKohta
palauttaa kohdan, jota käsitellään seuraavalla toiston kierroksella. Jos palautettu kohta oli null
, lopetetaan toisto.
Tekstiseikkailussa voi olla kysymyksiä, joihin on annettava oikea vastaus ennen kuin pelaaja pääsee eteenpäin. Tee luokka Kysymys
seuraavasti:
Kohta
-rajapinnan. asetaSeuraava
-metodilla. seuraavaKohta
-metodia kutsutaan oikealla vastauksella, metodi palauttaa seuraavan kohdan, muuten metodi ei päästä etenemään ja palauttaa arvon this
, eli viitteen tähän olioon. Luokkaa voi testata seuraavalla pääohjelmalla:
Scanner lukija = new Scanner(System.in); Kysymys alku = new Kysymys("Minä vuonna Javan ensimmäinen versio julkaistiin?", "1995"); Valivaihe hyva = new Valivaihe("Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling."); alku.asetaSeuraava(hyva); new Kayttoliittyma(lukija, alku).kaynnista();
Minä vuonna Javan ensimmäinen versio julkaistiin? > 2000 Minä vuonna Javan ensimmäinen versio julkaistiin? > 1995 Hyvä! Lisätietoa: Javan alkuperäinen ideoija on James Gosling. (jatka painamalla enteriä) >
Tällä hetkellä tekstiseikkailu tukee välivaiheita ja yksinkertaisia kysymyksiä. Tekstiseikkailu on siis lineaarinen, eli lopputulokseen ei voi käytännössä vaikuttaa. Lisätään seikkailuun monivalintakysymyksiä, joiden avulla pelin kehittäjä voi luoda vaihtoehtoista toimintaa.
Esimerkki vaihtoehtoisesta toiminnasta:
Kello on 13:37 ja päätät mennä syömään. Minne menet? 1. Exactumiin 2. Chemicumiin > 1 Ruoka on loppu :( (jatka painamalla enteriä) >
Kello on 13:37 ja päätät mennä syömään. Minne menet? 1. Exactumiin 2. Chemicumiin > 2 Mainio valinta! (jatka painamalla enteriä) >
Toteuta luokka Monivalinta
, jonka API on seuraavanlainen
Kohta
. public Monivalinta(String teksti)
public void lisaaVaihtoehto(String valinta, Kohta seuraava)
public String teksti()
public Kohta seuraavaKohta(String valinta)
Integer
luokkametodilla parseInt
. Testaa ohjelmasi toimintaa seuraavalla pääohjelmalla:
Scanner lukija = new Scanner(System.in); Monivalinta lounas = new Monivalinta("Kello on 13:37 ja päätät mennä syömään. Minne menet?"); Monivalinta chemicum = new Monivalinta("Lounasvaihtoehtosi ovat seuraavat:"); Valivaihe exactum = new Valivaihe("Exactumista on kaikki loppu, joten menet Chemicumiin."); exactum.asetaSeuraava(chemicum); lounas.lisaaVaihtoehto("Exactumiin", exactum); lounas.lisaaVaihtoehto("Chemicumiin", chemicum); Valivaihe nom = new Valivaihe("Olipas hyvää"); chemicum.lisaaVaihtoehto("Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta", nom); chemicum.lisaaVaihtoehto("Jauhelihakebakot, paprikakastiketta", nom); chemicum.lisaaVaihtoehto("Mausteista kalapataa", nom); new Kayttoliittyma(lukija, lounas).kaynnista();
Kello on 13:37 ja päätät mennä syömään. Minne menet? 1. Exactumiin 2. Chemicumiin > 1 Exactumista on kaikki loppu, joten menet Chemicumiin. (jatka painamalla enteriä) > Lounasvaihtoehtosi ovat seuraavat: 1. Punajuurikroketteja, ruohosipuli-soijajogurttikastiketta 2. Jauhelihakebakot, paprikakastiketta 3. Mausteista kalapataa > 2 Olipas hyvää (jatka painamalla enteriä) >
Luokan Monivalinta
sisäinen toteutus saattaa olla haastava. Kannattaa esimerkiksi käyttää listaa vastausvaihtoehtojen (merkkijonojen) tallentamiseen, ja hajautustaulua kohtien tallentamiseen valintavaihtoehdon indeksillä.
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. Rajapintoihin liittyneessä Julkaisupalvelu-esimerkissä on Javan metodi Thread.sleep
, jonka mahdollisesti heittämä poikkeus on pakko käsitellä. Sen käsittely tapahtuu esimerkiksi 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 }
Jos poikkeusta ei käsitellä, tulee metodin antaa 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 metodia nuku
-kutsuvan metodin tulee joko käsitellä poikkeus try-catch
-lohkossa, tai siirtää poikkeuksen käsittelyn vastuuta eteenpäin heittää poikkeus eteenpäin. Palaamme poikkeuksiin, jotka on pakko käsitellä tarkemmin tiedostojen käsittelyn yhteydessä.
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ä 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
.
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 allaolevasta 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 printStactTrace()
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.
// 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 StringBuilder
-olion käyttäminen lukemisessa siten, että jokaisen rivin jälkeen lisätään rivinvaihtomerkki.
public String lueTiedostoMerkkijonoon(File tiedosto) throws Exception { // tiedosto mistä luetaan Scanner lukija = new Scanner(tiedosto); StringBuilder stringBuilder = new StringBuilder(); while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); stringBuilder.append(rivi); stringBuilder.append("\n"); } lukija.close(); return stringBuilder.toString(); }
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öä.
Tässä tehtävässä tehdään tiedoston analysointityökalu, joka tarjoaa rivien ja merkkien laskemistoiminnallisuuden.
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.
Toteuta luokkaan Analyysi
metodi public int merkkeja()
, joka palauttaa luokan konstruktorille annetun tiedoston merkkien määrän.
Voit itse päättää miten reagoidaan jos konstruktorin parametrina saatua tiedostoa ei ole olemassa.
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"
.
Tiedosto löytyy projektin päähakemistosta. Kun tiedosto on NetBeans-projektisi päähakemistossa, voit käyttää sen polkuna lyhyttä muotoa "sanalista.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, 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...
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.
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äsittely vastuuta 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(); } }
Joskus tiedoston perään metodilla append
kirjoittamisen sijasta on helpompi kirjoittaa koko tiedosto uudelleen.
Tässä tehtävässä laajennetaan aiemmin toteutettua sanakirjaa siten, että sanakirjaan voi lukea sanat tiedostosta sekä tallettaa sanat tiedostoon. Tehtävänäsi on luoda luokka OmaMuistavaSanakirja
, joka toteuttaa tehtäväpohjassa annetun rajapinnan MuistavaSanakirja
. Toteuta luokka pakkaukseen sanakirja
, jossa rajapintakin sijaitsee.
Tehtäväpohjan rajapinnan koodi:
package sanakirja; public interface MuistavaSanakirja { void lisaa(String sana, String kaannos); String kaanna(String sana); void poista(String sana); void lataa() throws IOException; void talleta() throws IOException; }
Rajapinnan metodien kuvaukset:
void lisaa(String sana, String kaannos)
String kaanna(String sana)
void poista(String sana)
void lataa() throws IOException
IOException
) ei tule käsitellävoid talleta() throws IOException
IOException
) ei tule käsitelläVoit kopioida metodien lisaa
ja kaanna
toteutukset suoraan aiemmasta sanakirjatehtävästä. Toteuta näiden lisäksi metodi poista
.
Toteuta luokalle OmaMuistavaSanakirja
konstruktori, joka ottaa ainoaksi parametrikseen tiedoston nimen merkkijonona.
Toteuta sanakirjalle myös metodi lataa
, joka lukee sanakirjan sisällön tiedostosta (jonka nimi on annettu konstruktorissa).
Tiedosto koostuu tekstiriveistä jossa on sana-käännös-pareja:
olut beer apina monkey ohjelmoija programmer opiskelija student
Huom: jotta ohjelma löytää tiedoston, pitää se sijoittaa projektin juureen. Tämä onnistuu valitsemalla NetBeansista file -> new file -> other. Sanakirjaprojektin tulee olla pääprojektina kun luot tiedoston. Voit editoida tiedostoa joko NetBeansilla tai tekstieditorilla.
Huom2: voit lukea syötteen kahdella tavalla, joko Scannerin metodilla next
yksittäinen sana kerrallaa tai nextLine
:llä rivi kerrallaan. Jos luet rivi kerrallaan, pitää rivi hajoittaa kahdeksi merkkijonoksi (esim. "olut beer" -> "olut", "beer") jotta lisäys sanakirjaan onnistuu. Tämä voidaan tehdä esim. split
-komennolla:
String rivi = lukija.nextLine(); String[] osat = rivi.split(" "); // nyt osat[0] on rivin ensimmäinen sana ja osat[1] toinen
Sanakirjaa käytetään siis esimerkiksi näin:
public static void main(String[] args) { MuistavaSanakirja sanakirja = new OmaMuistavaSanakirja("suomi-englanti.txt"); sanakirja.lataa(); // voit testata tässä käännöksiä }
Tee sanakirjalle metodi talleta
, jota kutsuttaessa sanakirjan sisältö kirjoitetaan tiedostoon.
Talletus kannattanee hoitaa siten, että koko käännöslista kirjoitetaan uudelleen vanhan tiedoston päälle, eli materiaalissa esiteltyä append
-komentoa ei kannata käyttää.
Varmista että ohjelmasi toimii, eli että uuden käynnistyksen jälkeen edellisessä suorituksessa talletetut sanat löytyvät sanakirjasta.
Talletusominaisuutta voi käyttää vaikkapa näin:
public static void main(String[] args) { MuistavaSanakirja sanakirja = new OmaMuistavaSanakirja("suomi-englanti.txt"); sanakirja.lataa(); // lisää uusia sanoja, poista vanhoja sanakirja.talleta(); }
Muokaa vielä ohjelmasi sellaiseksi, että metodi lataa
toimii (eli ei tee mitään) jos sanakirjatiedostoa ei ole vielä olemassa. Voit poistaa sanakirjan esim. valitsemalla window -> files -> delete.
Rajapinta Set
kuvaa joukon toiminnallisuutta. Joukossa on kutakin alkioita korkeintaan yksi kappale, eli yhtäkään samanlaista oliota ei ole kahdesti. Olioiden samankaltaisuuden tarkistaminen toteutetaan equals
ja hashCode
-metodeja käyttämällä. Ehdimme jo aiemmin pikaisesti tutustua HashSet
-luokkaan, joka on eräs Javan Set
-rajapinnan toteutus. 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) { if (!this.tehdytTehtavat.containsKey(kayttaja)) { this.tehdytTehtavat.put(kayttaja, new HashSet<Integer>()); } Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja); tehdyt.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 esimerkiksi 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 vielä toinen testi, jossa tarkistetaan päteekö sama myös HashSet
-joukolle. Luodaan uusi HashSet
-joukko, ja lisätään siihen merkkijonoja.
Set<String> nimet = new HashSet<String>(); nimet.add("Matti"); nimet.add("Mikael"); nimet.add("Pekka"); nimet.add("Arto"); System.out.println(nimet);
[Arto, Matti, Pekka, Mikael]
Alkiot eivät ole järjestyksessä. Järjestyksessä olevat tulostukset ovat yleensä ottaen paljon mielekkäämpiä kuin ei-järjestyksessä olevat, ja olemmekin käyttäneet kurssilla jo aikaa Comparable
- ja Comparator
-rajapintojen kanssa.
Rajapinnan Set
toteuttava luokka TreeSet
pitää joukossa olevia alkioita järjestyksessä. Jos TreeSet
-luokan konstruktorille ei anneta parametreja, järjestetään siihen lisättävän alkiot niiden luonnollisen järjestyksen mukaan, eli Comparable
-rajapinnan määräämän järjestyksen mukaan. Muutetaan edellä ollutta esimerkkiä siten että käytämme TreeSet
-oliota nimien tallentamiseen.
Set<String> nimet = new TreeSet<String>(); nimet.add("Matti"); nimet.add("Mikael"); nimet.add("Pekka"); nimet.add("Arto"); System.out.println(nimet);
[Arto, Matti, Mikael, Pekka]
Luokan TreeSet
parametritonta konstruktoria käytettäessä tallennettavien olioiden tulee toteuttaa rajapinta Comparable
. Jos oliot eivät toteuta rajapintaa Comparable
ja haluaisimme silti ne järjestykseen, voimme luoda luokasta TreeSet
ilmentymän joka ottaa Comparator
-rajapinnan toteuttavan olion konstruktorin parametrina. Tällöin kaikki alkiot järjestetään Comparator
-rajapinnan toteuttavan olion määräämään järjestykseen.
Luodaan oma Comparator
-rajapinnan toteutus, joka järjestää kääntää merkkijonot ja järjestää merkkijonot niiden käänteisessä järjestyksessä. Luokka KaanteinenJarjestys
käytännössä järjestää merkkijonot siis vertailemalla ensin merkkijonojen viimeistä kirjainta, sitten toiseksi viimeistä jne -- mutta on niin fiksu että osaa hyödyntää String
-luokan valmiina tarjoamaa compareTo
-metodia.
public class KaanteinenJarjestys implements Comparator<String> { @Override public int compare(String t, String t1) { t = kaanna(t); t1 = kaanna(t1); return t.compareTo(t1); } private String kaanna(String merkkijono) { if (merkkijono == null) { return ""; } StringBuilder stringBuilder = new StringBuilder(merkkijono); stringBuilder.reverse(); return stringBuilder.toString(); } }
Set<String> nimet = new TreeSet<String>(new KaanteinenJarjestys()); nimet.add("Matti"); nimet.add("Mikael"); nimet.add("Pekka"); nimet.add("Arto"); System.out.println(nimet);
[Pekka, Matti, Mikael, Arto]
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:
void lisaa(String merkkijono)
int getHavaittujenDuplikaattienMaara()
Set<String> getUniikitMerkkijonot()
Set<String>
-rajapinnan toteuttavan olion, jossa on kaikki uniikit lisätyt merkkijonot (ei siis duplikaatteja!)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: []
Joukkojen järjestyksessä pitäminen onnistuu Set
rajapinnan toteuttavan TreeSet
-olion avulla. Aiemmassa Tehtavakirjanpito
-esimerkissä henkilökohtaiset tehtäväpisteet tallennettiin Map
-rajapinnan toteuttavaan HashMap
-olioon. Kuten HashSet
, HashMap
ei pidä alkioita järjestyksessä. Rajapinnasta Map
on olemassa toteutus TreeMap
, jossa hajautustaulun avaimia pidetään järjestyksessä. Muutetaan Tehtavakirjanpito
-luokkaa siten, että henkilökohtaiset pisteet tallennetaan TreeMap
-tyyppiseen hajautustauluun.
public class Tehtavakirjanpito { private Map<String, Set<Integer>> tehdytTehtavat; public Tehtavakirjanpito() { this.tehdytTehtavat = new TreeMap<String, Set<Integer>>(); } public void lisaa(String kayttaja, int tehtava) { if (!this.tehdytTehtavat.containsKey(kayttaja)) { this.tehdytTehtavat.put(kayttaja, new TreeSet<Integer>()); } Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja); tehdyt.add(tehtava); } public void tulosta() { for (String kayttaja: this.tehdytTehtavat.keySet()) { System.out.println(kayttaja + ": " + this.tehdytTehtavat.get(kayttaja)); } } }
Muunsimme samalla Set
-rajapinnan toteutukseksi TreeSet
-luokan. Huomaa että koska olimme käyttäneet rajapintoja, muutoksia tuli hyvin pieneen osaan koodista. Etsi kohdat jotka muuttuivat!
Käyttäjäkohtaiset tehtävät voidaan nyt tulostaa järjestyksessä.
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] Mikael: [3, 4] Pekka: [4]
Luokka TreeMap
vaatii että avaimena käytetyn luokan tulee toteuttaa Comparable
-rajapinta. Jos luokka ei toteuta rajapintaa Comparable
, voidaan luokalle TreeMap
antaa konstruktorin parametrina Comparator
-luokan toteuttama olio aivan kuten TreeSet
-luokalle.
Jatketaan vielä sanakirjan laajentamista. Tehtävänäsi on toteuttaa pakkaukseen sanakirja
luokka OmaUseanKaannoksenSanakirja
, joka voi tallettaa useamman käännöksen samalle sanalle. Luokan tulee toteuttaa tehtäväpohjassa annettu rajapinta UseanKaannoksenSanakirja
, jossa on seuraavat toiminnot:
void lisaa(String sana, String kaannos)
Set<String> kaanna(String sana)
Set
-olion, jossa on kaikki käännökset sanalle, tai null
-viitteen, jos sanaa ei ole sanakirjassavoid poista(String sana)
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); }
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"));
[six, spruce] null
Tehtävänäsi on toteuttaa sähköpostiohjelmaan komponentti, joka säilöö viestejä. Tehtäväpohjan mukana tulee luokka Sahkoposti
, joka esittää sähköpostiviestiä. Luokalla Sahkoposti
on oliomuuttujat:
Toteutetaan tässä luokka Viestivarasto
, joka tarjoaa sähköpostien hallintaan liittyviä toimintoja.
Luo pakkaukseen posti
luokka Viestivarasto
, ja lisää sille seuraavat metodit:
public void lisaa(Sahkoposti s)
lisää viestinpublic Sahkoposti hae(String otsikko)
palauttaa viestin jolla on annettu otsikko tai null jos sellaista ei ole. Voit olettaa että millään kahdella viestillä ei ole samaa otsikkoa.
Lisää luokkaan Viestivarasto
seuraavat metodit
public Sahkoposti hae(int aika)
palauttaa viestin joka saapui annettuun aikaan tai null jos sellaista ei ole. Voit olettaa että millään kahdella viestillä ei ole samaa saapumisaikaa.public Sahkoposti haeUusinViesti()
hakee uusimman viestin (eli sen jonka saapumisaika on isoin) ai null jos sellaista ei ole.public Sahkoposti haeUusinViesti(int ylaraja)
hakee uusimman viestin joka on saapunut ennen annettua aikaa tai null jos sellaista ei ole.Huom! Kannattaa käyttää kahta erillistä rakennetta viestien tallentamiseen. Otsikon perusteella tallentamiseen voit käyttää HashMap
pia, ja viestien tallentamiseen ajan mukaan TreeMap
pia. Näin saat toteutettua hae-operaatiot tehokkaasti. Tutustu myös TreeMap
in metodeihin lastKey()
ja floorKey()
.
Tehdään sovellus jonka avulla on mahdollista hallinnoida ihmisten puhelinnumeroita ja osoitteita.
Tehtävän voi suorittaa 1-4 pisteen laajuisena. Yhden pisteen laajuuteen on toteutettava seuraavat toiminnot:
kahteen pisteeseen vaadittaan edellisten lisäksi
kolmeen pisteeseen vaadittaan 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:
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ä, esimerkiksi Object
, 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]
Kaikki oliot ovat tyyppiä Object
, joten minkä tahansa tyyppisen olion voi antaa parametrina Object
-tyyppisiä parametreja vastaanottavalle metodille.
Tehtävän mukana tulee rajapinta Vertaaja
. Toteuta pakkaukseen samuus
luokka OlioidenVertaaja
, joka toteuttaa rajapinnan Vertaaja
. Metodien tulee toimia seuraavasti:
equals
-metodia. Tarkemmin equals
-metodin toiminnasta Javan Object luokan APIsta. Huomaa, että equals
-metodin toiminta riippuu siitä, onko verrattavan olion luokka korvannut Object
-luokassa määritellyn equals
-metodin. toString
-palauttamat merkkijonot) ovat samat. Muutoin metodi palauttaa false.Tehtävän mukana tulee luokka Henkilo
, jossa equals
- ja compareTo
-metodit on korvattu. Kokeile toteuttamiesi metodien toimintaa seuraavalla esimerkkikoodilla.
OlioidenVertaaja vertaaja = new OlioidenVertaaja(); Henkilo henkilo1 = new Henkilo("221078-123X", "Pekka", "Helsinki"); Henkilo henkilo2 = new Henkilo("221078-123X", "Pekka", "Helsinki"); // täysin samansisältöinen kuin eka Henkilo henkilo3 = new Henkilo("110934-123X", "Pekka", "Helsinki"); // eri pekka vaikka asuukin helsingissä System.out.println(vertaaja.samaOlio(henkilo1, henkilo1)); System.out.println(vertaaja.samaOlio(henkilo1, henkilo2)); System.out.println(vertaaja.vastaavat(henkilo1, henkilo2)); System.out.println(vertaaja.vastaavat(henkilo1, henkilo3)); System.out.println(vertaaja.samaMerkkijonoEsitys(henkilo1, henkilo2)); Henkilo henkilo4 = new Henkilo("221078-123X", "Pekka", "Savonlinna"); // henkilo1:n pekka mutta asuinpaikka muuttuu System.out.println(vertaaja.samaOlio(henkilo1, henkilo4)); System.out.println(vertaaja.vastaavat(henkilo1, henkilo4)); System.out.println(vertaaja.samaMerkkijonoEsitys(henkilo1, henkilo4));
Ylläolevan koodin tulostuksen pitäisi olla seuraava:
true false true false true false true false
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 StringBuilder
. Metodi tulostaMerkit
tulostaa annetun olion jokaisen merkin omalle rivilleen.
Tulostin tulostin = new Tulostin(); StringBuilder sb = new StringBuilder(); sb.append("toimii"); tulostin.tulostaMerkit(sb);
t o i m i i
Tehtävän mukana tulee siirtymistoiminnallisuutta kuvaava rajapinta Siirrettava
. Tehtävässä toteutat luokat Olio
ja Lauma
, jotka molemmat ovat siirrettäviä. Toteuta kaikki toiminnallisuus pakkaukseen siirrettava
.
Luo pakkaukseen siirrettava
luokka Olio
, joka toteuttaa rajapinnan Siirrettava
. Olion tulee tietää oma sijaintinsa (ilmaistaan x, y -koordinaatteina. Luokan Olio
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 Olio
toimintaa seuraavalla esimerkkikoodilla.
Olio olio = new Olio(20, 30); System.out.println(olio); olio.siirra(-10, 5); System.out.println(olio); olio.siirra(50, 20); System.out.println(olio);
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.liitaLaumaan(new Olio(73, 56)); lauma.liitaLaumaan(new Olio(57, 66)); lauma.liitaLaumaan(new Olio(46, 52)); lauma.liitaLaumaan(new Olio(19, 107)); System.out.println(lauma);
x: 73; y: 56 x: 57; y: 66 x: 46; y: 52 x: 19; y: 107
Olion käytössä olevat metodit määrittyvät muuttujan tyypin kautta. Esimerkiksi String
-tyyppinen olio, joka on esitelty Object
-tyyppisenä saa käyttöönsä vain Object
-luokassa määritellyt metodit. Jos oliolla on monta eri tyyppiä, on sillä käytössä jokaisen tyypin määrittelemät metodit. Esimerkiksi String
-tyyppisellä oliolla on käytössä String
-luokassa määritellyt metodit, Object
-luokassa määritellyt metodit, ja toteutettujen rajapintojen määrittelemät metodit.
Edellisessä tehtävässä toteutettiin metodi samaMerkkijonoEsitys
kahden olion tulostuksen vertailuun. Metodille annetaan parametrina kaksi Object
-tyyppistä muuttujaa, joiden toString
-metodin tuottamia tulostuksia vertaillaan. Koska parametrit ovat tyyppiä Object
, voi metodille antaa parametrina minkä tahansa olion. Laajennetaan Tulostin
-luokkaa esimerkkiä varten.
public class Tulostin { private String merkki; public Tulostin(String merkki) { this.merkki = merkki; } 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); } } @Override public String toString() { return this.merkki; } }
Luokan Tulostaja
toString
-metodi palauttaa siis aina konstruktorissa annetun parametrin. Edellisessä tehtävässä toteutetun samaMerkkijonoEsitys
-metodin toteutus on kutakuinkin seuraava.
public boolean samaMerkkijonoEsitys(Object eka, Object toka) { return eka.toString().equals(toka.toString()); }
OlioidenVertaaja vertaaja = new OlioidenVertaaja(); String merkkijono = "tulostuksen vertailu"; CharSequence merkkijonoSeq = "tulostuksen vertailu"; if (vertaaja.samaMerkkijonoEsitys(merkkijono, merkkijonoSeq)) { System.out.println("Vertailu 1. Sama merkkijonoesitys"); } else { System.out.println("Vertailu 1. Eri merkkijonoesitys"); } Tulostin tulostin = new Tulostin("Cannon"); if (vertaaja.samaMerkkijonoEsitys(merkkijono, tulostin)) { System.out.println("Vertailu 2. Sama merkkijonoesitys"); } else { System.out.println("Vertailu 2. Eri merkkijonoesitys"); }
Vertailu 1. Sama merkkijonoesitys Vertailu 2. Eri merkkijonoesitys
Ensimmäisessä vertailussa metodille samaMerkkijonoEsitys
annetaan parametrina String
- ja CharSequence
-tyyppiset muuttujat. Toisessa esimerkissä parametrina annetaan String
- ja Tulostin
-tyyppiset muuttujat. Metodin samaMerkkijonoEsitys
parametrit ovat Object
-tyyppisiä, ja metodissa voi käyttää vain Object
-luokassa määriteltyjä metodeja.
Kysymykseksi nousee Eikö metodin samaMerkkijonoEsitys
pitäisi käyttää Object
-luokassa määriteltyä toString
-metodia, jolloin metodin palauttaman merkkijonon pitäisi olla muotoa pakkaus.Luokannimi@...
?
Vastaus on ei. Suoritettava metodi valitaan olion tyypin perusteella, ei muuttujan tyypin perusteella. Vaikka metodissa samaMerkkijonoEsitys
käytetään Object
-tyyppisiä muuttujia, voidaan metodille antaa parametrina minkä tahansa tyyppisiä olioita. Jos olion luokka (tai joku sen perimä luokka) korvaa metodin toString
, valitaan käyttöön aina korvaava metodi Object
-luokassa määritellyn sijaan.
Hieman yleisemmin: Suoritettava metodi valitaan aina olion 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.
Ohjelmia rakennettaessa käytetään usein ohjelmarakennetta, jossa tiedon tallennusmekanismi kapseloidaan sovelluslogiikalta näkymättömäksi. Tallennustoiminnallisuuden piilottaminen sovelluslogiikalta perustellaan Single Responsibility Principle -periaatteella. Jokaisella luokalla tulee olla vain yksi syy muuttua: jos sovelluslogiikkaa tarjoava luokka hoitaisi sovelluslogiikan lisäksi tiedon tallentamiseen liittyvää toiminnallisuutta, olisi sillä jo ainakin kaksi vastuuta ja syytä muuttua: sovelluslogiikassa tapahtuva muutos ja tallennuslogiikassa tapahtuva muutos. Tämä rikkoo Single Responsibility Principle -periatteen. Haluamme että tallennukseen liittyvä toiminnallisuus toteutetaan sovelluslogiikasta erillisenä.
Yleisin tapa tallennuslogiikan eriyttämiseen on ns. DAO-suunnittelumalli. DAO-suunnittelumallissa tallennuslogiikan käyttäjälle tarjotaan rajapinta, jossa on määritelty toiminnallisuudet luomiseen, hakemiseen, päivittämiseen ja poistamiseen. Kunkin DAO-rajapinnan toteuttan vastuulla on yleensä aina tietyntyyppisten olioiden tallentaminen. Käytetään seuraavassa esimerkissä luokkaa Henkilo
, jolla on oliomuuttuja nimi ja henkilön yksilöivä henkilötunnus.
public class Henkilo { private String nimi; private String henkilotunnus; public Henkilo(String nimi, String henkilotunnus) { this.nimi = nimi; this.henkilotunnus = henkilotunnus; } public String getHenkilotunnus() { return henkilotunnus; } public String getNimi() { return nimi; } }
Haluamme tallentaa henkilöitä. Määritellään Henkilo
-luokan ilmentymien tallentamiseen liittyvä DAO-rajapinta, joka määrittelee toiminnallisuudet tallentamiseen, hakemiseen, ja poistamiseen.
import java.util.Collection; public interface HenkiloDAO { void talleta(Henkilo henkilo); Henkilo hae(String henkilotunnus); void poista(Henkilo henkilo); void poista(String henkilotunnus); void poistaKaikki(); Collection<Henkilo> haeKaikki(); }
Ohjelmoidessa lähdemme yleensä pienestä liikkelle. Luodaan rajapinnalle ensimmäinen toteutus, jossa tallennetaan vain yksi henkilö. Luokka YksinkertainenHenkiloDAO
tarjoaa konkreettista toiminnallisuutta vain osaan rajapinnan HenkiloDAO
määrittelemistä metodeista.
import java.util.ArrayList; import java.util.Collection; public class YksinkertainenHenkiloDAO implements HenkiloDAO { private Henkilo henkilo; @Override public void talleta(Henkilo henkilo) { this.henkilo = henkilo; } @Override public Henkilo hae(String henkilotunnus) { return this.henkilo; } @Override public void poista(Henkilo henkilo) { this.henkilo = null; } @Override public void poista(String henkilotunnus) { this.henkilo = null; } @Override public void poistaKaikki() { this.henkilo = null; } @Override public Collection<Henkilo> haeKaikki() { Collection<Henkilo> henkilot = new ArrayList<Henkilo>(); if (this.henkilo != null) { henkilot.add(this.henkilo); } return henkilot; } }
Yllä oleva HenkiloDAO-rajapinnan toteutus käsittelee vain yhtä Henkilo-olioa.
Mikä tässä on nyt niin hienoa?
Kuvittele seuraava ohjelmoinnissa usein eteen tuleva tilanne. Kehität useammasta luokasta koostuvaa ohjelmistoa ja haluat testata olioiden yhteistoiminnallisuutta ennen kuin koko ohjelma on valmis. Käyttäessäsi rajapintoja voit vaihtaa rajapinnan toteuttavaa luokkaa muuttamatta rajapintaa käyttävän luokan toiminnallisuutta. Tämä mahdollistaa ohjelman paloittain kehittämisen: voit aluksi toteuttaa vain pienen osan toiminnallisuudesta, ja testata ohjelman toimintaa sillä. Yllä olevaa tallentajaa voi käyttää esimerkiksi seuraavan käyttöliittymäluokan kautta:
import java.util.Scanner; public class Kayttoliittyma { private Scanner lukija; private HenkiloDAO henkiloDao; public Kayttoliittyma(Scanner lukija, HenkiloDAO henkiloDao) { this.lukija = lukija; this.henkiloDao = henkiloDao; } public void kaynnista() { while (true) { System.out.println("Toiminnallisuudet: "); System.out.println("\t1. Lisää henkilö"); System.out.println("\t2. Listaa henkilöt"); System.out.println("\t3. Lopeta"); System.out.println(""); System.out.print("Syötä komento: "); int komento = Integer.parseInt(this.lukija.nextLine()); if (komento == 3) { break; } } } private void hoidaKomento(int komento) { if (komento == 1) { lisaaHenkilo(); } else if (komento == 2) { listaaHenkilot(); } } private void lisaaHenkilo() { System.out.print("Anna nimi: "); String nimi = this.lukija.nextLine(); System.out.print("Anna hetu: "); String hetu = this.lukija.nextLine(); this.henkiloDao.talleta(new Henkilo(nimi, hetu)); } private void listaaHenkilot() { System.out.println("Listataan: "); for (Henkilo henkilo : this.henkiloDao.haeKaikki()) { System.out.println("\t" + henkilo.getNimi()); } } }
Käyttöliittymässä tarjotaan toiminnallisuus vain lisäämiseen ja listaamiseen. Huomaa että missään päin käyttöliittymäluokkaa ei mainita luokkaa YksinkertainenHenkiloDAO
. Henkilöiden lisääminen ja listaaminen tapahtuu HenkiloDAO
-rajapinnan kautta. Käyttöliittymän käynnistävässä luokassa annetaan käyttöliittymälle HenkiloDAO
-rajapinnan toteuttava luokka.
Scanner lukija = new Scanner(System.in); HenkiloDAO henkiloDao = new YksinkertainenHenkiloDAO(); Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija, henkiloDao); kayttoliittyma.kaynnista();
Kun sovellukseen toteutettu pieni osa toimii hyvin, voidaan HenkiloDAO-rajapinnan toteutusta laajentaa tai sille voidaan luoda uusi toteutus. Luodaan seuraavaksi hajautustaulua käyttävä toteutus MuistiHenkiloDAO
. Hajautustaulua käyttävässä toteutuksessa henkilöt tallennetaan hajautustaulun arvoina siten, että avaimena käytetään henkilön henkilötunnusta.
public class MuistiHenkiloDAO implements HenkiloDAO { private Map<String, Henkilo> henkilot; public MuistiHenkiloDAO() { this.henkilot = new HashMap<String, Henkilo>(); } @Override public void talleta(Henkilo henkilo) { this.henkilot.put(henkilo.getHenkilotunnus(), henkilo); } @Override public Henkilo hae(String henkilotunnus) { return this.henkilot.get(henkilotunnus); } @Override public void poista(Henkilo henkilo) { poista(henkilo.getHenkilotunnus()); } @Override public void poista(String henkilotunnus) { this.henkilot.remove(henkilotunnus); } @Override public void poistaKaikki() { this.henkilot.clear(); } @Override public Collection<Henkilo> haeKaikki() { return this.henkilot.values(); } }
Huomaa että käyttöliittymäluokan koodiin ei tarvitse koskea. Käyttöliittymässä käytetyn HenkiloDAO
-rajapinnan toteuttavan luokan vaihto tapahtuu helposti:
Scanner lukija = new Scanner(System.in); HenkiloDAO henkiloDao = new MuistiHenkiloDAO(); Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija, henkiloDao); kayttoliittyma.kaynnista();
Rajapintojen käyttäminen ohjelmoinnissa, jonka erityistapaus juuri esitelty tallennuslogiikan lähestymistapa on, on mahtava tapa helpottaa ohjelman osatoiminnallisuuksien testaamista yhdessä. Lähestymistavan vahvuus tulee erityisesti esiin silloin, kun ohjelmaa on rakentamassa useampi ihminen samaan aikaan.
Harjoitellaan seuraavaksi Data Access Object-suunnittelumallia ja kerrataan tiedostojen käyttöä. Tässä tehtävässä toteutetaan valmiiksi annetusta AlbumiDAO
-rajapinnasta kaksi erilaista toteutusta. Rajapinta määrittelee seuraavat toiminnot:
null
-viiteRajapinnan koodi:
package dao; import java.util.Collection; public interface AlbumiDAO { void talleta(Albumi albumi); Albumi hae(int id); void poista(Albumi albumi); void poista(int id); void poistaKaikki(); Collection<Albumi> haeKaikki(); }
Rajapinta käsittelee Albumi
-olioita, joiden toteutus on seuraavanlainen:
package dao; public class Albumi { private int id; private String artisti; private String nimi; private int julkaisuVuosi; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getArtisti() { return artisti; } public void setArtisti(String artisti) { this.artisti = artisti; } public String getNimi() { return nimi; } public void setNimi(String nimi) { this.nimi = nimi; } public int getJulkaisuVuosi() { return julkaisuVuosi; } public void setJulkaisuVuosi(int julkaisuVuosi) { this.julkaisuVuosi = julkaisuVuosi; } @Override public String toString() { return id + ": " + artisti + " - " + nimi + " (" + julkaisuVuosi + ")"; } }
Tehtävänäsi on toteuttaa luokka MuistiAlbumiDAO
, joka toteuttaa rajapinnan AlbumiDAO
siten, että albumeita talletetaan muistiin.
Vinkki: Map
-rajapinnan toteuttavasta tietorakenteesta on hyötyä tässä tehtävässä.
Toteutusta voi käyttää esimerkiksi näin:
AlbumiDAO dao = new MuistiAlbumiDAO(); Albumi albumi = new Albumi(); albumi.setId(4); albumi.setArtisti("Porcupine Tree"); albumi.setNimi("Deadwing"); albumi.setJulkaisuVuosi(2005); dao.talleta(albumi); System.out.println("Albumi ID-numerolla 4: " + dao.hae(4)); Albumi albumi2 = new Albumi(); albumi2.setId(137); albumi2.setArtisti("Flying Colors"); albumi2.setNimi("Flying Colors"); albumi2.setJulkaisuVuosi(2012); dao.talleta(albumi2); System.out.println("Kaikki talletetut albumit: " + dao.haeKaikki()); dao.poista(4); System.out.println("Kaikki talletetut albumit: " + dao.haeKaikki()); dao.poista(albumi2); System.out.println("Kaikki talletetut albumit: " + dao.haeKaikki());
Ylläoleva esimerkkiohjelma tulostaa: (albumien järjestys saattaa vaihdella)
Albumi ID-numerolla 4: 4: Porcupine Tree - Deadwing (2005) Kaikki talletetut albumit: [137: Flying Colors - Flying Colors (2012), 4: Porcupine Tree - Deadwing (2005)] Kaikki talletetut albumit: [137: Flying Colors - Flying Colors (2012)] Kaikki talletetut albumit: []
Kertaa tässä vaiheessa kappale 53. Tiedostojen käsittely.
CSV (Comma Separated Values) on tiedostomuoto, jossa yhtä kokonaisuutta (esim. oliota) vastaa yksi rivi. Rivillä arvot (olioiden kentät) on eroteltu pilkuilla.
Tässä tehtävässä käytetystä Albumi
-olioista on tarkoitus tuottaa seuraavan esimerkin kaltaisia CSV-tiedostoja:
4,Porcupine Tree,Deadwing,2005 137,Flying Colors,Flying Colors,2012
Pilkulla erotetut arvot ovat siis: ID, artistin nimi, albumin nimi ja julkaisuvuosi.
Huom: Tilannetta, jossa jokin arvo sisältäisi pilkun, ei tarvitse ottaa huomioon.
Tehtävänäsi on toteuttaa luokka OmaCSVKasittelija
, joka toteuttaa rajapinnan CSVKasittelija
. Rajapinnassa on seuraavat toiminnot:
Albumi
-oliot tiedostoonAlbumi
-olioita, joista muodostetaan Map<Integer, Albumi>
-rajapinnan toteuttava olio siten, että avaimena on albumin ID kokonaislukuna. Metodin tulee palauttaa tyhjä Map
-olio, jos tiedostoa ei löydy (OmaCSVKasittelija ei siis saa heittää poikkeusta tässä tilanteessa).Luokalla OmaCSVKasittelija
on lisäksi oltava konstruktori, jonka parametrina on käytettävän tiedoston nimi: public OmaCSVKasittelija(String tiedostonNimi)
.
Rajapinnan koodi:
package dao; import java.io.IOException; import java.util.Collection; import java.util.Map; public interface CSVKasittelija { void talleta(Collection<Albumi> albumit) throws IOException; Map<Integer, Albumi> lataa() throws IOException; }
Luokkaa voi käyttää esimerkiksi näin:
public static void main(String[] args) { Albumi albumi = new Albumi(); albumi.setId(4); albumi.setArtisti("Porcupine Tree"); albumi.setNimi("Deadwing"); albumi.setJulkaisuVuosi(2005); Albumi albumi2 = new Albumi(); albumi2.setId(137); albumi2.setArtisti("Flying Colors"); albumi2.setNimi("Flying Colors"); albumi2.setJulkaisuVuosi(2012); List<Albumi> albumit = new ArrayList<Albumi>(); albumit.add(albumi); albumit.add(albumi2); CSVKasittelija csvKasittelija = new OmaCSVKasittelija("albumit.csv"); try { csvKasittelija.talleta(albumit); } catch (IOException e) { System.out.println("Poikkeus tallettaessa: " + e); return; } Map<Integer, Albumi> ladatutAlbumit; try { ladatutAlbumit = csvKasittelija.lataa(); } catch (IOException e) { System.out.println("Poikkeus ladatessa: " + e); return; } System.out.println("Tiedostosta ladattiin albumit: " + ladatutAlbumit); }
Ylläoleva esimerkkiohjelma tulostaa: (albumien järjestys saattaa vaihdella)
Tiedostosta ladattiin albumit: {137=137: Flying Colors - Flying Colors (2012), 4=4: Porcupine Tree - Deadwing (2005)}
Muistathan aina sulkea tiedoston tallennuksen lopuksi! Tällöin viimeisetkin muutokset päätyvät varmasti tiedostoon.
Tee luokka HidasCSVAlbumiDAO
, joka toteuttaa AlbumiDAO
-rajapinnan. Luokan tulee toimia siten, että albumeita ei talleteta muistiin, vaan niitä luetaan tiedostosta ja kirjoitetaan tiedostoon (CSV-muodossa) aina tarvittaessa. Jokaisen talletus- ja hakuoperaation tulee ensin lukea albumit tiedostosta. Jos tiedostoa ei löydy, tai sitä ei saa luettua, tulee olettaa ettei albumeita ole vielä tallennettu. Hyödynnä edellisessä tehtävässä luotua OmaCSVKasittelija
-luokkaa.
Luokalla HidasCSVAlbumiDAO
täytyy olla seuraava konstruktori:
CSVKasittelija
-oliota.Voit käyttää ensimmäisen kohdan esimerkkiä testatessa tätä toteutusta, koska kumpikin toteuttaa saman rajapinnan. Kannattaa avata luotu CSV-tiedosto NetBeansissa ja katsoa mitä ohjelma kirjoitti tiedostoon.
Edellisen tehtävän toteutus on nimensä mukaisesti jokseenkin hidas, koska toteutus "unohtaa" aiemmin haetut tai tallennetut albumit. Tämän vuoksi albumit joudutaan lukemaan aina uudestaan tiedostosta, mikä on muistiin talletettujen olioiden käsittelyyn verrattuna moninkertaisesti hitaampaa.
Tee luokka NopeaCSVAlbumiDAO
, joka toteuttaa AlbumiDAO
-rajapinnan. Luokan tulee toimia siten, että annetun tiedoston sisältö luetaan heti konstruktorissa ja talletetaan muistiin (vastaavalla tavalla kuin MuistiAlbumiDAO
). Tällöin albumeja haettaessa tiedostoa ei tarvitse (eikä pidä!) lukea ollenkaan. Talletettaessa tai poistettaessa albumeja muutokset pitää tehdä muistissa oleviin albumeihin sekä tallettaa tiedostoon.
Luokalla NopeaCSVAlbumiDAO
täytyy olla seuraava konstruktori:
CSVKasittelija
-oliota.Voit käyttää ensimmäisen kohdan esimerkkiä testatessa tätä toteutusta, koska kumpikin toteuttaa saman rajapinnan. Kannattaa avata luotu CSV-tiedosto NetBeansissa ja katsoa mitä ohjelma kirjoitti tiedostoon.
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 luokata.
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. Huom! Jos metodilla tai muuttujalla on näkyvyysmääre private
, ei se näy aliluokille.
Yliluokan konstruktoria kutsutaan avainsanalla super
. Kutsu super
on käytännössä samanlainen kuin this
-konstruktorikutsu. Kutsulle annetaan parametrina konstruktorin vaatimat oliot.
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ää 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
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 mitää matalana, sillä hierarkian ylläpito ja jatkokehitys vaikeutuu perintöhierarkian kasvaessa. Yleisesti ottaen, jos perintähierarkian korkeus on yli 5, ohjelman rakenteessa on todennäköisesti parannettavaa.
Perinä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ä toStringiä.
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 kiinostavaa 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:
Täydennä Muutoshistoria-luokkaa analyysimetodein:
Havainnollista uusien metodien käyttöä ja toimivuutta pienellä ohjelmalla.
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.)
Havainnollista uusien metodien käyttöä ja toimivuutta pienellä ohjelmalla.
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
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.
Havainnollista kehiteltyä analyysiraporttia pienellä esimerkkiohjelmalla.
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. Kuten DAO-esimerkissä huomasimme, rajapinnat mahdollistavat ns. plug-and-play toiminnallisuuden. 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 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/53.0 Ammu 9.0/53.0 Ammu 0.0/53.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();
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Lehmät: Heluna 2.0/17.0 Rima 3.0/42.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/45.0 Hento 0.0/54.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 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 erilaisten pääset hyödyntämään 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..
Toteuta lisäksi 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
Tehtäväpohjan mukana tulee luokat Ympyra
, Suorakulmio
ja TasasivuinenKolmio
. Luokat liittyvät samaan aihepiiriin, ja niillä on hyvin paljon yhteistä toiminnallisuutta. Tutustu luokkiin ennenkuin lähdet tekemään, jolloin hahmotat tarkemmin syyt muutoksille. Jos huomaat että luokissa on alustavasti sisennys hieman pielessä, kannattaa sisennys hoitaa kuntoon luettavuuden helpottamiseksi.
Toteuta pakkaukseen kuviot
abstrakti luokka Kuvio
, jossa on kuvioihin liittyvää toiminnallisuutta. Luokan kuvio tulee sisältää konstruktori public Kuvio(int x, int y)
, aksessorit public int getX()
, public int getY()
, sekä abstraktit metodit public abstract double pintaAla()
ja public abstract double piiri()
.
Muuta luokan Ympyra
toteutusta siten, että se perii luokan Kuvio
. Luokan Ympyra
ulkoinen toiminnallisuus ei saa muuttua, eli sen tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio
avulla tai itse. Muistathan että konstruktorikutsun super
avulla voit käyttää yliluokan konstruktoria. Kun metodi public int getX()
on toteutettu jo yliluokassa se ei tarvitse erillistä toteutusta luokassa Ympyra
.
Kuvio kuvio = new Ympyra(10, 10, 15); System.out.println("X " + kuvio.getX()); System.out.println("Y " + kuvio.getY()); System.out.println("Pinta-ala " + kuvio.pintaAla()); System.out.println("Piiri " + kuvio.piiri());
X 10 Y 10 Pinta-ala 706.85834... Piiri 94.24777...
Muuta luokkien Suorakulmio
ja TasakylkinenKolmio
toteutusta siten, että ne perivät luokan Kuvio
. Luokkien ulkoinen toiminnallisuus ei saa muuttua, eli niiden tulee tarjota samat metodit kuin aiemminkin -- joko luokan Kuvio
avulla tai itse.
Kuvio kuvio = new Suorakulmio(10, 10, 15, 15); System.out.println("X " + kuvio.getX()); System.out.println("Y " + kuvio.getY()); System.out.println("Pinta-ala " + kuvio.pintaAla()); System.out.println("Piiri " + kuvio.piiri()); System.out.println(""); kuvio = new TasakylkinenKolmio(10, 10, 15); System.out.println("X " + kuvio.getX()); System.out.println("Y " + kuvio.getY()); System.out.println("Pinta-ala " + kuvio.pintaAla()); System.out.println("Piiri " + kuvio.piiri());
X 10 Y 10 Pinta-ala 225.0 Piiri 60.0 X 10 Y 10 Pinta-ala 97.42785... Piiri 45.0
Tämä tehtävä on neljän tehtäväpisteen arvoinen. Huom! Toteuta kaikki toiminnallisuus pakkaukseen luola
.
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. Allaolevassa 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. Allaolevassa 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ä ruutuun jossa on jo hirviö, arvotaan sille siirto uudestaan. 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, 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
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ä.
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
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]);
Testaa toteutustasi seuraavalla testiohjelmalla.
package game; import gameoflife.komentorivi.KomentoriviGameOfLife; 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); KomentoriviGameOfLife gom = new KomentoriviGameOfLife(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); KomentoriviGameOfLife gom = new KomentoriviGameOfLife(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(); } }
Errare humanum est
Ihminen on erehtyväinen ja paraskin ohjelmoija tekee virheitä. Ohjelman kehitysvaiheessa tapahtuvien virheiden lisäksi huomattava osa virheistä syntyy olemassa olevaa ohjelmaa muokattaessa. Ohjelman muokkauksen aikana tehdyt virheet eivät välttämättä näy muokattavassa osassa, vaan voivat ilmaantua välillisesti erillisessä osassa ohjelmaa: osassa, joka käyttää muutettua osaa.
Ohjelmien automaattinen testaaminen tarkoittaa toistettavien testien luomista. Testeillä varmistetaan että ohjelma toimii halutusti, ja että ohjelma säilyttää toiminnallisuutensa myös muutosten jälkeen. Sanalla automaattinen painotetaan sitä, että luodut testit ovat toistettavia ja että ne voidaan suorittaa aina haluttaessa -- ohjelmoijan ei tarvitse olla läsnä testejä suoritettaessa.
Otimme aiemmin askeleita kohti testauksen automatisointia antamalla Scanner-oliolle parametrina merkkijonon, jonka se tulkitsee käyttäjän näppäimistöltä antamaksi syötteeksi. Automaattisessa testaamisessa testaaminen viedään viedä pidemmälle: koneen tehtävänä on myös tarkistaa että ohjelman tuottama vastaus on odotettu.
Automaattisen testauksen tällä kurssilla painotettu osa-alue on yksikkötestaus, jossa testataan ohjelman pienten osakokonaisuuksien -- metodien ja luokkien -- toimintaa. Yksikkötestaamiseen käytetään Javalla yleensä JUnit-testauskirjastoa.
Pino on kaikille ihmisille tuttu asia. Esimerkiksi ravintola Unicafessa lautaset ovat yleensä pinossa. Pinon päältä voi ottaa lautasen ja pinon päälle voi lisätä lautasia. On myös helppo selvittää onko pinossa vielä lautasia jäljellä.
Pino on myös ohjelmoinnissa usein käytetty aputietorakenne. Rajapintana lukuja sisältävä pino näyttää seuraavalta.
public interface Pino { boolean tyhja(); boolean taynna(); void pinoon(int luku); int pinosta(); int huipulla(); int lukuja(); }
Rajapinnan määrittelemien metodien on tarkoitus toimia seuraavasti:
public boolean tyhja()
palauttaa true jos pino on tyhjäpublic boolean taynna()
palauttaa true jos pino on täynnäpublic void pinoon(int luku)
laittaa parametrina olevan luvun pinon päällepublic int huipulla()
kertoo pinon huipulla olevan alkionpublic int pinosta()
poistaa ja palauttaa pinon päällä olevan alkionpublic int lukuja()
kertoo pinossa olevien lukujen määränpublic int tilaa()
kertoo pinon vapaan tilan määränToteutetaan rajapinnan Pino
toteuttava luokka OmaPino
, johon talletetaan lukuja. Pinoon mahtuvien lukujen määrä annetaan pinon konstruktorissa. Toteutamme pinon hieman aiemmasta poikkeavasti -- emme testaa ohjelmaa pääohjelman avulla, vaan käytämme pääohjelman sijasta automatisoituja JUnit-testejä ohjelman testaamiseen.
NetBeansissa olevat ohjelmamme ovat tähän asti sijainneet aina Source Packagesissa tai sen sisällä olevissa pakkauksissa. Ohjelman lähdekoodit tulevat aina kansioon Source Packages. Automaattisia testejä luodessa testit luodaan valikon Test Packages alle. Uusia JUnit-testejä voi luoda valitsemalla projektin oikealla hiirennapilla ja valitsemalla avautuvasta valikosta New -> JUnit Test...
. Jos vaihtoehto JUnit test ei näy listassa, löydät sen valitsemalla Other.
JUnit-testit sijaitsevat luokassa. Uutta testitiedostoa luodessa ohjelma pyytää testitiedoston nimen. Tyypillisesti nimeksi annetaan testattavan luokan tai toiminnallisuuden nimi. Luokan nimen tulee aina päättyä sanaan Test
. Esimerkiksi alla luodaan testiluokka PinoTest
, joka sijaitsee pakkauksessa pino
. NetBeans haluaa luoda käyttöömme myös valmista runkoa testiluokalle -- joka käy hyvin.
Jos NetBeans kysyy minkä JUnit-version haluat käyttöösi, valitse JUnit 4.x
.
Kun testiluokka PinoTest
on luotu, näkyy se projektin valikon Test Packages alla.
Luokka PinoTest
näyttää aluksi seuraavalta
package pino; import org.junit.*; import static org.junit.Assert.*; public class PinoTest { public PinoTest() { } @BeforeClass public static void setUpClass() throws Exception { } @AfterClass public static void tearDownClass() throws Exception { } @Before public void setUp() { } @After public void tearDown() { } // TODO add test methods here. // The methods must be annotated with annotation @Test. For example: // // @Test // public void hello() {} }
Meille oleellisia osia luokassa PinoTest
ovat metodit public void setUp
, jonka yläpuolella on merkintä @Before
, ja kommentoitu metodipohja public void hello()
, jonka yläpuolella on merkintä @Test
. Metodit, joiden yläpuolella on merkintä @Test
ovat ohjelman toiminnallisuutta testaavia testimetodeja. Metodi setUp
taas suoritetaan ennen jokaista testiä.
Muokataan luokkaa PinoTest
siten, että sillä testataan rajapinnan Pino
toteuttamaa luokkaa OmaPino
. Älä välitä vaikkei luokkaa OmaPino
ole vielä luotu. Pino on testiluokan oliomuuttuja, joka alustetaan ennen jokaista testiä metodissa setUp
.
package pino; import org.junit.*; import static org.junit.Assert.*; public class PinoTest { Pino pino; @Before public void setUp() { pino = new OmaPino(3); } @Test public void alussaTyhja() { assertTrue(pino.tyhja()); } @Test public void lisayksenJalkeenEiTyhja() { pino.pinoon(5); assertFalse(pino.tyhja()); } @Test public void lisattyAlkioTuleePinosta() { pino.pinoon(3); assertEquals(3, pino.pinosta()); } @Test public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() { pino.pinoon(3); pino.pinosta(); assertTrue(pino.tyhja()); } @Test public void lisatytAlkiotTulevatPinostaOikeassaJarjestyksessa() { pino.pinoon(1); pino.pinoon(2); pino.pinoon(3); assertEquals(3, pino.pinosta()); assertEquals(2, pino.pinosta()); assertEquals(1, pino.pinosta()); } @Test public void tyhjennyksenJalkeenPinoonLaitettuAlkioTuleeUlosPinosta() { pino.pinoon(1); pino.pinosta(); pino.pinoon(5); assertEquals(5, pino.pinosta()); } // ... }
Jokainen testi, eli merkinnällä @Test
varustettu metodi, alkaa tilanteesta, jossa on luotu uusi tyhjä pino. Jokainen yksittäinen @Test-merkitty metodi on oma testinsä. Yksittäisellä testimetodilla testataan aina yhtä pientä osaa pinon toiminnallisuudesta. Testit suoritetaan toisistaan täysin riippumattomina, eli jokainen testi alkaa "puhtaaltä pöydältä", setUp
-metodin alustamasta tilanteesta.
Yksittäiset testit noudattavat aina samaa kaavaa. Ensin luodaan tilanne jossa tapahtuvaa toimintoa halutaan testata, sitten tehdään testattava toimenpide, ja lopuksi tarkastetaan onko tilanne odotetun kaltainen. Esimerkiksi seuraava testi testaa että lisäyksen ja poiston jälkeen pino on taas tyhjä -- huomaa myös kuvaava testimetodin nimentä:
@Test public void lisayksenJaPoistonJalkeenPinoOnTaasTyhja() { pino.pinoon(3); pino.pinosta(); assertTrue(pino.tyhja()); }
Ylläoleva testi testaa toimiiko metodi tyhja()
jos pino on tyhjennetty. Ensin laitetaan pinoon luku metodilla pinoon
, jonka jälkeen pino tyhjennetään kutsumalla metodia pinosta()
. Tällöin on saatu aikaan tilanne jossa pinon pitäisi olla tyhjennetty. Viimeisellä rivillä testataan, että pinon metodi tyhja()
palauttaa arvon true
testausmetodilla assertTrue()
. Jos metodi tyhja()
ei palauta arvoa true
näemme testejä suorittaessa virheen.
Jokainen testi päättyy jonkun assert
-metodin kutsuun. Esimerkiksi metodilla assertEquals()
voidaan varmistaa onko metodin palauttama luku tai merkkijono haluttu, ja metodilla assertTrue()
varmistetaan että metodin palauttama arvo on true
. Erilaiset assert
-metodit saadaan käyttöön luokan alussa olevalla määrittelyllä import static org.junit.Assert.*;
.
Testit suoritetaan joko painamalla alt ja F6 tai valitsemalla Run -> Test project. (Macintosh-koneissa tulee painaa ctrl ja F6). Punainen väri ilmaisee että testin suoritus epäonnistui -- testattava toiminnallisuus ei toiminut kuten toivottiin. Vihreä väri kertoo että testin testaama toiminnallisuus toimi kuten haluttiin.
Pinon toteuttaminen testien avulla tapahtuisi askel kerrallaan siten, että lopulta kaikki testit toimivat. Ohjelman rakentaminen aloitetaan yleensä hyvin varovasti. Rakennetaan ensin luokka OmaPino
siten, että ensimmäinen testi alussaTyhja alkaa toimimaan. Älä tee mitään kovin monimutkaista, "quick and dirty"-ratkaisu kelpaa näin alkuun. Kun testi menee läpi (eli näyttää vihreää), siirry ratkaisemaan seuraavaa kohtaa.
Testi alussaTyhja menee läpi aina kun palautamme arvon true
metodista tyhja
.
package pino; import java.util.ArrayList; import java.util.List; public class OmaPino implements Pino { public OmaPino(int maksimikoko) { } @Override public boolean tyhja() { return true; } // tyhjät metodirungot
Siirrytään ratkaisemaan kohtaa lisayksenJalkeenEiTyhja. Tarvitsemme toteutuksen metodille pinoon
. Yksi lähestymistapa on muokata luokkaa OmaPino
siten, että se sisältää taulukon. Taulukkoa käytetään, että pinottavat luvut talletetaan pinon taulukkoon yksi kerrallaan. Seuraava kuvasarja selkeyttää taulukossa olevien alkioiden pinoon laittamista ja pinosta ottamista.
pino = new OmaPino(4); 0 1 2 3 ----------------- | | | | | ----------------- alkioita: 0 pino.pinoon(5); 0 1 2 3 ----------------- | 5 | | | | ----------------- alkiota: 1 pino.pinoon(3); 0 1 2 3 ----------------- | 5 | 3 | | | ----------------- alkiota: 2 pino.pinoon(7); 0 1 2 3 ----------------- | 5 | 3 | 7 | | ----------------- alkiota: 3 pino.pinosta(); 0 1 2 3 ----------------- | 5 | 3 | | | ----------------- alkiota: 2
Ohjelman tulee siis muistaa kuinka monta alkiota pinossa on. Uusi alkio laitetaan jo pinossa olevien perään. Alkion poisto aiheuttaa sen, että taulukon viimeinen käytössä ollut paikka vapautuu ja alkiomäärän muistavan muuttujan arvo pienenee.
Luokan OmaPino
toteutusta jatketaan askel kerrallaan kunnes kaikki testit menevät läpi. Jossain vaiheessa ohjelmoija todennäköisesti huomaisi, että taulukko kannattaa vaihtaa ArrayList
-rakenteeksi.
Huomaat todennäköisesti ylläolevan esimerkin luettuasi että olet jo tehnyt hyvin monta testejä käyttävää ohjelmaa. Osa TMC:n toiminnallisuudesta rakentuu JUnit-testien varaan, ongelmat ovat varsinkin kurssin alkupuolella pilkottu pieniin testeihin, joiden avulla ohjelmoijaa on ohjattu eteenpäin. TMC:n mukana tulevat testit ovat kuitenkin usein monimutkaisempia kuin ohjelmien normaalissa automaattisessa testauksessa niiden tarvitsee olla. TMC:ssä ja kurssilla käytettävien testien kirjoittajien tulee muunmuassa varmistaa luokkien olemassaolo, jota normaalissa automaattisessa testauksessa harvemmin tarvitsee tehdä.
Harjoitellaan seuraavaksi ensin testien lukemista, jonka jälkeen kirjoitetaan muutama testi.
Tehtäväpohjassa on rajapinnan Tehtavalista
toteuttava luokka MuistiTehtavalist
. Ohjelmaa varten on koodattu valmiiksi testit, joita ohjelma ei kuitenkaan läpäise. Tehtävänäsi on tutustua testiluokkaan TehtavalistaTest
, ja korjata luokka MuistiTehtavalista
siten, että ohjelman testit menevät läpi.
Huom! Tässä tehtävässä sinun ei tarvitse koskea testiluokkaan TehtavalistaTest
.
Huom! Tässä tehtävässä on jo mukana testiluokka, johon sinun tulee kirjoittaa lisää testejä. Vastauksen oikeellisuus testataan vasta TMC-palvelimella: tehtävästä saa pisteet vasta kun molemmat tehtävät on suoritettu palvelimella hyväksytysti. Ole tarkka metodien nimennän ja lisättyjen lukujen kanssa.
Tehtävässä tulee pakkauksessa tilasto
sijaitseva luokka Lukutilasto
.
public void lisaaLuku(int luku)
public int summa()
public int lukujenMaara()
public boolean sisaltaa(int luku)
Testikansiossa olevassa pakkauksessa tilasto
on luokka LukutilastoTest
, johon sinun tulee lisätä uusia testimetodeja.
Lisää testiluokkaan testimetodi public void lukujenMaaraKasvaaKahdellaKunLisataanKaksiLukua()
, jossa lukutilastoon lisätään luvut 3 ja 5. Tämän jälkeen metodissa tarkistetaan että lukutilastossa on kaksi lukua käyttäen lukutilaston metodia lukujenMaara. Käytä Assert
-luokan assertEquals
-metodia palautettujen arvojen tarkastamiseen.
Lisää testiluokkaan testimetodi public void summaOikeinYhdellaLuvulla()
, jossa lukutilastoon lisätään luku 3. Tämän jälkeen metodissa tarkistetaan lukutilaston summa-metodin avulla että tilastossa olevien lukujen summa on 3. Käytä Assert
-luokan assertEquals
-metodia palautettujen arvojen tarkastamiseen.
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. Käyttöliittymät ovat suoritettavia, ja ne tulee käynnistää erikseen. Käytämme kurssilla seuraavanlaista käyttöliittymärunkoa, jonka päälle rakennamme toiminnallisuutta.
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 kurssilla Rinnakkaisohjelmointi.
private JFrame frame;
Käyttöliittymä sisältää oliomuuttujana JFrame
-olion -- ikkunan --, 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 -- leveydeksi tulee 200 pikseliä, korkeudeksi 100 pikseliä. Komento frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
kertoo JFrame-oliolle, että käyttöliittymä tulee sulkea 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 visuaalinen 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 Container
-olion 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!". 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)
.
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ä on 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) { 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 "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 JTextArea 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
. Tapahtumankuuntelijan tulee kopioida käyttöliittymässä olevan JTextField-kentän sisältö JLabel-kenttään. Ohjelman oleellinen toiminnallisuus on siis JTextField-kentän sisällön kopioiminen JLabel-kenttään napin painalluksella.
Huom! 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 kappaleessa 56. Tallennustoiminnallisuuden eriyttäminen määritelty rajapinta HenkiloDAO
, ja haluamme toteuttaa käyttöliittymän henkilöiden tallentamiseen.
public interface HenkiloDAO { 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äyttoliittymä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 HenkiloDAO-rajapinnasta sekä kentistä, joita se käyttää. Luodaan ActionListener
-rajapinnan toteuttava luokka HenkilonLisaysKuuntelija
, joka saa konstruktorissaan parametrina HenkiloDAO
-rajapinnan toteuttavan olion sekä kaksi JTextField
-oliota -- kentät nimelle ja hetulle. Metodissa actionPerformed
luodaan uusi Henkilo
-olio ja tallennetaan se HenkiloDAO
-olion tarjoamalla talleta
-metodilla.
public class HenkilonLisaysKuuntelija implements ActionListener { private HenkiloDAO henkiloDao; private JTextField nimiKentta; private JTextField hetuKentta; public HenkilonLisaysKuuntelija(HenkiloDAO henkiloDao, JTextField nimiKentta, JTextField hetuKentta) { this.henkiloDao = henkiloDao; this.nimiKentta = nimiKentta; this.hetuKentta = hetuKentta; } @Override public void actionPerformed(ActionEvent ae) { Henkilo henkilo = new Henkilo(nimiKentta.getText(), hetuKentta.getText()); this.henkiloDao.talleta(henkilo); } }
Jotta saamme HenkiloDAO
-viitteen HenkilonLisaysKuuntelija
-oliolle, tulee sen olla käyttöliittymän tiedossa. Lisätään käyttöliittymälle oliomuuttuja private HenkiloDAO henkiloDao
, joka asetetaan konstruktorissa. Luokan Kayttoliittyma
konstruktoria muokataan siten, että sille annetaan HenkiloDAO
-rajapinnan toteuttama luokka.
public class Kayttoliittyma implements Runnable { private JFrame frame; private HenkiloDAO henkiloDao; public Kayttoliittyma(HenkiloDAO henkiloDao) { this.henkiloDao = henkiloDao; } // ...
Voimme nyt luoda tapahtumankuuntelijan HenkilonLisaysKuuntelija
, jolle annetaan sekä HenkiloDAO
-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(henkiloDao, nimiKentta, hetuKentta); lisaaNappi.addActionListener(kuuntelija); container.add(nimiTeksti); container.add(nimiKentta); container.add(hetuTeksti); container.add(hetuKentta); container.add(new JLabel("")); container.add(lisaaNappi); }
Muutetaan vielä pääohjelmaluokan Main
sisältöä siten, että käyttöliittymälle annetaan HenkiloDAO
-rajapinnan toteuttava luokka. Voimme käyttää suoraan kappaleessa 56. Tallennustoiminnallisuuden eriyttäminen määriteltyä luokkaa MuistiHenkiloDAO
sillä se toteuttaa rajapinnan HenkiloDAO
.
public class Main { public static void main(String[] args) { HenkiloDAO henkiloDao = new MuistiHenkiloDAO(); SwingUtilities.invokeLater(new Kayttoliittyma(henkiloDao)); } }
Nyt käyttöliittymässä lisätyt henkilöt tallennetaan MuistiHenkiloDAO
-luokan määrittelemällä tavalla ja sovellus toimii. Huomaa että emme joutuneet toteuttamaan tallennuslogiikkaa erikseen, koska se oli toteutettu jo kertaalleen. Käytännössä aloitimme tekstikäyttöliittymästä graafiseen käyttöliittymään siirtymisen.
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 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ämä tehtävä on kolmen yksittäisen tehtäväpisteen arvoinen. Tehtävässä toteutetaan sovelluslogiikka jätkänshakille ja harjoitellaan ohjelmarakenteen osittaista omatoimista suunnittelua.
Tehtäväpohjassa tulee mukana käyttöliittymä jätkänshakille, jossa pelilaudan koko on aina 3x3 ruutua. Käyttöliittymä huolehtii ainoastaan pelilaudalla tehtyihin tapahtumiin reagoimisesta, sekä pelilaudan ja pelitilanteen tietojen päivittämisestä. Pelin logiikka on erotettu JatkanshakinSovelluslogiikka
-rajapinnan avulla omaksi luokakseen.
package jatkanshakki.sovelluslogiikka; public interface JatkanshakinSovelluslogiikka { char getNykyinenVuoro(); int getMerkkienMaara(); void asetaMerkki(int sarake, int rivi); char getMerkki(int sarake, int rivi); boolean isPeliLoppu(); char getVoittaja(); }
Rajapinnan JatkanshakinSovelluslogiikka
lisäksi tehtäväpohjassa on apuluokka, joka määrittelee pelilaudan ruutujen mahdolliset tilat char
-tyyppisinä kirjaimina. Ruutu voi olla joko tyhjä, tai siinä voi olla risti tai nolla. Apuluokassa Jatkanshakki
on näille määrittelyt:
package jatkanshakki.sovelluslogiikka; public class Jatkanshakki { public static final char RISTI = 'X'; public static final char NOLLA = 'O'; public static final char TYHJA = ' '; }
Tehtävänäsi on täydentää pakkauksessa jatkanshakki.sovelluslogiikka
olevaa rajapinnan JatkanshakinSovelluslogiikka
toteuttavaa luokkaa OmaJatkanshakinSovelluslogiikka
. Luokka OmaJatkanshakinSovelluslogiikka
mahdollistaa jätkänshakin pelaamisen.
Rajapinta JatkanshakinSovelluslogiikka
määrittelee seuraavat toiminnot, jotka luokan OmaJatkanshakinSovelluslogiikka
tulee toteuttaa:
RISTI
, NOLLA
tai pelin päätyttyä TYHJA
IllegalArgumentException
, jos sarake tai rivi on pelilaudan ulkopuolella tai ruudussa on jo merkki, ja poikkeuksen IllegalStateException
, jos peli on jo loppu.TYHJA
, RISTI
tai NOLLA
. Metodi heittää poikkeuksen IllegalArgumentException
, jos sarake tai rivi on pelilaudan ulkopuolella.true
, jos toinen pelaajista voitti pelin tai peli päättyi tasapeliin, muutoin metodi palauttaa false
TYHJA
, jos peli on kesken tai peli päättyi tasapeliin, muutoin metodi palauttaa voittajan merkin: RISTI
tai NOLLA
Ensimmäinen pelivuoro on aina merkillä RISTI
. Pelin voittaa se pelaaja, joka saa ensimmäisenä kolme merkkiä vaakasuoraan, pystysuoraan tai vinottain. Tasapeli todetaan vasta, kun pelilauta on täynnä merkkejä eli tyhjiä ruutuja ei enää ole.
Vinkki: Pelilaudan tilanteen voi esittää esimerkiksi yhdeksän alkion char
-taulukolla, jonne talletetaan peliruutujen tilat. Sarakkeen ja rivin perusteella voidaan laskea taulukon indeksi: rivi * 3 + sarake
.
Silloin tällöin eteen tulee tilanne, jossa käyttäjän pitää pystyä valitsemaan tiedosto tiedostojärjestelmästä. Java tarjoaa tiedostojen valintaan valmiin käyttöliittymäkomponentin JFileChooser
.
JFileChooser poikkeaa tähän mennessä käyttämistämme käyttöliittymäkomponenteista siinä, että se avaa uuden ikkunan. Avautuvan ikkunan ulkonäkö riippuu hieman käyttöjärjestelmästä, esimerkiksi hieman vanhemmassa Fedora-käyttöjärjestelmässä ikkuna on seuraavannäköinen.
JFileChooser-olio voidaan luoda missä tahansa. Olion metodille showOpenDialog
annetaan parametrina käyttöliittymäkomponentti, johon se liittyy -- esimerkiksi JFrame
-luokan ilmentymä. Metodi showOpenDialog
avaa tiedostonvalintaikkunan, ja palauttaa int
-tyyppisen statuskoodin riippuen käyttäjän valinnasta. Luokassa JFileChooser
on määritelty int
-tyyppiset luokkamuuttujat, jotka kuvaavat statuskoodeja. Esimerkiksi onnistuneella valinnalla on arvo JFileChooser.APPROVE_OPTION
.
Valittuun tiedostoon pääsee JFileChooser
-oliosta käsiksi metodilla getSelectedFile
.
JFileChooser chooser = new JFileChooser(); int valinta = chooser.showOpenDialog(frame); if (valinta == JFileChooser.APPROVE_OPTION) { File valittu = chooser.getSelectedFile(); System.out.println("Valitsit tiedoston: " + valittu.getName()); } else if (valinta == JFileChooser.CANCEL_OPTION) { System.out.println("Et valinnut tiedostoa!"); }
Yllä oleva esimerkki avaa valintaikkunan, ja tulostaa valitun tiedoston nimen jos valinta onnistuu. Jos valinta epäonnistuu, ohjelma tulostaa "Et valinnut tiedostoa!"
.
Tiedostojen filtteröinnillä tarkoitetaan vain tietynlaisten tiedostojen näyttämistä tiedostoikkunassa. JFileChooser-oliolle voi asettaa filtterin metodilla setFileFilter
. Metodi setFileFilter
saa parametrina abstraktin luokan FileFilter
-ilmentymän -- esimerkiksi luokasta FileNameExtensionFilter
tehdyn olion.
Luokka FileNameExtensionFilter
mahdollistaa tiedostojen filtteröinnin niiden päätteiden perusteella. Esimerkiksi pelkät txt
-päätteiset tekstitiedostot saa näkyviin seuraavasti.
JFileChooser chooser = new JFileChooser(); chooser.setFileFilter(new FileNameExtensionFilter("Tekstitiedostot", "txt")); int valinta = chooser.showOpenDialog(frame); if (valinta == JFileChooser.APPROVE_OPTION) { File valittu = chooser.getSelectedFile(); System.out.println("Valitsit tiedoston: " + valittu.getName()); } else if (valinta == JFileChooser.CANCEL_OPTION) { System.out.println("Et valinnut tiedostoa!"); }
Tässä tehtävässä toteutetaan ohjelma, joka lukee käyttäjän valitseman tiedoston ja näyttää sen sisällön käyttöliittymässä.
Ohjelmassa on eroteltu käyttöliittymään ja sovelluslogiikka. Tehtäväpohjassa on valmiina sovelluslogiikan rajapinta TiedostonLukija
sekä käyttöliittymäluokan runko Kayttoliittyma
.
Täydennä käyttöliittymäluokan metodi luoKomponentit
. Ohjelma tarvitsee toimiakseen kolme käyttöliittymäkomponenttia:
setEditable
. JTextArea
eroaa JTextField
-komponentista siten, että JTextArea
-komponentissa voi olla tekstiä useammalla rivillä.Koska tässä tehtävässä on vain kolme aseteltavaa komponenttia, riittävät asetteluun BorderLayout
-asettelijan vaihtoehdot: BorderLayout.NORTH
, BorderLayout.CENTER
ja BorderLayout.SOUTH
. Käyttöliittymäkomponentti JTextArea
kannattaa sijoittaa keskelle, jotta se saa mahdollisimman paljon tilaa tekstin näyttämiselle.
Käyttöliittymän pitäisi näyttää suunnilleen seuraavalta. Alla olevassa esimerkissä JLabel
-oliossa ei ole mitään tekstiä.
Luo pakkaukseen tiedostonnaytin.sovelluslogiikka
luokka OmaTiedostonLukija
, joka toteuttaa rajapinnan TiedostonLukija
. Rajapinnassa on yksi metodi, lueTiedosto
, joka lukee sille annetun tiedoston kokonaisuudessaan merkkijonoon ja palauttaa tämän merkkijonon.
Rajapinnan koodi:
package tiedostonnaytin.sovelluslogiikka; import java.io.File; public interface TiedostonLukija { String lueTiedosto(File tiedosto); }
Huom: Palautettavassa merkkijonossa tulee säilyttää myös rivinvaihdot "\n"
. Esimerkiksi Scanner
-lukijan metodi nextLine()
poistaa palauttamistaan merkkijonoista rivinvaihdot, joten joudut joko lisäämään ne takaisin tai lukemaan tiedostoa eri tavalla.
Viimeisessä tehtävän osassa toteutetaan käyttöliittymän JButton
-napille tapahtumankuuntelija. Saat itse päättää luokalle sopivan nimen.
Tapahtumankuuntelijan tehtävänä on näyttää JFileChooser
-tiedostonvalintaikkuna kun JButton
-nappia painetaan. Kun käyttäjä valitsee tiedoston, tulee tapahtumankuuntelijan lukea tiedoston sisältö ja näyttää se JTextArea
-kentässä. Tämän jälkeen tapahtumankuuntelijan tulee vielä päivittää JLabel
-kenttään näytetyn tiedoston nimi (ilman tiedostopolkua).
JFileChooser-olion metodille showOpenDialog
tulee antaa parametrina Kayttoliittyma
-luokassa oleva JFrame
-ikkunaolio. Jos käyttäjä valitsee tiedoston, tulee tiedosto lukea tapahtumankuuntelijassa Kayttoliittyma
-luokassa määriteltyä TiedostonLukija
-oliota apuna käyttäen. Kannattaa luoda tapahtumankuuntelija siten, että sille annetaan konstruktorissa kaikki tarvitut oliot.
Huomaa, että valintaikkunan voi myös sulkea valitsematta tiedostoa!
Kun tiedosto on avattu, tulee käyttöliittymän näyttää esimerkiksi 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, 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.
Tässä tehtävässä tehdään tilastoja keräävä versio tehtävästä 157. Tehtävänäsi on toteuttaa käyttöliittymäluokan Kysely
komponenttien luominen ja vastausten lisääminen Tilasto
-olioon. Käyttöliittymäluokka Kysely
sisältää kokoelman Kysymys
-olioita, jotka sen tulee näyttää käyttäjälle. Käyttäjä voi vastata kysymyksiin, ja lähettää lopuksi vastaukset nappia painamalla. Kun vastaukset lähetetään, ne tulee tilastoida Tilasto
-olioon.
Kysymykset ja tilasto annetaan käyttöliittymäluokalle konstruktorin parametreina.
List<Kysymys> kysymykset = new ArrayList<Kysymys>(); Kysymys kysymys = new Kysymys("Kumpi vai kampi?"); kysymys.lisaaVaihtoehto("Kumpi"); kysymys.lisaaVaihtoehto("Kampi"); kysymykset.add(kysymys); kysymys = new Kysymys("?"); kysymys.lisaaVaihtoehto("A"); kysymys.lisaaVaihtoehto("B"); kysymys.lisaaVaihtoehto("C"); kysymys.lisaaVaihtoehto("D"); kysymykset.add(kysymys); kysymys = new Kysymys("Onko MOOC kiva?"); kysymys.lisaaVaihtoehto("On!"); kysymykset.add(kysymys); SwingUtilities.invokeLater(new Kysely(kysymykset, new Tilasto()));
Käyttöliittymän tulee olla seuraavanlainen yllä annetuilla kysymyksillä.
Muokkaa käyttöliittymäluokan Kysely
toiminnallisuutta siten, että sen ulkonäkö on kuten ylläolevassa kuvassa. Huomaa että jokaisen kysymyksen vastausvaihtoehdot tulee olla omassa ButtonGroup
-ryhmässä, eli jokaiseen kysymykseen saa antaa vain yhden vastauksen.
Kannattanee myös mahdollisesti käyttää useampaa kuin yhtä Container
-oliota. Luokista JPanel
ja BoxLayout
lienee tässä hyötyä.
Huom! Kysymykset ja vaihtoehdot tulee piirtää käyttöliittymään siinä järjestyksessä kun ne annetaan. Vaihtoehdot tulee JRadioButton
-nappuloilla, joista vain yksi (per kysymys) voi olla valittuna.
Luo käyttöliittymän lopussa olevalle napille "Valmis" tapahtumankuuntelija, joka rekisteröi kaikki valitut vastaukset kutsumalla tilasto
-olion tilastoiVastaus
-metodia.
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
. Metodin 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
.
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 59. 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ä erillinen hahmo-olio. 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 jolla kuvioita hallitaan. Pääsemme myöhemmin piirtämään kuvioita ruudulle. Tee kaikki ohjelman luokat pakkaukseen liikkuvakuvio
.
Tee abstrakti luokka Kuvio
. Kuviolla on attribuutit 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 arvo asetetaan konstruktorissa. Konstruktorissa asetetaan myös alkuperäinen sijainti. Ympyra määrittelee metodin piirra
asiaan kuuluvalla tavalla -- käytä parametrina saadun Graphics
-olion fillOval
-metodia.
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. Koostekuvio piirtää itsensä pyytämällä osiaan piirtämään itsensä, koostekuvion siirtyminen tapahtuu samoin.
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 kuvan 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.
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 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.
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).
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.
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 hiukan ikävä, 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ä muuttujaa 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, FM, LuK, FilYO
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ä allaolevan 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
Luodaan seuraavaksi lueteltuja tyyppejä jotka sisältävät oliomuuttujia ja toteuttavat rajapinnan.
Luetellut tyypit voivat sisältää oliomuuttujia. Oliomuuttujien arvot tulee asettaa luetellun tyypin määrittelevän luokan sisäisessä konstruktorissa. Enum-tyyppisillä luokilla ei saa olla public
-konstruktoria.
public enum Vari { PUNAINEN("punainen"), // konstruktorin parametrit määritellään vakioarvoja lueteltaessa VIHREA("vihreä"), SININEN("sininen"); private String nimi; // oliomuuttuja private Vari(String nimi) { // konstruktori this.nimi = nimi; } public String getNimi() { return this.nimi; } }
Lueteltua tyyppiä Vari
voidaan käyttää esimerkiksi seuraavasti:
System.out.println(Vari.VIHREA.getNimi());
vihreä
Luetellut tyypit voivat toteuttaa rajapintoja samalla tavoin kuin muutkin luokat toteuttavat rajapintoja. Luodaan rajapinta Nimetty
public interface Nimetty { String getNimi(); }
ja määritellään luokka Vari
toteuttamaan rajapinta Nimetty
. Rajapinnan toteutus tapahtuu implements
-avainsanalla.
public enum Vari implements Nimetty { PUNAINEN("punainen"), VIHREA("vihreä"), SININEN("sininen"); private String nimi; private Vari(String nimi) { this.nimi = nimi; } @Override public String getNimi() { return nimi; } }
Kun lueteltu tyyppi toteuttaa rajapinnan, on se myös rajapinnan määrittelemää tyyppiä.
Vari vari = Vari.PUNAINEN; Nimetty nimetty = Vari.VIHREA; System.out.println("Vari: " + vari.getNimi()); System.out.println("Nimetty: " + nimetty.getNimi());
Vari: punainen Nimetty: vihreä
Netflix-niminen yritys 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/) -- emme valitettavasti voittaneet.
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 getArvosana()
, joka palauttaa arvioon liittyvän arvosanan. Arviotunnusten ja niihin liittyvien arvosanojen tulee olla seuraavat:
Tunnus Arvosana 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 + ", arvosana " + annettu.getArvosana()); annettu = Arvio.NEUTRAALI; System.out.println("Arvio " + annettu + ", arvosana " + annettu.getArvosana());
Arvio HYVA, arvosana 5 Arvio NEUTRAALI, arvosana 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 Map<Elokuva, List<Arvio>> elokuvienArviot()
palauttaa hajautustaulun, joka sisältää arvioidut elokuvat avaimina. Jokaiseen elokuvaan liittyy lista, joka sisältää elokuvaan lisatyt arviot.public List<Arvio> annaArviot(Elokuva elokuva)
palauttaa elokuvalle lisätyt arviot listana.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ääiseen.
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 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.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 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 ArvioRekisteri
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
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.
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ä taulukosta.
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));
24
Metodille voi määritellä vain yhden parametrin joka saa rajattoman määrän arvoja, ja sen tulee olla metodimäärittelyn viimeinen parametri. Esimerkiksi seuraavanlainen metodimäärittely ei ole sallittu.
public void tulosta(String... merkkijonot, int kertaa)
Seuraava metodimäärittely on sallittu.
public void tulosta(int kertaa, String... merkkijonot)
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.
Tässä tehtäväsarjassa toteutetaan henkilötietopalvelu, johon voi tehdä hakuja käyttäjän määrittelemien hakukriteerien avulla. Tehtäväpohjan mukana tulee graafinen käyttöliittymä, jonka avulla omaa toteutusta voi kokeilla helposti.
Huom: Graafinen käyttöliittymä käynnistyy vasta ensimmäisessä alitehtävässä käsiteltävien PersonAttribute
- ja Gender
-enumeraatioiden luomisen jälkeen.
Lueteltu tyyppi PersonAttribute
Luo pakkaukseen demographics.logic
Attribute
-rajapinnan toteuttava lueteltu tyyppi PersonAttribute
. PersonAttribute määrittelee tiedot, joita henkilöistä talletetaan henkilötietopalveluun. Attribute
-rajapinnassa on seuraavat metodit:
public String getId()
määrittelee attribuutille uniikin, ohjelman koodissa sisäisesti käytettävän merkkijonotunnuksen -- nämä tunnukset ovat tehtävässä ennaltamäärätytpublic String getDisplayName()
määrittelee attribuutille käyttöliittymässä näytettävän merkkijonokuvauksen -- tämän merkkijonon voit määritellä itseMäärittele lueteltuun tyyppiin demographics.logic.PersonAttribute
seuraavat vakiot: (suluissa getId()
-metodin palauttama merkkijono):
SOCIAL_SECURITY_NUMBER
(ssn
) - henkilötunnusAGE
(age
) - ikäGENDER
(gender
) - sukupuoliYEARLY_INCOME
(yearlyIncome
) - vuositulotSTUDENT
(student
) - opiskelija Attribute attr = PersonAttribute.AGE; System.out.println(attr.getId());
age
Lueteltu tyyppi Gender
Luo pakkaukseen demographics.logic
sukupuolta kuvaava lueteltuna tyyppinä Gender
. Tyypin Gender
tulee toteuttaa rajapinta EnumeratedValue
. Rajapinnalla EnumeratedValue
on vastaavat metodit kuin Attribute
-rajapinnalla.
Määrittele lueteltuun tyyppiin Gender
tunnukset (ID suluissa):
MALE
(M
)FEMALE
(F
)EnumeratedValue value = Gender.MALE; System.out.println(value.getId());
M
Luo pakkaukseen demographics.logic
luokka PersonImpl
, joka toteuttaa tehtäväpohjasta löytyvän henkilöä kuvaavan rajapinnan Person
. Henkilöluokka sisältää attribuutteja, joiden arvot kuvaavat henkilön tietoja.
Vinkki! Luokan PersonImpl
sisäinen toteutus kannattaa tehdä esimerkiksi hajautustaulun avulla.
Person
-rajapinnalla on seuraavat metodit:
Object get(Attribute attribute)
palauttaa parametriksi annetun attribuutin arvonvoid set(Attribute attribute, Object value)
asettaa attribuutin arvonCollection<Attribute> getDefinedAttributes()
palauttaa henkilölle määriteltyjen attribuuttien tunnuksetPersonImpl
-luokkaa voi käyttää esimerkiksi näin:
Person person = new PersonImpl(); person.set(PersonAttribute.SOCIAL_SECURITY_NUMBER, "190481-1871"); person.set(PersonAttribute.GENDER, Gender.FEMALE); person.set(PersonAttribute.AGE, 28); person.set(PersonAttribute.YEARLY_INCOME, 27580); person.set(PersonAttribute.STUDENT, true); System.out.println(person.get(PersonAttribute.YEARLY_INCOME)); System.out.println(person.get(PersonAttribute.STUDENT));
Ohjelma tulostaa:
27580 true
Jotta hakukriteereitä voisi käyttää järkevästi, täytyy niitä voida yhdistellä melko vapaasti. Tätä varten tehtäväpohjassa on runko luokalle CriteriaBuilderImpl
, joka toteuttaa CriteriaBuilder
-rajapinnan. Luokan avulla voi luoda predikaatteja. Predikaatti sisältää tiedot hakukriteeristä tai niiden yhdistelmästä ja sen avulla voidaan selvittää onko henkilö kriteerin mukainen vai ei.
Predikaatti on määritelty tehtäväpohjassa Predicate
-rajapintana:
boolean matches(Person person)
palauttaa true
, jos parametriksi annettu henkilö on predikaatin sisältämien kriteerien mukainen, muutoin metodi palauttaa false
package demographics.logic; public interface Predicate { boolean matches(Person person); }
CriteriaBuilder
-rajapinnan koodi:
package demographics.logic; public interface CriteriaBuilder { Predicate allOf(Predicate... predicates); Predicate oneOf(Predicate... predicates); Predicate not(Predicate predicate); Predicate equalTo(Attribute attribute, Object value); Predicate greaterThan(Attribute attribute, int value); Predicate lessThan(Attribute attribute, int value); Predicate greaterThanOrEqualTo(Attribute attribute, int value); Predicate lessThanOrEqualTo(Attribute attribute, int value); }
Toteutetaan CriteriaBuilderImpl
-luokan runkoon metodi equalTo
. Muut metodit voi jättää vielä toteuttamatta, koska niitä tarvitaan vasta myöhemmin.
Predicate equalTo(Attribute attribute, Object value)
palauttaa predikaatin ehdon, joka toteutuu kun henkilöllä olevan attribuutin arvo vastaa parametrina mainittua arvoaMetodia equalTo
varten tarvitset uuden Predicate
-rajapinnan toteuttavan luokan. Toteuta uusi predikaattiluokka siten, että se saa konstruktorissa parametrina Attribute
-rajapinnan toteuttavan attribuutin, sekä halutun arvon. Predikaatin metodin matches
tulee palauttaa true
jos sille parametrina annettu Person
-luokan ilmentymän arvot sopivat predikaattiluokassa määriteltyyn attribuuttiin ja sen arvoon.
Palauta uusi predikaattiluokan ilmentymä CriteriaBuilderImpl
-luokan metodissa equalTo
metodin saamilla parametreilla.
Kun ensimmäinen predikaatti on toteutettu, sen toteuttamaa ehtoa voi kokeilla matches()
-metodin avulla esimerkiksi seuraavalla tavalla:
Person person1 = new PersonImpl(); person1.set(PersonAttribute.SOCIAL_SECURITY_NUMBER, "213821871"); person1.set(PersonAttribute.GENDER, Gender.FEMALE); person1.set(PersonAttribute.AGE, 28); person1.set(PersonAttribute.YEARLY_INCOME, 27580); person1.set(PersonAttribute.STUDENT, true); CriteriaBuilder is = new CriteriaBuilderImpl(); System.out.println(is.equalTo(PersonAttribute.STUDENT, false).matches(person1)); System.out.println(is.equalTo(PersonAttribute.GENDER, Gender.FEMALE).matches(person1));
Ohjelma tulostaa:
false true
Henkilötietojen tutkimiseen tarvitaan tiedoille säilytyspaikka, jota varten tehtävässä luodaan henkilörekisteri. Täydennä luokkaa PersonRegistryImpl
siten, että se toteuttaa rajapinnan PersonRegistry
alla kuvatusti.
void add(Person person)
lisää henkilön rekisteriinvoid remove(Person person)
poistaa henkilön rekisteristävoid clear()
tyhjentää rekisterinList<Person> getPersons()
palauttaa rekisterissä olevat henkilötList<Person> query(Predicate predicate)
palauttaa kaikki rekisterin henkilöt, kun parametrina annettu predikaatti on null
-- tässä vaiheessa ei vielä tarvitse toteuttaa muuta toiminnallisuutta metodilleList<Person> remove(Predicate predicate)
palauttaa tyhjän listan, kun parametrina annettu predikaatti on null
-- tässä vaiheessa ei vielä tarvitse toteuttaa muuta toiminnallisuutta metodilleCriteriaBuilder getCriteriaBuilder()
palauttaa CriteriaBuilder
-olion, jolla voi luoda predikaatteja -- metodi palauttaa aina saman CriteriaBuilder-olionpackage demographics.logic; import java.util.List; public interface PersonRegistry { void add(Person person); void remove(Person person); void clear(); List<Person> getPersons(); List<Person> query(Predicate predicate); List<Person> remove(Predicate predicate); CriteriaBuilder getCriteriaBuilder(); }
Laajenna PersonRegistryImpl
-henkilörekisteriluokan metodeita seuraavasti:
List<Person> query(Predicate predicate)
palauttaa rekisterin henkilöt, joiden attribuutit toteuttavat annetun predikaatin ehdon -- aiemmin toteutettu toiminnallisuus parametrin null
-arvolla tulee myös säilyttääList<Person> remove(Predicate predicate)
hakee rekisteristä henkilöt, joiden attribuutit toteuttavat annetun predikaatin ehdon. Löydetyt henkilöt poistetaan rekisteristä ja palautetaan listassa -- aiemmin toteutettu toiminnallisuus parametrin null
-arvolla tulee myös säilyttääHenkilörekisteriin tulee myös pystyä tehdä hakuja yhtäsuuruuspredikaatin avulla esimerkiksi seuraavalla tavalla:
Person person1 = new PersonImpl(); person1.set(PersonAttribute.SOCIAL_SECURITY_NUMBER, "213821871"); person1.set(PersonAttribute.GENDER, Gender.FEMALE); person1.set(PersonAttribute.AGE, 28); person1.set(PersonAttribute.YEARLY_INCOME, 27580); person1.set(PersonAttribute.STUDENT, true); Person person2 = new PersonImpl(); person2.set(PersonAttribute.SOCIAL_SECURITY_NUMBER, "98423874"); person2.set(PersonAttribute.GENDER, Gender.MALE); person2.set(PersonAttribute.AGE, 54); person2.set(PersonAttribute.YEARLY_INCOME, 45200); person2.set(PersonAttribute.STUDENT, false); PersonRegistry registry = new PersonRegistryImpl(); registry.add(person1); registry.add(person2); CriteriaBuilder is = registry.getCriteriaBuilder(); List<Person> results1 = registry.query(is.equalTo(PersonAttribute.STUDENT, false)); List<Person> results2 = registry.query(is.equalTo(PersonAttribute.GENDER, Gender.FEMALE)); List<Person> removed = registry.remove(is.equalTo(PersonAttribute.GENDER, Gender.MALE));
allOf
-predikaatin toteuttaminenJotta hakuehtojen käyttö olisi mielekästä, täytyy niitä voida yhdistellä. allOf
-predikaatti yhdistää kaikki sille parametreina annetut predikaatit siten, että kaikkien predikaattien ehtojen on toteuduttava, jotta yhdistelmäpredikaatin ehto toteutuisi.
Esimerkiksi seuraava predikaatti toteutuu vain, jos henkilö on mies ja hän ei ole opiskelija:
CriteriaBuilder is = registry.getCriteriaBuilder(); Predicate predicate = is.allOf(is.equalTo(PersonAttribute.STUDENT, false), is.equalTo(PersonAttribute.Gender, Gender.MALE));
Toteuta CriteriaBuilderImpl
-luokalle allOf-predikaatti:
Predicate allOf(Predicate... predicates)
palauttaa JA-ehdon mukaisen predikaatin, jolloin predikaatin ehto toteutuu vain jos kaikkien parametriksi annettujen predikaattien ehdot toteutuvatHuom: allOf-predikaatin toteuttamisen jälkeen käyttöliittymän Filter...- ja Remove...-ominaisuudet alkavat toimia. Tässä vaiheessa voit tosin käyttää vasta yhtäsuuruusehtoa (=) ja "matching"-vaihtoehtoa (eli "non-matching" ei toimi), koska nämä toiminnot toteutetaan vasta myöhemmin.
Vinkki! Tässä kohdassa kannattaa esimerkiksi muistella miten tehtävän 164 koostekuvio koostui...
Toteuta CriteriaBuilderImpl
-luokalle seuraavat metodit:
Predicate greaterThan(Attribute attribute, Object value)
palauttaa predikaatin ehdon, joka toteutuu kun henkilöllä olevan attribuutin kokonaislukuarvo on suurempi kuin parametrina mainittu arvoPredicate lessThan(Attribute attribute, Object value)
palauttaa predikaatin ehdon, joka toteutuu kun henkilöllä olevan attribuutin kokonaislukuarvo on pienempi kuin parametrina mainittu arvoPredicate greaterThanOrEqualTo(Attribute attribute, Object value)
palauttaa predikaatin ehdon, joka toteutuu kun henkilöllä olevan attribuutin kokonaislukuarvo on yhtä suuri tai suurempi kuin parametrina mainittu arvoPredicate lessThanOrEqualTo(Attribute attribute, Object value)
palauttaa predikaatin ehdon, joka toteutuu kun henkilöllä olevan attribuutin kokonaislukuarvo on yhtä suuri tai pienempi kuin parametrina mainittu arvoHenkilörekisteriin voi tehdä hakuja kokonaislukujen vertailupredikaateilla esimerkiksi näin:
Person person1 = new PersonImpl(); person1.set(PersonAttribute.SOCIAL_SECURITY_NUMBER, "94726823"); person1.set(PersonAttribute.GENDER, Gender.FEMALE); person1.set(PersonAttribute.AGE, 37); person1.set(PersonAttribute.YEARLY_INCOME, 17500); person1.set(PersonAttribute.STUDENT, false); Person person2 = new PersonImpl(); person2.set(PersonAttribute.SOCIAL_SECURITY_NUMBER, "38271623"); person2.set(PersonAttribute.GENDER, Gender.MALE); person2.set(PersonAttribute.AGE, 60); person2.set(PersonAttribute.YEARLY_INCOME, 50000); person2.set(PersonAttribute.STUDENT, false); PersonRegistry registry = new PersonRegistryImpl(); registry.add(person1); registry.add(person2); CriteriaBuilder is = registry.getCriteriaBuilder(); List<Person> results1 = registry.query(is.greaterThan(PersonAttribute.AGE, 40)); List<Person> results2 = registry.query(is.lessThanOrEqualTo(PersonAttribute.YEARLY_INCOME, 50000));
Huom: Nyt voit käyttää käyttöliittymässä vertailuoperaatioita: >, <, >= ja <=.
not
-predikaatin toteuttaminenJatketaan predikaattien toteuttamista. not
-predikaatti yksinkertaisesti vaihtaa sille annetun predikaatin totuusarvon päinvastaiseksi.
not
-predikaatti toimii näin:
CriteriaBuilder is = registry.getCriteriaBuilder(); // Predikaatti toteutuu, jos henkilö ei ole 6-vuotias Predicate predicate1 = is.not(is.equalTo(PersonAttribute.AGE, 6)); // Predikaatti toteutuu, jos henkilö ei ole alle 18-vuotias Predicate predicate2 = is.not(is.lessThan(PersonAttribute.AGE, 18)); // Predikaatti toteutuu, jos henkilö ei ole mies Predicate predicate3 = is.not(is.equalTo(PersonAttribute.GENDER, Gender.MALE));
Toteuta CriteriaBuilderImpl
-luokalle metodi:
Predicate not(Predicate predicate)
kääntää parametriksi annetun predikaatin ehdon, eli kun annetun predikaatin ehto toteutuu, not-predikaatti ei toteudu, ja toisinpäinHuom: not
-predikaatin toteuttamisen jälkeen käyttöliittymän kaikki toiminnot ovat käytettävissä.
oneOf
-predikaatin toteuttaminenoneOf
-predikaatin ehto toteutuu, jos vähintään yksi sille annetuista predikaateista toteutuu.
Esimerkiksi seuraava oneOf
-predikaatti toteutuu, jos henkilö on joko nainen (opiskelijastatuksella ei ole väliä) tai opiskelija (jolloin sukupuolella ei ole väliä):
CriteriaBuilder is = registry.getCriteriaBuilder(); Predicate predicate = is.oneOf(is.equalTo(PersonAttribute.STUDENT, true), is.equalTo(PersonAttribute.Gender, Gender.FEMALE));
Predikaatteja voidaan myös yhdistellä lähes mielivaltaisesti. Edellinen ehto voidaan kääntää päinvastaiseksi not
-predikaatilla, jolloin se toteutuu vain, jos henkilö ei ole nainen eikä opiskelija:
CriteriaBuilder is = registry.getCriteriaBuilder(); Predicate predicate = is.not(is.oneOf(is.equalTo(PersonAttribute.STUDENT, true), is.equalTo(PersonAttribute.Gender, Gender.FEMALE)));
Toteuta CriteriaBuilderImpl
-luokalle metodi:
Predicate oneOf(Predicate... predicates)
palauttaa TAI-ehdon mukaisen predikaatin, jolloin predikaatin ehto toteutuu, jos parametriksi annettujen predikaattien ehdoista ainakin yksi toteutuuHuom: oneOf
-predikaattia ei voi testata käyttöliittymän kautta.
Luodaan ohjelma, joka mittaa kliksutteluvauhtia. Käyttöliittymä tulee näyttämään esimerkiksi seuraavalta.
Toteuta pakkaukseen nopeustesti
luokka Nappi
, joka perii JButtonin. Luokalla Nappi
tulee olla konstruktori public Nappi(String text, Color aktiivinen, Color passiivinen)
. Konstruktorin parametrina saama merkkijono text
tulee antaa parametrina yläluokan konstruktorille (kutsu super(text)
).
Korvaa luokasta JButton peritty metodi protected void paintComponent(Graphics g)
siten, että piirrät metodissa napin kokoisen värillisen ympyrän. Saat napin leveyden ja korkeuden JButton-luokalta perityistä metodeista getWidth()
ja getHeight()
. Kutsu korvatun metodin alussa yläluokan paintComponent
-metodia.
Ympyrän värin tulee riippua Napin tilasta: jos nappi on aktiivinen (metodi isEnabled
palauttaa true
tulee ympyrän väri olla konstruktorin parametrina saatu aktiivinenVari
. Muulloin käytetään väriä passiivinenVari
.
Toteuta luokkaan Nopeustesti
käyttöliittymä, jossa on neljä nappulaa ja teksti. Käytä asettelussa napeille omaa JPanel-alustaa, joka asetetaan BorderLayout-asettelijan keskelle. Teksti tulee BorderLayout-asettelijan alaosaan.
Käytä edellisessä osassa luomaasi Nappi
-luokkaa. Napeille tulee antaa konstruktorissa tekstit 1, 2, 3 ja 4.
Vain yhden nappulan kerrallaan tulee olla painettavissa (eli aktiivisena). Voit tehdä nappulasta ei-aktiivisen metodikutsulla nappi.setEnabled(false)
. Vastaavasti nappi muutetaan aktiiviseksi kutsulla nappi.setEnabled(true)
.
Kun aktiivisena olevaa nappulaa painetaan, tulee käyttöliittymän arpoa uusi aktiivinen nappi.
Tehdään peliin pisteytys: mitataan 20 painallukseen kuluva aika. Helpoin tapa ajan mittaamiseen on metodin System.currentTimeMillis()
kutsuminen. Metodi palauttaa kokonaisluvunu, joka laskee millisekunteja (tuhannesosasekunteja) jostain tietysti ajanhetkestä lähtien. Siispä voit mitata kulunutta aikaa kutsumalla currentTimeMillis
pelin alussa ja lopussa ja laskemalla erotuksen.
Toteuta siis seuraava: peli laskee napinpainallusten määrän, ja 20. painalluksen jälkeen asettaa kaikki nappulat epäaktiivisiksi ja näyttää JLabel
-komponentissa viestin "Pisteesi: XXXX"
, jossa XXXX
on painalluksiin kulunut aika (millisekunteina) jaettuna 20:lla. Pienempi pistemäärä on siis parempi.
Tässä tehtävässä luodaan rakenteet ja osa toiminnallisuudesta seuraavannäköiseen matopeliin.
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ä.
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 List<Pala> getPalat()
palauttaa listan pala-olioita, joista mato koostuu.public int getPituus()
palauttaa madon pituuden. Madon pituuden tulee olla sama kuin getPalat()
-metodikutsun palauttaman listan alkioiden määrä.public void liiku()
liikuttaa matoa yhden palan verran eteenpäin.public void kasva()
kasvattaa madon kokoa yhdellä. Madon kasvaminen tapahtuu seuraavan liiku
-metodikutsun yhteydessä.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 listalle, ja viimeisin poistetaan listan lopusta. Tällöin jokaisen palan koordinaatteja ei tarvitse päivittää erikseen. Toteuta kasvaminen siten, että palaa viimeisintä palaa 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)] [(6,5), (5,5)] [(7,5), (6,5), (5,5)] [(8,5), (7,5), (6,5)] [(8,5), (7,5), (6,5)] [(9,5), (8,5), (7,5), (6,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
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
siten, että piirrät metodissa paintComponent
madon ja omenan. Käytä madon piirtämiseen Graphics-olion tarjoamaa fill3DRect
-metodia. Madon värin tulee olla valkoinen (Color.WHITE
). Omenan piirtämisessä tulee käyttää Graphics-olion tarjoamaa fillOval
-metodia. Omenan värin tulee olla punainen (Color.RED
).
Toteuta luokalla Piirtoalusta
myös rajapinta Paivitettava
. Paivitettava-rajapinnan määrittelemän metodin paivita
tulee kutsua JPanel-luokan 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();