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
Kuten kurssilla ohjelmoinnin perusteet, myös kurssilla ohjelmoinnin jatkokurssi on selitystehtäviä. Ajattele tehtäviä niin, että harjoittelet kertomaan kurssiin liittyvistä asioista kaverillesi. Käytä kursseilta tutuksi tullutta termistöä -- kertaa myös aiempaa materiaalia tarvittaessa. Kukin selitystehtävä on kahden tehtäväpisteen arvoinen.
Kurssilla on jo useampaan otteeseen käytetty metodia public String toString()
olion merkkijonoesityksen muodostamiseen esim. tulostuskomennon yhteydessä. Emme ole saaneet selvyyttä miksi Java osaa käyttää kyseistä metodia. Olemattoman metodin kutsuminenhan tuottaa normaalisti virheen. Tutkitaan seuraavaa luokkaa Kirja
, jolla ei ole metodia public String toString()
, ja ohjelmaa joka yrittää tulostaa Kirja
-luokasta luodun olion System.out.println()
-komennolla.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } }
Kirja olioKirja = new Kirja("Oliokirja", 2000); System.out.println(olioKirja);
Ohjelmamme ei tulosta virheilmoitusta tai kaadu kun annamme Kirja
-luokasta tehdyn olion parametrina System.out.println
-komennolle. Näemme virheilmoituksen tai kaatumisen sijaan mielenkiintoisen tulosteen. Tuloste sisältää luokan Kirja
nimen ja epämääräisen @-merkkiä seuraavan merkkijonon. Huomaa että kutsussa System.out.println(olioKirja)
Java tekee oikeasti kutsun System.out.println(olioKirja.toString())
Selitys liittyy Javan luokkien rakenteeseen. Jokainen Javan luokka perii automaattisesti luokan Object
, joka sisältää joukon jokaiselle Javan luokalle hyödyllisiä perusmetodeja. Perintä tarkoittaa että oma luokkamme saa käyttöön perittävän luokan määrittelemiä toiminnallisuuksia ja ominaisuuksia. Luokka Object
sisältää muun muassa metodin toString
, joka periytyy luomiimme luokiin.
Object-luokassa määritelty toString
-metodin tuottama merkkijono ei yleensä ole toivomamme. Tämän takia meidän tulee korvata, eli syrjäyttää metodi omalla toteutuksellamme. Lisätään luokkaan Kirja
metodi public String toString()
, joka korvaa perityssä Object
luokassa olevan metodin toString
.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } @Override public String toString() { return this.nimi + " (" + this.julkaisuvuosi + ")"; } }
Nyt kun teemme oliosta ilmentymän ja annamme sen tulostusmetodille, näemme luokassa Kirja
olevan toString
-metodin tuottaman merkkijonon.
Kirja olioKirja = new Kirja("Oliokirja", 2000); System.out.println(olioKirja);
Oliokirja (2000)
Luokassa Kirja
olevan metodin toString
yläpuolella on annotaatio @Override
. Annotaatioilla annetaan vinkkejä sekä kääntäjälle että lukijalle siitä, miten metodeihin tulisi suhtautua. Annotaatio @Override
kertoo kääntäjälle että annotaatiota seuraava metodi korvaa perityssä luokassa määritellyn metodin. Jos korvattavaan metodiin ei liitetä annotaatiota, antaa kääntäjä tilanteesa varoituksen, overriden kirjottamatta jättäminen ei kuitenkaan ole virhe.
Luokasta Object
peritään muitakin hyödyllisiä metodeja. Tutustutaan seuraavaksi metodeihin equals
ja hashCode
.
Metodia equals
käytetään kahden olion yhtäsuuruusvertailuun. Metodia on jo käytetty muun muassa String
-olioiden yhteydessä.
Scanner lukija = new Scanner(System.in); System.out.print("Kirjoita salasana: "); String salasana = lukija.nextLine(); if(salasana.equals("salasana")) { System.out.println("Oikein meni!"); } else { System.out.println("Pieleen meni!"); }
Kirjoita salasana: mahtiporkkana Pieleen meni!
Luokassa Object
määritelty equals
-metodi tarkistaa onko parametrina annetulla oliolla sama viite kuin oliolla johon verrataan, eli toisinsanoen oletusarvoisesti vertaillaan onko kyse kahdesta samasta oliosta. Jos viite on sama, palauttaa metodi arvon true
, muuten false
. Tämä selvenee seuraavalla esimerkillä. Luokassa Kirja
ei ole omaa equals
-metodin toteutusta, joten se käyttää Object
-luokassa olevaa toteutusta.
Kirja olioKirja = new Kirja("Oliokirja", 2000); Kirja toinenOlioKirja = olioKirja; if (olioKirja.equals(toinenOlioKirja)) { System.out.println("Kirjat olivat samat"); } else { System.out.println("Kirjat eivät olleet samat"); } // nyt luodaan saman sisältöinen olio joka kuitenkin on oma erillinen olionsa toinenOlioKirja = new Kirja("Oliokirja", 2000); if (olioKirja.equals(toinenOlioKirja)) { System.out.println("Kirjat olivat samat"); } else { System.out.println("Kirjat eivät olleet samat"); }
Tulostuu:
Kirjat olivat samat Kirjat eivät olleet samat
Vaikka Kirja
-olioiden sisäinen rakenne (eli oliomuuttujien arvot) ovat molemmissa tapauksissa täsmälleen samat, vain ensimmäinen vertailu tulostaa merkkijonon "Kirjat olivat samat
". Tämä johtuu siitä että vain ensimmäisessä tapauksessa myös viitteet ovat samat eli vertaillaan olioa itseensä. Toisessa vertailussa kyse on kahdesta eri oliosta, vaikka muuttujilla onkin samat arvot.
Merkkijonojen eli Stringien yhteydessä equals
toimii odotetulla tavalla, eli se ilmoittaa kaksi samansisältöistä merkkijonoa "equalseiksi" vaikka kyseessä olisikin kaksi erillistä olioa. String-luokassa onkin korvattu oletusarvoinen equals
omalla toteutuksella.
Haluamme että kirjojen vertailu onnistuu myös nimen ja vuoden perusteella. Korvataan Object
-luokassa oleva metodi equals
määrittelemällä sille toteutus luokkaan Kirja
. Metodin equals
tehtävänä on selvittää onko olio sama kuin metodin parametrina saatu olio. Metodi saa parametrina Object
-tyyppisen viitteen olion. Määritellään ensin metodi, jonka mielestä kaikki oliot ovat samoja.
public boolean equals(Object olio) { return true; }
Metodimme on varsin optimistinen, joten muutetaan sen toimintaa hieman. Määritellään että oliot eivät ole samoja jos parametrina saatu olio on null tai jos olioiden tyypit eivät ole samat. Olion tyypin saa (Object
-luokassa määritellyllä) metodilla getClass()
. Muussa tapauksessa oletetaan että oliot ovat samat.
public boolean equals(Object olio) { if (olio == null) { return false; } if (this.getClass() != olio.getClass()) { return false; } return true; }
Metodi equals
huomaa eron erityyppisten olioiden välillä, mutta ei vielä osaa erottaa samanlaisia olioita toisistaan. Jotta voisimme verrata nykyistä oliota ja parametrina saatua Object
-tyyppisellä parametrilla viitattua olioa, tulee Object-viitteen tyyppiä muuttaa. Viitteen tyyppiä voidaan muuttaa tyyppimuunnoksella jos ja vain jos olion tyyppi on oikeasti sellainen, mihin sitä yritetään muuttaa. Tyyppimuunnos tapahtuu antamalla asetuslauseen oikealla puolella haluttu luokka suluissa, esimerkiksi:
HaluttuTyyppi muuttuja = (HaluttuTyyppi) vanhaMuuttuja;
Voimme tehdä tyyppimuunnoksen koska tiedämme olioiden olevan samantyyppisiä, jos ne ovat erityyppisiä yllä oleva metodi getClass
palauttaa arvon false. Muunnetaan metodissa equals
saatu Object
-tyyppinen parametri Kirja
-tyyppiseksi, ja todetaan kirjojen olevan eri jos niiden julkaisuvuodet ovat eri. Muuten kirjat ovat vielä samat.
public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if(this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } return true; }
Nyt vertailumetodimme osaa erottaa eri vuosina julkaistut kirjat. Lisätään vielä tarkistus, että kirjojemme nimet ovat samat ja että oman kirjamme nimi ei ole null.
public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) { return false; } return true; }
Mahtavaa, viimeinkin toimiva vertailumetodi! Alla vielä tämänhetkinen Kirja
-luokkamme.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } @Override public String toString() { return this.nimi + " (" + this.julkaisuvuosi + ")"; } @Override public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) { return false; } return true; } }
Nyt kirjojen vertailu palauttaa true
jos kirjojen sisällöt ovat samat.
Kirja olioKirja = new Kirja("Oliokirja", 2000); Kirja toinenOlioKirja = new Kirja("Oliokirja", 2000); if (olioKirja.equals(toinenOlioKirja)) { System.out.println("Kirjat olivat samat"); } else { System.out.println("Kirjat eivät olleet samat"); }
Kirjat olivat samat
Useat Javan valmiit tietorakenteet käyttävät equals
-metodia osana sisäistä hakumekanismiaan. Esimerkiksi luokan ArrayList
contains
-metodi vertailee olioiden yhtäsuuruutta equals
-metodin avulla. Jatketaan aiemmin määrittelemämme Kirja
-luokan käyttöä seuraavassa esimerkissä. Jos emme toteuta omissa olioissamme equals
-metodia, emme voi käyttää esimerkiksi contains
-metodia. Kokeile alla olevaa koodia kahdella erilaisella Kirja
-luokalla. Toisessa on equals
-metodi, ja toisessa sitä ei ole.
ArrayList<Kirja> kirjat = new ArrayList<>(); Kirja olioKirja = new Kirja("Oliokirja", 2000); kirjat.add(olioKirja); if (kirjat.contains(olioKirja)) { System.out.println("Oliokirja löytyi."); } olioKirja = new Kirja("Oliokirja", 2000); if (!kirjat.contains(olioKirja)) { System.out.println("Oliokirjaa ei löytynyt."); }
Metodi hashCode
luo oliosta numeerisen arvon eli hajautusarvon. Numeerista arvoa tarvitaan esimerkiksi jos olioa käytetään HashMap:in avaimena. Olemme tähän mennessä käyttäneet HashMapin avaimina ainoastaan String- ja Integer-tyyppisiä olioita, joilla on ollut valmiina sopivasti toteutetut hashCode
-metodit. Luodaan esimerkki jossa näin ei ole: jatketaan kirjojen parissa ja ruvetaan pitämään kirjaa lainassa olevista kirjoista. Päätetään ratkaista kirjanpito HashMapin avulla. Avaimena toimii kirja ja kirjaan liitetty arvo on merkkijono, joka keroo lainaajan nimen:
HashMap<Kirja, String> lainaajat = new HashMap<>(); Kirja oliokirja = new Kirja("Oliokirja", 2000); lainaajat.put(oliokirja, "Pekka"); lainaajat.put(new Kirja("Test Driven Development", 1999), "Arto"); System.out.println(lainaajat.get(oliokirja)); System.out.println(lainaajat.get(new Kirja("Oliokirja", 2000)); System.out.println(lainaajat.get(new Kirja("Test Driven Development", 1999));
Tulostuu:
Pekka null null
Löydämme lainaajan hakiessamme samalla oliolla, joka annettiin hajautustaulun put
-metodille avaimeksi. Täsmälleen samanlaisella kirjalla mutta eri oliolla haettaessa lainaajaa ei kuitenkaan löydy ja saamme null-viitteen. Syynä on taas Object
-luokassa oleva hashCode
-metodin oletustoteutus. Oletustoteutus luo hashCode
-arvon olion viitteen perusteella, eli samansisältöiset mutta eri oliot saavat eri tuloksen hashCode-metodista. Tämän takia olioa ei osata etsiä oikeasta paikasta HashMapia.
Jotta HashMap toimisi haluamallamme tavalla, eli palauttaisi lainaajan kun avaimeksi annetaan oikean sisältöinen olio (ei välttämässä siis sama olio kuin alkuperäinen avain), on avaimena toimivan luokan ylikirjoitettava metodin equals
lisäksi metodi hashCode
. Metodi on ylikirjoitettava siten, että se antaa saman numeerisen tuloksen kaikille samansisältöisille olioille. Myös jotkut erisisältöiset oliot saavat saada saman tuloksen hashCode-metodista, on kuitenkin HashMapin tehokkuuden kannalta oleellista, että erisisältöiset oliot saavat mahdollisimman harvoin saman tuloksen.
Olemme aiemmin käyttäneet String
-olioita menestyksekkäästi HashMapin avaimena, joten voimme päätellä että String
-luokassa on oma järkevästi toimiva hashCode
-toteutus. Delegoidaan, eli siirretään laskemisvastuu String
-oliolle.
public int hashCode() { return this.nimi.hashCode(); }
Yllä oleva ratkaisu on melko hyvä, mutta jos nimi
on null, näemme NullPointerException
-virheen. Korjataan tämä vielä määrittelemällä ehto: jos nimi
-muuttujan arvo on null, palautetaan arvo 7. Arvo 7 on "satunnaisesti" valittu alkuluku, yhtä hyvin olisi voitu valita esimerkiksi arvo 13.
public int hashCode() { if (this.nimi == null) { return 7; } return this.nimi.hashCode(); }
Parantelemme vielä hashCode
-metodia siten, että koodin laskennassa huomioidaan myös kirjan julkaisuvuosi:
public int hashCode() { if (this.nimi == null) { return 7; } return this.julkaisuvuosi + this.nimi.hashCode(); }
Sivuhuomautus: HashMapissa avaimina olevien olioiden hashCode-metodin tulos kertoo hajautusrakenteeseen talletettavien arvon paikan eli indeksin hajautustaulussa. Saatat tässä kohtaa miettiä "eikö tämä johda tilanteeseen jossa useampi olio päätyy samaan indeksiin hajautustaulussa?". Vastaus on kyllä ja ei. Vaikka metodi hashCode
antaisi kahdelle eri oliolle saman arvon, on hajautustaulut toteutettu sisäisesti siten että useampi olio voi olla samassa indeksissä. Jotta samassa indeksissä olevat oliot voi erottaa toisistaan, tulee hajautustaulun avaimina toimivina olioilla olla metodi equals
toteutettuna. Lisätietoa hajautustaulujen toteuttamisen periaatteista tulee kurssilla tietorakenteet ja algoritmit.
Luokka Kirja
nyt kokonaisuudessaan.
public class Kirja { private String nimi; private int julkaisuvuosi; public Kirja(String nimi, int julkaisuvuosi) { this.nimi = nimi; this.julkaisuvuosi = julkaisuvuosi; } public String getNimi() { return this.nimi; } public int getJulkaisuvuosi() { return this.julkaisuvuosi; } @Override public String toString() { return this.nimi + " (" + this.julkaisuvuosi + ")"; } @Override public boolean equals(Object olio) { if (olio == null) { return false; } if (getClass() != olio.getClass()) { return false; } Kirja verrattava = (Kirja) olio; if (this.julkaisuvuosi != verrattava.getJulkaisuvuosi()) { return false; } if (this.nimi == null || !this.nimi.equals(verrattava.getNimi())) { return false; } return true; } public int hashCode() { if (this.nimi == null) { return 7; } return this.julkaisuvuosi + this.nimi.hashCode(); } }
Kerrataan vielä: jotta luokkaa voidaan käyttää HashMap:in avaimena, tulee sille määritellä
equals
siten, että kaikki samansisältöisenä ajatellut oliot tuottavat vertailussa tuloksen true ja erisisältöiset falsehashCode
siten, että kaikki samansisältöisenä ajatelluille olioille metodin kutsu tuottaa saman arvonLuokalle Kirja
määrittelemämme equals ja hashCode selvästi täyttävät nämä ehdot. Nyt myös aiemmin kohtaamamme ongelma ratkeaa ja kirjojen lainaajat löytyvät:
HashMap<Kirja, String> lainaajat = new HashMap<>(); Kirja oliokirja = new Kirja("Oliokirja", 2000); lainaajat.put(oliokirja, "Pekka"); lainaajat.put(new Kirja("Test Driven Development",1999), "Arto"); System.out.println(lainaajat.get(oliokirja)); System.out.println(lainaajat.get(new Kirja("Oliokirja", 2000)); System.out.println(lainaajat.get(new Kirja("Test Driven Development", 1999));
Tulostuu:
Pekka Pekka Arto
NetBeans tarjoaa metodien equals
ja hashCode
automaattisen luonnin. Voit valita valikosta Source -> Insert Code, ja valita aukeavasta listasta equals() and hashCode(). Tämän jälkeen NetBeans kysyy oliomuuttujat joita metodeissa käytetään.
Eurooppalaiset rekisteritunnukset koostuvat kahdesta osasta: yksi tai kaksikirjaimisesta maatunnuksesta ja maakohtaisesti määrittyvästä rekisterinumerosta, joka taas koostuu numeroista ja merkeistä. Rekisterinumeroita esitetään seuraavanlaisen luokan avulla:
public class Rekisterinumero { // HUOM: oliomuuttujissa on määre final eli niiden arvoa ei voi muuttaa! private final String rekNro; private final String maa; public Rekisterinumero(String rekNro, String maa) { this.rekNro = rekNro; this.maa = maa; } public String toString(){ return maa+ " "+rekNro; } }
Rekisterinumeroja halutaan tallettaa esim. ArrayList:eille ja käyttää HashMap:in avaimina, eli kuten yllä mainittu, tulee niille toteuttaa metodit equals
ja hashCode
, muuten ne eivät toimi halutulla tavalla.
Vihje: ota equals- ja hashCode-metodeihin mallia yllä olevasta Kirja-esimerkistä. Rekisterinumeron hashCode voidaan muodostaa esim. maatunnuksen ja rekNro:n hashCodejen summana.
Esimerkkiohjelma:
public static void main(String[] args) { Rekisterinumero rek1 = new Rekisterinumero("FI", "ABC-123"); Rekisterinumero rek2 = new Rekisterinumero("FI", "UXE-465"); Rekisterinumero rek3 = new Rekisterinumero("D", "B WQ-431"); ArrayList<Rekisterinumero> suomalaiset = new ArrayList<>(); suomalaiset.add(rek1); suomalaiset.add(rek2); Rekisterinumero uusi = new Rekisterinumero("FI", "ABC-123"); if (!suomalaiset.contains(uusi)) { suomalaiset.add(uusi); } System.out.println("suomalaiset: " + suomalaiset); // jos equals-metodia ei ole ylikirjoitettu, menee sama rekisterinumero toistamiseen listalle HashMap<Rekisterinumero, String> omistajat = new HashMap<>(); omistajat.put(rek1, "Arto"); omistajat.put(rek3, "Jürgen"); System.out.println("omistajat:"); System.out.println(omistajat.get(new Rekisterinumero("FI", "ABC-123"))); System.out.println(omistajat.get(new Rekisterinumero("D", "B WQ-431"))); // jos hashCode ei ole ylikirjoitettu, eivät omistajat löydy }
Jos equals ja hashCode on toteutettu oikein, tulostus on seuraavanlainen
suomalaiset: [FI ABC-123, FI UXE-465] omistajat: Arto Jürgen
Toteuta luokka Ajoneuvorekisteri
jolla on seuraavat metodit:
public boolean lisaa(Rekisterinumero rekkari, String omistaja)
lisää parametrina olevaa rekisterinumeroa vastaavalle autolle parametrina olevan omistajan, metodi palauttaa true jos omistajaa ei ollut ennestään, jos rekisterinumeroa vastaavalla autolla oli jo omistaja, metodi palauttaa false ja ei tee mitäänpublic String hae(Rekisterinumero rekkari)
palauttaa parametrina olevaa rekisterinumeroa vastaavan auton omistajan. Jos auto ei ole rekisterissä, palautetaan null
public boolean poista(Rekisterinumero rekkari)
poistaa parametrina olevaa rekisterinumeroa vastaavat tiedot, metodi palauttaa true jos tiedot poistetiin, ja false jos parametria vastaavia tietoja ei ollut rekisterissäHuom: Ajoneuvorekisterin täytyy tallettaa omistajatiedot HashMap<Rekisterinumero, String> omistajat
-tyyppiseen oliomuuttujaan!
Huom! Kertaa tässä välissä viime viikon loppupuolelta HashMappeihin liittyneet metodit keySet()
ja values()
.
Lisää Ajoneuvorekisteriin vielä seuraavat metodit:
public void tulostaRekisterinumerot()
tulostaa rekisterissä olevat rekisterinumerotpublic void tulostaOmistajat()
tulostaa rekisterissä olevien autojen omistajat, yhden omistajan nimeä ei saa tulostaa kuin kertaalleen vaikka omistajalla olisikin useampi autoRajapinnan (engl. interface) avulla määritellään luokalta vaadittu käyttäytyminen, eli sen metodit. Rajapinnat määritellään kuten normaalit Javan olevat luokat, mutta luokan alussa olevan määrittelyn "public class ...
" sijaan käytetään määrittelyä "public interface ...
". Rajapinnat määrittelevät käyttäytymisen metodien niminä ja palautusarvoina, mutta ne eivät aina sisällä metodien konkreettista toteutusta. Näkyvyysmäärettä rajapintoihin ei erikseen merkitä, 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. Luettava kuvaa käyttäytymistä: esimerkiksi tekstiviesti tai sähköpostiviesti voi olla luettava.
Rajapinnan toteuttavat luokat päättävät miten rajapinnassa määritellyt metodit toteutetaan. Luokka toteuttaa rajapinnan lisäämällä luokan nimen jälkeen avainsanan 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.
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<>(); tekstiviestit.add(new Tekstiviesti("tuntematon numero", "I hid the body.");
Huikeaa menoa!
ArrayList<String> sivut = new ArrayList<g>(); sivut.add("Pilko metodisi lyhyiksi luettaviksi kokonaisuuksiksi."); sivut.add("Erota käyttöliittymälogiikka sovelluksen logiikasta."); sivut.add("Ohjelmoi aina ensin pieni osa, jolla ratkaiset 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 osa, jolla ratkaiset osan ongelmasta. Harjoittelu tekee mestarin. Keksi joku hauska oma projekti.
Tehtäväpohjassa on valmiina rajapinta Palvelusvelvollinen
, jossa on seuraavat toiminnot:
int getTJ()
palauttaa jäljellä olevien palveluspäivien määränvoid palvele()
vähentää yhden palveluspäivän. Palveluspäivien määrä ei saa mennä negatiiviseksi.public interface Palvelusvelvollinen { int getTJ(); void palvele(); }
Tee Palvelusvelvollinen
-rajapinnan toteuttava luokka Sivari
, jolla parametriton konstruktori. Luokalla on oliomuuttuja TJ, joka alustetaan konstruktorikutsun yhteydessä arvoon 362.
Tee Palvelusvelvollinen
-rajapinnan toteuttava luokka Asevelvollinen
, jolla on parametrillinen konstruktori, jolla määritellään palvelusaika (int tj
).
Uutta muuttujaa esitellessä esitellään aina muuttujan tyyppi. Muuttujatyyppejä on kahdenlaisia, alkeistyyppiset muuttujat (int, double, ...) ja viitetyyppiset muuttujat (kaikki oliot). Olemme tähän mennessä käyttäneet viitetyyppisten muuttujien tyyppinä olion luokkaa.
String merkkijono = "merkkijono-olio"; Tekstiviesti viesti = new Tekstiviesti("ope", "Kohta tapahtuu huikeita");
Olion tyyppi voi olla muutakin kuin sen luokka. Esimerkiksi rajapinnan Luettava
toteuttavan luokan Sahkokirja
tyyppi on sekä Sahkokirja
että Luettava
. Samalla tavalla myös tekstiviestillä on monta tyyppiä. 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<>(); 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<>(); 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")); ArrayList<String> sivut = new ArrayList<>(); sivut.add("Metodi voi kutsua itse itseään."); lukulista.add(new Sahkokirja("Rekursion alkeet.", sivut)); 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 jos ja vain jos // luettava on tyyppiä Tekstiviesti
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<>(); 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<>(); } public void lisaa(Luettava luettava) { this.luettavat.add(luettava); } public int luettavia() { return this.luettavat.size(); } }
Lukulistat ovat yleensä luettavia, joten toteutetaan luokalle Lukulista
rajapinta Luettava
. Lukulistan lue
-metodi lukee kaikki luettavat
-listalla olevat oliot läpi, ja lisää yksitellen niiden lue()
-metodin palauttaman merkkijonoon.
public class Lukulista implements Luettava { private ArrayList<Luettava> luettavat; public Lukulista() { this.luettavat = new ArrayList<>(); } public void lisaa(Luettava luettava) { this.luettavat.add(luettava); } public int luettavia() { return this.luettavat.size(); } public String lue() { String luettu = ""; for(Luettava luettava: this.luettavat) { luettu += luettava.lue() + "\n"; } // kun lukulista on luettu, tyhjennetään se this.luettavat.clear(); return luettu; } }
Lukulista joninLista = new Lukulista(); joninLista.lisaa(new Tekstiviesti("arto", "teitkö jo testit?")); joninLista.lisaa(new Tekstiviesti("arto", "katsoitko jo palautukset?")); System.out.println("Jonilla luettavia: " + joninLista.luettavia());
Jonilla luettavia: 2
Koska Lukulista
on tyyppiä Luettava
, voi lukulistalle lisätä Lukulista
-olioita. Alla olevassa esimerkissä Jonilla on paljon luettavaa. Onneksi Verna tulee hätiin ja lukee viestit Jonin puolesta.
Lukulista joninLista = new Lukulista(); for (int i = 0; i < 1000; i++) { joninLista.lisaa(new Tekstiviesti("arto", "teitkö jo testit?")); } System.out.println("Jonilla luettavia: " + joninLista.luettavia()); System.out.println("Delegoidaan lukeminen Vernalle"); Lukulista vernanLista = new Lukulista(); vernanLista.lisaa(joninLista); vernanLista.lue(); System.out.println(); System.out.println("Jonilla luettavia: " + joninLista.luettavia());
Jonilla luettavia: 1000 Delegoidaan lukeminen Vernalle Jonilla luettavia: 0
Ohjelmassa Vernan listalle kutsuttu lue
-metodi käy kaikki sen sisältämät Luettava
-oliot läpi, ja kutsuu niiden lue
-metodia. Kutsuttaessa lue
-metodia Vernan listalle käydään myös Vernan lukulistalla oleva Jonin lukulista läpi. Jonin lukulista käydään läpi kutsumalla sen lue
-metodia. Jokaisen lue
-metodin kutsun lopussa tyhjennetään juuri luettu lista. Eli Jonin lukulista tyhjenee kun Verna lukee sen.
Tässä on jo hyvin paljon viitteitä, kannattaa piirtää oliot paperille ja hahmotella miten vernanLista
-oliolle tapahtuva metodikutsu lue
etenee!
Muuton yhteydessa tarvitaan muuttolaatikoita. Laatikoihin talletetaan erilaisia esineitä. Kaikkien laatikoihin talletettavien esineiden on toteutettava seuraava rajapinta:
public interface Talletettava { double paino(); }
Lisää rajapinta ohjelmaasi. Rajapinta lisätään melkein samalla tavalla kuin luokka, new Java class sijaan valitaan new Java interface.
Tee rajapinnan toteuttavat luokat Kirja
ja CDLevy
. Kirja saa konstruktorin parametreina kirjan kirjoittajan (String), kirjan nimen (String), ja kirjan painon (double). CD-Levyn konstruktorin parametreina annetaan artisti (String), levyn nimi (String), ja julkaisuvuosi (int). Kaikkien CD-levyjen paino on 0.1 kg.
Muista toteuttaa luokilla myös rajapinta Talletettava
. Luokkien tulee toimia seuraavasti:
public static void main(String[] args) { Kirja kirja1 = new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2); Kirja kirja2 = new Kirja("Robert Martin", "Clean Code", 1); Kirja kirja3 = new Kirja("Kent Beck", "Test Driven Development", 0.5); CDLevy cd1 = new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973); CDLevy cd2 = new CDLevy("Wigwam", "Nuclear Nightclub", 1975); CDLevy cd3 = new CDLevy("Rendezvous Park", "Closer to Being Here", 2012); System.out.println(kirja1); System.out.println(kirja2); System.out.println(kirja3); System.out.println(cd1); System.out.println(cd2); System.out.println(cd3); }
Tulostus:
Fedor Dostojevski: Rikos ja Rangaistus Robert Martin: Clean Code Kent Beck: Test Driven Development Pink Floyd: Dark Side of the Moon (1973) Wigwam: Nuclear Nightclub (1975) Rendezvous Park: Closer to Being Here (2012)
Huom! Painoa ei ilmoiteta tulostuksessa.
Tee luokka laatikko, jonka sisälle voidaan tallettaa Talletettava
-rajapinnan toteuttavia tavaroita. Laatikko saa konstruktorissaan parametrina laatikon maksimikapasiteetin kiloina. Laatikkoon ei saa lisätä enempää tavaraa kuin sen maksimikapasiteetti määrää. Laatikon sisältämien tavaroiden paino ei siis koskaan saa olla yli laatikon maksimikapasiteetin.
Seuraavassa esimerkki laatikon käytöstä:
public static void main(String[] args) { Laatikko laatikko = new Laatikko(10); laatikko.lisaa(new Kirja("Fedor Dostojevski", "Rikos ja Rangaistus", 2)) ; laatikko.lisaa(new Kirja("Robert Martin", "Clean Code", 1)); laatikko.lisaa(new Kirja("Kent Beck", "Test Driven Development", 0.7)); laatikko.lisaa(new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973)); laatikko.lisaa(new CDLevy("Wigwam", "Nuclear Nightclub", 1975)); laatikko.lisaa(new CDLevy("Rendezvous Park", "Closer to Being Here", 2012)); System.out.println(laatikko); }
Tulostuu
Laatikko: 6 esinettä, paino yhteensä 4.0 kiloa
Huom: koska painot esitetään doubleina, saattaa laskutoimituksissa tulla pieniä pyöristysvirheitä. Tehtävässä ei tarvitse välittää niistä.
Jos teit laatikon sisälle oliomuuttujan double paino
, joka muistaa laatikossa olevien esineiden painon, korvaa se metodilla, joka laskee painon:
public class Laatikko { //... public double paino() { double paino = 0; // laske laatikkoon talletettujen tavaroiden yhteispaino return paino; } }
Kun tarvitset laatikon sisällä painoa esim. uuden tavaran lisäyksen yhteydessä, riittää siis kutsua laatikon painon laskevaa metodia.
Metodi toki voisi palauttaa myös oliomuuttujan arvon. Harjoittelemme tässä kuitenkin tilannetta, jossa oliomuuttujaa ei tarvitse eksplisiittisesti ylläpitää vaan se voidaan tarpeentullen laskea. Seuraavan tehtävän jälkeen laatikossa olevaan oliomuuttujaan talletettu painotieto ei kuitenkaan välttämättä enää toimisi. Miksi?
Rajapinnan Talletettava
toteuttaminen siis edellyttää että luokalla on metodi double paino()
. Laatikollehan lisättiin juuri tämä metodi. Laatikosta voidaan siis tehdä talletettava!
Laatikot ovat oliota joihin voidaan laittaa Talletettava
-rajapinnan toteuttavia olioita. Laatikot toteuttavat itsekin rajapinnan. Eli laatikon sisällä voi olla myös laatikoita!
Kokeile että näin varmasti on, eli tee ohjelmassasi muutama laatikko, laita laatikoihin tavaroita ja laita pienempiä laatikoita isompien laatikoiden sisään. Kokeile myös mitä tapahtuu kun laitat laatikon itsensä sisälle. Miksi näin käy?
Kuten mitä tahansa muuttujan tyyppiä, myös rajapintaa voi käyttää metodin paluuarvona. Seuraavassa Tehdas
, jota voi pyytää valmistamaan erilaisia Talletettava
-rajapinnan toteuttavia oliota. Tehdas valmistaa aluksi satunnaisesti kirjoja ja levyjä.
public class Tehdas { public Tehdas(){ // HUOM: parametritonta tyhjää konstruktoria ei ole pakko kirjoittaa, jos luokalla ei ole muita konstruktoreja // Java tekee automaattisesti tälläisissä tilanteissa luokalle oletuskonstruktorin eli parametrittoman tyhjän konstruktorin } public Talletettava valmistaUusi(){ Random arpa = new Random(); int luku = arpa.nextInt(4); if (luku == 0) { return new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973); } else if (luku == 1) { return new CDLevy("Wigwam", "Nuclear Nightclub", 1975); } else if (luku == 2) { return new Kirja("Robert Martin", "Clean Code", 1); } else { return new Kirja("Kent Beck", "Test Driven Development", 0.7); } } }
Tehdasta on mahdollista käyttää tuntematta tarkalleen mitä erityyppisiä Talletettava-rajapinnan luokkia on olemassa. Seuraavassa luokka Pakkaaja, jolta voi pyytää laatikollisen esineitä. Pakkaaja tuntee tehtaan, jota se pyytää luomaan esineet:
public class Pakkaaja { private Tehdas tehdas; public Pakkaaja() { this.tehdas = new Tehdas(); } public Laatikko annaLaatikollinen() { Laatikko laatikko = new Laatikko(100); for (int i = 0; i < 10; i++) { Talletettava uusiTavara = tehdas.valmistaUusi(); laatikko.lisaa(uusiTavara); } return laatikko; } }
Koska pakkaaja ei tunne rajapinnan Talletettava toteuttavia luokkia, on ohjelmaan mahdollisuus lisätä uusia luokkia jotka toteuttavat rajapinnan ilman tarvetta muuttaa pakkaajaa. Seuraavassa on luotu uusi Talletettava-rajapinnan toteuttava luokka, Suklaalevy
. Tehdasta on muutettu siten, että se luo kirjojen ja cd-levyjen lisäksi suklaalevyjä. Luokka Pakkaaja
toimii muuttamatta tehtaan laajennetun version kanssa.
public class Suklaalevy implements Talletettava { // koska Javan generoima oletuskonstruktori riittää, emme tarvitse konstruktoria! public double paino(){ return 0.2; } }
public class Tehdas { // koska Javan generoima oletuskonstruktori riittää, emme tarvitse konstruktoria! public Talletettava valmistaUusi() { Random arpa = new Random(); int luku = arpa.nextInt(5); if (luku == 0) { return new CDLevy("Pink Floyd", "Dark Side of the Moon", 1973); } else if (luku == 1) { return new CDLevy("Wigwam", "Nuclear Nightclub", 1975); } else if (luku == 2) { return new Kirja("Robert Martin", "Clean Code", 1 ); } else if (luku == 3) { return new Kirja("Kent Beck", "Test Driven Development", 0.7); } else { return new Suklaalevy(); } } }
Rajapintojen käyttö ohjelmoinnissa mahdollistaa luokkien välisten riippuvaisuuksien vähentämisen. Esimerkissämme Pakkaaja ei ole riippuvainen rajapinnan Talletettava-toteuttavista luokista vaan ainoastaan rajapinnasta. Tämä mahdollistaa rajapinnan toteuttavien luokkien lisäämisen ohjelmaan ilman tarvetta muuttaa luokkaa Pakkaaja. Myöskään pakkaajan käyttäjiin rajapinnan toteuttavien luokkien lisääminen ei vaikuta. Vähäisemmät riippuvuudet helpottavat ohjelman laajennettavuutta.
Javan API tarjoaa huomattavan määrän valmiita rajapintoja. Tutustutaan tässä neljään ehkä Javan eniten käytettyyn rajapintaan: List
, Map
, Set
ja Collection
.
Rajapinta List määrittelee listoihin liittyvän peruskäyttäytymisen. Koska ArrayList-luokka toteuttaa List
-rajapinnan, voi sitä käyttää myös List
-rajapinnan kautta.
List<String> merkkijonot = new ArrayList<>(); 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<>(); merkkijonot.add("merkkijono-olio linkedlist-oliossa!");
Molemmat rajapinnan List
toteutukset toimivat käyttäjän näkökulmasta samoin. Rajapinta siis abstrahoi niiden sisäisen toiminnallisuuden. ArrayListin ja LinkedListin sisäinen rakenne on kuitenkin huomattavan erilainen. ArrayList tallentaa alkioita taulukkoon, josta tietyllä indeksillä hakeminen on nopeaa. LinkedList taas rakentaa listan, jossa jokaisessa listan alkiossa on viite seuraavan listan alkioon. Kun linkitetyssä listassa haetaan alkiota tietyllä indeksillä, tulee listaa käydä läpi alusta indeksiin asti.
Isoilla listoille voimme nähdä huomattaviakin suorituskykyeroja. Linkitetyn listan vahvuutena on se, että listaan lisääminen on aina nopeaa. ArrayListillä taas taustalla on taulukko, jota täytyy kasvattaa aina kun se täyttyy. Taulukon kasvattaminen vaatii uuden taulukon luonnin ja vanhan taulukon tietojen kopioinnin uuteen taulukkoon. Toisaalta, indeksin perusteella hakeminen on Arraylististä erittäin nopeaa, kun taas linkitetyssä listassa joudutaan käymään listan alkioita yksitellen läpi tiettyyn indeksiin pääsemiseksi. Tietorakenteiden kuten linkitetyn listan ja ArrayListin sisäisestä toteutuksesta tulee lisää tietoa kurssilla Tietorakenteet ja algoritmit.
Tällä ohjelmointikurssilla eteen tulevissa tilanteissa kannattanee käytännössä valita aina 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<>(); kaannokset.put("gambatte", "tsemppiä"); kaannokset.put("hai", "kyllä");
Hajautustaulun avaimet saa hajautustaulusta keySet
-metodin avulla.
Map<String, String> kaannokset = new HashMap<>(); 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<>(); 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<>(); 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<>(); 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 käytettävä Map
-rajapintaa.
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 yksikkohinta
public int hinta()
palauttaa ostoksen hinnan. Hinta saadaan kertomalla kappalemäärä yksikköhinnallapublic void kasvataMaaraa()
kasvattaa ostoksen kappalemäärää yhdelläpublic String toString()
palauttaa ostoksen merkkijonomuodossa, joka on alla olevan esimerkin mukainenEsimerkki ostoksen käytöstä
Ostos ostos = new Ostos("maito", 4, 2); System.out.println("ostoksen joka sisältää 4 maitoa yhteishinta on " + ostos.hinta()); System.out.println(ostos); ostos.kasvataMaaraa(); System.out.println(ostos);
Tulostuu:
ostoksen joka sisältää 4 maitoa yhteishinta on 8 maito: 4 maito: 5
Huom: toString on siis muotoa tuote: kpl hintaa ei merkkijonoesitykseen tule!
Vihdoin pääsemme toteuttamaan luokan ostoskori!
Ostoskori tallettaa sisäisesti koriin lisätyt tuotteet Ostos-olioina. Ostoskorilla tulee olla oliomuuttuja jonka tyyppi on joko Map<String, Ostos>
tai List<Ostos>
. Älä laita mitään muita oliomuuttujia ostoskorille kuin ostosten talletukseen tarvittava Map tai List.
Huom: jos talletat Ostos-oliot Map-tyyppiseen apumuuttujaan, on tässä ja seuraavassa tehtävässä hyötyä Map:in metodista values(), jonka avulla on helppo käydä läpi kaikki talletetut ostos-oliot.
Tehdään aluksi ostoskorille parametriton konstruktori ja metodit:
public void lisaa(String tuote, int hinta)
lisää ostoskoriin ostoksen joka vastaa parametrina olevaa tuotetta ja jolla on parametrina annettu hinta.public int hinta()
palauttaa ostoskorin kokonaishinnanEsimerkki ostoksen käytöstä
Ostoskori kori = new Ostoskori(); kori.lisaa("maito", 3); kori.lisaa("piimä", 2); kori.lisaa("juusto", 5); System.out.println("korin hinta: " + kori.hinta()); kori.lisaa("tietokone", 899); System.out.println("korin hinta: " + kori.hinta());
Tulostuu:
korin hinta: 10 korin hinta: 909
Tehdään ostoskorille metodi public void tulosta()
joka tulostaa korin sisältämät Ostos-oliot. Tulostusjärjestyksessä ei ole merkitystä. Edellisen esimerkin ostoskori tulostetuna olisi:
piimä: 1 juusto: 1 tietokone: 1 maito: 1
Huomaa, että tulostuva numero on siis tuotteen korissa oleva kappalemäärä, ei hinta!
Täydennetään Ostoskoria siten, että jos korissa on jo tuote joka sinne lisätään, ei koriin luoda uutta Ostos-olioa vaan päivitetään jo korissa olevaa tuotetta vastaavaa ostosolioa kutsumalla sen metodia kasvataMaaraa().
Esimerkki:
Ostoskori kori = new Ostoskori(); kori.lisaa("maito", 3); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() + "\n"); kori.lisaa("piimä", 2); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() + "\n"); kori.lisaa("maito", 3); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() + "\n"); kori.lisaa("maito", 3); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta() + "\n");
Tulostuu:
maito: 1 korin hinta: 3 piimä: 1 maito: 1 korin hinta: 5 piimä: 1 maito: 2 korin hinta: 8 piimä: 1 maito: 3 korin hinta: 11
Eli ensin koriin lisätään maito ja piimä ja niille omat ostos-oliot. Kun koriin lisätään lisää maitoa, ei luoda uusille maidoille omaa ostosolioa, vaan päivitetään jo korissa olevan maitoa kuvaavan ostosolion kappalemäärää.
Nyt meillä on valmiina kaikki osat "verkkokauppaa" varten. Verkkokaupassa on varasto joka sisältää kaikki tuotteet. Jokaista asiakkaan asiointia varten on oma ostoskori. Aina kun asiakas valitsee ostoksen, lisätään se asiakkaan ostoskoriin jos tuotetta on varastossa. Samalla varastosaldoa pienennetään yhdellä.
Seuraavassa on valmiina verkkokaupan koodin runko. Tee projektiin luokka Kauppa
ja kopioi alla oleva koodi luokkaan.
import java.util.Scanner; public class Kauppa { private Varasto varasto; private Scanner lukija; public Kauppa(Varasto varasto, Scanner lukija) { this.varasto = varasto; this.lukija = lukija; } // metodi jolla hoidetaan yhden asiakkaan asiointi kaupassa public void asioi(String asiakas) { Ostoskori kori = new Ostoskori(); System.out.println("Tervetuloa kauppaan " + asiakas); System.out.println("valikoimamme:"); for (String tuote : varasto.tuotteet()) { System.out.println( tuote ); } while (true) { System.out.print("mitä laitetaan ostoskoriin (pelkkä enter vie kassalle):"); String tuote = lukija.nextLine(); if (tuote.isEmpty()) { break; } // tee tänne koodi joka lisää tuotteen ostoskoriin jos sitä on varastossa // ja vähentää varastosaldoa // älä koske muuhun koodiin! } System.out.println("ostoskorissasi on:"); kori.tulosta(); System.out.println("korin hinta: " + kori.hinta()); } }
Seuraavassa pääohjelma joka täyttää kaupan varaston ja laittaa Pekan asioimaan kaupassa:
Varasto varasto = new Varasto(); varasto.lisaaTuote("kahvi", 5, 10); varasto.lisaaTuote("maito", 3, 20); varasto.lisaaTuote("piimä", 2, 55); varasto.lisaaTuote("leipä", 7, 8); Kauppa kauppa = new Kauppa(varasto, new Scanner(System.in)); kauppa.asioi("Pekka");
Kauppa on melkein valmiina. Yhden asiakkaan asioinnin hoitavan metodin public void asioi(String asiakas)
on kommenteilla merkitty kohta jonka joudut täydentämään. Lisää kohtaan koodi joka tarkastaa onko asiakkaan haluamaa tuotetta varastossa. Jos on, vähennä tuotteen varastosaldoa ja lisää tuote ostoskoriin.
Vapise, verkkokauppa.com!
Olemme viikosta kolme lähtien kertoneet listaa luodessa listan sisältämän olion tyypin. Esimerkiksi String-tyyppisiä olioita sisältävä lista on esitelty muodossa ArrayList<String>
. Tässä on kuitenkin ihmetyttänyt se, että miten ihmeessä listat ja muutkin tietorakenteet voivat sisältää erityyppisiä oliota.
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<>(); 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<>(); luku.asetaArvo(5); System.out.println(luku.haeArvo());
5
Samalla tavalla ohjelmoija voisi toteuttaa esimerkiksi luokan Pari
, mihin voi laittaa kaksi halutun tyyppistä oliota.
public class Pari<T, K> { private T eka; private K toka; public void asetaArvot(T eka, K toka) { this.eka = eka; this.toka = toka; } public T haeEka() { return this.eka; } public K haeToka() { return this.toka; } }
Huomattava osa Javan tietorakenteista mahdollistaa eri tyyppisten muuttujien käytön. Esimerkiksi ArrayList saa yhden tyyppiparametrin, HashMap kaksi.
List<String> merkkijonot = new ArrayList<>(); Map<String, String> avainArvoParit = new HashMap<>();
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<>(); 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 isoimmasta palkasta pienimpään.
Saat valmiin luokan Opiskelija. Opiskelijalla on nimi. Muokkaa Opiskelija-luokasta Comparable
-rajapinnan toteuttava niin, että compareTo
-metodi lajittelee opiskelijat nimen mukaan aakkosjärjestykseen.
Vinkki: Opiskelijan nimi on String, ja String-luokka on itsessään Comparable
. Voit hyödyntää String-luokan compareTo
-metodia Opiskelija-luokan metodia toteuttaessasi. String.compareTo
kohtelee kirjaimia eriarvoisesti kirjainkoon mukaan, ja tätä varten String-luokalla on myös metodi compareToIgnoreCase
joka nimensä mukaisesti jättää kirjainkoon huomioimatta. Voit käyttää opiskelijoiden järjestämiseen kumpaa näistä haluat.
Tehtäväpohjan mukana on luokka, jonka oliot kuvaavat pelikortteja. Kortilla on arvo ja maa. Kortin arvo on 2, 3, ..., 10, J, Q, K tai A ja maa Risti, Ruutu, Hertta tai Pata. Arvo ja maa kuitenkin esitetään olioiden sisällä kokonaislukuina. Kortilla on myös metodi toString, jota käyttäen kortin arvo ja maa tulostuvat "ihmisystävällisesti".
Jotta korttien käyttäjän ei tarvitsisi käsitellä korttien maita numeroina, on luokkaan määritelty neljä vakioa eli public static final
-muuttujaa:
public class Kortti { public static final int RISTI = 0; public static final int RUUTU = 1; public static final int HERTTA = 2; public static final int PATA = 3; // ... }
Nyt ohjelmassa voidaan luvun 1 sijaan käyttää vakioa Kortti.RUUTU
. Seuraavassa esimerkissä luodaan kolme korttia ja tulostetaan ne:
Kortti eka = new Kortti(2, Kortti.RUUTU); Kortti toka = new Kortti(14, Kortti.PATA); Kortti kolmas = new Kortti(12, Kortti.HERTTA); System.out.println(eka); System.out.println(toka); System.out.println(kolmas);
Tulostuu:
Ruutu 2 Pata A Hertta Q
HUOM: vakioiden käyttö ylläolevaan tapaan ei ole tyylikkäin tapa asian hoitamiseen. Myöhemmin kurssilla opimme oikeaoppisen tyylin maan esittämiseen!
Tee Kortti-luokasta Comparable. Toteuta compareTo
-metodi niin, että korttien järjestys on arvon mukaan nouseva. Jos verrattavien Korttien arvot ovat samat, verrataan niitä maan perusteella nousevassa järjestyksessä: risti ensin, ruutu toiseksi, hertta kolmanneksi, pata viimeiseksi.
Järjestyksessä pienin kortti siis olisi risti kakkonen ja suurin pataässä.
Tehdään seuraavaksi luokka Kasi
joka edustaa pelaajan kädessään pitämää korttien joukkoa. Tee kädelle seuraavat metodit:
public void lisaa(Kortti kortti)
lisää käteen kortinpublic void tulosta()
tulostaa kädessä olevat kortit alla olevan esimerkin tyylilläKasi kasi = new Kasi(); kasi.lisaa(new Kortti(2, Kortti.RUUTU)); kasi.lisaa(new Kortti(14, Kortti.PATA)); kasi.lisaa(new Kortti(12, Kortti.HERTTA)); kasi.lisaa(new Kortti(2, Kortti.PATA)); kasi.tulosta();
Tulostuu:
Ruutu 2 Pata A Hertta Q Pata 2
Talleta käden sisällä olevat kortit ArrayListiin.
Tee kädelle metodi public void jarjesta()
jota kutsumalla käden sisällä olevat kortit menevät suuruusjärjestykseen. Järjestämisen jälkeen kortit tulostuvat järjestyksessä:
Kasi kasi = new Kasi(); kasi.lisaa(new Kortti(2, Kortti.RUUTU)); kasi.lisaa(new Kortti(14, Kortti.PATA)); kasi.lisaa(new Kortti(12, Kortti.HERTTA)); kasi.lisaa(new Kortti(2, Kortti.PATA)); kasi.jarjesta(); kasi.tulosta();
Tulostuu:
Ruutu 2 Pata 2 Hertta Q Pata A
Eräässä korttipelissä kahdesta korttikädestä arvokkaampi on se, jonka sisältämien korttien arvon summa on suurempi. Tee luokasta Kasi
vertailtava tämän kriteerin mukaan, eli laita luokka toteuttamaan rajapinta Comparable<Kasi>
.
Esimerkkiohjelma, jossa vertaillaan käsiä:
Kasi kasi1 = new Kasi(); kasi1.lisaa(new Kortti(2, Kortti.RUUTU)); kasi1.lisaa(new Kortti(14, Kortti.PATA)); kasi1.lisaa(new Kortti(12, Kortti.HERTTA)); kasi1.lisaa(new Kortti(2, Kortti.PATA)); Kasi kasi2 = new Kasi(); kasi2.lisaa(new Kortti(11, Kortti.RUUTU)); kasi2.lisaa(new Kortti(11, Kortti.PATA)); kasi2.lisaa(new Kortti(11, Kortti.HERTTA)); int vertailu = kasi1.compareTo(kasi2); if (vertailu < 0) { System.out.println("arvokkaampi käsi sisältää kortit"); kasi2.tulosta(); } else if (vertailu > 0){ System.out.println("arvokkaampi käsi sisältää kortit"); kasi1.tulosta(); } else { System.out.println("kädet yhtä arvokkaat"); }
Tulostuu
arvokkaampi käsi sisältää kortit Ruutu J Pata J Hertta J
Entä jos haluaisimme välillä järjestää kortit hieman eri tavalla, esim. kaikki saman maan kortit peräkkäin? Luokalla voi olla vain yksi compareTo-metodi, joten joudumme muunlaisia järjestyksiä saadaksemme turvautumaan muihin keinoihin.
Vaihtoehtoiset järjestämistavat toteutetaan erillisten vertailun suorittavien luokkien avulla. Korttien vaihtoehtoisten järjestyksen määräävän luokkien tulee toteuttaa Comparator<Kortti>
-rajapinta. Järjestyksen määräävän luokan olio vertailee kahta parametrina saamaansa korttia. Metodeja on ainoastaan yksi compare(Kortti k1, Kortti k2), jonka tulee palauttaa negatiivinen arvo, jos kortti k1 on järjestyksessä ennen korttia k2, positiivinen arvo jos k2 on järjestyksessä ennen k1:stä ja 0 muuten.
Periaatteena on luoda jokaista järjestämistapaa varten oma vertailuluokka, esim. saman maan kortit vierekkäin vievän järjestyksen määrittelevä luokka:
import java.util.Comparator; public class SamatMaatVierekkain implements Comparator<Kortti> { public int compare(Kortti k1, Kortti k2) { return k1.getMaa() - k2.getMaa(); } }
Maittainen järjestys on sama kuin kortin metodin compareTo
maille määrittelemä järjestys eli ristit ensin, ruudut toiseksi, hertat kolmanneksi, padat viimeiseksi.
Järjestäminen tapahtuu edelleen luokan Collections metodin sort avulla. Metodi saa nyt toiseksi parametrikseen järjestyksen määräävän luokan olion:
ArrayList<Kortti> kortit = new ArrayList<>(); kortit.add(new Kortti(3, Kortti.PATA)); kortit.add(new Kortti(2, Kortti.RUUTU)); kortit.add(new Kortti(14, Kortti.PATA)); kortit.add(new Kortti(12, Kortti.HERTTA)); kortit.add(new Kortti(2, Kortti.PATA)); SamatMaatVierekkain samatMaatVierekkainJarjestaja = new SamatMaatVierekkain(); Collections.sort(kortit, samatMaatVierekkainJarjestaja); for (Kortti k : kortit) { System.out.println(k); }
Tulostuu:
Ruutu 2 Hertta Q Pata 3 Pata A Pata 2
Järjestyksen määrittelevä olio voidaan myös luoda suoraan sort-kutsun yhteydessä:
Collections.sort(kortit, new SamatMaatVierekkain());
Tarkempia ohjeita vertailuluokkien tekemiseen täällä
Tee nyt luokka Comparator-rajapinnan toteuttava luokka SamatMaatVierekkainArvojarjestykseen
jonka avulla saat kortit muuten samanlaiseen järjestykseen kuin edellisessä esimerkissä paitsi, että saman maan kortit järjestyvät arvon mukaisesti.
Lisää luokalle Kasi
metodi public void jarjestaMaittain()
jota kutsumalla käden sisällä olevat kortit menevät edellisen tehtävän vertailijan määrittelemään järjestykseen. Järjestämisen jälkeen kortit tulostuvat järjestyksessä:
Kasi kasi = new Kasi(); kasi.lisaa(new Kortti(12, Kortti.HERTTA)); kasi.lisaa(new Kortti(4, Kortti.PATA)); kasi.lisaa(new Kortti(2, Kortti.RUUTU)); kasi.lisaa(new Kortti(14, Kortti.PATA)); kasi.lisaa(new Kortti(7, Kortti.HERTTA)); kasi.lisaa(new Kortti(2, Kortti.PATA)); kasi.jarjestaMaittain(); kasi.tulosta();
Tulostuu:
Ruutu 2 Hertta 7 Hertta Q Pata 2 Pata 4 Pata A
Luokkakirjasto Collections
on Javan yleishyödyllinen kokoelmaluokkiin liittyvä kirjasto. Kuten tiedämme, Collections
tarjoaa metodit olioiden järjestämiseen joko Comparable
- tai Comparator
-rajapinnan kautta. Järjestämisen lisäksi luokkakirjaston avulla voi etsiä esimerkiksi minimi- (min
-metodi) tai maksimialkioita (max
-metodi), hakea tiettyä arvoa (binarySearch
-metodi), tai kääntää listan (reverse
-metodi).
Collections-luokkakirjasto tarjoaa valmiiksi toteutetun binäärihaun. Metodi binarySearch()
palauttaa haetun alkion indeksin listasta jos se löytyy. Jos alkiota ei löydy, hakualgoritmi palauttaa negatiivisen arvon. Metodi binarySearch()
käyttää Comparable-rajapintaa haetun olion löytämiseen. Jos olion compareTo()
-metodi palauttaa arvon 0, eli olio on sama, ajatellaan arvon löytyneen.
Kerholainen-luokkamme vertaa pituuksia compareTo()
-metodissaan, eli listasta etsiessä etsisimme samanpituista kerholaista.
List<Kerholainen> kerholaiset = new ArrayList<>(); kerholaiset.add(new Kerholainen("mikael", 182)); kerholaiset.add(new Kerholainen("matti", 187)); kerholaiset.add(new Kerholainen("joel", 184)); Collections.sort(kerholaiset); Kerholainen haettava = new Kerholainen("Nimi", 180); int indeksi = Collections.binarySearch(kerholaiset, haettava); if(indeksi >= 0) { System.out.println("180 senttiä pitkä löytyi indeksistä " + indeksi); System.out.println("nimi: " + kerholaiset.get(indeksi).getNimi()); } haettava = new Kerholainen("Nimi", 187); int indeksi = Collections.binarySearch(kerholaiset, haettava); if(indeksi >= 0) { System.out.println("187 senttiä pitkä löytyi indeksistä " + indeksi); System.out.println("nimi: " + kerholaiset.get(indeksi).getNimi()); }
Esimerkkimme tulostaa seuraavaa
187 senttiä pitkä löytyi indeksistä 2 nimi: matti
Huomaa että esimerkissä kutsuttiin myös metodia Collections.sort()
. Tämä tehdään sen takia, että binäärihakua ei voida tehdä jos taulukko tai lista ei ole valmiiksi järjestyksessä.
Harjoitellaan taas ohjelman rakenteen omatoimista suunnittelua. Käyttöliittymän ulkomuoto ja vaadittu toiminnallisuus on määritelty ennalta, rakenteen saat toteuttaa vapaasti.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Mäkihyppy on suomalaisille erittäin rakas laji, jossa pyritään hyppäämään hyppyrimäestä mahdollisimman pitkälle mahdollisimman tyylikkäästi. Tässä tehtävässä tulee toteuttaa simulaattori mäkihyppykilpailulle.
Simulaattori kysyy ensin käyttäjältä hyppääjien nimiä. Kun käyttäjä antaa tyhjän merkkijonon (eli painaa enteriä) hyppääjän nimeksi siirrytään hyppyvaiheeseen. Hyppyvaiheessa hyppääjät hyppäävät yksitellen käänteisessä pistejärjestyksessä. Hyppääjä jolla on vähiten pisteitä kerättynä hyppää aina kierroksen ensimmäisenä, toiseksi vähiten pisteitä omaava toisena jne, ..., eniten pisteitä kerännyt viimeisenä.
Hyppääjän yhteispisteet lasketaan yksittäisten hyppyjen pisteiden summana. Yksittäisen hypyn pisteytys lasketaan hypyn pituudesta (käytä satunnaista kokonaisluku väliltä 60-120) ja tuomariäänistä. Jokaista hyppyä kohden annetaan 5 tuomariääntä (satunnainen luku väliltä 10-20). Tuomariääniä laskettaessa otetaan huomioon vain kolme keskimmäistä ääntä: pienintä ja suurinta ääntä ei oteta huomioon. Esimerkiksi jos Mikael hyppää 61 metriä ja saa tuomariäänet 11, 12, 13, 14 ja 15, on hänen hyppynsä yhteispisteet 100.
Kierroksia hypätään niin monta kuin ohjelman käyttäjä haluaa. Kun käyttäjä haluaa lopettaa tulostetaan lopuksi kilpailun lopputulokset. Lopputuloksissa tulostetaan hyppääjät, hyppääjien yhteispisteet ja hyppääjien hyppäämien hyppyjen pituudet. Lopputulokset on järjestetty hyppääjien yhteispisteiden mukaan siten, että eniten pisteitä kerännyt on ensimmäinen.
Tehtävän tekemisessä on hyötyä muun muassa metodeista Collections.sort
ja Collections.reverse
. Kannattaa aluksi hahmotella minkälaisia luokkia ja olioita ohjelmassa voisi olla. On myös hyvä pyrkiä tilanteeseen, jossa käyttöliittymäluokka on ainut luokka joka kutsuu tulostuskomentoa.
Kumpulan mäkiviikot Syötä kilpailun osallistujat yksi kerrallaan, tyhjällä merkkijonolla siirtyy hyppyvaiheeseen. Osallistujan nimi: Mikael Osallistujan nimi: Mika Osallistujan nimi: Kilpailu alkaa! Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: hyppaa 1. kierros Hyppyjärjestys: 1. Mikael (0 pistettä) 2. Mika (0 pistettä) Kierroksen 1 tulokset Mikael pituus: 95 tuomaripisteet: [15, 11, 10, 14, 14] Mika pituus: 112 tuomaripisteet: [14, 12, 18, 18, 17] Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: hyppaa 2. kierros Hyppyjärjestys: 1. Mikael (134 pistettä) 2. Mika (161 pistettä) Kierroksen 2 tulokset Mikael pituus: 96 tuomaripisteet: [20, 19, 15, 13, 18] Mika pituus: 61 tuomaripisteet: [12, 11, 15, 17, 11] Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: hyppaa 3. kierros Hyppyjärjestys: 1. Mika (260 pistettä) 2. Mikael (282 pistettä) Kierroksen 3 tulokset Mika pituus: 88 tuomaripisteet: [11, 19, 13, 10, 15] Mikael pituus: 63 tuomaripisteet: [12, 19, 19, 12, 12] Kirjoita "hyppaa" niin hypätään, muuten lopetetaan: lopeta Kiitos! Kilpailun lopputulokset: Sija Nimi 1 Mikael (388 pistettä) hyppyjen pituudet: 95 m, 96 m, 63 m 2 Mika (387 pistettä) hyppyjen pituudet: 112 m, 61 m, 88 m
Huom1: Testien kannalta on oleellista että käyttöliittymä toimii kuten yllä kuvattu, esim. rivien alussa olevien välilyöntien määrän on oltava oikea. Rivien alussa oleva tyhjä pitää tehdä välilyönneillä, testit eivät toimi jos tyhjä on tehty tabulaattoreilla. Ohjelman tulostamat tekstit kannattaneekin copypasteta ohjelmakoodiin joko tehtävänannosta tai testien virheilmoituksista. Tehtävä on neljän yksittäisen tehtäväpisteen arvoinen.
Huom2: älä käytä luokkein nimissä skandeja, ne saattavat aiheuttaa ongelmia testeihin!
Ohjelman tulee käynnistyä kun tehtäväpohjassa oleva main-metodi suoritetaan, muistutuksena vieltä, että tehtävässä saa luoda vain yhden Scanner-olion.
Screencast jossa tehdään tähän asti oppimiamme ydinasioita hyödyntävä hieman isompi sovellus: