Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.
Tekijät: Arto Vihavainen ja Matti Luukkainen
Tässä osallistut taas kokeellisessa psykologiassa käytettyyn tehtävänvaihtotestiin, missä tutkitaan aivojen mukautumista kahden tai useamman samanaikaisen tehtävän suorittamiseen. Tavoitteenamme on selvittää vaikuttaako ohjelmointi aivojen kykyyn hoitaa useampia samanaikaisia tehtäviä. Tämä testi tehdään useamman kerran kurssin aikana.
Käytössä olevassa testiohjelmassa on neljä osaa. Ensimmäisessä osassa mitataan reaktioaikaa, toisessa osassa parittoman (odd) ja parillisen (even) luvun tunnistamista, kolmannessa osassa konsonantin (consonant) ja vokaalin (vowel) tunnistamista, ja viimeisessä osassa yhdistetään osat kaksi ja kolme. Näet jokaisen osion jälkeen edellisen osion tulokset.
Testin tulokset tulevat vain tutkimuskäyttöön, eikä mahdollisesti raportoitavista tuloksista voida yksilöidä yksittäisiä vastaajia. Tehtävän suorittamiseen menee noin 10-15 minuuttia.
Kun olet vastannut allaolevaan lomakkeeseen, testi aukeaa uudessa ikkunassa. Varmistathan, että selaimesi sallii uusien ikkunoiden avaamisen tästä ohjelmointimateriaalista!
Muutamia kertaus- ja pähkinätehtäviä
Kurssin kymmenes viikko alkaa kertaus- ja pähkinätehtävillä, joilla pääset muistelemaan aiemmin käsiteltyjä asioita. Tehtävissä ei ole automaattisia testejä -- mieti, miten varmistat ohjelman toiminnallisuuden itse!
Huom! Kirjoita jokaiseen tehtävään kaksi ratkaisua, missä ratkaiset ongelman eri tavoilla. Ratkaisutapa lasketaan erilaiseksi jos ainakin osa ohjelmakoodista on toteutettu eri tavalla. Muuttujien nimien muuttamista ei lasketa eri tavoiksi, mutta ratkaisun pilkkominen useammaksi metodiksi tai täysin erilainen ongelmanratkaisulogiikka taas kelpaa.
Painoindeksi on mitta-arvo, jonka avulla voidaan arvioida ihmisen painon ja pituuden suhdetta. Painoindeksi lasketaan kaavalla:
Painoindeksi = paino / (pituus * pituus)
Painoindeksiä käytetään muunmuassa ali- ja ylipainon tunnistamisessa. Jos henkilön painoindeksi on alle 18.5 luokitellaan hänet alipainoiseksi ("alipaino"). Jos painoindeksi on vähintään 18.5 mutta alle 25, on luokittelu "normaali". Jos taas painoindeksi on vähintään 25 mutta alle 30, on luokittelu "ylipainoinen". Jos taas painoindeksi on vähintään 30, on luokittelu "merkittävästi ylipainoinen".
Luokissa Raportinluoja1
ja Raportinluoja2
on metodi public PainoindeksiRaportti painoindeksiRaportti(List<Henkilo> henkilotiedot)
, joka saa parametrina listan henkilöitä ja palauttaa painoindeksiraportin -- kannattaa tutustua luokkiin Painoindeksiraportti
ja Henkilo
.
Luo luokkiin Raportinluoja1
ja Raportinluoja2
erilaiset toteutukset metodille painoindeksiRaportti
painoindeksiraporttien luomiseen.
Tuotettavan painoindeksiraportin tulee sisältää lista nimistä (huom! ei henkilöistä) siten, että henkilöt ovat kategorisoitu heihin sopiviin painoindeksiluokkiin.
Testaa toteutustasi ennen sen palautusta. Koska tehtävässä ei ole automaattisia testejä, mieti myös miten ja minkälaisilla syötteillä testaat sen toimintaa.
Data-analytiikassa mittausten tasoittamisella tarkoitetaan liiallisen kohinan tai muiden häiriöiden poistamiseen datasta, jonka jälkeen oleellisten hahmojen tunnistamista datasta tulee mahdollisesti helpommaksi. Eräs suoraviivainen tekniikka mittausten tasoitukseen on muuttaa jokainen mittausarvo sen, sitä edeltävän mittausarvon ja sitä seuraavan mittausarvon mittausten keskiarvoksi. Jos oletamme, että poikkeukselliset arvot ovat häiriö mittadatassa, tämä keskiarvomenetelmä tasaa arvot potentiaalisesti luotettavimmiksi arvoiksi.
Tutkitaan esimerkiksi seuraavia sykemittauksia, jotka on kerätty henkilötietodatasta.
95 102 98 88 105
Jos ylläolevan mittausdatan tasaa keskiarvomenetelmällä, on tasauksen tuottama data seuraavanlainen:
95 98.33 96 97 105
Tässä:
Luokissa MittaustenTasoittaja1
ja MittaustenTasoittaja2
on metodi public List<Double> tasoita(List<Henkilo> henkilotiedot)
, joka saa parametrina listan henkilö-olioita (henkilöiden nimillä ei ole väliä, oleellista on sykemittausdata -- muuttuja syke
) ja palauttaa listan tasattuja sykemittauksia -- luokka Henkilo
on tässä sama kuin edellisessä tehtävässä.
Luo luokkiin MittaustenTasoittaja1
ja MittaustenTasoittaja2
erilaiset toteutukset metodille tasoita
listana annettujen henkilo-olioihin tallennettujen sykemittausten tasoittamiseen. Toteutusten tulee siis käsitellä lista henkilötietueita, joista jokaisessa on sykemittaus, ja palauttaa lista double-arvoja, jotka ovat tasoitettuja sykemittauksia.
Luokissa YleisimmatSanat1
ja YleisimmatSanat2
on metodi public List<String> yleisetSanat(List<String> sanat)
, joka saa parametrina listan merkkijonoja ja palauttaa listan merkkijonoja.
Luo luokkiin YleisimmatSanat1
ja YleisimmatSanat2
erilaiset toteutukset kolmen yleisimmän merkkijonon tunnistamiseen. Yleisimmät merkkijonot tulee tunnistaa metodille yleisetSanat
syötteeksi annetusta listasta, ja metodin tulee palauttaa yleisimmät merkkijonot listassa. Palauttava lista tulee olla järjestettynä siten, että listan ensimmäisenä alkiona on yleisin merkkijono, toisena alkiona on toiseksi yleisin merkkijono, ja kolmantena alkiona on kolmanneksi yleisin merkkijono.
Jos merkkijonot ovat yhtä yleisiä, aseta lyhin sana (vähiten merkkejä) ennen pidempää sanaa. Voit olettaa, että syötteen kolme yleisintä sanaa ovat eri pituisia. Voit lisäksi olettaa, että syötteessä on vähintään kolme eri sanaa.
Testaa toteutustasi ennen sen palautusta. Koska tehtävässä ei ole automaattisia testejä, mieti myös miten ja minkälaisilla syötteillä testaat sen toimintaa.
Geologit haluavat tarkastella paikallisen vuoren mahdollista maanjäristystoimintaa. He ovat asentaneet mittarin seismisen toiminnan (maan tärinän) mittaamiseen. Mittari lukee seismistä toimintaa tietyin aikavälein ja lähettää mitattua dataa mittausarvo kerrallaan tutkimuslaboratorion tietokoneelle.
Mittari lisää lisäksi mittausdataan päivämäärätietoja näyttämään seismisen toiminnan mittauspäivää. Mittarin lähettämä data on seuraavassa muodossa:
20151004 200 150 175 20151005 0.002 0.03 20151007 ...
Kahdeksanlukuiset arvot ovat päivämääriä (vuosi-kuukausi-päivä -muodossa) ja numerot nollan ja viidensadan välillä ovat värähtelyjen taajuuksia (hertzeinä). Ylläoleva esimerkki näyttää mittaukset 200, 150, ja 175 lokakuun neljäntenä päivänä vuonna 2015 ja mittaukset 0.002 ja 0.03 lokakuun viidentenä päivänä vuonna 2015. Lokakuun kuudennelta päivältä ei ole lainkaan mittausdataa (välillä verkkoyhteydessä on ongelmia, jolloin mittausdataa saattaa kadota).
Oleta, että mittausdata on järjestetty päivämäärien mukaan (myöhempi päivämäärä ei ikinä ilmesty datassa ennen aiempaa päivämäärää) ja että kaikki data on samalta vuodelta. Voit myös olettaa, että jokaiselta datassa olevalta päivältä on vähintään yksi mittausarvo.
Luokissa MittausRaportoija1
ja MittausRaportoija2
on tyhjä metodi List<SuurinTaajuusRaportti> paivittaisetMaksimit(List<Double> mittausData, int kuukausi)
, joka saa parametrina listan mittausdataa sekä kuukauden (oleta, että yksi (01) vastaa tammikuuta ja kaksitoista (12) vastaa joulukuuta). Metodin tulee tuottaa lista raportteja, joista jokainen sisältää suurimman mittaustuloksen kuukauden yksittäiselle päivälle, josta löytyy mittausdataa.
Suunnittele ja toteuta kaksi erilaista toteutusta metodille paivittaisetMaksimit
ja toteuta ne luokkiin MittausRaportoija1
ja MittausRaportoija2
. Metodin tulee siis käsitellä lista Double-muotoisia syötteitä, joista löytyy sekä päivämääriä että mittausarvoja. Metodin tulee käsitellä vain parametrina annettuun kuukauteen liittyviä arvoja, ja syötteiden perusteella tulee tunnistaa jokaiselle parametrina annetulle kuukauden päivälle suurin päiväkohtainen arvo. Suurimmat päiväkohtaiset arvot asetetaan palautettavaan listaan SuurinTaajuusRaportti
-muotoisina olioina, ja metodi palauttaa lopulta listan tulevaa käsittelyä varten.
Kuten edellisissä tehtävissä, testaa tässäkin toteutustasi ennen sen palautusta. Koska tehtävässä ei ole automaattisia testejä, mieti myös miten ja minkälaisilla syötteillä testaat sen toimintaa.
Kun olet tehnyt neljä edellistä tehtävää (tai ainakin yrittänyt tehdä kaikkia neljää tehtävää), vastaa vielä osoitteessa http://goo.gl/forms/VZ2yyRNUVB olevaan kyselyyn.
Isompia ohjelmia suunniteltaessa pohditaan usein mille luokalle minkäkin asian toteuttaminen kuuluu. Jos kaikki ohjelmaan kuuluva toiminnallisuus asetetaan samaan luokkaan, on tuloksena väistämättä kaaos. Ohjelmistojen suunnittelun osa-alue oliosuunnittelu sisältää periaatteen Single responsibility principle, jota meidän kannattaa seurata.
Single responsibility principle sanoo että jokaisella luokalla tulee olla vain yksi vastuu ja selkeä tehtävä. Jos luokalla on yksi selkeä tehtävä, on tehtävässä tapahtuvien muutosten toteuttaminen helppoa, muutos tulee tehdä vain yhteen luokkaan. Jokaisella luokalla tulisi olla vain yksi syy muuttua.
Tutkitaan seuraavaa luokkaa Tyontekija
, jolla on metodit palkan laskemiseen ja tuntien raportointiin.
public class Tyontekija { // oliomuuttujat // työntekijään liittyvät konstruktorit ja metodit public double laskePalkka() { // palkan laskemiseen liittyvä logiikka } public String raportoiTunnit() { // työtuntien raportointiin liittyvä logiikka } }
Vaikka yllä olevasta esimerkistä puuttuvat konkreettiset toteutukset, tulisi hälytyskellojen soida. Luokalla Tyontekija
on ainakin kolme eri vastuualuetta. Se kuvaa sovelluksessa työntekijää, se toteuttaa palkanlaskennan tehtävää palkan laskemisesta, ja tuntiraportointijärjestelmän tehtävää työtuntien raportoinnista. Yllä oleva luokka tulee pilkkoa kolmeen osaan: yksi osa kuvaa työntekijää, toinen osa palkanlaskentaa ja kolmas osa tuntikirjanpitoa.
public class Tyontekija { // oliomuuttujat // työntekijään liittyvät konstruktorit ja metodit }
public class Palkanlaskenta { // oliomuuttujat // palkanlaskentaan liittyvät metodit public double laskePalkka(Henkilo henkilo) { // palkan laskemiseen liittyvä logiikka } }
public class Tuntikirjanpito { // oliomuuttujat // tuntikirjanpitoon liittyvät metodit public String luoTuntiraportti(Henkilo henkilo) { // työtuntien raportointiin liittyvä logiikka } }
Jokainen muuttuja, jokainen koodirivi, jokainen metodi, jokainen luokka, ja jokainen ohjelma pitäisi olla vain yhtä tarkoitusta varten. Usein ohjelman "parempi" rakenne on ohjelmoijalle selkeää vasta kun ohjelma on toteutettu jo kertaalleen. Tämä on täysin hyväksyttävää: vielä tärkeämpää on se, että ohjelmaa pyritään muuttamaan aina selkeämpään suuntaan. Refaktoroi eli muokkaa ohjelmaasi aina tarpeen tullen!
Suurempia ohjelmia suunniteltaessa ja toteutettaessa luokkamäärä kasvaa helposti suureksi. Luokkien määrän kasvaessa niiden tarjoamien toiminnallisuuksien ja metodien muistaminen vaikeutuu. Järkevä luokkien nimeäminen helpottaa toiminnallisuuksien muistamista. Järkevän nimennän lisäksi lähdekooditiedostot kannattaa jakaa toiminnallisuutta, käyttötarkoitusta tai jotain muuta loogista kokonaisuutta kuvaaviin pakkauksiin. Pakkaukset (package) ovat käytännössä hakemistoja, joihin lähdekooditiedostot organisoidaan. Windowsissa ja puhekielessä hakemistoja (engl. directory) kutsutaan usein kansioiksi. Me käytämme kuitenkin termiä hakemisto.
Ohjelmointiympäristöt tarjoavat valmiit työkalut pakkausten hallintaan. Olemme tähän mennessä luoneet luokkia ja rajapintoja vain projektiin liittyvän lähdekoodipakkaukset-osion (Source Packages
) oletuspakkaukseen (default package
). Uuden pakkauksen voi luoda NetBeansissa projektin pakkauksiin liittyvässä Source Packages
-osiossa oikeaa hiirennappia painamalla ja valitsemalla New -> Java Package...
. Luodun pakkauksen sisälle voidaan luoda luokkia aivan kuten oletuspakkaukseenkin (default package
).
Pakkaus, jossa luokka sijaitsee, näkyy lähdekooditiedoston alussa ennen muita komentoja olevasta lauseesta package pakkaus
. Esimerkiksi alla oleva luokka Sovellus
sijaitsee pakkauksessa kirjasto
.
package kirjasto; public class Sovellus { public static void main(String[] args) { System.out.println("Hello packageworld!"); } }
Pakkaukset voivat sisältää pakkauksia. Esimerkiksi pakkausmäärittelyssä package kirjasto.domain
pakkaus domain
on pakkauksen kirjasto
sisällä. Asettamalla pakkauksia pakkausten sisään rakennetaan sovelluksen luokille ja rajapinnoille hierarkiaa. Esimerkiksi kaikki Javan luokat sijaitsevat pakkauksen java
alla olevissa pakkauksissa. Pakkausnimeä domain
käytetään usein kuvaamaan sovellusalueen käsitteisiin liittyvien luokkien säilytyspaikkaa. Esimerkiksi luokka Kirja
voisi hyvin olla pakkauksen kirjasto.domain
sisällä, sillä se kuvaa kirjastosovellukseen liittyvää käsitettä.
package kirjasto.domain; public class Kirja { private String nimi; public Kirja(String nimi) { this.nimi = nimi; } public String getNimi() { return this.nimi; } }
Pakkauksissa olevia luokkia tuodaan luokan käyttöön import
-lauseen avulla. Esimerkiksi kirjasto
-pakkauksessa oleva luokka Sovellus
saisi käyttöönsä pakkauksessa kirjasto.domain
olevan luokan määrittelyllä import kirjasto.domain.Kirja
.
package kirjasto; import kirjasto.domain.Kirja; public class Sovellus { public static void main(String[] args) { Kirja kirja = new Kirja("pakkausten ABC!"); System.out.println("Hello packageworld: " + kirja.getNimi()); } }
Hello packageworld: pakkausten ABC!
Import-lauseet asetetaan lähdekooditiedostossa pakkausmäärittelyn jälkeen mutta ennen luokkamäärittelyä. Niitä voi olla myös useita -- esimerkiksi kun haluamme käyttää useita luokkia. Javan valmiit luokat sijaitsevat yleensä ottaen pakkauksen java
alipakkauksissa. Luokkiemme alussa usein esiintyvät lauseet import java.util.ArrayList
ja import java.util.Scanner;
alkavat nyt toivottavasti vaikuttaa merkityksellisimmiltä.
Jatkossa kaikissa tehtävissämme käytetään pakkauksia. Luodaan seuraavaksi ensimmäiset pakkaukset itse.
Luo projektipohjaan pakkaus mooc
. Rakennetaan tämän pakkauksen sisälle sovelluksen toiminta. Lisää sovellukseen pakkaus ui
(tämän jälkeen pitäisi olla käytössä pakkaus mooc.ui
), ja lisää sinne rajapinta Kayttoliittyma
.
Rajapinnan Kayttoliittyma
tulee määritellä metodi void paivita()
.
Luo samaan pakkaukseen luokka Tekstikayttoliittyma
, joka toteuttaa rajapinnan Kayttoliittyma
. Toteuta luokassa Tekstikayttoliittyma
rajapinnan Kayttoliittyma
vaatima metodi public void paivita()
siten, että sen ainut tehtävä on merkkijonon "Päivitetään käyttöliittymää
"-tulostaminen System.out.println
-metodikutsulla.
Luo tämän jälkeen pakkaus mooc.logiikka
, ja lisää sinne luokka Sovelluslogiikka
. Sovelluslogiikan APIn tulee olla seuraavanlainen.
public Sovelluslogiikka(Kayttoliittyma kayttoliittyma)
import mooc.ui.Kayttoliittyma;
public void suorita(int montaKertaa)
montaKertaa
-muuttujan määrittelemän määrän merkkijonoa "Sovelluslogiikka toimii". Jokaisen "Sovelluslogiikka toimii"-tulostuksen jälkeen tulee kutsua konstruktorin parametrina saadun rajapinnan Kayttoliittyma
-toteuttaman olion määrittelemää paivita()
-metodia.Voit testata sovelluksen toimintaa seuraavalla pääohjelmaluokalla.
import mooc.logiikka.Sovelluslogiikka; import mooc.ui.Kayttoliittyma; import mooc.ui.Tekstikayttoliittyma; public class Main { public static void main(String[] args) { Kayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(); new Sovelluslogiikka(kayttoliittyma).suorita(3); } }
Ohjelman tulostuksen tulee olla seuraava:
Sovelluslogiikka toimii Päivitetään käyttöliittymää Sovelluslogiikka toimii Päivitetään käyttöliittymää Sovelluslogiikka toimii Päivitetään käyttöliittymää
Kaikki NetBeansissa näkyvät projektit ovat tietokoneesi tiedostojärjestelmässä. Jokaiselle projektille on olemassa oma hakemisto (eli kansio), jonka sisällä on projektiin liittyvät tiedostot ja hakemistot.
Projektin hakemistossa src
on ohjelmaan liittyvät lähdekoodit. Jos luokan pakkauksena on kirjasto, sijaitsee se projektin lähdekoodihakemiston src
sisällä olevassa hakemistossa kirjasto
. Jos olet kiinnostunut, NetBeansissa voi käydä katsomassa projektien konkreettista rakennetta Files-välilehdeltä joka on normaalisti Projects-välilehden vieressä. Jos et näe välilehteä Files, saa sen näkyville valitsemalla vaihtoehdon Files valikosta Window.
Sovelluskehitystä tehdään normaalisti Projects-välilehdeltä, jossa NetBeans on piilottanut projektiin liittyviä tiedostoja joista ohjelmoijan ei tarvitse välittää.
Olemme aiemmin tutustuneet kahteen näkyvyysmääreeseen. Näkyvyysmääreellä private
varustetut metodit ja muuttujat ovat näkyvissä vain sen luokan sisällä joka määrittelee ne. Niitä ei voi käyttää luokan ulkopuolelta. Näkyvyysmääreellä public
varustetut metodit ja muuttujat ovat taas kaikkien käytettävissä.
package kirjasto.ui; public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { tulostaOtsikko(); // muu toiminnallisuus } private void tulostaOtsikko() { System.out.println("************"); System.out.println("* KIRJASTO *"); System.out.println("************"); } }
Yllä olevasta Kayttoliittyma
-luokasta tehdyn olion konstruktori ja kaynnista
-metodi on kutsuttavissa mistä tahansa ohjelmasta. Metodi tulostaOtsikko
ja lukija
-muuttuja on käytössä vain luokan sisällä.
Pakkausnäkyvyyttä käytettäessä muuttujille tai metodeille ei aseteta mitään näkyvyyteen liittyvää etuliitettä. Muutetaan yllä olevaa esimerkkiä siten, että metodilla tulostaOtsikko
on pakkausnäkyvyys.
package kirjasto.ui; public class Kayttoliittyma { private Scanner lukija; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; } public void kaynnista() { tulostaOtsikko(); // muu toiminnallisuus } void tulostaOtsikko() { System.out.println("************"); System.out.println("* KIRJASTO *"); System.out.println("************"); } }
Nyt saman pakkauksen sisällä olevat luokat voivat käyttää metodia tulostaOtsikko
.
package kirjasto.ui; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner lukija = new Scanner(System.in); Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija); kayttoliittyma.tulostaOtsikko(); // onnistuu! } }
Jos luokka on eri pakkauksessa, ei metodia tulostaOtsikko
pysty käyttämään.
package kirjasto; import java.util.Scanner; import kirjasto.ui.Kayttoliittyma; public class Main { public static void main(String[] args) { Scanner lukija = new Scanner(System.in); Kayttoliittyma kayttoliittyma = new Kayttoliittyma(lukija); kayttoliittyma.tulostaOtsikko(); // ei onnistu! } }
Viime viikolla tutustuimme rajapintoihin. Rajapinta siis määrittelee yhden tai useamman metodin, jotka sen toteuttavan luokan on pakko toteuttaa. Rajapintoja, kuten kaikkia luokkia voi asettaa pakkauksiin. Esimerkiksi seuraava Tunnistettava
-rajapinta sijaitsee pakkauksessa sovellus.domain
, ja määrittelee että Tunnistettava
-rajapinnan toteuttavien luokkien tulee toteuttaa metodi public String getTunnus()
.
package sovellus.domain; public interface Tunnistettava { String getTunnus(); }
Luokka toteuttaa rajapinnan implements
-avainsanalla. Toteutetaan luokka Henkilo
, joka toteuttaa rajapinnan tunnistettava. Henkilo-luokan metodi getTunnus
palauttaa aina henkilön henkilötunnuksen.
package sovellus.domain; public class Henkilo implements Tunnistettava { private String nimi; private String henkilotunnus; public Henkilo(String nimi, String henkilotunnus) { this.nimi = nimi; this.henkilotunnus = henkilotunnus; } public String getNimi() { return this.nimi; } public String getHenkilotunnus() { return this.henkilotunnus; } @Override public String getTunnus() { return getHenkilotunnus(); } @Override public String toString() { return this.nimi + " hetu: " + this.henkilotunnus; } }
Rajapintojen vahvuus on se, että rajapinnan toteuttava luokka voi käyttää rajapintaa muuttujatyyppinä. Tämä helpottaa sovellusten rakentamista huomattavasti.
Tehdään luokka Rekisteri
, josta voimme hakea henkilöitä tunnisteen perusteella. Yksittäisten henkilöiden hakemisen lisäksi Rekisteri
tarjoaa metodin kaikkien henkilöiden hakemiseen listana.
public class Rekisteri { private Map<String, Tunnistettava> rekisteroidyt; public Rekisteri() { this.rekisteroidyt = new HashMap<>(); } public void lisaa(Tunnistettava lisattava) { this.rekisteroidyt.put(lisattava.getTunnus(), lisattava); } public Tunnistettava hae(String tunnus) { return this.rekisteroidyt.get(tunnus); } public List<Tunnistettava> haeKaikki() { return new ArrayList<Tunnistettava>(rekisteroidyt.values()); } }
Rekisterin käyttö on helppoa
Rekisteri henkilokunta = new Rekisteri(); henkilokunta.lisaa(new Henkilo("Pekka", "221078-123X")); henkilokunta.lisaa(new Henkilo("Jukka", "110956-326B")); System.out.println(henkilokunta.hae("280283-111A")); Henkilo loydetty = (Henkilo) henkilokunta.hae("110956-326B"); System.out.println(loydetty.getNimi());
Koska henkilöt on talletettu rekisteriin Tunnistettava
-tyyppisinä, joudumme muuntanaan ne takaisin oikeaan tyyppiin jos haluamme käsitellä henkilöitä sellaisten metodien kautta, joita ei rajapinnassa ole määritelty. Näin tapahtuu yllä olevan esimerkin kahdella viimeisellä rivillä.
Entä jos haluaisimme operaation, joka palauttaa rekisteriin talletetut henkilöt tunnisteen mukaan järjestettynä?
Yksi luokka voi toteuttaa useamman rajapinnan, eli voimme toteuttaa Henkilo
-luokalla rajapinnan Tunnistettava
lisäksi viime viikolta tutun rajapinnan Comparable
. Useamman rajapinnan toteuttaminen tapahtuu erottamalla toteutettavat rajapinnat toisistaan pilkuilla (public class ... implements RajapintaEka, RajapintaToka ...
). Toteuttaessamme useampaa rajapintaa, tulee meidän toteuttaa kaikki rajapintojen vaatimat metodit. Toteutetaan seuraavaksi luokalla Henkilo
rajapinta Comparable
.
package sovellus.domain; public class Henkilo implements Tunnistettava, Comparable<Henkilo> { private String nimi; private String henkilotunnus; public Henkilo(String nimi, String henkilotunnus) { this.nimi = nimi; this.henkilotunnus = henkilotunnus; } public String getNimi() { return this.nimi; } public String getHenkilotunnus() { return this.henkilotunnus; } @Override public String getTunnus() { return getHenkilotunnus(); } @Override public int compareTo(Henkilo toinen) { return this.getTunnus().compareTo(toinen.getTunnus()); } }
Nyt voimme lisätä rekisterille metodin haeKaikkiJarjestyksessa:
public List<Tunnistettava> haeKaikkiJarjestyksessa() { ArrayList<Tunnistettava> kaikki = new ArrayList<>(rekisteroidyt.values()); Collections.sort(kaikki); return kaikki; }
Huomaamme kuitenkin, että ratkaisumme ei toimi. Koska henkilöt on talletettu rekisteriin Tunnistettava
-tyyppisinä, on Henkilön toteutettava rajapinta Comparable<Tunnistettava>
, jotta rekisteri osaisi järjestää henkilöt metodin Collections.sort()
avulla. Eli muutamme henkilön toteuttamaa rajapintaa:
public class Henkilo implements Tunnistettava, Comparable<Tunnistettava> { // ... @Override public int compareTo(Tunnistettava toinen) { return this.getTunnus().compareTo(toinen.getTunnus()); } }
Nyt ratkaisu toimii!
Rekisteri on täysin tietämätön sinne talletettavien olioiden todellisesta tyypistä. Voimmekin käyttää luokkaa rekisteri myös muuntyyppisten olioiden kuin henkilöiden rekisteröintiin, kunhan olioiden luokka vaan toteuttaa rajapinnan Tunnistettava
. Esim. seuraavassa käytetään rekisteriä kaupassa myytävien tuotteiden hallintaan:
public class Tuote implements Tunnistettava { private String nimi; private String viivakoodi; private int varastosaldo; private int hinta; public Tuote(String nimi, String viivakoodi) { this.nimi = nimi; this.viivakoodi = viivakoodi; } public String getTunnus() { return viivakoodi; } // ... }
Rekisteri tuotteet = new Rekisteri(); tuotteet.lisaa(new Tuote("maito", "11111111")); tuotteet.lisaa(new Tuote("piimä", "11111112")); tuotteet.lisaa(new Tuote("juusto", "11111113")); System.out.println(tuotteet.hae("99999999")); Tuote tuote = (Tuote) tuotteet.hae("11111112"); tuote.kasvataSaldoa(100); tuote.muutaHinta(23);
Eli teimme luokasta Rekisteri
melko yleiskäyttöisen pitämällä sen riippumattomana konkreettisista luokista. Mikä tähänsa luokka, joka toteuttaa rajapinnan Tunnistettava
, on rekisterin kanssa käyttökelpoinen. Metodin haeKaikkiJarjestyksessä
toimiminen tosin edellyttää luokalta myös vertailtavuuden eli Comparable<Tunnistettava>-rajapinnan toteuttamisen.
Oletetaan että ohjelmassasi on rajapinta Rajapinta
, ja olet tekemässä rajapinnan toteuttavaa luokkaa Luokka
. Joudut näkemään hieman vaivaa kirjoittaessasi toteuttavaan luokkaan rajapinnan määrittelemien metodien esittelyrivit.
On kuitenkin mahdollista pyytää NetBeansia täydentämään automaattisesti metodirungot toteuttavalle luokalle. Kun olet määritellyt luokan toteuttavan rajapinnan, eli kirjoittanut
public class Luokka implements Rajapinta { }
NetBeans värjää luokan nimen punaisella. Mene rivin vasemmassa reunassa olevan lamppusymbolin kohdalle, klikkaa ja valitse Implement all abstract methods ja metodirungot ilmestyvät koodiin!
Tietyissä tilanteissa NetBeans saattaa mennä sekaisin ja yrittää ajaa koodista versiota johon ei ole huomioitu kaikkia koodiin kirjoitettuja muutoksia. Yleensä huomaat tilanteen siten, että jotain "outoa" vaikuttaa tapahtuvan. Ongelman korjaa usein Clean and build -operaation suorittaminen. Operaatio löytyy Run-valikosta ja sen voi suorittaa myös painamalla harja ja vasara -symbolia. Clean and build poistaa koodista olemassa olevat käännetyt versiot ja tekee uuden käännöksen.
Muuttokuormaa pakattaessa esineitä lisätään muuttolaatikoihin siten, että tarvittujen muuttolaatikoiden määrä on mahdollisimman pieni. Tässä tehtävässä simuloidaan esineiden pakkaamista muuttolaatikoihin. Jokaisella esineellä on tilavuus, ja muuttolaatikoilla on maksimitilavuus.
Muuttomiehet siirtävät tavarat myöhemmin rekka-autoon (ei toteuteta tässä), joten toteutetaan ensin kaikkia esineitä ja laatikoita kuvaava Tavara
-rajapinta.
Tavara-rajapinnan tulee määritellä metodi int getTilavuus()
, jonka avulla tavaroita käsittelevät saavat selville kyseisen tavaran tilavuuden. Toteuta rajapinta Tavara
pakkaukseen muuttaminen.domain
.
Toteuta seuraavaksi pakkaukseen muuttaminen.domain
luokka Esine
, joka saa konstruktorin parametrina esineen nimen (String) ja esineen tilavuuden (int). Luokan tulee toteuttaa rajapinta Tavara
.
Lisää luokalle Esine
myös metodit public String getNimi()
ja korvaa metodi public String toString()
siten että se tuotta merkkijonoja muotoa "nimi (tilavuus dm^3)
". Esineen pitäisi toimia nyt jotakuinkin seuraavasti
Tavara esine = new Esine("hammasharja", 2); System.out.println(esine);
hammasharja (2 dm^3)
Pakatessamme esineitä muuttolaatikkoon haluamme aloittaa pakkaamisen järjestyksessä olevista esineistä. Toteuta Esine
-luokalla rajapinta Comparable
siten, että esineiden luonnollinen järjestys on tilavuuden mukaan nouseva. Kun olet toteuttanut esineellä rajapinnan Comparable
, tulee niiden toimia Collections
-luokan sort
-metodin kanssa seuraavasti.
List<Esine> esineet = new ArrayList<>(); esineet.add(new Esine("passi", 2)); esineet.add(new Esine("hammasharja", 1)); esineet.add(new Esine("sirkkeli", 100)); Collections.sort(esineet); System.out.println(esineet);
[hammasharja (1 dm^3), passi (2 dm^3), sirkkeli (100 dm^3)]
Toteuta tämän jälkeen pakkaukseen muuttaminen.domain
luokka Muuttolaatikko
. Tee aluksi muuttolaatikolle seuraavat:
public Muuttolaatikko(int maksimitilavuus)
public boolean lisaaTavara(Tavara tavara)
Tavara
-rajapinnan toteuttaman esineen. Jos laatikkoon ei mahdu, metodi palauttaa arvon false
. Jos tavara mahtuu laatikkoon, metodi palauttaa arvon true
. Muuttolaatikon tulee tallettaa tavarat listaan.Laita vielä Muuttolaatikko
toteuttamaan rajapinta Tavara
. Metodilla getTilavuus
tulee saada selville muuttolaatikossa olevien tavaroiden tämänhetkinen yhteistilavuus.
Toteuta luokka Pakkaaja
pakkaukseen muuttaminen.logiikka
. Luokan Pakkaaja
konstruktorille annetaan parametrina int laatikoidenTilavuus
, joka määrittelee minkä kokoisia muuttolaatikoita pakkaaja käyttää.
Toteuta tämän jälkeen luokalle metodi public List<Muuttolaatikko> pakkaaTavarat(List<Tavara> tavarat)
, joka pakkaa tavarat muuttolaatikoihin.
Tee metodista sellainen, että kaikki parametrina annetussa listassa olevat tavarat päätyvät palautetussa listassa oleviin muuttolaatikoihin. Sinun ei tarvitse varautua tilanteisiin, joissa tavarat ovat suurempia kuin pakkaajan käyttämä muuttolaatikon koko. Testit eivät välitä siitä kuinka täyteen pakkaaja täyttää muuttolaatikot.
Seuraavassa pakkaajan toimintaa demonstroiva esimerkki:
// tavarat jotka haluamme pakata List<Tavara> tavarat = new ArrayList<>(); tavarat.add(new Esine("passi", 2)); tavarat.add(new Esine("hammasharja", 1)); tavarat.add(new Esine("kirja", 4)); tavarat.add(new Esine("sirkkeli", 8)); // luodaan pakkaaja, joka käyttää tilavuudeltaan 10:n kokoisia muuttolaatikoita Pakkaaja pakkaaja = new Pakkaaja(10); // pyydetään pakkaajaa pakkaamaan tavarat laatikoihin List<Muuttolaatikko> laatikot = pakkaaja.pakkaaTavarat(tavarat); System.out.println("laatikoita: " + laatikot.size()); for (Muuttolaatikko laatikko : laatikot) { System.out.println(" laatikossa tavaraa: " + laatikko.getTilavuus() + " dm^3"); }
Tulostuu:
laatikoita: 2 laatikossa tavaraa: 7 dm^3 laatikossa tavaraa: 8 dm^3
Pakkaaja on siis pakannut tavarat kahteen laatikkoon, ensimmäiseen laatikkoon on mennyt 3 ensimmäistä tavaraa, yhteistilavuudeltaan 7, ja listan viimeinen tavara eli sirkkeli jonka tilavuus on 8 on mennyt toiseen laatikkoon. Testit eivät aseta rajoitusta pakkaajan käyttävien muuttolaatioiden määrälle, tavarat olisi siis voitu pakata vaikka jokainen eri laatikkoon, eli tuloste olisi ollut:
Tulostuu:
laatikoita: 4 laatikossa tavaraa: 2 dm^3 laatikossa tavaraa: 1 dm^3 laatikossa tavaraa: 7 dm^3 laatikossa tavaraa: 8 dm^3
Huom: tehtävän testaamista helpottamaan kannatanee tehdä luokalle Muuttolaatikko
esim. toString-metodi, jonka avulla voi printata laatikon sisällön.
Rajapintoihin voi määritellä oletusmetodeja, joiden mukana annetaan myös toteutus. Oletusmetodien määrittely alkaa avainsanalla default
, jota seuraa metodin määrittely. Kuten aiemmissa rajapintojen metodeissa, myös tässäkään näkyvyyttä ei tarvitse määritellä. Rajapinnoissa määriteltyjen metodien näkyvyys on aina public
.
Alla olevassa esimerkissä Luettava
-rajapintaan on lisätty oletusmetodi lueTulostaen
, joka tulostaa lue
-metodin palauttaman arvon.
public interface Luettava { String lue(); default void lueTulostaen() { System.out.println(lue()); } }
Tekstiviestin ja sähköpostin toiminta muuttuu myös:
Tekstiviesti viesti = new Tekstiviesti("G. Hopper", "COBOL kicks ass"); viesti.lueTulostaen(); Sahkoposti posti = new Sahkoposti ("D. Knuth", "If you optimize everything, you will always be unhappy."); System.out.println(posti.onLuettu()); posti.lueTulostaen(); System.out.println(posti.onLuettu());
COBOL kicks ass false If you optimize everything, you will always be unhappy. true
Oletusmetodien suurin hyöty ilmenee tilanteissa, missä rajapinta on määritelty aiemmin, ja useampi luokka toteuttaa sen. Jos rajapintaan lisätään uusi metodi, tulee sille ohjelmoida toteutus kaikkiin rajapinnan toteuttamiin luokkiin, jos uusi metodi ei tarjoa oletustoteutusta.
Palaamme rajapintojen oletusmetodien hyödyllisyyteen noin viikolla 14 keskustellessamme funktionaalisesta ohjelmoinnista.
Poikkeustilanteet ovat tilanteita joissa ohjelman suoritus ei 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: "); int numero = -1; try { 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: "); int numero = -1; try { 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.
Aiemmin nähdyn tehtävän kellosta olio bonusversiossa kerrottiin, että ohjelma saadaan viivyttämään itseään sekunnin verran kutsumalla komentoa Thread.sleep(1000)
. Komento saattaa heittää poikkeuksen, joka on pakko käsitellä. Poikkeuksen käsittely siis tapahtuu try-catch
-lauseella, seuraavassa esimerkissä olemme välittämättä mahdollisista poikkeustilanteista ja jätimme catch
-lohkon tyhjäksi:
try { // nukutaan 1000 millisekuntia Thread.sleep(1000); } catch (Exception e) { // ei tehdä mitään poikkeustilanteessa }
Metodeissa on myös mahdollista jättää poikkeus itse käsittelemättä ja siirtää vastuu poikkeuksen käsittelystä metodin kutsujalle. Vastuun siirto tapahtuu heittämällä poikkeus metodista eteenpäin sanomalla throws Exception
.
public void nuku(int sekuntia) throws Exception { Thread.sleep(sekuntia * 1000); // nyt try-catchia ei tarvita! }
Nyt metodia nuku
-kutsuvan metodin tulee joko käsitellä poikkeus try-catch
-lohkossa, tai siirtää poikkeuksen käsittelyn vastuuta eteenpäin heittää poikkeus eteenpäin. Joskus poikkeuksen käsittelyä pakoillaan viimeiseen asti, ja main
-metodikin heittää poikkeuksen käsiteltäväksi eteenpäin:
public class Paaohjelma { public static void main(String[] args) throws Exception { // ... } }
Tällöin poikkeus päätyy Javan virtuaalikoneelle, joka keskeyttää ohjelman suorituksen poikkeukseen johtavan virheen tapahtuessa.
Osa poikkeuksista, kuten Integer.parseInt
-metodin heittämä NumberFormatException
tai NullPointerException
, on sellaisia joihin ohjelmoijan ei ole pakko varautua. Poikkeukset, joihin käyttäjän ei tarvitse varautua ovat aina myös tyyppiä RuntimeException
, palaamme siihen miksi poikkeuksilla ja 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()
. Seuraava ohjelma siis päätyy aina poikkeustilaan.
public class Ohjelma { public static void main(String[] args) throws Exception { throw new NumberFormatException(); // Koodin suoritus tyssää tähän } }
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, eli varmistaa että se täyttää tietyt kriteerit. Arvosanan tulee olla aina välillä 0-5. Jos arvosana on jotain muuta, haluamme heittää poikkeuksen. Lisätään Arvosana
-luokan konstruktoriin ehtolause, joka tarkistaa onko arvosana arvovälin 0-5 ulkopuolella. Jos on, heitetään poikkeus IllegalArgumentException
sanomalla throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5");
.
public class Arvosana { private int arvosana; public Arvosana(int arvosana) { if (arvosana < 0 || arvosana > 5) { throw new IllegalArgumentException("Arvosanan tulee olla välillä 0-5"); } this.arvosana = arvosana; } public int getArvosana() { return this.arvosana; } }
Arvosana arvosana = new Arvosana(3); System.out.println(arvosana.getArvosana()); Arvosana virheellinenArvo = new Arvosana(22); // tapahtuu poikkeus, tästä ei jatketa eteenpäin
3 Exception in thread "..." java.lang.IllegalArgumentException: Arvosanan tulee olla välillä 0-5
Harjoitellaan hieman parametrien validointia IllegalArgumentException
-poikkeuksen avulla. Tehtäväpohjassa tulee kaksi luokkaa, Henkilo
ja Laskin
. Muuta luokkia seuraavasti:
Luokan Henkilo
konstruktorin tulee varmistaa että parametrina annettu nimi ei ole null, tyhjä tai yli 40 merkkiä pitkä. Myös iän tulee olla väliltä 0-120. Jos joku edelläolevista ehdoista ei päde, tulee konstruktorin heittää IllegalArgumentException
-poikkeus.
Luokan Laskin
metodeja tulee muuttaa seuraavasti: Metodin kertoma
tulee toimia vain jos parametrina annetaan ei-negatiivinen luku (0 tai suurempi). Metodin binomikerroin
tulee toimia vain jos parametrit ovat ei-negatiivisia ja osajoukon koko on pienempi kuin joukon koko. Jos jompikumpi metodeista saa epäkelpoja arvoja metodikutsujen yhteydessä, tulee metodien heittää poikkeus IllegalArgumentException
.
Kaikki sovelluksessa oleva koodi tulee sijoittaa pakkaukseen sovellus
.
Käytössämme on seuraava rajapinta:
public interface Sensori { boolean onPaalla(); // palauttaa true jos sensori on päällä void paalle(); // käynnistä sensorin void poisPaalta(); // sulkee sensorin int mittaa(); // palauttaa sensorin lukeman jos sensori on päällä // jos sensori ei päällä heittää poikkeuksen IllegalStateException }
Tee luokka Vakiosensori
joka toteuttaa rajapinnan Sensori
.
Vakiosensori on koko ajan päällä. Metodien paalle ja poisPaalta kutsuminen ei tee mitään. Vakiosensorilla tulee olla konstruktori, jonka parametrina on kokonaisluku. Metodikutsu mittaa
palauttaa aina konstruktorille parametrina annetun luvun.
Esimerkki:
public static void main(String[] args) { Vakiosensori kymppi = new Vakiosensori(10); Vakiosensori miinusViis = new Vakiosensori(-5); System.out.println(kymppi.mittaa()); System.out.println(miinusViis.mittaa()); System.out.println(kymppi.onPaalla()); kymppi.poisPaalta(); System.out.println(kymppi.onPaalla()); }
Tulostuu:
10 -5 true true
Tee luokka Lampomittari
joka toteuttaa rajapinnan Sensori
.
Aluksi lämpömittari on poissa päältä. Kutsuttaessa metodia mittaa
kun mittari on päällä mittari arpoo luvun väliltä -30...30 ja palauttaa sen kutsujalle. Jos mittari ei ole päällä, heitetään poikkeus IllegalStateException
.
Tee luokka Keskiarvosensori
joka toteuttaa rajapinnan Sensori
.
Keskiarvosensori sisältää useita sensoreita. Rajapinnan Sensori
määrittelemien metodien lisäksi keskiarvosensorilla on metodi public void lisaaSensori(Sensori lisattava)
jonka avulla keskiarvosensorin hallintaan lisätään uusi sensori.
Keskiarvosensori on päällä silloin kuin kaikki sen sisältävät sensorit ovat päällä. Kun keskiarvosensori käynnistetään, täytyy kaikkien sen sisältävien sensorien käynnistyä jos ne eivät ole käynnissä. Kun keskiarvosensori suljetaan, täytyy ainakin yhden sen sisältävän sensorin mennä pois päältä. Saa myös käydä niin että kaikki sen sisältävät sensorit menevät pois päältä.
Keskiarvosensorin metodi mittaa
palauttaa sen sisältämien sensoreiden lukemien keskiarvon (koska paluuarvo on int
, pyöristyy lukema alaspäin kuten kokonaisluvuilla tehdyissä jakolaskuissa). Jos keskiarvosensorin metodia mittaa
kutsutaan sensorin ollessa poissa päältä, tai jos keskiarvosensorille ei vielä ole lisätty yhtään sensoria heitetään poikkeus IllegalStateException
.
Seuraavassa sensoreja käyttävä esimerkkiohjelma (huomaa, että sekä Lämpömittarin että Keskiarvosensorin konstruktorit ovat parametrittomia):
public static void main(String[] args) { Sensori kumpula = new Lampomittari(); kumpula.paalle(); System.out.println("lämpötila Kumpulassa " + kumpula.mittaa() + " astetta"); Sensori kaisaniemi = new Lampomittari(); Sensori helsinkiVantaa = new Lampomittari(); Keskiarvosensori paakaupunki = new Keskiarvosensori(); paakaupunki.lisaaSensori(kumpula); paakaupunki.lisaaSensori(kaisaniemi); paakaupunki.lisaaSensori(helsinkiVantaa); paakaupunki.paalle(); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); }
tulostuu (tulostetut lukuarvot riippuvat tietenkin arvotuista lämpötiloista):
lämpötila Kumpulassa 11 astetta lämpötila Pääkaupunkiseudulla 8 astetta
Huom: kannattaa käyttää Vakiosensori-oliota keskiarvosensorin testaamiseen!
Lisää luokalle Keskiarvosensori metodi public List<Integer> mittaukset()
, joka palauttaa listana kaikkien keskiarvosensorin avulla suoritettujen mittausten tulokset. Seuraavassa esimerkki metodin toiminnasta:
public static void main(String[] args) { Sensori kumpula = new Lampomittari(); Sensori kaisaniemi = new Lampomittari(); Sensori helsinkiVantaa = new Lampomittari(); Keskiarvosensori paakaupunki = new Keskiarvosensori(); paakaupunki.lisaaSensori(kumpula); paakaupunki.lisaaSensori(kaisaniemi); paakaupunki.lisaaSensori(helsinkiVantaa); paakaupunki.paalle(); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); System.out.println("lämpötila Pääkaupunkiseudulla "+paakaupunki.mittaa() + " astetta"); System.out.println("mittaukset: "+paakaupunki.mittaukset()); }
tulostuu (tulostetut lukuarvot riippuvat jälleen arvotuista lämpötiloista):
lämpötila Pääkaupunkiseudulla -10 astetta lämpötila Pääkaupunkiseudulla -4 astetta lämpötila Pääkaupunkiseudulla 5 astetta mittaukset: [-10, -4, 5]
Rajapintaluokilla ei ole metodirunkoa, mutta metodimäärittely on vapaasti rajapinnan suunnittelijan toteutettavissa. Rajapintaluokat voivat määritellä myös poikkeusten heiton. Esimerkiksi seuraavan rajapinnan Tiedostopalvelin
toteuttavat luokat heittävät mahdollisesti poikkeuksen lataa
- ja tallenna
-metodissa.
public interface Tiedostopalvelin { String lataa(String tiedosto) throws Exception; void tallenna(String tiedosto, String merkkijono) throws Exception; }
Jos rajapinta määrittelee metodeille throws Exception
-määreet, eli että metodit heittävät mahdollisesti poikkeuksen, tulee samat määreet olla myös rajapinnan toteuttavassa luokassa. Luokan ei kuitenkaan ole pakko heittää poikkeusta kuten alla olevasta esimerkistä näkee.
public class Tekstipalvelin implements Tiedostopalvelin { private Map<String, String> data; public Tekstipalvelin() { this.data = new HashMap<>(); } @Override public String lataa(String tiedosto) throws Exception { return this.data.get(tiedosto); } @Override public void tallenna(String tiedosto, String merkkijono) throws Exception { this.data.put(tiedosto, merkkijono); } }
Poikkeusten käsittelytoiminnallisuuden sisältämä catch
-lohko määrittelee catch-osion sisällä poikkeuksen johon varaudutaan catch (Exception e)
. Poikkeuksen tiedot tallennetaan e
-muuttujaan.
try { // ohjelmakoodi, joka saattaa heittää poikkeuksen } catch (Exception e) { // poikkeuksen tiedot ovat tallessa muuttujassa e }
Luokka Exception
tarjoaa hyödyllisiä metodeja. Esimerkiksi metodi printStackTrace()
tulostaa polun, joka kertoo mistä päädyttiin poikkeukseen. Tutkitaan seuraavaa metodin printStackTrace()
tulostamaa virhettä.
Exception in thread "main" java.lang.NullPointerException at pakkaus.Luokka.tulosta(Luokka.java:43) at pakkaus.Luokka.main(Luokka.java:29)
Poikkeuspolun lukeminen tapahtuu alhaalta ylöspäin. Alimpana on ensimmäinen kutsu, eli ohjelman suoritus on alkanut luokan Luokka
metodista main()
. Luokan Luokka
main-metodin rivillä 29 on kutsuttu metodia tulosta()
. Metodin tulosta
rivillä 43 on tapahtunut poikkeus NullPointerException
. Poikkeuksen tiedot ovatkin hyvin hyödyllisiä virhekohdan selvittämisessä.
Tarkennetaan hieman viikolla 6 tarkasteltua tiedoston lukemista.
Huomattava osa ohjelmista käsittelee jollain tavalla tallennettua tietoa. Otetaan ensiaskeleet tiedostojen käsittelyyn Javassa. Javan API tarjoaa luokan File, jonka sisältö voidaan lukea kurssilla jo tutuksi tulleen Scanner-luokan avulla.
Luokan File
API-kuvausta lukiessamme huomaamme File
-luokalla on konstruktori File(String pathname)
(Creates a new File instance by converting the given pathname string into an abstract pathname). Voimme siis antaa avattavan tiedoston polun File
-luokan konstruktorille.
NetBeans-ohjelmointiympäristössä tiedostoille on oma välilehti nimeltä Files. Files-välilehdellä on määritelty kaikki projektiin liittyvät tiedostot. Jos projektin juureen, eli ei yhdenkään hakemiston sisälle, lisätään tiedosto, voidaan siihen viitata projektin sisältä suoraan tiedoston nimellä. Tiedosto-olion luominen tapahtuu antamalla sille parametrina polku tiedostoon, esimerkiksi seuraavasti
File tiedosto = new File("tiedoston-nimi.txt");
Scanner-luokan konstruktorille voi antaa myös muita lukemislähteitä kuin System.in
-syöttövirran. Lukemislähteenä voi olla näppäimistön lisäksi muun muassa tiedosto. Scanner tarjoaa tiedoston lukemiseen samat metodit kuin näppäimistöltä syötetyn syötteen lukemiseen. Seuraavassa esimerkissä avataan tiedosto ja tulostetaan kaikki tiedoston sisältämän tekstit System.out.println
-komennolla Lopuksi tiedosto suljetaan komennolla close
.
// tiedosto mistä luetaan File tiedosto = new File("tiedosto.txt"); Scanner lukija = new Scanner(tiedosto); while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); } lukija.close();
Scanner-luokan konstruktori public Scanner(File source)
(Constructs a new Scanner that produces values scanned from the specified file.) heittää FileNotFoundException
-poikkeuksen jos luettavaa tiedostoa ei löydy. Poikkeus FileNotFoundException
ei ole tyyppiä RuntimeException
, joten se tulee joko käsitellä tai heittää eteenpäin. Tässä vaiheessa riittää tietää että ohjelmointiympäristö kertoo jos sinun tulee käsitellä poikkeus erikseen. Luodaan ensin vaihtoehto, jossa poikkeus käsitellään tiedostoa avattaessa.
public void lueTiedosto(File tiedosto) { // tiedosto mistä luetaan Scanner lukija = null; try { lukija = new Scanner(tiedosto); } catch (Exception e) { System.out.println("Tiedoston lukeminen epäonnistui. Virhe: " + e.getMessage()); return; // poistutaan metodista } while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); } lukija.close(); }
Toinen vaihtoehto poikkeuksen käsittelyyn on poikkeuksen käsittelyvastuun siirtäminen metodin kutsujalle. Poikkeuksen käsittelyvastuu siirretään metodin kutsujalle lisäämällä metodiin määre throws PoikkeuksenTyyppi
, eli esimerkiksi throws Exception
sillä kaikki poikkeukset ovat tyyppiä Exception
. Kun metodilla on määre throws Exception
, tietävät kaikki sitä kutsuvat että se saattaa heittää poikkeuksen johon tulee varautua.
public void lueTiedosto(File tiedosto) throws Exception { // tiedosto mistä luetaan Scanner lukija = new Scanner(tiedosto); while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); } lukija.close(); }
Esimerkki avaa tiedoston tiedosto.txt
projektin juuripolusta ja tulostaa sen rivi riviltä käyttäjälle näkyville. Lopuksi lukija suljetaan, jolloin tiedosto myös suljetaan. Määre throws Exception
kertoo että metodi saattaa heittää poikkeuksen. Samanlaisen määreen voi laittaa kaikkiin metodeihin jotka käsittelevät tiedostoja.
Huomaa että Scanner
-olio ei liitä rivinvaihtomerkkejä osaksi nextLine
-metodin palauttamaa merkkijonoa. Yksi vaihtoehto tiedoston lukemiseen siten, että rivinvaihdot säilyvät, on lisätä jokaisen rivin jälkeen rivinvaihtomerkki:
public String lueTiedostoMerkkijonoon(File tiedosto) throws Exception { // tiedosto mistä luetaan Scanner lukija = new Scanner(tiedosto); String merkkijono = ""; while (lukija.hasNextLine()) { String rivi = lukija.nextLine(); merkkijono += rivi; merkkijono += "\n"; } lukija.close(); return merkkijono; }
Koska käytämme tiedoston lukemiseen Scanner
-luokkaa, käytössämme on kaikki Scanner-luokan tarjoamat metodit. Esimerkiksi metodi hasNext()
palauttaa totuusarvon true
, jos luettavassa tiedostossa on vielä luettavaa jäljellä, ja metodi next()
lukee seuraavan sanan metodin palauttamaan String
-olioon.
Seuraava ohjelma luo Scanner
-olion, joka avaa tiedoston tiedosto.txt
. Sen jälkeen se tulostaa joka viidennen sanan tiedostosta.
File tiedosto = new File("tiedosto.txt"); Scanner lukija = new Scanner(tiedosto); int monesko = 0; while (lukija.hasNext()) { monesko++; String sana = lukija.next(); if (monesko % 5 == 0) { System.out.println(sana); } }
Alla on ensin luetun tiedoston sisältämä teksti ja sitten ohjelman tulostus
Poikkeukset (exceptions) ovat "poikkeuksellisia tilanteita" kesken normaalin ohjelmansuorituksen: tiedosto loppuu, merkkijono ei kelpaa kokonaisluvuksi, odotetun olion tilalla onkin null-arvo, taulukon indeksi menee ohjelmointivirheen takia sopimattomaksi, ...
tilanteita" loppuu, odotetun taulukon sopimattomaksi,
Try with resources
Edellisissä esimerkeissä avattua tiedostoa ei erikseen suljeta. Lukijan sulkeminen tapahtuu Scanner
-olion close
-metodin avulla.
Osa tiedostoja ja muita syötevirtoja käsittelevistä luokista toteuttaa rajapinnan AutoCloseable. Tämä mahdollistaa ohjelmakoodin kirjoittamisen, missä luokasta tehty ilmentymä suljetaan automaattisesti erityisen try with resources-rakenteen avulla.
Tässä resurssin avaaminen tehdään try
-lohkon parametrina, jonka jälkeen try
-lohkossa käsitellään resurssia. Varsinaista sulje-tapahtumaa ei tarvitse tehdä lainkaan.
ArrayList<String> rivit = new ArrayList<>(); // avataan resurssi try (Scanner lukija = new Scanner(new File("tiedosto.txt"))) { // käsitellään resurssia while (lukija.hasNextLine()) { rivit.add(lukija.nextLine()); } } catch (Exception e) { // käsitellään mahdollinen poikkeus System.out.println("Virhe: " + e.getMessage()); } // tee jotain luetuilla riveillä
Tekstiä tiedostosta luettaessa (tai tiedostoon tallennettaessa) Java joutuu päättelemään käyttöjärjestelmän käyttämän merkistön. Merkistön tuntemusta tarvitaan sekä tekstin tallentamiseen tietokoneen kovalevylle binäärimuotoiseksi että binäärimuotoisen datan tekstiksi kääntämiseksi.
Merkistöihin on kehitetty standardeja, joista "UTF-8" on nykyään yleisin. UTF-8 -merkistö sisältää sekä jokapäiväisessä käytössä olevien aakkosten että erikoisempien merkkien kuten Japanin kanji-merkistön tai shakkipelin nappuloiden tallentamiseen ja lukemiseen tarvittavat tiedot. Ohjelmointimielessä merkistöä voi hieman yksinkertaistaen ajatella hajautustauluna merkistä numeroon ja numerosta merkkiin. Merkistä numeroon oleva hajautustaulu kuvaa minkälaisena binäärilukuna kukin merkki tallennetaan tiedostoon. Numerosta merkkiin oleva hajautustaulu taas kuvaa miten tiedostoa luettaessa saadut luvut muunnetaan merkeiksi.
Lähes jokaisella käyttöjärjestelmävalmistajalla on myös omat standardinsa. Osa tukee ja haluaa osallistua avoimien standardien käyttöön, osa ei. Mikäli sinulla on ongelmia ääkkösellisten sanojen kanssa (eritoten mac ja windows käyttäjät) voit kertoa Scanner
-oliota luodessa käytettävän merkistön. Tällä kurssilla käytämme aina merkistöä "UTF-8".
UTF-8 -merkistöä käyttävän tiedostoa lukevan Scanner-olion voi luoda seuraavasti:
File tiedosto = new File("esimerkkitiedosto.txt"); Scanner lukija = new Scanner(tiedosto, "UTF-8");
Toinen vaihtoehto merkistön asettamiseksi on ympäristömuuttujan käyttäminen. Macintosh ja Windows-käyttäjät voivat asettaa ympäristömuuttujan JAVA_TOOL_OPTIONS
arvoksi merkkijonon -Dfile.encoding=UTF8
. Tällöin Java käyttää oletuksena aina UTF-8-merkistöä.
Tee luokka Tulostaja
ja sille konstruktori public Tulostaja(String tiedostonNimi)
, joka saa parametrinaan tiedoston nimeä vastaavan merkkijonon sekä metodi public void tulostaRivitJoilla(String sana)
tulostaa tiedostosta ne rivit, joilla esiintyy parametrina oleva sana (pienet ja isot kirjaimet erotellaan tehtävässä, eli esim. "koe" ja "Koe" eivät ole sama sana), rivit tulostetaan samassa järjestyksessä missä ne ovat tiedostossa.
Jos parametri on tyhjä merkkijono, tulostuu koko tiedosto.
Jos tiedostoa ei ole olemassa, heittää konstruktori aiheutuvan poikkeuksen eteenpäin, eli try-catch-komentoa ei tarvita, riittää määritellä konstruktori seuraavasti:
public Tulostaja { public Tulostaja(String tiedostonNimi) throws Exception { // ... } // ... }
Projektisi default-pakkauksessa on testausta varten tiedosto testitiedosto.txt. Ohjelmasta avatessa tiedoston nimeksi tulee antaa src/testitiedosto.txt
. Tiedoston sisältö on seuraava:
Siinä vanha Väinämöinen katseleikse käänteleikse Niin tuli kevätkäkönen näki koivun kasvavaksi Miksipä on tuo jätetty koivahainen kaatamatta Sanoi vanha Väinämöinen
Seuraavassa esimerkki ohjelman toiminnasta testitiedostolla:
Tulostaja tulostaja = new Tulostaja("src/testitiedosto.txt"); tulostaja.tulostaRivitJoilla("Väinämöinen"); System.out.println("-----"); tulostaja.tulostaRivitJoilla("Frank Zappa"); System.out.println("-----"); tulostaja.tulostaRivitJoilla(""); System.out.println("-----");
Tulostuu:
Siinä vanha Väinämöinen Sanoi vanha Väinämöinen ----- ----- Siinä vanha Väinämöinen katseleikse käänteleikse Niin tuli kevätkäkönen näki koivun kasvavaksi Miksipä on tuo jätetty koivahainen kaatamatta Sanoi vanha Väinämöinen
Projektipohjasta löytyy myös koko Kalevala, tiedoston nimi on src/kalevala.txt
Tässä tehtävässä tehdään sovellus tiedoston rivi- ja merkkimäärän laskemiseen.
Tee pakkaukseen tiedosto
luokka Analyysi
, jolla on konstruktori public Analyysi(File tiedosto)
. Toteuta luokalle metodi public int rivimaara()
, joka palauttaa konstruktorille annetun tiedoston rivimäärän.
Metodi ei saa olla "kertakäyttöinen", eli sen pitää tuottaa oikea tulos myös usealla peräkkäisellä kutsulla. Huomaa, että kun teet tiedostoa vastaavan Scanner-olion, ja luet tiedoston koko sisällön nextLine
-komennoilla, et voi käyttää enää samaa skanneria tiedoston uudelleenlukemiseen!
Huom: jos testit sanovat timeout, et todennäköisesti muista lukea tiedostoa ollenkaan, eli nextLine
-kutsut puuttuvat!
Toteuta luokkaan Analyysi
metodi public int merkkeja()
, joka palauttaa luokan konstruktorille annetun tiedoston merkkien määrän.
Metodi ei saa olla "kertakäyttöinen", eli sen pitää tuottaa oikea tulos myös usealla peräkkäisellä kutsulla.
Voit itse päättää miten reagoidaan jos konstruktorin parametrina saatua tiedostoa ei ole olemassa.
Projektisi testipakkauksessa on testausta varten tiedosto testitiedosto.txt. Ohjelmasta avatessa tiedoston nimeksi tulee antaa test/testitiedosto.txt
. Tiedoston sisältö on seuraava:
rivejä tässä on 3 ja merkkejä koska rivinvaihdotkin ovat merkkejä
Ohjelman toiminta testaustiedostolla:
File tiedosto = new File("src/testitiedosto.txt"); Analyysi analyysi = new Analyysi(tiedosto); System.out.println("Rivejä: " + analyysi.rivimaara()); System.out.println("Merkkejä: " + analyysi.merkkeja());
Rivejä: 3 Merkkejä: 67
Tee luokka Sanatutkimus, jolla voi tehdä erilaisia tutkimuksia tiedoston sisältämille sanoille. Toteuta luokka pakkaukseen sanatutkimus
.
Kotimaisten kielten tutkimuskeskus (Kotus) on julkaissut netissä suomen kielen sanalistan. Tässä tehtävässä käytetään listan muokattua versiota, joka löytyy tehtäväpohjasta src
-hakemistosta nimellä sanalista.txt
, eli suhteellisesta polusta "src/sanalista.txt"
.
Koska sanalista on varsin pitkä, on projektissa testausta varten myös pienilista.txt
joka löytyy polusta "src/pienilista.txt"
.
Mikäli sinulla on ongelmia ääkkösellisten sanojen kanssa (mac ja windows käyttäjät) luo Scanner
-olio antaen sille parametrina merkistö "UTF-8" seuraavasti: Scanner lukija = new Scanner(tiedosto, "UTF-8");
Ongelmat liittyvät erityisesti testien suoritukseen.
Luo Sanatutkimus-luokalle konstruktori public Sanatutkimus(File tiedosto)
joka luo uuden Sanatutkimus-olion, joka tutkii parametrina annettavaa tiedostoa.
Tee luokkaan metodi public int sanojenMaara()
, joka lukee tiedostossa olevat sanat ja tulostaa niiden määrän. Tässä vaiheessa sanoilla ei tarvitse tehdä mitään, riittää laskea niiden määrä. Voit olettaa tässä tehtävässä, että tiedostossa on vain yksi sana riviä kohti.
Tee luokkaan metodi public List<String> kirjaimenZSisaltavatSanat()
, joka palauttaa tiedoston kaikki sanat, joissa on z-kirjain. Tällaisia sanoja ovat esimerkiksi jazz ja zombi.
Tee luokkaan metodi public List<String> kirjaimeenLPaattyvatSanat()
, joka palauttaa tiedoston kaikki sanat, jotka päättyvät l-kirjaimeen. Tällaisia sanoja ovat esimerkiksi kannel ja sammal.
Huom! Jos luet tiedoston uudestaan ja uudestaan jokaisessa metodissa huomaat viimeistään tässä vaiheessa copy-paste koodia. Kannattaa miettiä olisiko tiedoston lukeminen helpompi tehdä osana konstruktoria tai metodina, jota konstruktori kutsuu. Metodeissa voitaisiin käyttää tällöin jo luettua listaa ja luoda siitä aina uusi, hakuehtoihin sopiva lista. myöhemmin on tulossa oikeaoppinen tapa copypasten eliminointiin.
Tee luokkaan metodi public List<String> palindromit()
, joka palauttaa tiedoston kaikki sanat, jotka ovat palindromeja. Tällaisia sanoja ovat esimerkiksi ala ja enne.
Tee luokkaan metodi public List<String> kaikkiVokaalitSisaltavatSanat()
, joka palauttaa tiedoston kaikki sanat, jotka sisältävät kaikki suomen kielen vokaalit (aeiouyäö). Tällaisia sanoja ovat esimerkiksi myöhäiselokuva ja ympäristönsuojelija.
HashMapiin voi asettaa tiettyä avainta kohti vaan yhden arvon. Seuraavassa esimerkissä tallennamme henkilöiden puhelinnumeroita HashMap:iin.
Map<String, String> puhelinnumerot = new HashMap<>(); puhelinnumerot.put("Pekka", "040-12348765"); System.out.println("Pekan numero: "+ puhelinnumerot.get("Pekka")); puhelinnumerot.put("Pekka", "09-111333"); System.out.println("Pekan numero: "+ puhelinnumerot.get("Pekka"));
Kuten odotettua, tulostus kertoo, että
Pekan numero: 040-12348765 Pekan numero: 09-111333
Entä jos haluaisimmekin tallettaa yhtä avainta kohti useita arvoja, eli esim yhtä henkilöä kohti monta puhelinnumeroa? Onnistuuko se HashMap:in avulla? Pariohjelmointitehtäviä tehneille tämä saattaa olla jo tuttua: Tallettamalla HashMap:in arvoiksi Stringien sijaan esim. ArrayListejä, voidaan yhteen avaimeen "liittää" useampia oliota. Muutetaan puhelinnumeroiden talletustapaa seuraavasti:
Map<String, List<String>> puhelinnumerot = new HashMap<>();
Nyt ylläolevassa HashMapissa on jokaiseen avaimeen liitettynä lista. Vaikka new-komento luokin HashMapin, on mapin sisälle talletettavat listat luotava erikseen. Seuraavassa lisätään HashMapiin Pekalle kaksi numeroa ja tulostetaan ne:
Map<String, List<String>> puhelinnumerot = new HashMap<>(); // liitetään Pekka-nimeen ensin tyhjä ArrayList puhelinnumerot.put("Pekka", new ArrayList<>()); // ja lisätään Pekkaa vastaavalle listalle puhelinnumero puhelinnumerot.get("Pekka").add("040-12348765"); // ja lisätään toinenkin puhelinnumero puhelinnumerot.get("Pekka").add("09-111333"); System.out.println("Pekan numerot: " + puhelinnumerot.get("Pekka"));
Tulostuu
Pekan numero: [040-12348765, 09-111333]
Määrittelimme muuttujan puhelinnumero tyypiksi Map<String, List<String>>
eli Map jonka avaimena on merkkijono ja arvona merkkijonoja sisältävä lista. Konkreettinen toteutus, eli luotu olio oli HasMap. Olisimme voineet määritellä muuttujan myös seuraavasti:
Map<String, List<String>> puhelinnumero = new HashMap<>();
Eli nyt muuttujan tyyppi on Map, jonka avaimena on merkkijono ja arvona merkkijonoja sisältävä List
, joka siis on rajapinta joka määrittelee listatoiminnallisuuden, esim. ArrayList toteuttaa tämän rajapinnan. Konkreettinen olio on HashMap.
HashMap:iin talletettavat arvot siis ovat List<String>
-rajapinnan toteuttavia konkreettisia olioita, esim. ArrayListeja. Eli lisäys HashMapiin tapahtuu edelleen seuraavasti:
// liitetään Pekka-nimeen ensin tyhjä ArrayList puhelinnumerot.put("Pekka", new ArrayList<>()); // ...
Jatkossa pyrkimyksemme on käyttää tyyppimäärittelyissä konkreettisten luokkien, esim. HashMap
ja ArrayList
sijaan niitä vastaavia rajapintoja Map
ja List
.
Rajapinta Set
kuvaa joukon toiminnallisuutta. Toisin kuin listalla, on joukossa kutakin alkioita korkeintaan yksi kappale, eli yhtään samanlaista oliota ei ole kahdesti. Olioiden samankaltaisuuden tarkistaminen toteutetaan equals
ja hashCode
-metodeja käyttämällä.
Yksi rajapinnan Set
toteuttava luokka on HashSet
. Toteutetaan sen avulla luokka Tehtavakirjanpito
, joka tarjoaa mahdollisuuden tehtävien kirjanpitoon ja tehtyjen tehtävien tulostamiseen. Oletetaan että tehtävät ovat aina kokonaislukuja.
public class Tehtavakirjanpito { private Set<Integer> tehdytTehtavat; public Tehtavakirjanpito() { this.tehdytTehtavat = new HashSet<>(); } 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<>(); } public void lisaa(String kayttaja, int tehtava) { // huomaa miten uudelle käyttäjälle on lisättävä HashMapiin ensin tyhjä tehtäväjoukko if (!this.tehdytTehtavat.containsKey(kayttaja)) { this.tehdytTehtavat.put(kayttaja, new HashSet<>()); } // haetaan ensin käyttäjän tehtävät sisältävä joukko ja tehdään siihen lisäys Set<Integer> tehdyt = this.tehdytTehtavat.get(kayttaja); tehdyt.add(tehtava); // edellinen olisi onnitunut myös ilman apumuuttujaa seuraavasti // this.tehdytTehtavat.get(kayttaja).add(tehtava); } public void tulosta() { for (String kayttaja: this.tehdytTehtavat.keySet()) { System.out.println(kayttaja + ": " + this.tehdytTehtavat.get(kayttaja)); } } }
Tehtavakirjanpito kirjanpito = new Tehtavakirjanpito(); kirjanpito.lisaa("Ada", 3); kirjanpito.lisaa("Ada", 4); kirjanpito.lisaa("Ada", 3); kirjanpito.lisaa("Ada", 3); kirjanpito.lisaa("Pekka", 4); kirjanpito.lisaa("Pekka", 4); kirjanpito.lisaa("Matti", 1); kirjanpito.lisaa("Matti", 2); kirjanpito.tulosta();
Matti: [1, 2] Pekka: [4] Ada: [3, 4]
Huomaamme että käyttäjien nimet eivät tulostu esimerkissä järjestyksessä. Tämä selittyy sillä että HashMap
-tyyppisessä hajautustaulussa alkioiden tallennus tapahtuu hashCode
-metodin palauttaman hajautusarvon perusteella, eikä se liity millään tavalla alkioiden järjestykseen.
Tehdään hieman laajennettu versio aiemmin kurssilla tehdystä sanakirjasta. Tehtävänäsi on toteuttaa pakkaukseen sanakirja
luokka OmaUseanKaannoksenSanakirja
, joka voi tallettaa yhden tai useamman käännöksen jokaiselle sanalle. Luokan tulee toteuttaa tehtäväpohjassa annettu rajapinta UseanKaannoksenSanakirja
, jossa on seuraavat toiminnot:
public void lisaa(String sana, String kaannos)
public Set<String> kaanna(String sana)
Set
-olion, jossa on kaikki käännökset sanalle, tai null
-viitteen, jos sanaa ei ole sanakirjassapublic void poista(String sana)
Käännökset kannattanee tallentaa yllä olevan esimerkin Tehtavakirjanpito tapaan Map<String, Set<String>>
-tyyppiseen oliomuuttujaan.
Rajapinnan koodi:
package sanakirja; import java.util.Set; public interface UseanKaannoksenSanakirja { void lisaa(String sana, String kaannos); Set<String> kaanna(String sana); void poista(String sana); }
Esimerkkiohjelma:
UseanKaannoksenSanakirja sanakirja = new OmaUseanKaannoksenSanakirja(); sanakirja.lisaa("kuusi", "six"); sanakirja.lisaa("kuusi", "spruce"); sanakirja.lisaa("pii", "silicon"); sanakirja.lisaa("pii", "pi"); System.out.println(sanakirja.kaanna("kuusi")); sanakirja.poista("pii"); System.out.println(sanakirja.kaanna("pii"));
Tulostuu:
[six, spruce] null
Tehtävänäsi on toteuttaa pakkaukseen tyokalut
luokka OmaDuplikaattienPoistaja
, joka tallettaa annetut merkkijonot siten, että annetuista merkkijonoista poistetaan samanlaiset merkkijonot (eli duplikaatit). Lisäksi luokka pitää kirjaa duplikaattien määrästä. Luokan tulee toteuttaa tehtäväpohjassa annettu rajapinta DuplikaattienPoistaja
, jossa on seuraavat toiminnot:
public void lisaa(String merkkijono)
public int getHavaittujenDuplikaattienMaara()
public Set<String> getUniikitMerkkijonot()
Set<String>
-rajapinnan toteuttavan olion, jossa on kaikki uniikit lisätyt merkkijonot (ei siis duplikaatteja!). Jos merkkijonoja ei ole, palautetaan tyhjä joukko-olio.public void tyhjenna()
Rajapinnan koodi:
package tyokalut; import java.util.Set; public interface DuplikaattienPoistaja { void lisaa(String merkkijono); int getHavaittujenDuplikaattienMaara(); Set<String> getUniikitMerkkijonot(); void tyhjenna(); }
Rajapintaa voi käyttää esimerkiksi näin:
public static void main(String[] args) { DuplikaattienPoistaja poistaja = new OmaDuplikaattienPoistaja(); poistaja.lisaa("eka"); poistaja.lisaa("toka"); poistaja.lisaa("eka"); System.out.println("Duplikaattien määrä nyt: " + poistaja.getHavaittujenDuplikaattienMaara()); poistaja.lisaa("vika"); poistaja.lisaa("vika"); poistaja.lisaa("uusi"); System.out.println("Duplikaattien määrä nyt: " + poistaja.getHavaittujenDuplikaattienMaara()); System.out.println("Uniikit merkkijonot: " + poistaja.getUniikitMerkkijonot()); poistaja.tyhjenna(); System.out.println("Duplikaattien määrä nyt: " + poistaja.getHavaittujenDuplikaattienMaara()); System.out.println("Uniikit merkkijonot: " + poistaja.getUniikitMerkkijonot()); }
Yllä oleva ohjelma tulostaisi: (merkkijonojen järjestys saa vaihdella, sillä ei ole merkitystä)
Duplikaattien määrä nyt: 1 Duplikaattien määrä nyt: 2 Uniikit merkkijonot: [eka, toka, vika, uusi] Duplikaattien määrä nyt: 0 Uniikit merkkijonot: []
Kuten muistamme, oliomuuttujat ovat viitetyyppisiä, eli muuttuja ei tallenna olioa itseään vaan viitteen olioon. Vastaavasti jos olio laitetaan esim. ArrayListiin, ei listalle talleteta olioa itseään vaan viite olioon. Mikään ei estäkään tallentamasta samaan olioon viitettä esim. useaan listaan tai HashMapiin.
Tarkastellaan esimerkkinä kirjastoa joka tallettaa kirjat hashMapeihin sekä kirjailijan että kirjan isbn-numeron perusteella. Tämän lisäksi kirjasto pitää kaikkia lainassa olevia sekä hyllyssä olevia kirjoja omalla listallaan.
public class Kirja { private String ISBN; private String kirjailija; private String nimi; private int vuosi; // ... }
public class Kirjasto { private Map<String, Kirja> kirjaIsbnNumeronPerusteella; private Map<String, List<Kirja>> kirjatKirjailijanPerusteella; private List<Kirja> lainassaOlevatKirjat; private List<Kirja> hyllyssaOlevatKirjat; public Kirjasto() { this.kirjaIsbnNumeronPerusteella = new HashMap<>(); this.kirjatKirjailijanPerusteella = new HashMap<>(); this.lainassaOlevatKirjat = new ArrayList<>(); this.hyllyssaOlevatKirjat = new ArrayList<>(); } public void lisaaKirjaKokoelmaan(Kirja uusiKirja) { kirjaIsbnNumeronPerusteella.put(uusiKirja.getIsbn(), uusiKirja); if(!kirjatKirjailijanPerusteella.containsKey(uusiKirja.getKirjailija()) { this.kirjatKirjailijanPerusteella.put(uusiKirja.getKirjailija(), new ArrayList<>()); } kirjatKirjailijanPerusteella.get(uusikirja.getKirjailija()).add(uusiKirja); hyllyssaOlevatKirjat.add(uusiKirja); } public Kirja haeKirjaIsbnNumeronPerusteella(String isbn){ return kirjaIsbnNumeronPerusteella.get(isbn); } // ... }
Jos olio on yhtäaikaa useassa kokoelmassa (listalla, joukossa tai map-rakenteessa), on kiinnitettävä erityistä huomiota, että kokoelmien tila on konsistentti. Jos esim. kirja päätetään poistaa, on se poistettava molemmista mapeista sekä lainassa/hyllyssä olevia kuvaavalta listalta.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Tehdään sovellus jonka avulla on mahdollista hallinnoida ihmisten puhelinnumeroita ja osoitteita.
Tehtävän voi suorittaa 1-5 pisteen laajuisena. Yhden pisteen laajuuteen on toteutettava seuraavat toiminnot:
kahteen pisteeseen vaaditaan edellisten lisäksi
kolmeen pisteeseen vaaditaan edellisten lisäksi
neljään pisteeseen vaaditaan toiminto
ja täysiin pisteeseen vaaditaan vielä
Esimerkki ohjelman toiminnasta:
numerotiedustelu käytettävissä olevat komennot: 1 lisää numero 2 hae numerot 3 hae puhelinnumeroa vastaava henkilö 4 lisää osoite 5 hae henkilön tiedot 6 poista henkilön tiedot 7 filtteröity listaus x lopeta komento: 1 kenelle: pekka numero: 040-123456 komento: 2 kenen: jukka ei löytynyt komento: 2 kenen: pekka 040-123456 komento: 1 kenelle: pekka numero: 09-222333 komento: 2 kenen: pekka 040-123456 09-222333 komento: 3 numero: 02-444123 ei löytynyt komento: 3 numero: 09-222333 pekka komento: 5 kenen: pekka osoite ei tiedossa puhelinnumerot: 040-123456 09-222333 komento: 4 kenelle: pekka katu: ida ekmanintie kaupunki: helsinki komento: 5 kenen: pekka osoite: ida ekmanintie helsinki puhelinnumerot: 040-123456 09-222333 komento: 4 kenelle: jukka katu: korsontie kaupunki: vantaa komento: 5 kenen: jukka osoite: korsontie vantaa ei puhelinta komento: 7 hakusana (jos tyhjä, listataan kaikki): kk jukka osoite: korsontie vantaa ei puhelinta pekka osoite: ida ekmanintie helsinki puhelinnumerot: 040-123456 09-222333 komento: 7 hakusana (jos tyhjä, listataan kaikki): vantaa jukka osoite: korsontie vantaa ei puhelinta komento: 7 hakusana (jos tyhjä, listataan kaikki): seppo ei löytynyt komento: 6 kenet: jukka komento: 5 kenen: jukka ei löytynyt komento: x
Huomioita: