Kahdeksas osa aloittaa Ohjelmoinnin jatkokurssin. Jos tulet tässä kohtaa mukaan, kannattaa lukaista edellisten osien materiaali. Erityisesti listojen käsittely arvojen virtana on todennäköisesti täysin uutta. Tästä löytyy lisätietoa viidennen osan puolivälistä lähtien. Jos taas haluat lopettaa ohjelmointiin tutustumisen toistaiseksi kurssiin Ohjelmoinnin perusteet, kiitoksia osallistumisesta ja tervetuloa takaisin joskus hamassa tulevaisuudessa! :)
Jos teet kurssia Helsingin yliopistolla, joudut vaihtamaan tässä kohtaa kurssia TMC:stä. Kurssin nimeksi tulee valita hy-ohja-k2017
. Jos teet kurssia MOOCissa, ei sinun tarvitse tehdä yhtään mitään. Ohjelmoinnin jatkokurssille mukaan hyppääminen onnistuu valitsemalla TMC:stä aikataulutetun Ohjelmoinnin MOOC (lisätietoja kurssin pääsivulta osoitteesta https://2017-ohjelmointi.github.io/).
Tässä osassa tutustumme kolmeen uuteen asiaan. Hajautustaulu mahdollistaa tehokkaan avain-arvo -parien käsittelyn, luokka Object selventää muun muassa miksi metodia toString voi kutsua vaikkei sitä ole luokassa määritelty, ja rajapinnat tarjoavat mahdollisuuksia samankaltaisten mutta erilaisten luokkien käsittelyyn.
Tämän osion jälkeen osaat käyttää hajautustauluja. Osaat toteuttaa olioiden vertailussa oleelliset metodit equals
ja hashCode
sekä ymmärrät niiden tehtävän. Ymmärrät, että jokainen luokka perii Javan valmiissa Object-luokassa olevat metodit, jotka ohjelmoija voi korvata omalla luokkaan lisättävällä toteutuksella. Tiedät miksi ArrayListin ja HashMapin (hajautustaulun) tyyppiparametrit ovat aina viittaustyyppisiä muuttujia, ja osaat käyttää valmiita rajapintoja kuten List, Map, Set ja Collection.
Hajautustaulu (HashMap)
Hajautustaulu on eräs ohjelmoinnissa paljon käytetyistä tietorakenteista. Hajautustaulua käytetään kun halutaan käsitellä tietoa avain-arvo -pareina, missä avaimen perusteella voidaan lisätä, hakea ja poistaa avaimeen liittyvä arvo.
Alla olevassa esimerkissä on luotu HashMap-olio kaupunkien hakemiseen postinumeron perusteella, jonka jälkeen HashMap-olioon on lisätty neljä postinumero-kaupunki -paria. Sekä postinumero että kaupunki on esitetty merkkijonona.
HashMap<String, String> postinumerot = new HashMap<>();
postinumerot.put("00710", "Helsinki");
postinumerot.put("90014", "Oulu");
postinumerot.put("33720", "Tampere");
postinumerot.put("33014", "Tampere");
Hajautustaulua luodessa tarvitaan kaksi tyyppiparametria, avainmuuttujan tyyppi ja lisättävän arvon tyyppi. Kuten yllä, myös seuraavassa esimerkissä sekä avainmuuttujan että lisättävän arvon tyyppi on String.
HashMap<String, String> numerot = new HashMap<>();
numerot.put("Yksi", "Uno");
numerot.put("Kaksi", "Dos");
String kaannos = numerot.get("Yksi");
System.out.println(kaannos);
System.out.println(numerot.get("Kaksi"));
System.out.println(numerot.get("Kolme"));
System.out.println(numerot.get("Uno"));
Uno Dos null null
Yllä olevassa esimerkissä luodaan hajatustaulu, jonka avaimena ja tallennettavana oliona on merkkijono. Hajautustauluun lisätään tietoa kaksiparametrisella metodilla put
, jolle annetaan parametrina sekä avain- että arvomuuttuja. Yksiparametrinen metodi get
palauttaa parametrina annettuun avaimeen liittyvän viitteen tai null
-viitteen jos avaimella ei löydy viitettä.
Hajautustaulussa on jokaista avainta kohden korkeintaan yksi arvo. Jos hajautustauluun lisätään uusi avain-arvo -pari, missä avain on jo aiemmin liittynyt toiseen hajautustauluun tallennettuun arvoon, vanha arvo katoaa hajautustaulusta.
HashMap<String, String> numerot = new HashMap<>();
numerot.put("Uno", "Yksi");
numerot.put("Dos", "Zwei");
numerot.put("Uno", "Ein");
String kaannos = numerot.get("Uno");
System.out.println(kaannos);
System.out.println(numerot.get("Dos"));
System.out.println(numerot.get("Tres"));
System.out.println(numerot.get("Uno"));
Ein Zwei null Ein
Luo main
-metodissa uusi HashMap<String,String>
-olio. Tallenna luomaasi olioon seuraavien henkilöiden nimet ja lempinimet niin, että nimi on avain ja lempinimi on arvo. Käytä pelkkiä pieniä kirjaimia.
- matin lempinimi on mage
- mikaelin lempinimi on mixu
- arton lempinimi on arppa
Tämän jälkeen hae HashMapistä mikaelin lempinimi ja tulosta se.
Testit edellyttävät että kirjoitat nimet pienellä alkukirjaimella.
Viittaustyyppinen muuttuja hajautustaulun arvona
Tutkitaan hajautustaulun toimintaa kirjastoesimerkin avulla. Kirjastosta voi hakea kirjoja kirjan nimen perusteella. Jos haetulla nimellä löytyy kirja, palauttaa kirjasto kirjan viitteen. Luodaan ensin esimerkkiluokka Kirja
, jolla on oliomuuttujina nimi, kirjaan liittyvä sisältö sekä kirjan julkaisuvuosi.
public class Kirja {
private String nimi;
private String sisalto;
private int julkaisuvuosi;
public Kirja(String nimi, int julkaisuvuosi, String sisalto) {
this.nimi = nimi;
this.julkaisuvuosi = julkaisuvuosi;
this.sisalto = sisalto;
}
public String getNimi() {
return this.nimi;
}
public void setNimi(String nimi) {
this.nimi = nimi;
}
public int getJulkaisuvuosi() {
return this.julkaisuvuosi;
}
public void setJulkaisuvuosi(int julkaisuvuosi) {
this.julkaisuvuosi = julkaisuvuosi;
}
public String getSisalto() {
return this.sisalto;
}
public void setSisalto(String sisalto) {
this.sisalto = sisalto;
}
public String toString() {
return "Nimi: " + this.nimi + " (" + this.julkaisuvuosi + ")\n"
+ "Sisältö: " + this.sisalto;
}
}
Luodaan seuraavaksi hajautustaulu, joka käyttää avaimena kirjan nimeä eli String-tyyppistä oliota, ja arvona edellä luomaamme kirjaa.
HashMap<String, Kirja> hakemisto = new HashMap<>();
Yllä oleva hajautustaulu käyttää avaimena String
-oliota. Laajennetaan esimerkkiä siten, että hakemistoon lisätään kaksi kirjaa, "Järki ja tunteet"
ja "Ylpeys ja ennakkoluulo"
.
Kirja jarkiJaTunteet = new Kirja("Järki ja tunteet", 1811, "...");
Kirja ylpeysJaEnnakkoluulo = new Kirja("Ylpeys ja ennakkoluulo", 1813, "....");
HashMap<String, Kirja> hakemisto = new HashMap<>();
hakemisto.put(jarkiJaTunteet.getNimi(), jarkiJaTunteet);
hakemisto.put(ylpeysJaEnnakkoluulo.getNimi(), ylpeysJaEnnakkoluulo);
Hakemistosta voi hakea kirjoja kirjan nimellä. Haku kirjalla "Viisasteleva sydän"
ei tuota osumaa, jolloin hajautustaulu palauttaa null
-viitteen. Kirja "Ylpeys ja ennakkoluulo" kuitenkin löytyy.
Kirja kirja = hakemisto.get("Viisasteleva sydän");
System.out.println(kirja);
System.out.println();
kirja = hakemisto.get("Ylpeys ja ennakkoluulo");
System.out.println(kirja);
null Nimi: Ylpeys ja ennakkoluulo (1813) Sisältö: ...
Hajautustauluun lisättäessä avain-arvo -parin arvo voi olla käytännössä mitä tahansa. Arvo voi olla kokonaisluku, lista, tai vaikkapa toinen hajautustaulu.
Hajautustaulu oliomuuttujana
Edellä kuvatun esimerkin ongelma on se, että kirjan kirjoitusmuoto tulee muistaa täsmälleen oikein. Joku saattaa etsiä kirjaa pienellä alkukirjaimella ja joku toinen saattaa vaikkapa painaa välilyöntiä nimen kirjoituksen aluksi. Tarkastellaan seuraavaksi erästä tapaa hieman sallivampaan kirjan nimen perusteella tapahtuvaan hakemiseen.
Hyödynnämme hakemisessa String-luokan tarjoamia välineitä merkkijonojen käsittelyyn. Metodi toLowerCase()
luo merkkijonosta uuden merkkijonon, jonka kaikki kirjaimet on muunnettu pieniksi. Metodi trim()
taas luo merkkijonosta uuden merkkijonon, jonka alusta ja lopusta on poistettu tyhjät merkit kuten välilyönnit.
String teksti = "Ylpeys ja ennakkoluulo ";
teksti = teksti.toLowerCase(); // teksti nyt "ylpeys ja ennakkoluulo "
teksti = teksti.trim() // teksti nyt "ylpeys ja ennakkoluulo"
Jos mietit "kuka kirjoittaisi välilyöntejä ja miksi?", etsi ja lue kirja Ender's Game (suom. Ender).
Luodaan luokka Kirjasto
, joka kapseloi kirjat sisältävän hajautustaulun ja mahdollistaa kirjoitusasusta riippumattoman kirjojen haun. Lisätään luokalle Kirjasto
metodit lisäämiseen, hakemiseen ja poistamiseen. Jokainen näistä tapahtuu siistityn nimen perusteella -- siistiminen sisältää nimen muuntamisen pienellä kirjoitetuksi sekä ylimääräisten alussa ja lopussa olevien välilyöntien poistamisen.
Huomaamme jo nyt että merkkijonon siistimiseen liittyvää koodia tarvitsisi jokaisessa kirjaa käsittelevässä metodissa, joten siitä on hyvä tehdä erillinen metodi.
public class Kirjasto {
private HashMap<String, Kirja> hakemisto;
public Kirjasto() {
this.hakemisto = new HashMap<>();
}
public void lisaaKirja(Kirja kirja) {
String nimi = siistiMerkkijono(kirja.getNimi());
if (this.hakemisto.containsKey(nimi)) {
System.out.println("Kirja on jo kirjastossa!");
} else {
hakemisto.put(nimi, kirja);
}
}
public Kirja haeKirja(String kirjanNimi) {
kirjanNimi = siistiMerkkijono(kirjanNimi);
return this.hakemisto.get(kirjanNimi);
}
public void poistaKirja(String kirjanNimi) {
kirjanNimi = siistiMerkkijono(kirjanNimi);
if (this.hakemisto.containsKey(kirjanNimi)) {
this.hakemisto.remove(kirjanNimi);
} else {
System.out.println("Kirjaa ei löydy, ei voida poistaa!");
}
}
public String siistiMerkkijono(String merkkijono) {
if (merkkijono == null) {
return "";
}
merkkijono = merkkijono.toLowerCase();
return merkkijono.trim();
}
}
Yllä käytetään hajautustaulun tarjoamaa metodia containsKey
avaimen olemassaolon tarkastamiseen. Metodi palauttaa arvon true
, jos hajautustauluun on lisätty haetulla avaimella mikä tahansa arvo, muulloin metodi palauttaa arvon false
.
Edeltävässä esimerkissä noudatimme ns. DRY-periaatetta (Don't Repeat Yourself), jonka tarkoituksena on saman koodin toistumisen välttäminen. Merkkijonon siistiminen eli pienellä kirjoitetuksi muuttaminen sekä trimmaus, eli tyhjien merkkien poisto alusta ja lopusta, olisi toistunut useasti kirjastoluokassamme ilman metodia siistiMerkkijono
. Toistuvaa koodia ei usein huomaa ennen kuin sitä on jo kirjoittanut, jolloin sitä päätyy koodiin lähes pakosti. Tässä ei ole mitään pahaa -- tärkeintä on että siistit koodiasi sitä mukaa kun huomaat siistimistä vaativia tilanteita.
Hajautustaulun kaikkien avain-arvo -parien läpikäynti
Hajautustaululla on kolme hyödyllistä metodia hajautustauluun lisättyjen avain-arvo -parien läpikäyntiin. Metodi keySet()
palauttaa hajautustauluun lisättyjen avainten joukon, metodi values()
palauttaa hajautustauluun lisättyjen arvojen kokoelman, ja metodi entrySet()
palauttaa hajautustaulussa olevat avain-arvo -parit.
Jokaisen edellämainitun metodin palauttamasta joukosta voidaan luoda virta (stream), jota voidaan käsitellä. Tarkastellaan tätä kirjastoesimerkin kautta.
Haluamme joskus etsiä kirjaa nimen osan perusteella. Hajautustaulun metodi get
ei tähän sovellu, sillä sitä käytetään tietyllä avaimella etsimiseen. Toinen vaihtoehto on kaikkien avainten läpikäynti yksitellen, jolloin tarkastelemme jokaista avainta erikseen.
Alla olevassa esimerkissä haetaan kaikki ne kirjat, joiden nimessä esiintyy annettu merkkijono.
public ArrayList<Kirja> haeKirjaNimenOsalla(String nimenOsa) {
nimenOsa = siistiMerkkijono(nimenOsa);
// haetaan kaikki ne kirjan nimet, joissa esiintyy nimen osa,
// ja noudetaan niihin liittyvät kirjat
return this.hakemisto.keySet().stream()
.filter(avain -> avain.contains(nimenOsa))
.map(avain -> this.hakemisto.get(avain))
.collect(Collectors.toCollection(ArrayList::new));
}
Tällä tavalla etsiessä menetämme kuitenkin hajautustauluun liittyvän nopeusedun. Hajautustaulu on toteutettu siten, että avaimen perusteella hakeminen on tyypillisesti binäärihakuakin tehokkaampaa. Yllä olevassa esimerkissä käydään kaikkien kirjojen nimet läpi, kun tietyllä avaimella etsittäessä tarkasteltaisiin tasan yhden kirjan olemassaoloa.
Vastaavasti kaikki nimi-kirja -parit saataisiin selville metodin entrySet
avulla. Metodi palauttaa joukon (Set) avain-arvo -pareja, jonka läpikäynti onnistuu virran avulla.
public ArrayList<Kirja> haeKirjaNimenOsalla(String nimenOsa) {
nimenOsa = siistiMerkkijono(nimenOsa);
// haetaan kaikki ne kirjan nimet, joissa esiintyy nimen osa,
// ja noudetaan niihin liittyvät kirjat
return this.hakemisto.entrySet().stream()
.filter(entry -> entry.getKey().contains(nimenOsa))
.map(entry -> entry.getValue())
.collect(Collectors.toCollection(ArrayList::new));
}
Metodin entrySet
palauttama joukon sisältämät alkiot ovat avain-arvo -pareja. Jokaisella alkiolla on metodi getKey()
, jolla saa selville avaimen, ja metodi getValue()
, jolla saa selville arvon. Yllä avaimena on kirjan nimi ja arvona kirja -- kirjan nimen perusteella tapahtuva rajaaminen onnistuu filter-metodilla, jonka jälkeen jäljelle jääneistä alkioista otetaan arvo (eli kirja) metodin getValue avulla.
Alkeistyyppiset muuttujat hajautustaulussa
HashMap olettaa, että siihen lisätään viittaustyyppisiä muuttujia (samoin kuin ArrayList). Java muuntaa alkeistyyppiset muuttujat viittaustyyppisiksi käytännössä kaikkia Javan valmiita tietorakenteita (kuten ArrayList ja HashMap) käytettäessä. Vaikka luku 1
voidaan esittää alkeistyyppisen muuttujan int
arvona, tulee sen tyypiksi määritellä Integer
ArrayListissä ja HashMapissa.
HashMap<Integer, String> taulu = new HashMap<>(); // toimii
taulu.put(1, "Ole!");
HashMap<int, String> taulu2 = new HashMap<>(); // ei toimi
Hajautustaulun avain ja tallennettava olio ovat aina viittaustyyppisiä muuttujia. Jos haluat käyttää alkeistyyppisiä muuttujia avaimena tai tallennettavana arvona, on niille olemassa viittaustyyppiset vastineet. Alla on esitelty muutama.
Alkeistyyppi | Viittaustyyppinen vastine |
---|---|
int | Integer |
double | Double |
char | Character |
Java muuntaa alkeistyyppiset muuttujat automaattisesti viittaustyyppisiksi kun niitä lisätään HashMapiin tai ArrayListiin. Tätä automaattista muunnosta viittaustyyppisiksi kutsutaan Javassa auto-boxingiksi, eli automaattiseksi "laatikkoon" asettamiseksi. Automaattinen muunnos onnistuu myös toiseen suuntaan.
int avain = 2;
HashMap<Integer, Integer> taulu = new HashMap<>();
taulu.put(avain, 10);
int arvo = taulu.get(avain);
System.out.println(arvo);
10
Seuraava esimerkki kuvaa rekisterinumeroiden bongausten laskemiseen käytettävää luokkaa. Metodeissa metodeissa lisaaBongaus
ja montakoKertaaBongattu
tapahtuu automaattinen tyyppimuunnos.
public class Rekisteribongauslaskuri {
private HashMap<String, Integer> bongatut;
public Rekisteribongauslaskuri() {
this.bongatut = new HashMap<>();
}
public void lisaaBongaus(String bongattu) {
if (!this.bongatut.containsKey(bongattu)) {
this.bongatut.put(bongattu, 0);
}
int montakobongausta = this.bongatut.get(bongattu);
montakobongausta++;
this.bongatut.put(bongattu, montakobongausta);
}
public int montakoKertaaBongattu(String bongattu) {
this.bongatut.get(bongattu);
}
}
Tyyppimuunnoksissa piilee kuitenkin vaara. Jos yritämme muuntaa null-viitettä -- eli esimerkiksi bongausta, jota ei ole HashMapissa -- kokonaisluvuksi, näemme virheen java.lang.reflect.InvocationTargetException. Kun teemme automaattista muunnosta, tulee varmistaa että muunnettava arvo ei ole null. Yllä olevassa ohjelmassa oleva montakoKertaaBongattu
-metodi tulee korjata esimerkiksi seuraavasti.
public int montakoKertaaBongattu(String bongattu) {
return this.bongatut.getOrDefault(bongattu, 0);
}
HashMapin metodi getOrDefault
hakee sille ensimmäisenä parametrina annettua avainta HashMapista. Jos avainta ei löydy, palauttaa se toisena parametrina annetun arvon. Metodin toiminta vastaa seuraavaa metodia.
public int montakoKertaaBongattu(String bongattu) {
if(this.bongatut.containsKey(bongattu) {
return this.bongatut.get(bongattu);
}
return 0;
}
Siistitään vielä lisaaBongaus-metodia hieman. Alkuperäisessä versiossa metodin alussa lisätään hajautustauluun bongausten lukumääräksi arvo 0, jos bongattua ei löydy. Tämän jälkeen bongausten määrä haetaan, sitä kasvatetaan yhdellä, ja vanha bongausten lukumäärä korvataan lisäämällä arvo uudestaan hajautustauluun. Osan tästäkin toiminnallisuudesta voi korvata metodilla getOrDefault.
public class Rekisteribongauslaskuri {
private HashMap<String, Integer> bongatut;
public Rekisteribongauslaskuri() {
this.bongatut = new HashMap<>();
}
public void lisaaBongaus(String bongattu) {
int montakobongausta = this.bongatut.getOrDefault(bongattu, 0);
montakobongausta++;
this.bongatut.put(bongattu, montakobongausta);
}
public int montakoKertaaBongattu(String bongattu) {
return this.bongatut.getOrDefault(bongattu, 0);
}
}
Luo luokka Velkakirja
, jolla on seuraavat toiminnot:
- konstruktori
public Velkakirja()
luo uuden velkakirjan - metodi
public void asetaLaina(String kenelle, double maara)
tallettaa velkakirjaan merkinnän lainasta tietylle henkilölle. - metodi
public double paljonkoVelkaa(String kuka)
palauttaa velan määrän annetun henkilön nimen perusteella. Jos henkilöä ei löydy, palautetaan 0.
Luokkaa käytetään seuraavalla tavalla:
Velkakirja matinVelkakirja = new Velkakirja();
matinVelkakirja.asetaLaina("Arto", 51.5);
matinVelkakirja.asetaLaina("Mikael", 30);
System.out.println(matinVelkakirja.paljonkoVelkaa("Arto"));
System.out.println(matinVelkakirja.paljonkoVelkaa("Joel"));
Yllä oleva esimerkki tulostaisi:
51.5 0.0
Ole tarkkana tilanteessa, jossa kysytään velattoman ihmisen velkaa.
Huom! Velkakirjan ei tarvitse huomioida vanhoja lainoja. Kun asetat uuden velan henkilölle jolla on vanha velka, vanha velka unohtuu.
Velkakirja matinVelkakirja = new Velkakirja();
matinVelkakirja.asetaLaina("Arto", 51.5);
matinVelkakirja.asetaLaina("Arto", 10.5);
System.out.println(matinVelkakirja.paljonkoVelkaa("Arto"));
10.5
Tässä tehtäväsarjassa toteutetaan sanakirja, josta voi hakea suomen kielen sanoille englanninkielisiä käännöksiä. Sanakirjan tekemisessä käytetään HashMap
-tietorakennetta.
Luokka Sanakirja
Toteuta luokka nimeltä Sanakirja
. Luokalla on aluksi seuraavat metodit:
-
public String kaanna(String sana)
metodi palauttaa parametrinsa käännöksen. Jos sanaa ei tunneta, palautetaan null. -
public void lisaa(String sana, String kaannos)
metodi lisää sanakirjaan uuden käännöksen
Toteuta luokka Sanakirja siten, että sen ainoa oliomuuttuja on HashMap
-tietorakenne.
Testaa sanakirjasi toimintaa:
Sanakirja sanakirja = new Sanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
sanakirja.lisaa("cembalo", "harpsichord");
System.out.println(sanakirja.kaanna("apina"));
System.out.println(sanakirja.kaanna("porkkana"));
monkey null
Sanojen lukumäärä
Lisää sanakirjaan metodi public int sanojenLukumaara()
, joka palauttaa sanakirjassa olevien sanojen lukumäärän.
Sanakirja sanakirja = new Sanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
System.out.println(sanakirja.sanojenLukumaara());
sanakirja.lisaa("cembalo", "harpsichord");
System.out.println(sanakirja.sanojenLukumaara());
2 3
Tässä osassa kannattaa tutkiskella HashMapin valmiiksi tarjoamia metodeja...
Kaikkien sanojen listaaminen
Lisää sanakirjaan metodi public ArrayList<String> kaannoksetListana()
joka palauttaa sanakirjan sisällön listana avain = arvo muotoisia merkkijonoja.
Sanakirja sanakirja = new Sanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
sanakirja.lisaa("cembalo", "harpsichord");
ArrayList<String> kaannokset = sanakirja.kaannoksetListana();
kaannokset.stream().forEach(k -> System.out.println(k));
banaani = banana apina = monkey cembalo = harpsichord
Tekstikäyttöliittymän alku
Harjoitellaan tässäkin tehtävässä erillisen tekstikäyttöliittymän tekemistä. Luo luokka Tekstikayttoliittyma
, jolla on seuraavat metodit:
- konstruktori
public Tekstikayttoliittyma(Scanner lukija, Sanakirja sanakirja)
- metodi
public void kaynnista()
, joka käynnistää tekstikäyttöliittymän.
Tekstikäyttöliittymä tallettaa konstruktorin parametrina saamansa lukijan ja sanakirjan oliomuuttujiin. Muita oliomuuttujia ei tarvita. Käyttäjän syötteen lukeminen tulee hoitaa konstruktorin parametrina saatua lukija-olioa käyttäen! Myös kaikki käännökset on talletettava konstruktorin parametrina saatuun sanakirja-olioon. Tekstikäyttöliittymä ei saa luoda olioita itse!
HUOM: vielä uudelleen edellinen, eli Tekstikäyttöliittymä ei saa luoda itse skanneria vaan sen on käytettävä parametrina saamaansa skanneria syötteiden lukemiseen!
Tekstikäyttöliittymässä tulee aluksi olla vain komento lopeta
, joka poistuu tekstikäyttöliittymästä. Jos käyttäjä syöttää jotain muuta, käyttäjälle sanotaan "Tuntematon komento".
Scanner lukija = new Scanner(System.in);
Sanakirja sanakirja = new Sanakirja();
Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja);
kayttoliittyma.kaynnista();
Komennot: lopeta - poistuu käyttöliittymästä Komento: apua Tuntematon komento. Komento: lopeta Hei hei!
Sanojen lisääminen ja kääntäminen
Lisää tekstikäyttöliittymälle komennot lisaa
ja kaanna
. Komento lisaa
lisää kysyy käyttäjältä sanaparin ja lisää sen sanakirjaan. Komento kaanna
kysyy käyttäjältä sanaa ja tulostaa sen käännöksen.
Scanner lukija = new Scanner(System.in);
Sanakirja sanakirja = new Sanakirja();
Tekstikayttoliittyma kayttoliittyma = new Tekstikayttoliittyma(lukija, sanakirja);
kayttoliittyma.kaynnista();
Komennot: lisaa - lisää sanaparin sanakirjaan kaanna - kysyy sanan ja tulostaa sen käännöksen lopeta - poistuu käyttöliittymästä Komento: lisaa Suomeksi: porkkana Käännös: carrot Komento: kaanna Anna sana: porkkana Käännös: carrot Komento: lopeta Hei hei!
Object
Olemme useampaan otteeseen käyttäneet metodia public String toString()
olion merkkijonoesityksen muodostamiseen. Emme ole kuitenkaan 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);
System.out.println(olioKirja.toString());
Ohjelma ei tulosta virheilmoitusta tai kaadu kun annamme Kirja
-luokasta tehdyn olion parametrina System.out.println
-komennolle tai kutsumme oliolle metodia toString
. 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 luokkiimme. Tämän takia metodi toString on jokaisen luomamme luokan käytössä, riippumatta siitä lisäämmekö metodille toteutuksen luokkaamme vai emme.
Object-luokassa määritelty toString
-metodi ei yleensä ole toivomamme, minkä takia se tyypillisesti korvataan omalla toteutuksellamme. Tämä tapahtuu luomalla omaan luokkaamme public String toString()
-metodi, jossa on toivomamme toiminnallisuus.
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ä siitä, miten metodeihin tulisi suhtautua. Annotaatio @Override
kertoo lukijalle että annotaatiota seuraava metodi korvaa perityssä luokassa määritellyn metodin. Jos korvattavaan metodiin ei liitetä annotaatiota, antaa kääntäjä tilanteessa varoituksen, overriden kirjottamatta jättäminen ei kuitenkaan ole virhe.
Luokasta Object
peritään muitakin hyödyllisiä metodeja. Tutustutaan seuraavaksi metodeihin equals
ja hashCode
.
Samanarvoisuudesta kertova metodi "equals"
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 metodi equals
tarkastaa 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");
}
Kirjat olivat samat Kirjat eivät olleet samat
Vaikka edellisessä esimerkissä olevien kirjaolioiden sisäinen rakenne (eli oliomuuttujien arvot) on täsmälleen sama, vain ensimmäinen vertailu tulostaa merkkijonon "Kirjat olivat samat
". Tämä johtuu siitä että vain ensimmäisessä tapauksessa viitteet ovat samat, eli olioa vertaillaan 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
Laajennetaan alkukurssin tehtävässä "Päivämäärien erotus" toteutettua Paivays-luokkaa siten, että se osaa myös sanoa ovatko päivämäärät täsmälleen samat.
Lisää Paivays
-luokkaan metodi public boolean equals(Object object)
, joka kertoo onko metodille parametrina annettu olio päiväys ja onko parametrina annetun olion päiväys sama kuin käytetyn olion päiväys.
Metodin tulee toimia seuraavasti:
Paivays p = new Paivays(1, 2, 2000);
System.out.println(p.equals("heh"));
System.out.println(p.equals(new Paivays(5, 2, 2012)));
System.out.println(p.equals(new Paivays(1, 2, 2000)));
false false true
Equals ja ArrayList
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, ei contains
-metodi toimi oikein, sillä se käyttää omassa toteutuksessaan equals-metodia olioiden vertailemiseen. 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.");
}
Tämä oletusmetodeihin kuten equals
tukeutuminen on oikeastaan syy sille, miksi Java haluaa, että ArrayListiin lisättävät muuttujat ovat viittaustyyppisiä. Koska jokaisella luokalla on Object-luokasta periytyvä equals-metodi, ei luokan ArrayList sisäistä toteutusta tarvitse muuttaa lainkaan erilaisia muuttujia lisättäessä. Alkeistyyppisillä muuttujilla tällaisia metodeja ei ole, jolloin ArrayList ei löydä niihin liittyvää equals-metodia.
Hajautusarvo "hashCode"
Object-luokasta periytyvää metodia hashCode
käytetään oliota kuvaavan hajautusarvon luomiseen. Hajautusarvoa käytetään suurpiirteiseen vertailuun. Jos kahdella oliolla on sama hajautusarvo, ne saattavat olla samanarvoiset. Jos taas kahdella oliolla on eri hajautusarvot, ne ovat varmasti eriarvoiset.
Hajautusarvoa tarvitaan muunmuassa HashMapissa. HashMapin sisäinen toiminta perustuu siihen, että avain-arvo -parit on tallennettu avaimen hajautusarvon perusteella listoja sisältävään taulukkoon. Jokainen taulukon indeksi viittaa listaan. Hajautusarvon perusteella tunnistetaan taulukon indeksi, jonka jälkeen taulukon indeksistä löytyvä lista käydään läpi. Avaimeen liittyvä arvo palautetaan jos ja vain jos listasta löytyy täsmälleen sama arvo (samansuuruisuuden vertailu tapahtuu equals-metodilla). Näin etsinnässä tarvitsee tarkastella vain murto-osaa hajautustauluun tallennetuista avaimista.
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 pidetää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));
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 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.
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 hajautusarvon.
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 hajautusarvoksi julkaisuvuosi.
public int hashCode() {
if (this.nimi == null) {
return this.julkaisuvuosi;
}
return this.nimi.hashCode();
}
Nyt ylläolevassa ratkaisussa kaikki saman nimiset kirjat niputetaan samaan joukkoon. Parannetaan toteutusta vielä siten, että kirjan julkaisuvuosi huomioidaan myös nimeen perustuvassa hajautusarvon laskennassa.
public int hashCode() {
if (this.nimi == null) {
return this.julkaisuvuosi;
}
return this.julkaisuvuosi + this.nimi.hashCode();
}
Laajennetaan vielä edellisessä tehtävässä nähtyä Paivays
-luokkaa siten, että sillä on myös oma hashCode
-metodi.
Lisää Paivays
-luokkaan metodi public int hashCode()
, joka laskee päiväys-oliolle hajautusarvon. Toteuta hajautusarvon laskeminen siten, että vuosien 1900 ja 2100 välillä löytyy mahdollisimman vähän samankaltaisia hajautusarvoja.
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 this.julkaisuvuosi;
}
return this.julkaisuvuosi + this.nimi.hashCode();
}
}
Kerrataan vielä: jotta luokkaa voidaan käyttää HashMap:in avaimena, tulee sille määritellä
- metodi
equals
siten, että kaikki samansuuruisena (tai saman sisältöisinä) ajatellut oliot tuottavat vertailussa tuloksen true ja muut false - metodi
hashCode
siten, että mahdollisimman harvalla erisuuruisella oliolla on sama hajautusarvo
Luokalle 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. Nämä NetBeansin generoimat metodit ovat tyypillisesti "tarpeeksi hyviä" omiin tarpeisiimme.
Rekisterinumeron equals ja hashCode
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 {
// tässä määre final tarkoittaa sitä, että arvoa ei voi muuttaa asetuksen jälkeen
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. Toteuta luokalle rekisterinumero metodit equals
ja hashCode
.
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
Omistaja rekisterinumeron perusteella
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ä, palautetaannull
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!
Ajoneuvorekisteri laajenee
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 auto
Rajapinta
Rajapinnan (engl. interface) avulla määritellään luokalta vaadittu käyttäytyminen, eli sen metodit. Rajapinnat määritellään kuten normaalit Javan 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.
Kun luokka toteuttaa rajapinnan, se allekirjoittaa sopimuksen. Sopimuksessa luvataan, että luokka toteuttaa rajapinnan määrittelemät metodit. Jos metodeja ei ole luokassa toteutettu, ei ohjelma toimi.
Rajapinta määrittelee vain vaadittujen metodien nimet, parametrit, ja paluuarvot. Rajapinta ei kuitenkaan ota kantaa metodien sisäiseen toteutukseen. Ohjelmoijan vastuulla on määritellä metodien sisäinen toiminnallisuus.
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<>();
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 ja tee omia kokeiluja ja projekteja.");
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 ja tee omia kokeiluja ja projekteja.
Tehtäväpohjassa on valmiina rajapinta Palvelusvelvollinen
, jossa on seuraavat toiminnot:
- metodi
int paiviaJaljella()
palauttaa jäljellä olevien palveluspäivien määrän - metodi
void palvele()
vähentää yhden palveluspäivän. Palveluspäivien määrä ei saa mennä negatiiviseksi.
public interface Palvelusvelvollinen {
int paiviaJaljella();
void palvele();
}
Sivari
Tee Palvelusvelvollinen
-rajapinnan toteuttava luokka Sivari
, jolla parametriton konstruktori. Luokalla on oliomuuttuja paivia, joka alustetaan konstruktorikutsun yhteydessä arvoon 362.
Asevelvollinen
Tee Palvelusvelvollinen
-rajapinnan toteuttava luokka Asevelvollinen
, jolla on parametrillinen konstruktori, jolla määritellään palvelusaika (int paivia
).
Rajapinta muuttujan tyyppinä
Uutta muuttujaa esitellessä kerrotaan aina muuttujan tyyppi. Tyyppejä on kahdenlaisia, alkeistyyppiset muuttujat (int, double, ...) ja viittaustyyppiset muuttujat (kaikki oliot). Olemme tähän mennessä käyttäneet viittaustyyppisten muuttujien tyyppinä olion luokkaa.
String merkkijono = "merkkijono-olio";
Tekstiviesti viesti = new Tekstiviesti("ope", "samalla oliolla monta tyyppiä");
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));
lukulista.stream().forEach(l -> System.out.println(l.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.
Rajapinta metodin parametrina
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 = this.luettavat.stream()
.reduce("", (a, luettava) -> a + luettava.lue() + "\n");
// yllä oleva on sama kuin
/*
String luettu = "";
for (int i = 0; i < this.luettavat.size(); i++) {
luettu += this.luettavat.get(i).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.
Kuten huomaat, ohjelmassa on jo hyvin paljon viitteitä. Kannattaa piirtää ohjelman tilaa askeleittain paperille, ja hahmotella miten vernanLista
-oliolle tapahtuva metodikutsu lue
etenee!
Edellisessä esimerkissä käytettiin virtaan liittyvää reduce-metodia. Reduce-metodi on hyödyllinen kun virrassa olevat alkiot halutaan yhdistää jonkinlaiseen toiseen muotoon. Metodin saamat parametrit ovat seuraavaa muotoa: reduce(alkutila, (edellinen, olio) -> mitä oliolla tehdään)
.
Esimerkiksi kokonaislukuja sisältävän listan summan saa luotua reduce-metodin avulla seuraavasti.
ArrayList<Integer> luvut = new ArrayList<>();
luvut.add(7);
luvut.add(3);
luvut.add(2);
luvut.add(1);
int summa = luvut.stream()
.reduce(0, (edellinenSumma, luku) -> edellinenSumma + luku);
System.out.println(summa);
13
Vastaavasti merkkijonoista koostuvasta listasta saa luotua rivitetyn merkkijonon seuraavasti.
ArrayList<String> sanat = new ArrayList<>();
sanat.add("Eka");
sanat.add("Toka");
sanat.add("Kolmas");
sanat.add("Neljäs");
String yhdistetty = sanat.stream()
.reduce("", (edellinenMjono, sana) -> edellinenMjono + sana + "\n");
System.out.println(yhdistetty);
Eka Toka Kolmas Neljäs
Talletettavia
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.
Laatikko
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ä.
Laatikon paino
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 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. Pohdi tehtävän tekemisen jälkeen miksi näin on.
Laatikkokin on talletettava!
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 olioita 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?
Rajapinta metodin paluuarvona
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ä.
import java.util.Random;
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() {
// Tässä käytettyä Random-oliota voi käyttää satunnaisten lukujen arpomiseen
Random arpa = new Random();
// arpoo luvun väliltä [0, 4[. Luvuksi tulee 0, 1, 2 tai 3.
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;
}
}
import java.util.Random;
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 pakkaaja-luokkaa käyttäviin luokkiin uusien Talletettava-rajapinnan toteuttavien luokkien lisääminen ei vaikuta.
Vähäisemmät riippuvuudet helpottavat ohjelman laajennettavuutta.
Valmiit rajapinnat
Javan API tarjoaa huomattavan määrän valmiita rajapintoja. Tutustutaan tässä neljään usein käytettyyn rajapintaan: List
, Map
, Set
ja Collection
.
List-rajapinta
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.
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.
Toteuta luokkaan ListanTarkistin
metodi palautaKoko
, joka saa parametrina List-olion ja palauttaa sen koon kokonaislukuna.
Metodin tulee toimia esimerkiksi seuraavasti:
List<String> nimet = new ArrayList<>();
nimet.add("eka");
nimet.add("toka");
nimet.add("kolmas");
System.out.println(new ListanTarkistin().palautaKoko(nimet));
3
Map-rajapinta
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ä");
kaannokset.keySet().stream()
.forEach(avain -> System.out.println(avain + ": " + kaannokset.get(avain)));
gambatte: tsemppiä hai: kyllä
Metodi keySet
palauttaa Set
-rajapinnan toteuttavan joukon alkioita. Set
-rajapinnan toteuttavan joukon voi käydä läpi virtana. Hajautustaulusta saa talletetut arvot metodin values
-avulla. Metodi values
palauttaa Collection
rajapinnan toteuttavan joukon alkioita. Tutustutaan vielä pikaisesti Set- ja Collection-rajapintoihin.
Toteuta luokkaan HajautustaulunTarkistin
metodi palautaKoko
, joka saa parametrina Map-olion ja palauttaa sen koon kokonaislukuna.
Metodin tulee toimia esimerkiksi seuraavasti:
Map<String, String> nimet = new HashMap<>();
nimet.put("eka", "first");
nimet.put("toka", "second");
System.out.println(new HajautustaulunTarkistin().palautaKoko(nimet));
2
Set-rajapinta
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 seuraavasti.
Set<String> joukko = new HashSet<>();
joukko.add("yksi");
joukko.add("yksi");
joukko.add("kaksi");
joukko.stream().forEach(alkio -> System.out.println(alkio));
yksi kaksi
Huomaa että HashSet ei ota millään tavalla kantaa joukon alkioiden järjestykseen.
Toteuta luokkaan JoukonTarkistin
metodi palautaKoko
, joka saa parametrina Set-olion ja palauttaa sen koon kokonaislukuna.
Metodin tulee toimia esimerkiksi seuraavasti:
Set<String> nimet = new HashSet<>();
nimet.add("eka");
nimet.add("eka");
nimet.add("toka");
nimet.add("toka");
nimet.add("toka");
System.out.println(palautaKoko(nimet));
Tulostaa:
2
Collection-rajapinta
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
).
Collection-rajapinta määrää myös virtatoteutuksesta. Jokaisella luokalla, joka toteuttaa Collection-rajapinnan joko välillisesti tai suoraan, tulee olla virran luomiseen käytettävä metodi stream
.
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:");
avainKokoelma.stream().forEach(avain -> System.out.println(avain));
System.out.println();
System.out.println("Arvot:");
Collection<String> arvot = kaannokset.values();
arvot.stream().forEach(arvo -> System.out.println(arvo));
Avaimet: gambatte hai Arvot: kyllä tsemppiä
Seuraavassa tehtävässä rakennetaan verkkokauppaan liittyvää toiminnallisuutta ja harjoitellaan luokkien käyttämistä niiden tarjoamien rajapintojen kautta.
Teemme tehtävässä muutamia verkkokaupan hallinnointiin soveltuvia ohjelmakomponentteja.
Varasto
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 -99
Varaston 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
Tuotteen varastosaldo
Aseta tuotteiden varastosaldot samaan tapaan Map<String, Integer>
-tyyppiseen muuttujaan kuin hinnat. Täydennä varastoa seuraavilla metodeilla:
public int saldo(String tuote)
palauttaa parametrina olevan tuotteen varastosaldon. Jos tuotetta ei ole varastossa lainkaan, tulee palauttaa 0.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
Tuotteiden listaus
Listätään varastolle vielä yksi metodi:
public Set<String> tuotteet()
palauttaa joukkona varastossa olevien tuotteiden nimet.
Metodi on helppo toteuttaa HashMapin avulla. 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:");
varasto.tuotteet().stream().forEach(t -> System.out.println(t));
tuotteet: piimä jugurtti kahvi maito
Ostos
Ostoskoriin lisätään ostoksia. Ostoksella tarkoitetaan tiettyä määrää tiettyjä tuotteita. Koriin voidaan laittaa esim. ostos joka vastaa yhtä leipää tai ostos joka vastaa 24:ää kahvia.
Tee luokka Ostos
jolla on seuraavat toiminnot:
public Ostos(String tuote, int kpl, int yksikkohinta)
konstruktori joka luo ostoksen joka vastaa parametrina annettua tuotetta. Tuotteita ostoksessa on kpl kappaletta ja yhden tuotteen hinta on kolmantena parametrina annettu yksikkohintapublic int hinta()
palauttaa ostoksen hinnan. Hinta saadaan kertomalla kappalemäärä yksikköhinnallapublic void kasvataMaaraa()
kasvattaa ostoksen kappalemäärää yhdelläpublic String toString()
palauttaa ostoksen merkkijonomuodossa, joka on alla olevan esimerkin mukainen
Esimerkki ostos-luokan 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);
ostoksen joka sisältää 4 maitoa yhteishinta on 8 maito: 4 maito: 5
Huom: toString on siis muotoa tuote: kpl hintaa ei merkkijonoesitykseen tule!
Ostoskori
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 kokonaishinnan
Esimerkki ostoskorin 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());
korin hinta: 10 korin hinta: 909
Ostoskorin tulostus
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!
Yksi ostos tuotetta kohti
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");
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ää.
Kauppa
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 tekstikäyttöliittymän 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:");
varasto.tuotteet().stream().forEach(t -> System.out.println(t));
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.
Todellisuudessa verkkokauppa toteutettaisiin hieman eri tavalla. Verkkosovelluksia tehtäessä käyttöliittymä toteutetaan HTML-sivuna, ja sivuilla tapahtuvat klikkaukset ohjataan palvelinohjelmistolle. Teemaan liittyen löytyy useampia kursseja Helsingin yliopistolta.
Maatiloilla on lypsäviä eläimiä, jotka tuottavat maitoa. Maatilat eivät itse käsittele maitoa, vaan se kuljetetaan Maitoautoilla meijereille. Meijerit ovat yleisiä maitotuotteita tuottavia rakennuksia. Jokainen meijeri erikoistuu yhteen tuotetyyppiin, esimerkiksi Juustomeijeri tuottaa Juustoa, Voimeijeri tuottaa voita ja Maitomeijeri tuottaa maitoa.
Rakennetaan maidon elämää kuvaava simulaattori.
Maitosäiliö
Jotta maito pysyisi tuoreena, täytyy se säilöä sille tarkoitettuun säiliöön. Säiliöitä valmistetaan sekä oletustilavuudella 2000 litraa, että asiakkaalle räätälöidyllä tilavuudella. Toteuta luokka Maitosailio jolla on seuraavat konstruktorit ja metodit.
- public Maitosailio()
- public Maitosailio(double tilavuus)
- public double getTilavuus()
- public double getSaldo()
- public double paljonkoTilaaJaljella()
- public void lisaaSailioon(double maara) lisää säiliöön vain niin paljon maitoa kuin sinne mahtuu, ylimääräiset jäävät lisäämättä, maitosäiliön ei siis tarvitse huolehtia tilanteesta jossa maitoa valuu yli
- public double otaSailiosta(double maara) ottaa säiliöstä pyydetyn määrän, tai niin paljon kuin siellä on jäljellä
Huomaa, että teet kaksi konstruktoria. Kutsuttava konstruktori määräytyy sille annettujen parametrien perusteella. Jos kutsut new Maitosailio()
, suoritetaan ensimmäisen konstruktorin lähdekoodi. Toista konstruktoria taas kutsutaan antamalla konstruktorille parametrina tilavuus, esim. new Maitosailio(300.0)
.
Toteuta Maitosailio
-luokalle myös toString()
-metodi, jolla kuvaat sen tilaa. Ilmaistessasi säiliön tilaa toString()
-metodissa, pyöristä litramäärät ylöspäin käyttäen Math
-luokan tarjoamaa ceil()
-metodia.
Testaa maitosailiötä seuraavalla ohjelmapätkällä:
Maitosailio sailio = new Maitosailio();
sailio.otaSailiosta(100);
sailio.lisaaSailioon(25);
sailio.otaSailiosta(5);
System.out.println(sailio);
sailio = new Maitosailio(50);
sailio.lisaaSailioon(100);
System.out.println(sailio);
20.0/2000.0 50.0/50.0
Lehmä
Saadaksemme maitoa tarvitsemme myös lehmiä. Lehmällä on nimi ja utareet. Utareiden tilavuus on satunnainen luku väliltä 15 ja 40, luokkaa Random
voi käyttäää satunnaislukujen arpomiseen, esimerkiksi int luku = 15 + new Random().nextInt(26);
. Luokalla Lehma
on seuraavat toiminnot:
- public Lehma() luo uuden lehmän satunnaisesti valitulla nimellä
- public Lehma(String nimi) luo uuden lehmän annetulla nimellä
- public String getNimi() palauttaa lehmän nimen
- public double getTilavuus() palauttaa utareiden tilavuuden
- public double getMaara() palauttaa utareissa olevan maidon määrän
- public String toString() palauttaa lehmää kuvaavan merkkijonon (ks. esimerkki alla)
Lehma
toteuttaa myös rajapinnat: Lypsava
, joka kuvaa lypsämiskäyttäytymistä, ja Eleleva
, joka kuvaa elelemiskäyttäytymistä.
public interface Lypsava {
public double lypsa();
}
public interface Eleleva {
public void eleleTunti();
}
Lehmää lypsettäessä sen koko maitovarasto tyhjennetään jatkokäsittelyä varten. Lehmän elellessä sen maitovarasto täyttyy hiljalleen. Suomessa maidontuotannossa käytetyt lehmät tuottavat keskimäärin noin 25-30 litraa maitoa päivässä. Simuloidaan tätä tuotantoa tuottamalla noin 0.7 - 2 litraa tunnissa.
Simuloi tuotantoa tuottamalla noin 0.7 - 2 litraa tunnissa. Random-luokan metodista nextDouble
, joka palauttaa satunnaisluvun 0 ja 1 välillä lienee tässä hyötyä.
Lisäksi, jos lehmälle ei anneta nimeä, valitse sille nimi satunnaisesti seuraavasta taulukosta. Tässä on hyötyä Random-luokan metodista nextInt
, jolle annetaan parametrina yläraja. Kannattaa tutustua Random-luokan toimintaan erikseen ennen kuin lisää sen osaksi tätä ohjelmaa.
private static final String[] NIMIA = new String[]{
"Anu", "Arpa", "Essi", "Heluna", "Hely",
"Hento", "Hilke", "Hilsu", "Hymy", "Matti", "Ilme", "Ilo",
"Jaana", "Jami", "Jatta", "Laku", "Liekki",
"Mainikki", "Mella", "Mimmi", "Naatti",
"Nina", "Nyytti", "Papu", "Pullukka", "Pulu",
"Rima", "Soma", "Sylkki", "Valpu", "Virpi"};
Toteuta luokka Lehma ja testaa sen toimintaa seuraavan ohjelmapätkän avulla.
Lehma lehma = new Lehma();
System.out.println(lehma);
Eleleva elelevaLehma = lehma;
elelevaLehma.eleleTunti();
elelevaLehma.eleleTunti();
elelevaLehma.eleleTunti();
elelevaLehma.eleleTunti();
System.out.println(lehma);
Lypsava lypsavaLehma = lehma;
lypsavaLehma.lypsa();
System.out.println(lehma);
System.out.println("");
lehma = new Lehma("Ammu");
System.out.println(lehma);
lehma.eleleTunti();
lehma.eleleTunti();
System.out.println(lehma);
lehma.lypsa();
System.out.println(lehma);
Ohjelman tulostus on erimerkiksi seuraavanlainen.
Liekki 0.0/23.0 Liekki 7.0/23.0 Liekki 0.0/23.0 Ammu 0.0/35.0 Ammu 9.0/35.0 Ammu 0.0/35.0
Lypsyrobotti
Nykyaikaisilla maatiloilla lypsyrobotit hoitavat lypsämisen. Jotta lypsyrobotti voi lypsää lypsävää otusta, tulee lypsyrobotin olla kiinnitetty maitosäiliöön:
- public Lypsyrobotti() luo uuden lypsyrobotin
- public Maitosailio getMaitosailio() palauttaa kiinnitetyn maitosäiliö tai
null
-viitteen, jos säiliötä ei ole vielä kiinnitetty - public void setMaitosailio(Maitosailio maitosailio) kiinnittää annetun säiliön lypsyrobottiin
- public void lypsa(Lypsava lypsava) lypsää lehmän robottiin kiinnitettyyn maitosäiliöön. Jos robottiin ei ole kiinnitetty maitosäiliötä, ohjelma ilmoittaa että maito menee hukkaan.
Toteuta luokka Lypsyrobotti ja testaa sitä seuraavien ohjelmanpätkien avulla. Varmista että lypsyrobotti voi lypsää kaikkia Lypsava-rajapinnan toteuttavia olioita!
Lypsyrobotti lypsyrobotti = new Lypsyrobotti();
Lehma lehma = new Lehma();
lypsyrobotti.lypsa(lehma);
Maidot menevät hukkaan!
Lypsyrobotti lypsyrobotti = new Lypsyrobotti();
Lehma lehma = new Lehma();
System.out.println("");
Maitosailio sailio = new Maitosailio();
lypsyrobotti.setMaitosailio(sailio);
System.out.println("Säiliö: " + sailio);
for (int i = 0; i < 2; i++) {
System.out.println(lehma);
System.out.println("Elellään..");
for (int j = 0; j < 5; j++) {
lehma.eleleTunti();
}
System.out.println(lehma);
System.out.println("Lypsetään...");
lypsyrobotti.lypsa(lehma);
System.out.println("Säiliö: " + sailio);
System.out.println("");
}
Ohjelman tulostus on esimerkiksi seuraavanlainen.
Säiliö: 0.0/2000.0 Mella 0.0/23.0 Elellään.. Mella 6.2/23.0 Lypsetään... Säiliö: 6.2/2000.0 Mella 0.0/23.0 Elellään.. Mella 7.8/23.0 Lypsetään... Säiliö: 14.0/2000.0
Navetta
Lehmät hoidetaan (eli tässä tapauksessa lypsetään) navetassa. Alkukantaisissa navetoissa on maitosäiliö ja tilaa yhdelle lypsyrobotille. Huomaa että lypsyrobottia asennettaessa se kytketään juuri kyseisen navetan maitosäiliöön. Jos navetassa ei ole lypsyrobottia, ei siellä voida myöskään hoitaa lehmiä. Toteuta luokka Navetta
jolla on seuraavat konstruktorit ja metodit:
- public Navetta(Maitosailio maitosailio)
- public Maitosailio getMaitosailio() palauttaa navetan maitosailion
- public void asennaLypsyrobotti(Lypsyrobotti lypsyrobotti) asentaa lypsyrobotin ja kiinnittää sen navetan maitosäiliöön
- public void hoida(Lehma lehma) lypsää parametrina annetun lehmän lypsyrobotin avulla, metodi heittää poikkeuksen
IllegalStateException
, jos lypsyrobottia ei ole asennettu - public void hoida(Collection<Lehma> lehmat) lypsää parametrina annetut lehmät lypsyrobotin avulla, metodi heittää poikkeuksen
IllegalStateException
, jos lypsyrobottia ei ole asennettu - public String toString() palauttaa navetan sisältämän maitosäiliön tilan
Testaa luokkaa Navetta
seuraavan ohjelmapätkän avulla.
Navetta navetta = new Navetta(new Maitosailio());
System.out.println("Navetta: " + navetta);
Lypsyrobotti robo = new Lypsyrobotti();
navetta.asennaLypsyrobotti(robo);
Lehma ammu = new Lehma();
ammu.eleleTunti();
ammu.eleleTunti();
navetta.hoida(ammu);
System.out.println("Navetta: " + navetta);
List<Lehma> lehmaLista = new ArrayList<>();
lehmaLista.add(ammu);
lehmaLista.add(new Lehma());
lehmaLista.stream().forEach(lehma -> {
lehma.eleleTunti();
lehma.eleleTunti();
});
navetta.hoida(lehmaLista);
System.out.println("Navetta: " + navetta);
Tulostuksen tulee olla esimerkiksi seuraavanlainen:
Navetta: 0.0/2000.0 Navetta: 2.8/2000.0 Navetta: 9.6/2000.0
Maatila
Maatilalla on omistaja ja siihen kuuluu navetta sekä joukko lehmiä. Maatila toteuttaa myös aiemmin nähdyn rajapinnan Eleleva
, jonka metodia eleleTunti()
-kutsumalla kaikki maatilaan liittyvät lehmät elelevät tunnin. Toteuta luokka maatila siten, että se toimii seuraavien esimerkkiohjelmien mukaisesti.
Maitosailio sailio = new Maitosailio();
Navetta navetta = new Navetta(sailio);
Maatila maatila = new Maatila("Esko", navetta);
System.out.println(maatila);
System.out.println(maatila.getOmistaja() + " on ahkera mies!");
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Ei lehmiä. Esko on ahkera mies!
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio()));
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Lehmät: Naatti 0.0/19.0 Hilke 0.0/30.0 Sylkki 0.0/29.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio()));
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.eleleTunti();
maatila.eleleTunti();
System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Lehmät: Heluna 2.0/17.0 Rima 3.0/32.0 Ilo 3.0/25.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio()));
Lypsyrobotti robo = new Lypsyrobotti();
maatila.asennaNavettaanLypsyrobotti(robo);
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.lisaaLehma(new Lehma());
maatila.eleleTunti();
maatila.eleleTunti();
maatila.hoidaLehmat();
System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 18.0/2000.0 Lehmät: Hilke 0.0/30.0 Sylkki 0.0/35.0 Hento 0.0/34.0
Edellä otettiin ensiaskeleet simulaattorin tekemiseen. Ohjelmaa voisi jatkaa vaikkapa lisäämällä maitoauton sekä luomalla useampia navettoja. Maitoautot voisivat kulkea tehtaalle, jossa tehtäisiin juustoa, jnejne..