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
Ohjelmoinnin perusteiden kuudennellaKuudennella viikolla teimme lintubongaria varten havaintotietokannan. Jatkamme hieman samasta teemasta, tällä kertaa teemme rengastustoimistolle ohjelman, jonka avulla pidetään kirjaa tiettynä vuotena rengastettujen lintujen havaintopaikoista.
HUOM: saatat törmätä tehtävässä kummalliseen virheilmoitukseen NoSuchMethodError: Lintu.equals(LLintu;)Z
, jos näin käy suorita clean and build eli paina harja ja vasara -kuvakkeesta.
Rengastustoimisto tallettaa tiettynä vuotena rengastettettujen lintujen tiedot Lintu
-olioihin:
public class Lintu { private String nimi; private String latinankielinenNimi; private int rengastusvuosi; public Lintu(String nimi, String latinankielinenNimi, int rengastusvuosi) { this.nimi = nimi; this.latinankielinenNimi = latinankielinenNimi; this.rengastusvuosi = rengastusvuosi; } @Override public String toString() { return this.latinankielinenNimi + "(" + this.rengastusvuosi + ")"; } }
Ideana on toteuttaa rengastustoimistoon toiminnallisuus jonka avulla voidaan pitää kirjaa tiettynä vuotena rengastettujen lintujen havaintojen lukumääristä ja havaintopaikoista. Havaintomääriä ja -paikkoja ei kuitenkaan talleteta Lintu-olioihin, vaan erilliseen HashMap:iin jonka avaimena käytetään Lintu-olioita. Kuten muistamme viikolta 8, on tälläisessä tapauksessa toteutettava luokalle Lintu
metodit equals(Object t)
ja hashCode()
.
Joillain linnuilla on useita suomenkielisä nimiä (esim. punakottaraisesta käytetään edelleen joskus sen vanhaa nimitystä rusokottarainen), latinankielinen nimi on kuitenkin aina yksikäsitteinen. Tee luokalle Lintu
equals-
ja hashCode-
metodit jotka toimivat siten, että lintu-oliot tulkitaan samoiksi jos niiden latinankielinen nimi ja rengastusvuosi ovat samat.
Esimerkki:
Lintu lintu1 = new Lintu("punakottarainen", "Sturnus roseus", 2012); Lintu lintu2 = new Lintu("rusokottarainen", "Sturnus roseus", 2012); Lintu lintu3 = new Lintu("varis", "Corvus corone cornix", 2012); Lintu lintu4 = new Lintu("punakottarainen", "Sturnus roseus", 2000); System.out.println( lintu1.equals(lintu2)); // ovat sama koska sama latinankielinen nimi ja rengastusvuosi System.out.println( lintu1.equals(lintu3)); // eivät ole sama koska latinankielinen nimi eri System.out.println( lintu1.equals(lintu4)); // eivät ole sama koska eri rengastusvuosi System.out.println( lintu1.hashCode()==lintu2.hashCode() );
tulostuu:
true false false true
Rengastustoimistolla on kaksi metodia: public void havaitse(Lintu lintu, String paikka)
lisää linnulle havainnon ja havaintopaikan ja metodi public void havainnot(Lintu lintu)
tulostaa alla olevan esimerkin mukaisesti parametrina olevan linnun havaintojen määrän ja havaintopaikat. Havaintopaikkojen tulostusjärjestyksellä ei ole testien läpimenon kannalta merkitystä.
Rengastustoimisto tallettaa havaintopaikat Map<Lintu, List<String>>
-tyyppiseen oliomuuttujaan. Havaintojen lukumäärä selviää havaintopaikkojen listan pituudesta. Tarvittaessa voit ottaa tehtävään mallia luvusta 50.
Esimerkki rengastustoimiston käytöstä:
Rengastustoimisto kumpulanRengas = new Rengastustoimisto(); kumpulanRengas.havaitse( new Lintu("punakottarainen", "Sturnus roseus", 2012), "Arabia" ); kumpulanRengas.havaitse( new Lintu("rusokottarainen", "Sturnus roseus", 2012), "Vallila" ); kumpulanRengas.havaitse( new Lintu("harmaalokki", "Larus argentatus", 2008), "Kumpulanmäki" ); kumpulanRengas.havaitse( new Lintu("punakottarainen", "Sturnus roseus", 2008), "Mannerheimintie" ); kumpulanRengas.havainnot( new Lintu("rusokottarainen", "Sturnus roseus", 2012 ) ); System.out.println("--"); kumpulanRengas.havainnot( new Lintu("harmaalokki", "Larus argentatus", 2008 ) ); System.out.println("--"); kumpulanRengas.havainnot( new Lintu("harmaalokki", "Larus argentatus", 1980 ) );
tulostuu:
Sturnus roseus (2012) havaintoja: 2 Arabia Vallila -- Larus argentatus (2008) havaintoja: 1 Kumpulanmäki -- Larus argentatus (1980) havaintoja: 0
Olemme aiemmissa kappaleissa törmänneet tilanteisiin, joissa muuttujilla on oman tyyppinsä lisäksi muita tyyppejä. Esimerkiksi kappaleessa 45 huomasimme että kaikki oliot ovat tyyppiä Object
. Jos olio on jotain tiettyä tyyppiä, voidaan se myös esittää Object
-tyyppisenä muuttujana. Esimerkiksi String
on myös tyyppiä Object
, joten kaikki String
-tyyppiset muuttujat voidaan esitellä Object
tyypin avulla.
String merkkijono = "merkkijono"; Object merkkijonoString = "toinen merkkijono";
Merkkijono-olion asettaminen Object
-tyyppiseen viitteeseen onnistuu.
String merkkijono = "merkkijono"; Object merkkijonoString = merkkijono;
Toiseen suuntaan asettaminen ei onnistu. Koska Object
-tyyppiset muuttujat eivät ole tyyppiä String
, ei object-tyyppistä muuttujaa voi asettaa String
-tyyppiseen muuttujaan.
Object merkkijonoString = "toinen merkkijono"; String merkkijono = merkkijonoString; // EI ONNISTU!
Mistä tässä oikein on kyse?
Muuttujilla on oman tyyppinsä lisäksi aina perimiensä luokkien ja toteuttamiensa rajapintojen tyypit. Luokka String
perii Object
-luokan, joten String
-oliot ovat aina myös tyyppiä Object
. Luokka Object
ei peri String
-luokkaa, joten Object
-tyyppiset muuttujat eivät ole automaattisesti tyyppiä String
. Tutustutaan tarkemmin String
-luokan API-dokumentaatioon, erityisesti HTML-sivun yläosaan.
String-luokan API-dokumentaatio alkaa yleisellä otsakkeella jota seuraa luokan pakkaus (java.lang
). Pakkauksen jälkeen tulee luokan nimi (Class String
), jota seuraa luokan perintähierarkia.
java.lang.Object java.lang.String
Perintähierarkia listaa luokat, jotka luokka on perinyt. Perityt luokat listataan perimisjärjestyksessä, tarkasteltava luokka aina alimpana. String-luokan perintähierarkiasta näemme, että String
-luokka perii luokan Object
. Javassa jokainen luokka voi periä korkeintaan yhden luokan, mutta välillisesti niitä voi periä useampia.
Perintähierarkiaa voi ajatella myös listana tyypeistä, joita olio toteuttaa.
Se, että kaikki oliot ovat tyyppiä Object
helpottaa ohjelmointia. Jos tarvitsemme metodissa vain Object
-luokassa määriteltyjä toimintoja, voimme käyttää metodin parametrina tyyppiä Object
. Koska kaikki oliot ovat myös tyyppiä object, voi metodille antaa minkä tahansa olion parametrina. Luodaan metodi tulostaMonesti
, joka saa parametrinaan Object
-tyyppisen muuttujan ja tulostusten lukumäärän.
public class Tulostin { ... public void tulostaMonesti(Object object, int kertaa) { for (int i = 0; i < kertaa; i++) { System.out.println(object.toString()); } } ... }
Metodille tulostaMonesti
voi antaa parametrina minkä tahansa olion. Metodin tulostaMonesti
sisässä oliolla on käytössään vain Object
-luokassa määritellyt metodit koska olio esitellään metodissa Object
-tyyppisenä.
Tulostin tulostin = new Tulostin(); String merkkijono = " o "; List<String> sanat = new ArrayList<String>(); sanat.add("polymorfismi"); sanat.add("perintä"); sanat.add("kapselointi"); sanat.add("abstrahointi"); tulostin.tulostaMonesti(merkkijono, 2); tulostin.tulostaMonesti(sanat, 3);
o o [polymorfismi, perintä, kapselointi, abstrahointi] [polymorfismi, perintä, kapselointi, abstrahointi] [polymorfismi, perintä, kapselointi, abstrahointi]
Jatketaan String
-luokan API-kuvauksen tarkastelua. Kuvauksessa olevaa perintähierarkiaa seuraa listaus luokan toteuttamista rajapinnoista.
All Implemented Interfaces: Serializable, CharSequence, Comparable<String>
Luokka String
toteuttaa rajapinnat Serializable
, CharSequence
, ja Comparable<String>
. Myös rajapinta on tyyppi. Luokan String API-kuvauksen mukaan String-olion tyypiksi voi asettaa seuraavat rajapinnat.
Serializable serializableString = "merkkijono"; CharSequence charSequenceString = "merkkijono"; Comparable<String> comparableString = "merkkijono";
Koska metodeille voidaan määritellä metodin parametrin tyyppi, voimme määritellä metodeja jotka vastaanottavat tietyn rajapinnan toteuttavan olion. Kun metodille määritellään parametrina rajapinta, sille voidaan antaa parametrina mikä tahansa olio joka toteuttaa kyseisen rajapinnan, metodi ei välitä olion oikeasta tyypistä.
Täydennetään Tulostin
-luokkaa siten, että sillä on metodi CharSequence
-rajapinnan toteuttavien olioiden merkkien tulostamiseen. Rajapinta CharSequence
tarjoaa muunmuassa metodit int length()
, jolla saa merkkijonon pituuden, ja char charAt(int index)
, jolla saa merkin tietyssä indeksissä.
public class Tulostin { ... public void tulostaMonesti(Object object, int kertaa) { for (int i = 0; i < kertaa; i++) { System.out.println(object.toString()); } } public void tulostaMerkit(CharSequence charSequence) { for (int i = 0; i < charSequence.length(); i++) { System.out.println(charSequence.charAt(i); } } ... }
Metodille tulostaMerkit
voi antaa minkä tahansa CharSequence
-rajapinnan toteuttavan olion. Näitä on muunmuassa String
ja merkkijonojen rakentamisessa usein Stringiä tehokkaampi StringBuilder
. Metodi tulostaMerkit
tulostaa annetun olion jokaisen merkin omalle rivilleen.
Tulostin tulostin = new Tulostin(); String mjono = "toimii"; tulostin.tulostaMerkit(mjono);
t o i m i i
Tässä tehtävässä teemme eliöita ja eliöistä koostuvia laumoja jotka liikkuvat ympäriinsä. Eliöiden sijaintien ilmoittamiseen käytetään kaksiulotteista koordinaatistoa. Jokaiseen sijaintiin liittyy kaksi lukua, x
- ja y
-koordinaatti. Koordinaatti x
kertoo, kuinka pitkällä "nollapisteestä" mitattuna sijainti on vaakasuunnassa, ja koordinaatti y
vastaavasti kuinka pitkällä sijainti on pystysuunnassa. Jos koordinaatiston käsite ei ole tuttu, voit lukea siitä lisää esimerkiksi wikipediasta.
Tehtävän mukana tulee rajapinta Siirrettava
, joka kuvaa asiaa jota voidaan siirtää paikasta toiseen. Rajapinta sisältää metodin void siirra(int dx, int dy)
. Parametri dx
kertoo, paljonko asia siirtyy x-akselilla ja dy
y-akselilla.
Tehtävässä toteutat luokat Elio
ja Lauma
, jotka molemmat ovat siirrettäviä. Toteuta kaikki toiminnallisuus pakkaukseen siirrettava
.
Luo pakkaukseen siirrettava
luokka Elio
, joka toteuttaa rajapinnan Siirrettava
. Eliön tulee tietää oma sijaintinsa (x, y -koordinaatteina). Luokan Elio
APIn tulee olla seuraava:
"x: 3; y: 6"
. Huomaa että koordinaatit on erotettu puolipisteellä (;
)dx
sisältää muutoksen koordinaattiin x
, muuttuja dy
sisältää muutoksen koordinaattiin y
. Esimerkiksi jos muuttujan dx
arvo on 5, tulee oliomuuttujan x
arvoa kasvattaa viidelläKokeile luokan Elio
toimintaa seuraavalla esimerkkikoodilla.
Elio elio = new Elio(20, 30); System.out.println(elio); elio.siirra(-10, 5); System.out.println(elio); elio.siirra(50, 20); System.out.println(elio);
x: 20; y: 30 x: 10; y: 35 x: 60; y: 55
Luo seuraavaksi pakkaukseen siirrettava
luokka Lauma
, joka toteuttaa rajapinnan Siirrettava
. Lauma koostuu useasta Siirrettava
-rajapinnan toteutavasta oliosta, jotka tulee tallettaa esimerkiksi listarakenteeseen.
Luokalla Lauma
tulee olla seuraavanlainen API.
Siirrettava
-rajapinnan toteuttavan olionKokeile ohjelmasi toimintaa alla olevalla esimerkkikoodilla.
Lauma lauma = new Lauma(); lauma.lisaaLaumaan(new Elio(73, 56)); lauma.lisaaLaumaan(new Elio(57, 66)); lauma.lisaaLaumaan(new Elio(46, 52)); lauma.lisaaLaumaan(new Elio(19, 107)); System.out.println(lauma);
x: 73; y: 56 x: 57; y: 66 x: 46; y: 52 x: 19; y: 107
Luokat ovat ohjelmoijan tapa ratkaistavan ongelma-alueen käsitteiden selkeyttämiseen. Lisäämme jokaisella luomallamme luokalla uutta toiminnallisuutta ohjelmointikieleen. Toiminnallisuutta tarvitaan kohtaamiemme ongelmien ratkomiseen, ratkaisut syntyvät luokista luotujen olioiden välisen interaktion avulla. Olio-ohjelmoinnissa olio on itsenäinen kokonaisuus, jolla on olion tarjoamien metodien avulla muutettava tila. Olioita käytetään yhteistyössä; jokaisella oliolla on oma vastuualue. Esimerkiksi käyttöliittymäluokkamme ovat tähän mennessä hyödyntäneet Scanner
-olioita.
Jokainen Javan luokka perii luokan Object
, eli jokainen luomamme luokka saa käyttöönsä kaikki Object
-luokassa määritellyt metodit. Jos haluamme muuttaa Object
-luokassa määriteltyjen metodien toiminnallisuutta tulee ne korvata (Override
) määrittelemällä niille uusi toteutus luodussa luokassa.
Luokan Object
perimisen lisäksi myös muiden luokkien periminen on mahdollista. Javan ArrayList
-luokan APIa tarkasteltaessa huomaamme että ArrayList
perii luokan AbstractList
. Luokka AbstractList
perii luokan AbstractCollection
, joka perii luokan Object
.
java.lang.Object java.util.AbstractCollection<E> java.util.AbstractList<E> java.util.ArrayList<E>
Kukin luokka voi periä suoranaisesti yhden luokan. Välillisesti luokka kuitenkin perii kaikki perimänsä luokan ominaisuudet. Luokka ArrayList
perii suoranaisesti luokan AbstractList
, ja välillisesti luokat AbstractCollection
ja Object
. Luokalla ArrayList
on siis käytössään luokkien AbstractList
, AbstractCollection
ja Object
muuttujat, metodit ja rajapinnat.
Luokan ominaisuudet peritään avainsanalla extends
. Luokan perivää luokkaa kutsutaan aliluokaksi (subclass), perittävää luokkaa yliluokaksi (superclass). Tutustutaan erään autonvalmistajan järjestelmään, joka hallinnoi auton osia. Osien hallinan peruskomponentti on luokka Osa
, joka määrittelee tunnuksen, valmistajan ja kuvauksen.
public class Osa { private String tunnus; private String valmistaja; private String kuvaus; public Osa(String tunnus, String valmistaja, String kuvaus) { this.tunnus = tunnus; this.valmistaja = valmistaja; this.kuvaus = kuvaus; } public String getTunnus() { return tunnus; } public String getKuvaus() { return kuvaus; } public String getValmistaja() { return valmistaja; } }
Yksi osa autoa on moottori. Kuten kaikilla osilla, myös moottorilla on valmistaja, tunnus ja kuvaus. Näiden lisäksi moottoriin liittyy moottorityyppi: esimerkiksi polttomoottori, sähkömoottori tai hybridi. Luodaan luokan Osa
perivä luokka Moottori
: moottori on osan erikoistapaus.
public class Moottori extends Osa { private String moottorityyppi; public Moottori(String moottorityyppi, String tunnus, String valmistaja, String kuvaus) { super(tunnus, valmistaja, kuvaus); this.moottorityyppi = moottorityyppi; } public String getMoottorityyppi() { return moottorityyppi; } }
Luokkamäärittely public class Moottori extends Osa
kertoo että luokka Moottori
perii luokan Osa
toiminnallisuuden. Luokassa Moottori
määritellään oliomuuttuja moottorityyppi
.
Moottori-luokan konstruktori on mielenkiintoinen. Konstruktorin ensimmäisellä rivillä on avainsana super
, jolla kutsutaan yliluokan konstruktoria. Kutsu super(tunnus, valmistaja, kuaus)
kutsuu luokassa Osa
määriteltyä konstruktoria public Osa(String tunnus, String valmistaja, String kuvaus
, jolloin yliluokassa määritellyt oliomuuttujat saavat arvonsa. Tämän jälkeen oliomuuttujalle moottorityyppi
asetetaan siihen liittyvä arvo.
Kun luokka Moottori
perii luokan Osa
, saa se käyttöönsä kaikki luokan Osa
tarjoamat metodit. Luokasta Moottori
voi tehdä ilmentymän aivan kuten mistä tahansa muustakin luokasta.
Moottori moottori = new Moottori("polttomoottori", "hz", "volkswagen", "VW GOLF 1L 86-91"); System.out.println(moottori.getMoottorityyppi()); System.out.println(moottori.getValmistaja());
polttomoottori volkswagen
Kuten huomaat, luokalla Moottori
on käytössä luokassa Osa
määritellyt metodit.
Jos metodilla tai muuttujalla on näkyvyysmääre private
, ei se näy aliluokille eikä aliluokalla ole mitään suoraa tapaa päästä käsiksi yliluokan siihen. Edellisessä esimerkissä Moottori ei siis pääse suoraan käsiksi yliluokassa Osa määriteltyihin ominaisuuksiinsa (tunnus, valmistaja, kuvaus). Aliluokka näkee luonnollisesti kaiken yliluokan julkisen eli public
-määreellä varustetun kaluston. Jos halutaan määritellä yliluokkaan joitain muuttujia tai metodeja joiden näkeminen halutaan sallia aliluokille, mutta estää muilta voidaan käyttää näkyvyysmäärettä protected
.
Yliluokan konstruktoria kutsutaan avainsanalla super
. Kutsu super
on käytännössä samanlainen kuin this
-konstruktorikutsu. Kutsulle annetaan parametrina yliluokan konstruktorin vaatiman tyyppiset arvot.
Konstruktoria kutsuttaessa yliluokassa määritellyt muuttujat alustetaan. Konstruktorikutsussa tapahtuu käytännössä täysin samat asiat kuin normaalissa konstruktorikutsussa. Jos yliluokassa ei ole määritelty parametritonta konstruktoria, tulee aliluokan konstruktorikutsuissa olla aina mukana yliluokan konstruktorikutsu.
Huom! Kutsun super
tulee olla aina konstruktorin ensimmäisellä rivillä!
Yliluokassa määriteltyjä metodeja voi kutsua super
-etuliitteen avulla, aivan kuten tässä luokassa määriteltyjä metodeja voi kutsua this
-etuliitteellä. Esimerkiksi yliluokassa määriteltyä toString
-metodia voi hyödyntää sen korvaavassa metodissa seuraavasti:
@Override public String toString() { return super.toString() + "\n Ja oma viestini vielä!"; }
Tee pakkaus henkilot
ja sinne luokka Henkilo
, joka toimii seuraavan pääohjelman yhteydessä
public static void main(String[] args) { Henkilo pekka = new Henkilo("Pekka Mikkola", "Korsontie 1 03100 Vantaa"); Henkilo esko = new Henkilo("Esko Ukkonen", "Mannerheimintie 15 00100 Helsinki"); System.out.println(pekka); System.out.println(esko); }
siten että tulostuu
Pekka Mikkola Korsontie 1 03100 Vantaa Esko Ukkonen Mannerheimintie 15 00100 Helsinki
Tee pakkaukseen luokka Opiskelija
joka perii luokan Henkilo
.
Opiskelijalla on aluksi 0 opintopistettä. Aina kun opiskelija opiskelee, kasvaa opintopistemäärä. Toteuta luokka siten, että seuraava pääohjelma:
public static void main(String[] args) { Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); System.out.println(olli ); System.out.println("opintopisteitä " + olli.opintopisteita()); olli.opiskele(); System.out.println("opintopisteitä "+ olli.opintopisteita()); }
tuottaa tulostuksen:
Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0 opintopisteitä 1
Edellisessä tehtävässä Opiskelija
perii toString-metodin luokalta Henkilo
. Perityn metodin voi myös ylikirjoittaa, eli korvata omalla versiolla. Tee nyt Opiskelija
:lle oma versio toString:istä joka toimii seuraavan esimerkin mukaan:
public static void main(String[] args) { Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); System.out.println( olli ); olli.opiskele(); System.out.println( olli ); }
Tulostuu:
Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0 Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 1
Tee pakkaukseen luokka Henkilo
:n perivä luokka Opettaja
. Opettajalla on palkka joka tulostuu opettajan merkkijonoesityksessä.
Testaa, että seuraava pääohjelma
public static void main(String[] args) { Opettaja pekka = new Opettaja("Pekka Mikkola", "Korsontie 1 03100 Vantaa", 1200); Opettaja esko = new Opettaja("Esko Ukkonen", "Mannerheimintie 15 00100 Helsinki", 5400); System.out.println( pekka ); System.out.println( esko ); Opiskelija olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); for ( int i=0; i < 25; i++ ) { olli.opiskele(); } System.out.println( olli ); }
Aikaansaa tulostuksen
Pekka Mikkola Korsontie 1 03100 Vantaa palkka 1200 euroa/kk Esko Ukkonen Mannerheimintie 15 00100 Helsinki palkka 5400 euroa/kk Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 25
Toteuta oletuspakkauksessa olevaan Main
-luokkaan luokkametodi public static void tulostaLaitoksenHenkilot(List<Henkilo> henkilot)
, joka tulostaa kaikki metodille parametrina annetussa listassa olevat henkilöt. Metodin tulee toimia seuraavasti main
-metodista kutsuttaessa.
public static void tulostaLaitoksenHenkilot(List<Henkilo> henkilot) { // tulostetaan kaikki listan henkilöt } public static void main(String[] args) { List<Henkilo> henkilot = new ArrayList<Henkilo>(); henkilot.add( new Opettaja("Pekka Mikkola", "Korsontie 1 03100 Vantaa", 1200) ); henkilot.add( new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki") ); tulostaLaitoksenHenkilot(henkilot); }
Pekka Mikkola Korsontie 1 03100 Vantaa palkka 1200 euroa/kk Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0
Olion kutsuttavissa olevat metodit määrittyvät muuttujan tyypin kautta. Esimerkiksi jos Opiskelija
-tyyppisen olion viite on talletettu Henkilo
-tyyppiseen muuttujaan, on oliosta käytössä vain Henkilo
-luokassa määritellyt metodit:
Henkilo olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); olli.opintopisteita(); // EI TOIMI! olli.opiskele(); // EI TOIMI! String.out.println( olli ); // olli.toString() TOIMII
Jos oliolla on monta eri tyyppiä, on sillä käytössä jokaisen tyypin määrittelemät metodit. Esimerkiksi Opiskelija
-tyyppisellä oliolla on käytössä Henkilo
-luokassa määritellyt metodit sekä Object
-luokassa määritellyt metodit.
Edellisessä tehtävässä korvasimme Opiskelijan luokalta Henkilö perimän toString
uudella versiolla. Myös luokka Henkilö oli jo korvannut Object-luokalta perimänsä toStringin. Jos käsittelemme olioa jonkin muun kuin sen todellisen tyypin omaavan muuttujan kautta, mitä versiota olion metodista kutsutaan? Esim. seuraavassa on kaksi opiskelijaa joiden viitteet on talletettu Henkilo- ja Object-tyyppisiin muuttujiin. Molemmille kutsutaan metodia toString
. Mikä versio metodista suoritetaan, luokassa Object, Henkilo vai Opiskelija määritelty?
Henkilo olli = new Opiskelija("Olli", "Ida Albergintie 1 00400 Helsinki"); String.out.println( olli ); Object liisa = new Opiskelija("Liisa", "Väinö Auerin katu 20 00500 Helsinki"); String.out.println( liisa );
Tulostuu:
Olli Ida Albergintie 1 00400 Helsinki opintopisteitä 0 Liisa Väinö Auerin katu 20 00500 Helsinki opintopisteitä 0
Eli suoritettava metodi valitaan olion todellisen tyypin perusteella, ei viitteen tallettavan muuttujan tyypin perusteella!
Hieman yleisemmin: Suoritettava metodi valitaan aina olion todellisen tyypin perusteella riippumatta käytetyn muuttujan tyypistä. Oliot ovat monimuotoisia, eli olioita voi käyttää usean eri muuttujatyypin kautta. Suoritettava metodi liittyy aina olion todelliseen tyyppiin. Tätä monimuotoisuutta kutsutaan polymorfismiksi.
Kaksiulotteisessa koordinaatiostossa sijaitsevaa pistettä voisi kuvata seuraavan luokan avulla:
public class Piste { private int x; private int y; public Piste(int x, int y) { this.x = x; this.y = y; } public int manhattanEtaisyysOrigosta(){ return Math.abs(x)+Math.abs(y); } protected String sijainti(){ return x+", "+y; } @Override public String toString() { return "("+this.sijainti()+") etäisyys "+this.manhattanEtaisyysOrigosta(); } }
Metodi sijainti
ei ole tarkoitettu ulkoiseen käyttöön, joten se on näkyvyysmääreeltään protected, eli aliluokat pääsevät siihen käsiksi. Esim. reitinhakualgoritmien hyödyntämässä Manhattan-etäisyydellä tarkoitetaan pisteiden etäisyyttä, jos niiden välin voi kulkea ainoastaan koordinaattiakselien suuntaisesti.
Värillinen piste on muuten samanlainen kuin piste, mutta se sisältää merkkijonona ilmaistavan värin. Luokka voidaan siis tehdä perimällä Piste:
public class VariPiste extends Piste { private String vari; public VariPiste(int x, int y, String vari) { super(x, y); this.vari = vari; } @Override public String toString() { return super.toString()+" väri: "+vari; } }
Luokka määrittelee oliomuuttujan värin talletusta varten. Koordinaatit talletetaan yliluokkaan. Merkkijonoesityksestä halutaan muuten samanlainen kuin pisteellä, mutta väri tulee myös ilmaista. Ylikirjoitettu metodi toString
kutsuukin yliluokan toStringiä ja lisää sen tulokseen pisteen värin.
Seuraavassa esimerkki, jossa listalle laitetaan muutama piste, osa normaaleja ja osa väripisteitä ja tulostetaan listalla olevat pisteet. Polymorfismin ansioista kaikille tulee kutsutuksi olion todellisen tyypin toString-metodi vaikka lista tuntee kaikki pisteet Piste
-tyyppisinä:
public class Main { public static void main(String[] args) { List<Piste> pisteet = new ArrayList<Piste>(); pisteet.add(new Piste(4, 8)); pisteet.add(new VariPiste(1, 1, "vihreä")); pisteet.add(new VariPiste(2, 5, "sininen")); pisteet.add(new Piste(0, 0)); for (Piste piste : pisteet) { System.out.println(piste); } } }
Tulostuu:
(4, 8) etäisyys 12 (1, 1) etäisyys 2 väri: vihreä (2, 5) etäisyys 7 väri: sininen (0, 0) etäisyys 0
Haluamme ohjelmaamme myös kolmiulotteisen pisteen. Koska kyseessä ei ole värillinen versio, periytetään se pisteestä:
public class Piste3D extends Piste { private int z; public Piste3D(int x, int y, int z) { super(x, y); this.z = z; } @Override protected String sijainti() { return super.sijainti()+", "+z; // tulos merkkijono muotoa "x, y, z" } @Override public int manhattanEtaisyysOrigosta() { // kysytään ensin yliluokalta x:n ja y:n perusteella laskettua etäisyyttä // ja lisätään tulokseen z-koordinaatin vaikutus return super.manhattanEtaisyysOrigosta()+Math.abs(z); } @Override public String toString() { return "("+this.sijainti()+") etäisyys "+this.manhattanEtaisyysOrigosta(); } }
Kolmiulotteinen piste siis määrittelee kolmatta koordinaattia vastaavan oliomuuttujan ja ylikirjoittaa metodit sijainti
,
manhattanEtaisyysOrigosta
ja toString
siten, että ne huomioivat kolmannen ulottuvuuden. Voimme nyt laajentaa edellistä esimerkkiä ja lisätä listalle myös kolmiulotteisia pisteitä:
public class Main { public static void main(String[] args) { Listpisteet = new ArrayList (); pisteet.add(new Piste(4, 8)); pisteet.add(new VariPiste(1, 1, "vihreä")); pisteet.add(new VariPiste(2, 5, "sininen")); pisteet.add(new Piste3D(5, 2, 8)); pisteet.add(new Piste(0, 0)); for (Piste piste : pisteet) { System.out.println(piste); } } }
Tulostus on odotusten mukainen
(4, 8) etäisyys 12 (1, 1) etäisyys 2 väri: vihreä (2, 5) etäisyys 7 väri: sininen (5, 2, 8) etäisyys 15 (0, 0) etäisyys 0
Huomamme, että kolmiulotteisen pisteen metodi toString
on täsmälleen sama kuin pisteen toString. Voisimmeko jättää toStringin ylikirjoittamatta? Vastaus on kyllä! Kolmiulotteinen piste pelkistyy seuraavanlaiseksi:
public class Piste3D extends Piste { private int z; public Piste3D(int x, int y, int z) { super(x, y); this.z = z; } @Override protected String sijainti() { return super.sijainti()+", "+z; } @Override public int manhattanEtaisyysOrigosta() { return super.manhattanEtaisyysOrigosta()+Math.abs(z); } }
Mitä tarkalleenottaen tapahtuu kuin kolmiulotteiselle pisteelle kutsutaan toString-metodia? Suoritus etenee seuraavasti:
return "("+this.sijainti()+") etäisyys "+this.manhattanEtaisyysOrigosta();
Metodikutsun aikaansaama toimintoketju siis on varsin monivaiheinen. Periaate on kuitenkin selkeä: suoritettavan metodin määrittelyä etsitään ensin olion todellisen tyypin määrittelystä ja jos sitä ei löydy edetään yliluokkaan. Ja jos yliluokastakaan ei löydy metodin toteutusta siirrytään etsimään yliluokan yliluokasta jne...
Perintä on väline käsitehierarkioiden rakentamiseen ja erikoistamiseen; aliluokka on aina yliluokan erikoistapaus. Jos luotava luokka on olemassaolevan luokan erikoistapaus, voidaan uusi luokka luoda perimällä olemassaoleva luokka. Esimerkiksi auton osiin liittyvässä esimerkissä moottori on osa, mutta moottoriin liittyy lisätoiminnallisuutta mitä jokaisella osalla ei ole.
Perittäessä aliluokka saa käyttöönsä yliluokan toiminnallisuudet. Jos aliluokka ei tarvitse tai käytä perittyä toiminnallisuutta, ei perintä ole perusteltua. Perityt luokat perivät yliluokkiensa metodit ja rajapinnat, eli aliluokkia voidaan käyttää missä tahansa missä yliluokkaa on käytetty. Perintähierarkia kannattaa pitää matalana, sillä hierarkian ylläpito ja jatkokehitys vaikeutuu perintöhierarkian kasvaessa. Yleisesti ottaen, jos perintähierarkian korkeus on yli 2 tai 3, ohjelman rakenteessa on todennäköisesti parannettavaa.
Perinnän käyttöä tulee miettiä. Esimerkiksi luokan Auto
periminen luokasta Osa
(tai Moottori
) on väärin. Auto sisältää moottorin ja osia, mutta auto ei ole moottori tai osa. Voimme yleisemmin ajatella että jos olio omistaa tai koostuu toisista olioista, ei perintää tule käyttää.
Perintää käytettäessä tulee varmistaa että Single Responsibility Principle pätee myös perittäessä. Jokaisella luokalla tulee olla vain yksi syy muuttua. Jos huomaat että perintä lisää luokan vastuita, tulee luokka pilkkoa useammaksi luokaksi.
Pohditaan postituspalveluun liittyviä luokkia Asiakas
, joka sisältää asiakkaan tiedot, ja Tilaus
, joka perii asiakkaan tiedot ja sisältää tilattavan tavaran tiedot. Luokassa Tilaus
on myös metodi postitusOsoite
, joka kertoo tilauksen postitusosoitteen.
public class Asiakas { private String nimi; private String osoite; public Asiakas(String nimi, String osoite) { this.nimi = nimi; this.osoite = osoite; } public String getNimi() { return nimi; } public String getOsoite() { return osoite; } public void setOsoite(String osoite) { this.osoite = osoite; } }
public class Tilaus extends Asiakas { private String tuote; private String lukumaara; public Tilaus(String tuote, String lukumaara, String nimi, String osoite) { super(nimi, osoite); this.tuote = tuote; this.lukumaara = lukumaara; } public String getTuote() { return tuote; } public String getLukumaara() { return lukumaara; } public String postitusOsoite() { return this.getNimi() + "\n" + this.getOsoite(); } }
Yllä perintää on käytetty väärin. Luokkaa perittäessä aliluokan tulee olla yliluokan erikoistapaus; tilaus ei ole asiakkaan erikoistapaus. Väärinkäyttö ilmenee single responsibility principlen rikkomisena: luokalla Tilaus
on vastuu sekä asiakkaan tietojen ylläpidosta, että tilauksen tietojen ylläpidosta.
Ratkaisussa piilevä ongelma tulee esiin kun mietimme mitä käy asiakkaan osoitteen muuttuessa.
Osoitteen muuttuessa joudumme muuttamaan jokaista kyseiseen asiakkaaseen liittyvää tilausoliota, mikä kertoo huonosta tilanteesta. Parempi ratkaisu olisi kapseloida Asiakas
Tilaus
-luokan oliomuuttujaksi. Jos ajattelemme tarkemmin tilauksen semantiikkaa, tämä on selvää. Tilauksella on asiakas. Muutetaan luokkaa Tilaus
siten, että se sisältää Asiakas
-viitteen.
public class Tilaus { private Asiakas asiakas; private String tuote; private String lukumaara; public Tilaus(Asiakas asiakas, String tuote, String lukumaara) { this.asiakas = asiakas; this.tuote = tuote; this.lukumaara = lukumaara; } public String getTuote() { return tuote; } public String getLukumaara() { return lukumaara; } public String postitusOsoite() { return this.asiakas.getNimi() + "\n" + this.asiakas.getOsoite(); } }
Yllä oleva luokka Tilaus
on nyt parempi. Metodi postitusosoite
käyttää asiakas-viitettä postitusosoitteen saamiseen sen sijaan että luokka perisi luokan Asiakas
. Tämä helpottaa sekä ohjelman ylläpitoa, että sen konkreettista toiminnallisuutta.
Nyt asiakkaan muuttaessa tarvitsee muuttaa vain asiakkaan tietoja, tilauksiin ei tarvitse tehdä muutoksia.
Tehtäväpohjassa tulee mukana luokka Varasto
, jonka tarjoamat konstruktorit ja metodit ovat seuraavat:
Tehtävässä rakennetaan Varasto
-luokasta useampia erilaisia varastoja. Huom! Toteuta kaikki luokat pakkaukseen varastot
.
Luokka Varasto
hallitsee tuotteen määrään liittyvät toiminnot. Nyt tuotteelle halutaan lisäksi tuotenimi ja nimen käsittelyvälineet. Ohjelmoidaan Tuotevarasto Varaston aliluokaksi! Toteutetaan ensin pelkkä yksityinen oliomuuttuja tuotenimelle, konstruktori ja getteri nimikentälle:
Muista millä tavoin konstruktori voi ensi toimenaan suorittaa yliluokan konstruktorin!
Käyttöesimerkki:
Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0); mehu.lisaaVarastoon(1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice System.out.println(mehu); // saldo = 988.7, tilaa 11.3
Juice saldo = 988.7, vielä tilaa 11.3
Kuten edellisestä esimerkistä näkee, Tuotevarasto-olion perimä toString()
ei tiedä (tietenkään!) mitään tuotteen nimestä. Asialle on tehtävä jotain! Lisätään samalla myös setteri tuotenimelle:
Uuden toString()
-metodin voisi toki ohjelmoida käyttäen yliluokalta perittyjä gettereitä, joilla perittyjen, mutta piilossa pidettyjen kenttien arvoja saa käyttöönsä. Koska yliluokkaan on kuitenkin jo ohjelmoitu tarvittava taito varastotilanteen merkkiesityksen tuottamiseen, miksi nähdä vaivaa sen uudelleen ohjelmointiin. Käytä siis hyväksesi perittyä toString
iä.
Muista miten korvattua metodia voi kutsua aliluokassa!
Käyttöesimerkki:
Tuotevarasto mehu = new Tuotevarasto("Juice", 1000.0); mehu.lisaaVarastoon(1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, tilaa 10.299999999999955
Juice Juice: saldo = 989.7, tilaa 10.299999999999955
Toisinaan saattaa olla kiinnostavaa tietää, millä tavoin jonkin tuotteen varastotilanne muuttuu: onko varasto usein hyvin vajaa, ollaanko usein ylärajalla, onko vaihelu suurta vai pientä, jne. Varustetaan siksi Tuotevarasto
-luokka taidolla muistaa tuotteen määrän muutoshistoriaa.
Aloitetaan apuvälineen laadinnalla.
Muutoshistorian muistamisen voisi toki toteuttaa suoraankin ArrayList<Double>
-oliona luokassa Tuotevarasto, mutta nyt laaditaan kuitenkin oma erikoistettu väline tähän tarkoitukseen. Väline toteutetaan kapseloimalla ArrayList<Double>
-olio.
Muutoshistoria
-luokan julkiset konstruktorit ja metodit:
Muutoshistoria
-olion.
Täydennä Muutoshistoria
-luokkaa analyysimetodein:
Täydennä Muutoshistoria
-luokkaa analyysimetodein:
Ohjeen varianssin laskemiseksi voit katsoa esimerkiksi Wikipediasta kohdasta populaatio- ja otosvarianssi. Esimerkiksi lukujen 3, 2, 7, 2 keskiarvo on 3.5, joten otosvarianssi on ((3 - 3.5)² + (2 - 3.5)² + (7 - 3.5)² + (2 - 3.5)²)/(4 - 1) ≈ 5,666667.)
Toteuta luokan Tuotevarasto
aliluokkana MuistavaTuotevarasto
.
Uusi versio tarjoaa vanhojen lisäksi varastotilanteen muutoshistoriaan liittyviä
palveluita. Historiaa hallitaan Muutoshistoria
-oliolla.
Julkiset konstruktorit ja metodit:
Huomaa että tässä esiversiossa historia ei vielä toimi kunnolla; nyt vasta vain aloitussaldo muistetaan.
Käyttöesimerkki:
// tuttuun tapaan: MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, vielä tilaa 10.3 ... // mutta vielä historia() ei toimi kunnolla: System.out.println(mehu.historia()); // [1000.0] // saadaan siis vasta konstruktorin asettama historian alkupiste... ...
Tulostus siis:
Juice Juice: saldo = 989.7, vielä tilaa 10.299999999999955 [1000.0]
On aika aloittaa historia! Ensimmäinen versio ei historiasta tiennyt kuin alkupisteen. Täydennä luokkaa metodein
Varasto
-luokan metodi, mutta muuttunut tilanne kirjataan historiaan. Huom: historiaan tulee kirjata poiston jälkeinen varastosaldo, ei poistettavaa määrää!Käyttöesimerkki:
// tuttuun tapaan: MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); System.out.println(mehu.getNimi()); // Juice mehu.lisaaVarastoon(1.0); System.out.println(mehu); // Juice: saldo = 989.7, vielä tilaa 10.3 ... // mutta nyt on historiaakin: System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7] ...
Tulostus siis:
Juice Juice: saldo = 989.7, vielä tilaa 10.299999999999955 [1000.0, 988.7, 989.7]
Muista miten korvaava metodi voi käyttää hyväkseen korvattua metodia!
Täydennä luokkaa metodilla
Käyttöesimerkki:
MuistavaTuotevarasto mehu = new MuistavaTuotevarasto("Juice", 1000.0, 1000.0); mehu.otaVarastosta(11.3); mehu.lisaaVarastoon(1.0); //System.out.println(mehu.historia()); // [1000.0, 988.7, 989.7] mehu.tulostaAnalyysi();
Metodi tulostaAnalyysi kirjoittaa ilmoituksen tyyliin:
Tuote: Juice Historia: [1000.0, 988.7, 989.7] Suurin tuotemäärä: 1000.0 Pienin tuotemäärä: 988.7 Keskiarvo: 992.8
Täydennä analyysin tulostus sellaiseksi, että mukana ovat myös muutoshistorian suurin muutos ja historian varianssi.
Perintä ei sulje pois rajapintojen käyttöä, eikä rajapintojen käyttö sulje pois perinnän käyttöä. Rajapinnat toimivat sopimuksena luokan tarjoamasta toteutuksesta, ja mahdollistavat konkreettisen toteutuksen abstrahoinnin. Rajapinnan toteuttavan luokan vaihto on hyvin helppoa.
Aivan kuten rajapintaa toteuttaessa, sitoudumme perittäessä siihen, että aliluokkamme tarjoaa kaikki yliluokan metodit. Monimuotoisuuden ja polymorfismin takia perintäkin toimii kuin rajapinnat. Voimme antaa yliluokkaa käyttävälle metodille sen aliluokan ilmentymän.
Tehdään seuraavaksi maatilasimulaattori, jossa simuloidaan maatilan elämää. Huomaa että ohjelmassa ei käytetä perintää, ja rajapintojenkin käyttö on melko vähäistä. Usein ohjelmat tehdäänkin niin että ensin toteutetaan yksi versio, jota lähdetään parantamaan myöhemmin. Tyypillistä on että ensimmäistä versiota toteutettaessa ongelma-aluetta ei vielä ymmärretä kunnolla, jolloin rajapintojen ja käsitehierarkioiden suunnittelu ennalta on hyvin vaikeaa ja saattaa jopa hidastaa työskentelyä.
Maatiloilla on lypsäviä eläimiä, jotka tuottavat maitoa. Maatilat eivät itse käsittele maitoa, vaan se kuljetetaan Maitoautoilla meijereille. Meijerit ovat yleisiä maitotuotteita tuottavia rakennuksia. Jokainen meijeri erikoistuu yhteen tuotetyyppiin, esimerkiksi Juustomeijeri tuottaa Juustoa, Voimeijeri tuottaa voita ja Maitomeijeri tuottaa maitoa.
Rakennetaan maidon elämää kuvaava simulaattori. Toteuta kaikki luokat pakkaukseen maatilasimulaattori
.
Jotta maito pysyisi tuoreena, täytyy se säilöä sille tarkoitettuun säiliöön. Säiliöitä valmistetaan sekä oletustilavuudella 2000 litraa, että asiakkaalle räätälöidyllä tilavuudella. Toteuta luokka Maitosailio jolla on seuraavat konstruktorit ja metodit.
Toteuta Maitosailio
-luokalle myös toString()
-metodi, jolla kuvaat sen tilaa.
Ilmaistessasi säiliön tilaa toString()
-metodissa, pyöristä litramäärät
ylöspäin käyttäen Math
-luokan tarjoamaa ceil()
-metodia.
Testaa maitosailiötä seuraavalla ohjelmapätkällä:
Maitosailio sailio = new Maitosailio(); sailio.otaSailiosta(100); sailio.lisaaSailioon(25); sailio.otaSailiosta(5); System.out.println(sailio); sailio = new Maitosailio(50); sailio.lisaaSailioon(100); System.out.println(sailio);
Ohjelman tulostuksen tulee olla seuraavankaltainen:
20.0/2000.0 50.0/50.0
Huomaa että kutsuttaessa System
-luokan out
-olioon liittyvää
println()
-metodia, joka saa parametrikseen Object
-tyyppisen
muuttujan, tulostus käyttää Maitosailio
-luokassa korvattua
toString()
-metodia! Tässä on kyse polymorfismista, eli
ajonaikaisesta käytettävien metodien päättelystä.
Saadaksemme maitoa tarvitsemme myös lehmiä. Lehmällä on nimi ja utareet. Utareiden tilavuus on satunnainen luku väliltä 15 ja 40, luokkaa Random
voi käyttäää satunnaislukujen arpomiseen, esimerkiksi int luku = 15 + new Random().nextInt(26);
. Luokalla Lehma
on seuraavat toiminnot:
Lehma
toteuttaa myös rajapinnat: Lypsava
, joka kuvaa lypsämiskäyttäytymistä, ja Eleleva
, joka kuvaa elelemiskäyttäytymistä.
public interface Lypsava { public double lypsa(); } public interface Eleleva { public void eleleTunti(); }
Lehmää lypsettäessä sen koko maitovarasto tyhjennetään jatkokäsittelyä varten. Lehmän elellessä sen maitovarasto täyttyy hiljalleen. Suomessa maidontuotannossa käytetyt lehmät tuottavat keskimäärin noin 25-30 litraa maitoa päivässä. Simuloidaan tätä tuotantoa tuottamalla noin 0.7 - 2 litraa tunnissa.
Jos lehmälle ei anneta nimeä, valitse sille nimi satunnaisesti seuraavasta listasta.
private static final String[] NIMIA = new String[]{ "Anu", "Arpa", "Essi", "Heluna", "Hely", "Hento", "Hilke", "Hilsu", "Hymy", "Ihq", "Ilme", "Ilo", "Jaana", "Jami", "Jatta", "Laku", "Liekki", "Mainikki", "Mella", "Mimmi", "Naatti", "Nina", "Nyytti", "Papu", "Pullukka", "Pulu", "Rima", "Soma", "Sylkki", "Valpu", "Virpi"};
Toteuta luokka Lehma ja testaa sen toimintaa seuraavan ohjelmapätkän avulla.
Lehma lehma = new Lehma(); System.out.println(lehma); Eleleva elelevaLehma = lehma; elelevaLehma.eleleTunti(); elelevaLehma.eleleTunti(); elelevaLehma.eleleTunti(); elelevaLehma.eleleTunti(); System.out.println(lehma); Lypsava lypsavaLehma = lehma; lypsavaLehma.lypsa(); System.out.println(lehma); System.out.println(""); lehma = new Lehma("Ammu"); System.out.println(lehma); lehma.eleleTunti(); lehma.eleleTunti(); System.out.println(lehma); lehma.lypsa(); System.out.println(lehma);
Ohjelman tulostus on erimerkiksi seuraavanlainen.
Liekki 0.0/23.0 Liekki 7.0/23.0 Liekki 0.0/23.0 Ammu 0.0/35.0 Ammu 9.0/35.0 Ammu 0.0/35.0
Nykyaikaisilla maatiloilla lypsyrobotit hoitavat lypsämisen. Jotta lypsyrobotti voi lypsää lypsävää otusta, tulee lypsyrobotin olla kiinnitetty maitosäiliöön:
null
-viitteen, jos säiliötä ei ole vielä kiinnitettyIllegalStateException
, jos säiliötä ei ole kiinnitetty Toteuta luokka Lypsyrobotti ja testaa sitä seuraavien ohjelmanpätkien avulla. Varmista että lypsyrobotti voi lypsää kaikkia Lypsava-rajapinnan toteuttavia olioita!
Lypsyrobotti lypsyrobotti = new Lypsyrobotti(); Lehma lehma = new Lehma(); lypsyrobotti.lypsa(lehma);
Exception in thread "main" java.lang.IllegalStateException: Maitosäiliötä ei ole asennettu at maatilasimulaattori.Lypsyrobotti.lypsa(Lypsyrobotti.java:17) at maatilasimulaattori.Main.main(Main.java:9) Java Result: 1
Lypsyrobotti lypsyrobotti = new Lypsyrobotti(); Lehma lehma = new Lehma(); System.out.println(""); Maitosailio sailio = new Maitosailio(); lypsyrobotti.setMaitosailio(sailio); System.out.println("Säiliö: " + sailio); for(int i = 0; i < 2; i++) { System.out.println(lehma); System.out.println("Elellään.."); for(int j = 0; j < 5; j++) { lehma.eleleTunti(); } System.out.println(lehma); System.out.println("Lypsetään..."); lypsyrobotti.lypsa(lehma); System.out.println("Säiliö: " + sailio); System.out.println(""); }
Ohjelman tulostus on esimerkiksi seuraavanlainen.
Säiliö: 0.0/2000.0 Mella 0.0/23.0 Elellään.. Mella 6.2/23.0 Lypsetään... Säiliö: 6.2/2000.0 Mella 0.0/23.0 Elellään.. Mella 7.8/23.0 Lypsetään... Säiliö: 14.0/2000.0
Lehmät hoidetaan (eli tässä tapauksessa lypsetään) navetassa.
Alkukantaisissa navetoissa on maitosäiliö ja tilaa yhdelle
lypsyrobotille. Huomaa että lypsyrobottia asennettaessa se kytketään
juuri kyseisen navetan maitosäiliöön. Jos navetassa ei ole
lypsyrobottia, ei siellä voida myöskään hoitaa lehmiä.
Toteuta luokka Navetta
jolla on seuraavat konstruktorit ja metodit:
IllegalStateException
, jos lypsyrobottia ei ole asennettuIllegalStateException
, jos lypsyrobottia ei ole asennettuCollection
on Javan oma rajapinta joka kuvaa kokoelmien käyttäytymistä. Esimerkiksi luokat ArrayList
ja LinkedList
toteuttavat rajapinnan Collection
. Jokaista Collection
-rajapinnan toteuttavaa ilmentymää voi myös iteroida for-each-tyyppisesti.
Testaa luokkaa Navetta
seuraavan ohjelmapätkän avulla.
Älä hermoile luokasta LinkedList
, se toimii ulkoapäin katsottuna
kuin ArrayList
, mutta sen kapseloima toteutus on hieman erilainen.
Tästä lisää tietorakenteet-kurssilla!
Navetta navetta = new Navetta(new Maitosailio()); System.out.println("Navetta: " + navetta); Lypsyrobotti robo = new Lypsyrobotti(); navetta.asennaLypsyrobotti(robo); Lehma ammu = new Lehma(); ammu.eleleTunti(); ammu.eleleTunti(); navetta.hoida(ammu); System.out.println("Navetta: " + navetta); LinkedList<Lehma> lehmaLista = new LinkedList(); lehmaLista.add(ammu); lehmaLista.add(new Lehma()); for(Lehma lehma: lehmaLista) { lehma.eleleTunti(); lehma.eleleTunti(); } navetta.hoida(lehmaLista); System.out.println("Navetta: " + navetta);
Tulostuksen tulee olla esimerkiksi seuraavanlainen:
Navetta: 0.0/2000.0 Navetta: 2.8/2000.0 Navetta: 9.6/2000.0
Maatilalla on omistaja ja siihen kuuluu navetta sekä joukko lehmiä.
Maatila toteuttaa myös aiemmin nähdyn rajapinnan Eleleva
,
jonka metodia eleleTunti()
-kutsumalla kaikki maatilaan
liittyvät lehmät elelevät tunnin. Toteuta luokka maatila siten,
että se toimii seuraavien esimerkkiohjelmien mukaisesti.
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); System.out.println(maatila); System.out.println(maatila.getOmistaja() + " on ahkera mies!");
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Ei lehmiä. Esko on ahkera mies!
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Lehmät: Naatti 0.0/19.0 Hilke 0.0/30.0 Sylkki 0.0/29.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.eleleTunti(); maatila.eleleTunti(); System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 0.0/2000.0 Lehmät: Heluna 2.0/17.0 Rima 3.0/32.0 Ilo 3.0/25.0
Maatila maatila = new Maatila("Esko", new Navetta(new Maitosailio())); Lypsyrobotti robo = new Lypsyrobotti(); maatila.asennaNavettaanLypsyrobotti(robo); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.lisaaLehma(new Lehma()); maatila.eleleTunti(); maatila.eleleTunti(); maatila.hoidaLehmat(); System.out.println(maatila);
Odotettu tulostus:
Maatilan omistaja: Esko Navetan maitosäiliö: 18.0/2000.0 Lehmät: Hilke 0.0/30.0 Sylkki 0.0/35.0 Hento 0.0/34.0
Abstrakti luokka yhdistää rajapintoja ja perintää. Niistä ei voi tehdä ilmentymiä, vaan ilmentymät tehdään tehdään abstraktin luokan aliluokista. Abstrakti luokka voi sisältää sekä normaaleja metodeja, joissa on metodirunko, että abstrakteja metodeja, jotka sisältävät ainoastaan metodimäärittelyn. Abstraktien metodien toteutus jätetään perivän luokan vastuulle. Yleisesti ajatellen abstrakteja luokkia käytetään esimerkiksi kun abstraktin luokan kuvaama käsite ei ole selkeä itsenäinen käsite. Tällöin siitä ei tule pystyä tekemään ilmentymiä.
Sekä abstraktin luokan että abstraktien metodien määrittelyssä käytetään avainsanaa abstract
. Abstrakti luokka määritellään lauseella public abstract class LuokanNimi
, abstrakti metodi taas lauseella public abstract palautustyyppi metodinNimi
. Pohditaan seuraavaa abstraktia luokkaa Toiminto
, joka tarjoaa rungon toiminnoille ja niiden suorittamiselle.
public abstract class Toiminto { private String nimi; public Toiminto(String nimi) { this.nimi = nimi; } public String getNimi() { return this.nimi; } public abstract void suorita(Scanner lukija); }
Abstrakti luokka Toiminto
toimii runkona erilaisten toimintojen toteuttamiseen. Esimerkiksi pluslaskun voi toteuttaa perimällä luokka Toiminto
seuraavasti.
public class Pluslasku extends Toiminto { public Pluslasku() { super("Pluslasku"); } @Override public void suorita(Scanner lukija) { System.out.print("Anna ensimmäinen luku: "); int eka = Integer.parseInt(lukija.nextLine()); System.out.print("Anna toinen luku: "); int toka = Integer.parseInt(lukija.nextLine()); System.out.println("Lukujen summa on " + (eka + toka)); } }
Koska kaikki Toiminto
-luokan perivät luokat ovat myös tyyppiä toiminto, voimme rakentaa käyttöliittymän Toiminto
-tyyppisten muuttujien varaan. Seuraava luokka Kayttoliittyma
sisaltaa listan toimintoja ja lukijan. Toimintoja voi lisätä käyttöliittymään dynaamisesti.
public class Kayttoliittyma { private Scanner lukija; private List<Toiminto> toiminnot; public Kayttoliittyma(Scanner lukija) { this.lukija = lukija; this.toiminnot = new ArrayList<Toiminto>(); } public void lisaaToiminto(Toiminto toiminto) { this.toiminnot.add(toiminto); } public void kaynnista() { while (true) { tulostaToiminnot(); System.out.println("Valinta: "); String valinta = this.lukija.nextLine(); if (valinta.equals("0")) { break; } suoritaToiminto(valinta); System.out.println(); } } private void tulostaToiminnot() { System.out.println("\t0: Lopeta"); for (int i = 0; i < this.toiminnot.size(); i++) { String toiminnonNimi = this.toiminnot.get(i).getNimi(); System.out.println("\t" + (i + 1) + ": " + toiminnonNimi); } } private void suoritaToiminto(String valinta) { int toiminto = Integer.parseInt(valinta); Toiminto valittu = this.toiminnot.get(toiminto - 1); valittu.suorita(lukija); } }
Käyttöliittymä toimii seuraavasti:
Kayttoliittyma kayttolittyma = new Kayttoliittyma(new Scanner(System.in)); kayttolittyma.lisaaToiminto(new Pluslasku()); kayttolittyma.kaynnista();
Toiminnot: 0: Lopeta 1: Pluslasku Valinta: 1 Anna ensimmäinen luku: 8 Anna toinen luku: 12 Lukujen summa on 20 Toiminnot: 0: Lopeta 1: Pluslasku Valinta: 0
Rajapintojen ja abstraktien luokkien ero on siinä, että abstraktit luokat tarjoavat enemmän rakennetta ohjelmaan. Koska abstrakteihin luokkiin voidaan määritellä toiminnallisuutta, voidaan niitä käyttää esimerkiksi oletustoiminnallisuuden määrittelyyn. Yllä käyttöliittymä käytti abstraktissa luokassa määriteltyä toiminnan nimen tallentamista.
Tehtäväpohjan mukana tulee luokat Tavara
ja Laatikko
. Luokka Laatikko
on abstrakti luokka, jossa useamman tavaran lisääminen on toteutettu siten, että kutsutaan aina lisaa
-metodia. Yhden tavaran lisäämiseen tarkoitettu metodi lisaa
on abstrakti, joten jokaisen Laatikko
-luokan perivän laatikon tulee toteuttaa se. Tehtävänäsi on muokata luokkaa Tavara
ja toteuttaa muutamia erilaisia laatikoita luokan Laatikko
pohjalta.
Lisää kaikki uudet luokat pakkaukseen laatikot
.
package laatikot; import java.util.Collection; public abstract class Laatikko { public abstract void lisaa(Tavara tavara); public void lisaa(Collection<Tavara> tavarat) { for (Tavara tavara : tavarat) { lisaa(tavara); } } public abstract boolean onkoLaatikossa(Tavara tavara); }
Lisää Tavara
-luokan konstruktoriin tarkistus, jossa tarkistetaan että tavaran paino ei ole koskaan negatiivinen (paino 0 hyväksytään). Jos paino on negatiivinen, tulee konstruktorin heittää IllegalArgumentException
-poikkeus. Toteuta Tavara
-luokalle myös metodit equals
ja hashCode
, joiden avulla pääset hyödyntämään erilaisten listojen ja kokoelmien contains
-metodia. Toteuta metodit siten, että Tavara-luokan oliomuuttujan paino
arvolla ei ole väliä. Voit hyvin hyödyntää NetBeansin tarjoamaa toiminnallisuutta equalsin ja hashCoden toteuttamiseen.
Toteuta pakkaukseen laatikot
luokka MaksimipainollinenLaatikko
, joka perii luokan Laatikko
. Maksimipainollisella laatikolla on konstruktori public MaksimipainollinenLaatikko(int maksimipaino)
, joka määrittelee laatikon maksimipainon. Maksimipainolliseen laatikkoon voi lisätä tavaraa jos ja vain jos tavaran lisääminen ei ylitä laatikon maksimipainoa.
MaksimipainollinenLaatikko kahviLaatikko = new MaksimipainollinenLaatikko(10); kahviLaatikko.lisaa(new Tavara("Saludo", 5)); kahviLaatikko.lisaa(new Tavara("Pirkka", 5)); kahviLaatikko.lisaa(new Tavara("Kopi Luwak", 5)); System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Saludo"))); System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Pirkka"))); System.out.println(kahviLaatikko.onkoLaatikossa(new Tavara("Kopi Luwak")));
true true false
Toteuta seuraavaksi pakkaukseen laatikot
luokka YhdenTavaranLaatikko
, joka perii luokan Laatikko
. Yhden tavaran laatikolla on konstruktori public YhdenTavaranLaatikko()
, ja siihen mahtuu tasan yksi tavara. Jos tavara on jo laatikossa sitä ei tule vaihtaa. Laatikkoon lisättävän tavaran painolla ei ole väliä.
YhdenTavaranLaatikko laatikko = new YhdenTavaranLaatikko(); laatikko.lisaa(new Tavara("Saludo", 5)); laatikko.lisaa(new Tavara("Pirkka", 5)); System.out.println(laatikko.onkoLaatikossa(new Tavara("Saludo"))); System.out.println(laatikko.onkoLaatikossa(new Tavara("Pirkka")));
true false
Toteuta seuraavaksi pakkaukseen laatikot
luokka HukkaavaLaatikko
, joka perii luokan Laatikko
. Hukkaavalla laatikolla on konstruktori public HukkaavaLaatikko()
. Hukkaavaan laatikkoon voi lisätä kaikki tavarat, mutta tavaroita ei löydy niitä etsittäessä. Laatikkoon lisäämisen tulee siis aina onnistua, mutta metodin onkoLaatikossa
kutsumisen tulee aina palauttaa false.
HukkaavaLaatikko laatikko = new HukkaavaLaatikko(); laatikko.lisaa(new Tavara("Saludo", 5)); laatikko.lisaa(new Tavara("Pirkka", 5)); System.out.println(laatikko.onkoLaatikossa(new Tavara("Saludo"))); System.out.println(laatikko.onkoLaatikossa(new Tavara("Pirkka")));
false false
Seuraavassa tehtävässä saatat joutua tilanteeseen, jossa haluat poistaa ArrayListiä läpikäydessäsi osan listan olioista:
// joissain määritelty seuraavasti: // ArrayList<Olio> lista = new ... for ( Olio olio : lista ) { if ( tarvitseePoistaa(olio) ) { lista.remove(olio); } }
Ratkaisu ei toimi ja aiheuttaa poikkeuksen ConcurrentModificationException
, sillä listaa foreach-tyylillä läpikäydessä listaa ei saa muokata. Palaamme aiheeseen hieman tarkemmin viikolla 612. Jos törmäät tilanteeseen, voit hoitaa sen esim. seuraavasti:
// joissain määritelty seuraavasti: // ArrayList<Olio> lista = new ... ArrayList<Olio> poistettavat = new ArrayList<Olio>(); for ( Olio olio : lista ) { if ( tarvitseePoistaa(olio) ) { poistattavat.add(olio); } } lista.removeAll(poistettavat);
Eli poistettavat oliot kerätään listan läpikäyntinä erilliselle listalle ja poisto-operaatio suoritetaan vasta listan läpikäynnin jälkeen.
Tämä tehtävä on neljän tehtäväpisteen arvoinen. Huom! Toteuta kaikki toiminnallisuus pakkaukseen luola
.
Huom: jotta testit toimisivat, ohjelmasi saa luoda vain yhden Scanner-olion. Älä käytä luokkien nimissä skandeja. Älä myöskään käytä staattisia muuttujia, testit suorittavat ohjelman useita kertoja joten staattisiin muuttujiin edellisillä suorituskerroilla jääneet arvot todennäköisesti häiritsevät testien toimintaa!
Tässä tehtävässä pääset toteuttamaan luolapelin. Pelissä pelaaja on luolassa hirviöitten kanssa. Pelaajan tehtävänä on ehtiä tallata kaikki hirviöt ennen kuin hänen lampustaan loppuu virta ja hirviöt pääsevät pimeän turvin syömään hänet. Pelaaja voi nähdä hirviöiden sijainnit välkäyttämällä lamppua, jonka jälkeen hänen on liikuttava sokkona ennen seuraavaa välkäytystä. Pelaaja voi kulkea monta askelta yhden siirron aikana.
Pelitilanne eli luola, pelaaja ja hirviöt esitetään pelaajalle tekstimuotoisesti. Tulostuksen ensimmäinen rivi kertoo jäljellä olevien siirtojen (eli lampun jäljellä olevan virran) määrän. Virran määrää seuraa pelaajan ja hirviöitten sijainnit, joiden jälkeen on pelitilanteesta piirretty kartta. Alla olevassa esimerkissä näet pelaajan (@
) ja kolme hirviötä (h
). Alla olevassa esimerkissä pelaajalla on virtaa neljääntoista siirtoon.
14 @ 1 2 h 6 1 h 7 3 h 12 2 ................. ......h.......... .@.........h..... .......h.........
Yllä olevassa esimerkissä virtaa on 14 välkäytykseen. Pelaaja @
sijatsee koordinaatissa 1 2
. Huomaa että koordinaatit lasketaan aina pelialueen vasemmasta ylälaidasta lähtien. Alla olevassa kartassa merkki X
on koordinaatissa 0 0
, Y
koordinaatissa 2 0
ja Z
koordinaatissa 0 2
.
X.Y.............. ................. Z................ .................
Käyttäjä voi liikkua antamalla sarjan komentoja ja painamalla rivinvaihtoa. Komennot ovat:
w
liiku ylöspäins
liiku alaspäina
liiku vasemmalled
liiku oikealleKun käyttäjän antamat komennot on suoritettu (niitä voi olla useampi), piirretään uusi pelitilanne. Lampun virta vähenee yhdellä aina kun uusi pelitilanne piirretään. Jos virta menee nollaan, peli loppuu ja ruudulle tulostetaan teksti HÄVISIT
Hirviöt liikkuvat pelissä satunnaisesti, yhden askeleen jokaista pelaajan askelta kohti. Jos pelaaja ja hirviö osuvat samaan ruutuun (vaikka vain tilapäisesti), hirviö tuhoutuu. Jos hirviö yrittää siirtyä pelilaudalta ulos tai ruutuun jossa on jo hirviö, jätetään siirto suorittamatta. Kun kaikki hirviöt on tuhottu, peli loppuu ja tulostetaan VOITIT
.
Testaamisen helpottamiseksi tee peliisi luokka Luola
, jolla on :
public Luola(int leveys, int korkeus, int hirvioita, int siirtoja, boolean hirviotLiikkuvat)
Luvut leveys
ja korkeus
antavat luolan koon (se on aina neliskulmainen), hirvioita
antaa hirviöiden lukumäärän alussa (hirviöiden sijainnin voi arpoa), siirtoja
antaa siirtojen lukumäärän alussa ja jos hirviotLiikkuvat
on false
, hirviöt eivät liiku.
public void run()
joka käynnistää pelinHuom! pelaajan tulee aloittaa sijainnista 0,0!
Huom! jos pelaaja tai hirviö koittaa liikkua ulos luolasta tai kaksi hirviötä koittaa liikkua samaan ruutuun, ei liikettä tule tapahtua!
Alla vielä selkeyden vuoksi vielä esimerkkipeli:
14 @ 0 0 h 1 2 h 7 8 h 7 5 h 8 0 h 2 9 @.......h. .......... .h........ .......... .......... .......h.. .......... .......... .......h.. ..h....... ssd 13 @ 1 2 h 8 8 h 7 4 h 8 3 h 1 8 .......... .......... .@........ ........h. .......h.. .......... .......... .......... .h......h. .......... ssss 12 @ 1 6 h 6 9 h 6 5 h 8 3 .......... .......... .......... ........h. .......... ......h... .@........ .......... .......... ......h... dd 11 @ 3 6 h 5 9 h 6 7 h 8 1 .......... ........h. .......... .......... .......... .......... ...@...... ......h... .......... .....h.... ddds 10 @ 6 7 h 6 6 h 5 0 .....h.... .......... .......... .......... .......... .......... ......h... ......@... .......... .......... w 9 @ 6 6 h 4 0 ....h..... .......... .......... .......... .......... .......... ......@... .......... .......... .......... www 8 @ 6 3 h 4 0 ....h..... .......... .......... ......@... .......... .......... .......... .......... .......... .......... aa 7 @ 4 3 h 4 2 .......... .......... ....h..... ....@..... .......... .......... .......... .......... .......... .......... w VOITIT