Matti Paksula, Arto Vihavainen, Matti Luukkainen, Antti Laaksonen, Pekka Mikkola, Juhana Laurinharju, Martin Pärtel
Tämä on suoraa jatkoa kevään 2011 Ohjelmoinnin perusteet -kurssin materiaaliin. Kurssi alkaa siitä mihin OhPe loppui ja oikeastaan kaikki OpPe:ssa opitut asiat oletetaan nyt osattavan. Saattaakin olla hyvä kerrata tässä vaiheessa Ohjelmoinnin perusteiden materiaalista ainakin luvut 20 ja 23-27.
Kerrataan ja täsmennetään vielä Ohjelmoinnin perusteiden luvuissa 16 ja 23.16 jo käsiteltyä asiaa.
Ehkä kannattaa lukea ensin Ohjelmoinnin perusteiden materiaalista vastaavat kohdat.
Javassa on kaksi erilaista muuttujatyyppiä, alkeistyyppiset muuttujat ja viittaustyyppiset muuttujat. Alkeistyyppisillä muuttujilla tallennetaan tietoa yksittäisiin lokeroihin, kun taas viittaustyyppiset muuttujat sisältävät viitteen, joka osoittaa tietoon.
Alkeistyyppiset muuttujat tallentavat arvonsa omaan lokeroon. Uutta alkeistyyppistä muuttujaa alustettaessa
luodaan aina uusi lokero. Alkeistyyppiset muuttujat alustetaan sijoitusoperaatiolla =
. Katsotaan esimerkkiä.
int vitonen = 5; int kutonen = 6;
Esimerkki luo kaksi alkeistyyppistä muuttujaa, nimiltään vitonen
ja kutonen
. Muuttujan vitonen
lokeroon asetetaan arvo 5, ja muuttujan kutonen
lokeroon arvo 6. Kaikki alkeistyyppiset muuttujat, kuten javan
kaikki muutkin muuttujat, ovat tietyn tyyppisiä. Muuttujat vitonen
ja kutonen
ovat kumpikin int-tyyppisiä,
eli kokonaislukuja. Kuvana alkeistyyppiset muuttujat kannattaa ajatella laatikkoina jonka sisällä muuttujan arvo on talletettuna:
----- viitonen | 5 | ----- ----- kuutonen | 6 | -----
Tarkastellaan vielä alkeistyyppisten muuttujien asettamista toisen muuttujan avulla.
int vitonen = 5; int kutonen = 6; vitonen = kutonen; // muuttuja vitonen sisältää nyt arvon 6, eli arvon joka oli muuttujassa kutonen kutonen = 42; // muuttuja kutonen sisältää nyt arvon 42 // muuttuja vitonen sisältää vieläkin arvon 6
Esimerkissä alustetaan ensiksi muuttujat vitonen
ja kutonen
. Tämän jälkeen muuttujan
vitonen
lokeroon asetetaan muuttujan kutonen
lokeron sisältämä arvo. Tässä vaiheessa siis muuttujan
vitonen
lokeroon tallentuu muuttujan kutonen
sisältämä arvo. Jos muuttujan kutonen
arvoa
muutetaan tämän jälkeen, ei muuttujan vitonen
sisältämä arvo muutu. Lopputilanne kuvana
------ viitonen | 6 | ------ ------ kuutonen | 42 | ------
Kun alkeistyyppinen muuttuja annetaan metodille parametrina, saa metodin
parametrimuuttuja kopion annetun muuttujan arvosta. Katsotaan seuraavaa metodia
lisaaLukuun(int luku, int paljonko)
.
public int lisaaLukuun(int luku, int paljonko) { return (luku + paljonko); }
Metodi lisaaLukuun()
saa kaksi parametria, kokonaisluvut
luku
ja paljonko
. Metodi palauttaa uuden luvun, joka on
annettujen parametrien summa. Tutkitaan vielä metodin kutsumista.
int omaLuku = 10; omaLuku = lisaaLukuun(omaLuku, 15); // muuttuja omaLuku sisältää nyt arvon 25
Esimerkissä kutsutaan lisaaLukuun()
-metodia muuttujalla
omaLuku
ja arvolla 15
. Metodin muuttujiin luku
ja paljonko
kopioituvat siis arvot 10, eli muuttujan
omaLuku
sisältö, ja 15. Metodi palauttaa muuttujien luku
ja
paljonko
summan, eli 10 + 15 = 25
.
Eri tietotyypeillä on omat minimi- ja maksimiarvonsa, eli arvot joita pienempiä tai suurempia ne eivät voi olla. Tämä johtuu Javan (ja muidenkin useimpien ohjelmointikielten) sisäisestä tiedon esitysmuodosta, jossa tietotyyppien koot on ennalta määrätty.
Alla vielä muutama Javan alkeistyyppi ja niiden minimi- ja maksimiarvot
Muuttujatyyppi | Selitys | Minimiarvo | Maksimiarvo |
---|---|---|---|
int | Kokonaisluku | -2 147 483 648 (Integer.MIN_VALUE ) | 2 147 483 647 (Integer.MAX_VALUE ) |
long | Iso kokonaisluku | -9 223 372 036 854 775 808 (Long.MIN_VALUE ) | 9 223 372 036 854 775 807 (Long.MAX_VALUE ) |
boolean | Totuusarvo | true tai false | |
double | Liukuluku | Double.MIN_VALUE | Double.MAX_VALUE |
Liukulukuja käyttäessä kannattaa muistaa että liukuluvun arvo on aina arvio oikeasta arvosta. Koska liukuluvun, kuten kaikkien muidenkin alkeistyyppien sisältämä tietomäärä on rajoitettu, voidaan huomata yllättäviäkin pyöristysvirheitä. Esimerkiksi seuraava tilanne.
double eka = 0.39; double toka = 0.35; System.out.println(eka - toka);
Esimerkki tulostaa arvon
0.040000000000000036
. Pyöristysvirheisiin varaudutaan usein muunmuassa
vertaamalla arvon kuulumista tiettyyn arvoväliin. Ohjelmointikielet tarjoavat usein
työkalut vastaavien tilanteiden välttämiseen, esimerkiksi Javassa on olemassa luokka
BigDecimal liukulukujen tarkempaa laskemista varten.
Viittaustyyppiset muuttujat tallentavat niihin liittyvän tiedon viitteen taakse eli "langan päähän", ja itse muuttuja toimii vain viitteenä tiedon sisältävään paikkaan. Toisin kuin alkeistyyppisillä muuttujilla, viittaustyyppisillä muuttujilla ei ole rajoitettua arvoaluetta, koska niiden oikea arvo tai tieto on viitteen takana.
Viittaustyyppisistä muuttujista puhutaan olioina, ja ne luodaan
new
-kutsulla. Muuttujan arvo asetetaan vieläkin sijoitusoperaattorilla
=, mutta komento new
luo olion ja palauttaa viitteen olioon. Tämä viite
asetetaan muuttujan arvoksi. Katsotaan kahden viittaustyyppisen muuttujan
luontia. Esimerkeissä käytetään luokkaa Laskuri:
public class Laskuri { int arvo; public Laskuri(int alkuarvo) { // Konstruktori arvo = alkuarvo; } public void kasvataArvoa() { arvo = arvo + 1; } public int annaArvo() { return arvo; } }
Pääohjelma:
Laskuri matinLaskuri = new Laskuri(5); Laskuri artonLaskuri = new Laskuri(3);
Esimerkissä luodaan ensiksi viittaustyyppinen muuttuja
matinLaskuri
. Komentoa new
kutsuessa viitteen taakse
varataan tila muuttujan tiedolle, luodaan Laskuri-tyyppinen olio, ja palautetaan
viite siihen. Palautettu viite asetetaan sijoitusoperaattorilla =
muuttujaan matinLaskuri
. Sama tapahtuu muuttujalle nimeltä
artonLaskuri
. Kuvana viittaustyyppi kannattaa ajatella siten, että
muuttuja sisältää "langan" tai "nuolen", jonka päässä on olio itse. Muuttuja siis ei
säilytä olioa vaan tiedon eli viitteen sinne missä olio on.
----- --olio---- matinLaskuri | --|----------> | arvo 5 | ----- ---------- ----- --olio---- artonLaskuri | --|----------> | arvo 3 | ----- ----------
Katsotaan seuraavaksi viittaustyyppisen muuttujan asettamista toisen muuttujan avulla.
Laskuri matinLaskuri = new Laskuri(5); Laskuri artonLaskuri = new Laskuri(3); matinLaskuri = artonLaskuri; // muuttuja matinLaskuri sisältää nyt muuttujan artonLaskuri sisältämän viitteen, // eli viitteen Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 3 artonLaskuri = new Laskuri(10); // muuttujaan artonLaskuri asetetaan uusi viite, joka osoittaa // new Laskuri(10) - kutsulla luotuun Laskuri-olioon // muuttuja matinLaskuri sisältää vieläkin viitteen Laskuri-olioon, joka sai konstruktorissaan arvon 3
Esimerkissä tehdään käytännössä samat operaatiot kuin alkeistyyppi-kappaleessa olevassa asetusesimerkissä. Äskeisessä esimerkissä asetimme viittaustyyppisten muuttujien viitteitä, kun taas alkeistyyppi-esimerkissä asetimme alkeistyyppien arvoja. Lopussa kukaan ei viittaa Laskuriolioon, joka sai arvokseen konstruktorissa 5. Javan roskienkeruu huolehtii tälläisistä turhista oliosta. Lopputilanne uvana:
----- --olio---- matinLaskuri | --|-- | arvo 5 | tämä olio on muuttunut roskaksi, jonka Java pian hävittää ----- -- ---------- --- -- --olio---- ----- --> | arvo 3 | artonLaskuri | --|-- ---------- ----- -- --- -- --olio----- --> | arvo 10 | -----------
Tarkastellaan vielä kolmatta esimerkkiä, joka näyttää viite- ja alkeistyyppisten muuttujien konkreettisen eron.
Laskuri matinLaskuri = new Laskuri(5); Laskuri artonLaskuri = new Laskuri(3); matinLaskuri = artonLaskuri; // muuttuja matinLaskuri sisältää nyt muuttujan artonLaskuri sisältämän viitteen, // eli viitteen Laskuri-tyyppiseen olioon joka on saanut konstruktorissaan arvon 3 artonLaskuri.kasvataArvoa(); // kasvatetaan artonLaskuri-viitteen takana olevan olion arvoa yhdellä System.out.println(matinLaskuri.annaArvo()); // koska matinLaskuri-muuttujan viite osoittaa samaan olioon, kuin // artonLaskuri-muuttuja, on matinLaskuri.annaArvo() - kutsun palauttama arvo 4
Viittaustyyppiset muuttujat siis viittaavat aina toisaalla oleviin
olioihin. Useat viittaustyyppiset muuttujat voivat sisältää saman viitteen, jolloin
kaikki muuttujat osoittavat samaan olioon. Seuraavassa esimerkissä näemme kolme
Laskuri
-tyyppistä viitemuuttujaa, mutta vain yhden
Laskuri
-olion.
Laskuri mattiV = new Laskuri(5); Laskuri mattiL = mattiV; Laskuri mattiP = mattiV;
Esimerkissä luodaan vain yksi Laskuri
-olio, mutta kaikki
Laskuri
-tyyppiset viitemuuttujat osoittavat lopulta siihen. Tällöin
kaikki metodikutsut viitteelle mattiL
, mattiP
ja
mattiV
muokkaavat samaa viitteen takana olevaa oliota. Viittaustyyppisiä
muuttujia asetettaessa toisten muuttujien avulla viitteet siis kopioituvat
viitemuuttujan lokeroon. Kuvana:
----- mattiV | --|--- ----- --- ---- ----- ----> --olio---- mattiL | --|--------------> | arvo 5 | ----- ----> ---------- ---- ----- --- MattiP | --|--- -----
Katsotaan kopioitumista vielä esimerkillä.
Laskuri mattiV = new Laskuri(5); Laskuri mattiP = mattiV; // muuttuja mattiP saa arvokseen mattiV-muuttujan sisältämän viitteen Laskuri mattiL = mattiP; // muuttuja mattiL saa arvokseen mattiP-muuttujan sisältämän viitteen mattiP = new Laskuri(3); // muuttuja mattiP saa arvokseen uuden viitteen, joka palautuu new Laskuri(3) - kutsusta
Esimerkissä muuttujan mattiL
sisältö ei muutu muuttujan
mattiP
saadessa uuden viitteen, sillä muuttujaan mattiL
on
luotu asetuksessa mattiL = mattiP
kopio muuttujan mattiP
sisällöstä, eli viitteestä. Kun muuttujan mattiP
sisältö, eli viite
muuttuu, se ei vaikuta muuttujan mattiL
sisältämään viitteeseen koska se
on asetettu jo aiemmin. Kuvana:
----- mattiV | --|--- ----- --- ---- ----- ----> --olio---- mattiL | --|--------------> | arvo 5 | ----- ---------- ----- --olio---- MattiP | --|--------------> | arvo 3 | ----- ----------
Kun viittaustyyppinen muuttuja annetaan metodille parametrina, saa metodin
parametrimuuttuja kopion annetun muuttujan viitteestä. Katsotaan seuraavaa metodia
lisaaLaskuriin(Laskuri laskuri, int paljonko)
.
public void lisaaLaskuriin(Laskuri laskuri, int paljonko) { for (int i = 0; i < paljonko; i++) { laskuri.kasvataArvoa(); } }
Metodi lisaaLaskuriin()
saa kaksi parametria, viittaustyyppisen
parametrin laskuri
ja alkeistyyppisen (kokonaisluvun)
paljonko
. Metodi kutsuu Laskuri
-tyyppisen parametrin
metodia kasvataArvoa()
paljonko
-muuttujan sisältämän arvon
verran. Tutkitaan vielä metodin kutsumista.
int x = 10; Laskuri mattiL = new Laskuri(10); lisaaLaskuriin(mattiL, x); // muuttujan mattiL sisäinen arvo on nyt 20
Esimerkissä kutsutaan lisaaLaskuriin()
-metodia muuttujalla
mattiL
ja muuttujalla x
jonka arvo on 19. Metodin
muuttujiin laskuri
ja paljonko
kopioituvat siis
viittaustyyppisen muuttujan mattiL
viite, ja arvo 10. Metodi suorittaa
viitteelle laskuri
paljonko
muuttujan määrittelemän määrän
kasvataArvoa()
-metodikutsuja. Kuvana:
main: metodissa: ----- --olio----- ----- mattiL | --|---------> | arvo 10 | <-------------|-- | laskuri ----- ----------- ----- ------ ------ x | 10 | | 10 | paljonko ------ ------
Metodi siis näkee saman laskurin johon mattiL viittaa, eli metodin tekemä muutos
vaikuttaa suoraan parametrina olevaan olioon. Alkeistyyppien suhteen tilanne on
toinen, eli metodille tulee ainoastaan kopio x
:n arvosta.
Kun metodi palauttaa viittaustyyppisen muuttujan, palauttaa se viitteen
muualla sijaitsevaan olioon. Metodin palauttaman viittaustyyppisen muuttujan voi
asettaa muuttujalle samalla tavalla kuin normaalikin asetus tapahtuu, eli
yhtäsuuruusmerkin avulla. Katsotaan metodia luoLaskuri()
, joka luo uuden
viittaustyyppisen muuttujan.
public Laskuri luoLaskuri(int alkuarvo) { Laskuri uusiLaskuri = new Laskuri(alkuarvo); return uusiLaskuri; }
Metodi luoLaskuri palauttaa siis metodissa luotuun olioon viittaavan viitteen
uusiLaskuri
. Uusi olio luodaan aina metodia kutsuttaessa, seuraavassa
esimerkissä luomme kaksi erillistä Laskuri
-tyyppistä oliota.
Laskuri mattiV = luoLaskuri(10); Laskuri mattiP = luoLaskuri(10);
Metodi luoLaskuri
luo aina uuden Laskuri
-tyyppisen
olion. Ensimmäisessä kutsussa, eli kutsussa Laskuri mattiV =
luoLaskuri(10);
asetetaan metodin palauttama viite viittaustyyppiseen
muuttujaan mattiV
. Toisessa metodikutsussa luodaan uusi viite, joka
asetetaan muuttujaan mattiP
. Muuttujat mattiV
ja
mattiP
eivät sisällä samaa viitettä, sillä metodi luo aina uuden olion
ja palauttaa viitteen juuri luotuun olioon.
Kerrataan ja täsmennetään Ohjelmoinnin perusteiden luvussa 26 jo käsiteltyä asiaa.
Staattisilla ja ei-staattisilla metodeilla ja muuttujilla erotetaan se, mihin muuttuja tai metodi liittyy. Staattiset metodit ja muuttujat liittyvät luokkaan, kun taas ei-staattiset metodit ja muuttujat ovat oliokohtaisia.
Static-määreen saavat muuttujat eivät liity olioihin vaan
luokkiin. Esimerkiksi Integer.MAX_VALUE
, Long.MIN_VALUE
ja
Double.MAX_VALUE
ovat kaikki staattisia muuttujia. Staattisia muuttujia
ja metodeja käytetään luokan nimen kautta, esimerkiksi
LuokanNimi.muuttuja
tai LuokanNimi.metodi()
, tietysti
riippuen metodien ja muuttujien näkyvyydestä. Vain public-näkyvyydellä määritetyt
muuttujat ja metodit ovat suoraan käytettävissä.
Luokkakirjastoksi kutsutaan luokkaa, jossa on yleiskäyttöisiä metodeja ja
muuttujia. Esimerkiksi Javan Math
-luokka on
sellainen. Math
-luokkahan tarjoaa muunmuassa
Math.PI
-muuttujan, jossa on piin likiarvo, sekä
Math.random()
-metodin, joka palauttaa satunnaisen liukuluvun väliltä 0
ja 1. Omien luokkakirjastojen toteuttaminen on usein hyödyllistä. Esimerkiksi
Helsingin Seudun Liikenne (HSL) voisi pitää lippujensa hintoja luokkakirjastossa,
josta ne löytyisi aina tarvittaessa.
public class HslHinnasto { public static double KERTALIPPU_AIKUINEN = 2.50; public static double RAITIOVAUNULIPPU_AIKUINEN = 2.50; }
Tällöin kaikki ohjelmat, jotka käyttävät kerta- tai raitiovaunulipun hintaa
voivat käyttää niitä HslHinnasto
-luokan kautta. Seuraavassa esimerkissä
esitellään yksinkertainen Ihminen
-luokka, jolla on metodi
onkoRahaaKertalippuun()
, joka käyttää HslHinnasto
-luokasta
löytyvää lipun hintaa.
public class Ihminen { private double rahat; // rahat ... public boolean onkoRahaaKertalippuun() { if(rahat >= HslHinnasto.KERTALIPPU_AIKUINEN) { return true; } return false; } ... }
Metodi onkoRahaaKertalippuun()
siis vertaa luokan
Ihminen
ei-staattista muuttujaa rahat
HslHinnasto
-luokan staattiseen muuttujaan
KERTALIPPU_AIKUINEN
. Metodia onkoRahaaKertalippuun()
voi
kutsua vain olion yhteydessä, koska se ei ole staattinen.
Huomaa nimeämiskäytäntö! Kaikki staattiset alkeistyyppiset muuttujat
kirjoitetaan ISOLLA_JA_ALAVIIVOILLA. Staattiset metodit toimivat
vastaavasti. Esimerkiksi Luokka HslHinnasto
saattaisi
kapseloida muuttujat ja antaa vain aksessorit niihin. Aksessoriksi
kutsutaan metodia, jolla voi joko lukea muuttujan arvon tai sijoittaa muuttujalle
uuden arvon.
public class HslHinnasto { private static double KERTALIPPU_AIKUINEN = 2.50; private static double RAITIOVAUNULIPPU_AIKUINEN = 2.50; public static double annaKertalipunHinta() { // Aksessori return KERTALIPPU_AIKUINEN; } public static double annaRaitiovaunulipunHinta() { // Aksessori return RAITIOVAUNULIPPU_AIKUINEN; } }
Tällöin Ihminen
-luokan toteutuksessa täytyisikin kutsua
metodiaannaKertalipunHinta()
sen sijaan että kutsuttaisiin muuttujaa
suoraan.
public class Ihminen { private double rahat; // rahat ... public boolean onkoRahaaKertalippuun() { if(rahat >= HslHinnasto.annaKertalipunHinta()) { return true; } return false; } ... }
Ei-staattiset metodit ja muuttujat liittyvät olioihin. Oliomuuttujat, eli
attribuutit, määritellään luokan alussa. Kun oliota luodaan
new
-kutsulla, kaikki oliomuuttujat saavat arvon olion liittyvän viitteen
päässä, jolloin niihin pääsee käsiksi oliokohtaisesti. Esimerkiksi taas
yksinkertainen luokka Ihminen
, jolla on kaksi oliomuuttujaa:
nimi
ja rahat
.
public class Ihminen { private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo private double rahat; ... }
Kun luokasta Ihminen luodaan uusi ilmentymä, alustetaan myös siihen
liittyvät muuttujat. Jos viittaustyyppistä muuttujaa nimi
ei
alusteta, saa se arvokseen null-viitteen. Lisätään luokan
Ihminen toteutukseen vielä konstruktori ja muutama metodi.
public class Ihminen { private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo private double rahat; // konstruktori public Ihminen(String nimi, double rahat) { this.nimi = nimi; this.rahat = rahat; } // annaNimi public String annaNimi() { return this.nimi; } // annaRahat public double annaRahat() { return this.rahat; } // lisaaRahaa, lisätään vain jos yritetään lisätä positiivinen määrä public void lisaaRahaa(double summa) { if(summa > 0) { this.rahat += summa; } } ... }
Konstruktori Ihminen(String nimi, double rahat)
luo uuden
ihmisolion ja palauttaa viitteen siihen. Aksessori annaNimi()
palauttaa viitteen nimi
-olioon, ja annaRahat()
-metodi
palauttaa alkeistyyppisen muuttujan rahat
. Metodi
lisaaRahaa(double summa)
lisaa oliomuuttujaan rahat
parametrina annetun summan jos parametrin arvo on suurempi kuin 0.
Oliometodeja kutsutaan olion viitteen kautta. Seuraava koodiesimerkki luo
uuden Ihmis-olion, lisää sille rahaa, ja lopuksi tulostaa sen nimen. Huomaa että
metodikutsut ovat muotoa olionNimi.metodinNimi()
Ihminen mattiV = new Ihminen("Matti V", 3.0); mattiV.lisaaRahaa(5); // palkka, jes! System.out.println(mattiV.annaNimi());
Esimerkki tulostaa "Matti V
".
Luokan sisäisiä ei-staattisia metodeja voi tietysti kutsua myös ilman
olio-etuliitettä. Esimerkiksi seuraava toString()
-metodi
Ihminen
luokalle, joka kutsuu metodia
annaNimi()
. Metodi toString()
:han mahdollistaa olion
tilan tulostamisen vain olion nimeä parametrina käyttäen.
public class Ihminen { private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo ... public String annaNimi() { return this.nimi; } ... public String toString() { return annaNimi(); } }
Metodi toString()
kutsuu siis luokan sisäistä, tähän olioon
liittyvää annaNimi()
-metodia. Metodikutsuun voi lisätä etuliitteen
this
jos haluaa korostaa kutsun liittyvän juuri tähän
ilmentymään.
public class Ihminen { private String nimi; // oliomuuttujia, jokaiselle näistä on oliokohtainen arvo ... public String annaNimi() { return this.nimi; } ... public String toString() { return this.annaNimi(); } }
Ei-staattiset metodit voivat kutsua myös staattisia, eli luokkakohtaisia metodeja. Toisaalta, luokkakohtaiset metodit eivät voi kutsua oliokohtaisia metodeja ilman viitettä itse olioon, sillä ilman viitettä ei ole tietoa oliosta.
Poikkeuksena "Ei staattiset metodit ja muuttujat liittyvät olioihin" -sääntöön
on metodien sisällä määriteltävät muuttujat. Metodien sisällä määriteltävät
muuttujat eivät saa static-määrettä (eivätkä muitakaan määreitä tyyppinsä
lisäksi). Metodien sisällä määriteltävät muuttujat ovat metodien suorituksessa
käytettäviä apumuuttujia, eikä niitä tule sekoittaa oliomuuttujiin. Alla esimerkki metodista, jossa luodaan metodiin paikallinen muuttuja. Muuttuja
indeksi
on olemassa ja käytössä vain metodin suorituksen ajan.
public class ... { ... public static void tulostaTaulukko(String[] taulukko) { int indeksi = 0; while(indeksi < taulukko.length) { System.out.println(taulukko[indeksi]); indeksi++; } } ... }
Metodissa tulostaTaulukko()
luodaan siis metodin sisäinen
apumuuttuja indeksi
, jota käytetään taulukon läpikäynnissä
avuksi. Muuttuja indeksi
on käytössä vain metodin suorituksen
ajan.
Nyt on korkea aika alottaa ohjelmointi
Hajautustaulu
on yksi Javan yleishyödyllisistä tietorakenteista. Hajautustaulun ideana on
laskea oliota kuvaavalle avaimelle, esimerkiksi ihmisen nimi, yksilöivä
arvo. Tätä yksilöivää arvoa voidaan käyttää taulukon indeksinä, johon olion viite
tallennetaan. Kun hajautustaulusta haetaan avaimen perusteella, löydetään suoraan
taulun indeksi jossa olioviite on. Javan luokka HashMap
kapseloi
hajautustaulun toteutuksen, ja tarjoaa valmiit metodit sen käyttöön.
Hajautustaulu ottaa kaksi tyyppiparametria, avaimen tyypin ja tallennettavan
olion tyypin. Seuraava esimerkki käyttää avaimena Integer
-tyyppistä
oliota, ja oliona String
-tyyppistä oliota.
HashMap<Integer, String> numerot = new HashMap<Integer, String>(); numerot.put(1, "Yksi"); numerot.put(2, "Kaksi"); String merkkijono = numerot.get(1); System.out.println(merkkijono); merkkijono = numerot.get(42); System.out.println(merkkijono);
Esimerkissä siis luodaan hajatustaulu, jonka avaimena on kokonaisluku (huomaa
että avaimet ovat myös aina viitetyyppisiä muuttujia), ja tallennettavana oliona
merkkijono. Hajautustauluun lisätään tietoa put()
-metodilla, joka
ottaa parametreikseen viitteet avaimeen ja tallennettavaan olioon. Metodi
get()
-palauttaa annettuun avaimeen liittyvän viitteen.
Koska hajautustauluun ei ole lisättyä oliota avaimelle 42
,
palauttaa get()
-metodi avaimelle 42
null
-viitteen. Esimerkin tulostus on siis seuraavanlainen.
Yksi null
Hajautustaulussa tietty avain osoittaa aina tiettyyn paikkaan, jolloin avain ei voi osoittaa kahteen eri olioon samalla aikaa. Jos samalla avaimelle tallennetaan uusi olio, poistuu vanhan olion viite hajautustaulusta.
HashMap<Integer, String> numerot = new HashMap<Integer, String>(); numerot.put(1, "Yksi"); numerot.put(2, "Kaksi"); numerot.put(1, "Iiso yksi!"); String merkkijono = numerot.get(1); System.out.println(merkkijono); merkkijono = numerot.get(42); System.out.println(merkkijono);
Koska avain 1
asetetaan uudestaan, on yllä olevan esimerkin
tulostus seuraavanlainen.
Iiso yksi! null
Tutkitaan hajautustaulun toimintaa yksinkertaisen kirjastoesimerkin
avulla. Kirjastosta voi hakea kirjoja kirjan nimellä, nimi toimii siis
avaimena. Jos annetulle nimelle löytyy kirja, saadaan siihen liittyvä viite
ja samalla kirjan tiedot. Toteutetaan ensiksi esimerkkiluokka Kirja
,
jolla on oliomuuttujina nimi ja sisältö.
public class Kirja { private String nimi; private String sisalto; public Kirja() { } public Kirja(String nimi, String sisalto) { this.nimi = nimi; this.sisalto = sisalto; } public String getNimi() { return nimi; } public void setNimi(String nimi) { this.nimi = nimi; } public String getSisalto() { return sisalto; } public void setSisalto(String sisalto) { this.sisalto = sisalto; } public String toString() { String palautus = "Nimi: " + this.nimi + "\n" + "Sisältö: " + this.sisalto; return palautus; } }
Luodaan seuraavaksi hajautustaulu, joka ottaa avaimekseen kirjan nimen, eli
String-tyyppisen olion, ja tallettaa viitteitä Kirja
-olioihin.
HashMap<String, Kirja> kirjahakemisto = new HashMap<String, Kirja>();
Yllä oleva hajautustaulu siis käyttää avaimena
String
-oliota. Hajautustaulu käyttää avaimen arvon laskemiseen
Object
-luokassa määriteltyä hashCode()
-metodia, jonka
perivän luokat voivat ylikirjoittaa. Emme kuitenkaan tutustu hajautustaulun
toteutukseen tarkemmin tällä kurssilla.
Laajennetaan esimerkkiä siten, että kirjahakemistoon lisätään kaksi kirjaa,
Matti L:n Matkat
ja Matti P:n Tarinat
.
Kirja mattiLMatkat = new Kirja(); mattiLMatkat.setNimi("Matti L:n Matkat"); mattiLMatkat.setSisalto("Eräänä rauhallisena iltana Saksassa, poliisi lähestyi tuttavallisesti minua.."); Kirja mattiPTarinat = new Kirja(); mattiPTarinat.setNimi("Matti P:n Tarinat"); mattiPTarinat.setSisalto("Palatessani eräältä lomalta ikkuna oli säpäleinä ja kodistani oli siivottu vanha elektroniikka.."); HashMap<String, Kirja> kirjahakemisto = new HashMap<String, Kirja>(); kirjahakemisto.put(mattiLMatkat.getNimi(), mattiLMatkat); kirjahakemisto.put(mattiPTarinat.getNimi(), mattiPTarinat);
Nyt kirjahakemistosta voi hakea kirjan nimellä kirjoja. Seuraavan esimerkin
ensimmäinen haku ei tuota osumaa, ja hajautustaulu palauttaa
null
-viitteen. Kirja "Matti L:n Matkat" kuitenkin löytyy.
Kirja k = kirjahakemisto.get("Matti V:n Jorinat"); System.out.println(k); System.out.println(); k = kirjahakemisto.get("Matti L:n Matkat"); System.out.println(k);
null Nimi: Matti L:n Matkat Sisältö: Eräänä rauhallisena iltana Saksassa, poliisi lähestyi tuttavallisesti minua..
Hajautustaulu on hyödyllinen silloin kun tiedetään millaista tietoa halutaan hakea. Avaimet ovat aina yksilöllisiä, joten saman avaimen taakse ei voi tallettaa montaa eri oliota. Tallennettava olio voi toki olla lista tai toinen hajautustaulukko!
Yllä olevan kirjahakemiston ongelmana on se, että kirjoja haettaessa täytyy
muistaa kirjan nimi merkki merkiltä oikein. Javan valmis
String
-luokka tarjoaa meille välineet tähänkin. Metodi
toLowerCase()
muuttaa merkkijonon kirjaimet pieniksi, ja metodi
trim()
poistaa merkkijonon alusta ja lopusta tyhjät merkit
(esimerkiksi välilyönnit).
String teksti = " JEEEEEEEeeeEE"; teksti = teksti.toLowerCase(); // teksti nyt " jeeeeeeeeeeee" teksti = teksti.trim() // teksti nyt "jeeeeeeeeeeee"
Luodaan luokka Kirjasto
, joka kapseloi hajautustaulun siten, että
avaimien kirjainkoolla ei ole väliä. Lisätään Kirjasto
-luokalle myös
metodit lisaaKirja(Kirja kirja)
ja poistaKirja(String
kirjanNimi)
.
public class Kirjasto { private HashMap<String, Kirja> hakemisto; public Kirjasto() { hakemisto = new HashMap<String, Kirja>(); } public void lisaaKirja(Kirja kirja) { String avainNimi = kirja.getNimi(); avainNimi = avainNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi avainNimi = avainNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta if(hakemisto.containsKey(avainNimi)) { System.out.println("Kirja on jo kirjastossa!"); } else { hakemisto.put(avainNimi, kirja); } } public void poistaKirja(String kirjanNimi) { kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta if(hakemisto.containsKey(kirjanNimi)) { Kirja poistettava = hakemisto.get(kirjanNimi); hakemisto.remove(kirjanNimi); } else { System.out.println("Kirjaa ei löydy, ei voida poistaa!"); } } }
Yllä olevasta kirjastosta puuttuu vielä lista kaikista kirjoista, mikä on
kirjastoille hyvin tärkeää. Käytetään tuttua luokkaa ArrayList
kirjojen listaamiseen. Joudumme myös muuttamaan kirjojen lisäystä ja poistoa
siten, että viitteet poistetaan kummastakin tietorakenteesta.
public class Kirjasto { private HashMap<String, Kirja> hakemisto; private ArrayList<Kirja> kirjat; public Kirjasto() { hakemisto = new HashMap<String, Kirja>(); kirjat = new ArrayList<Kirja>(); } public void lisaaKirja(Kirja kirja) { String avainNimi = kirja.getNimi(); avainNimi = avainNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi avainNimi = avainNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta if(hakemisto.containsKey(avainNimi)) { System.out.println("Kirja on jo kirjastossa!"); } else { hakemisto.put(avainNimi, kirja); kirjat.add(kirja); } } public void poistaKirja(String kirjanNimi) { kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta if(hakemisto.containsKey(kirjanNimi)) { Kirja poistettava = hakemisto.get(kirjanNimi); kirjat.remove(poistettava); hakemisto.remove(kirjanNimi); } else { System.out.println("Kirjaa ei löydy, ei voida poistaa!"); } } }
Kirjojen listaaminen ArrayList
in avulla jää lukijan itse
toteutettavaksi. Toteutetaan vielä kirjan hakutoiminnallisuus siten, että kirjaa
haetaan hajautusrakenteesta sen nimellä.
public Kirja haeKirja(String kirjanNimi) { kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta return hakemisto.get(kirjanNimi); }
Yllä oleva metodi palauttaa haetun kirjan jos sellainen löytyy, muulloin
null
-arvon. Voimme myös käydä kaikki hakemiston avaimet läpi
yksitellen, etsien esimerkiksi alkuosaa kirjan nimestä. Tällä tavalla etsiessä
menetämme kuitenkin hajautustaulun nopeusedun, sillä huonoimmassa tapauksessa
joudumme käymään kaikkien kirjojen nimet läpi.
public Kirja haeKirja(String kirjanNimi) { kirjanNimi = kirjanNimi.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi kirjanNimi = kirjanNimi.trim(); // poistetaan tyhjät merkit alusta ja lopusta for(String nimi: hakemisto.keySet()) { if(nimi.startsWith(kirjanNimi)) { return hakemisto.get(nimi); } } return null; }
Yksi ohjelmoinnin periaatteista on ns. DRY-periaate (Don't Repeat
Yourself), jolla pyritään välttämään saman koodin olemista useassa
paikassa. Merkkijonon pieneksi muuttaminen ja trimmaus, eli tyhjien
merkkien poisto alusta ja lopusta, toistuu useasti
kirjastoluokassamme. Siistitään Kirjasto
-luokkaa siten, että kirjan
nimen siistiminen tehdään erillisessä metodissa. Lisätään metodi
siisti()
, joka ottaa parametrina String
-olion, ja
palauttaa oliosta siistityn version.
private String siisti(String siistittava) { siistittava = siistittava.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi return siistittava.trim(); // poistetaan tyhjät merkit alusta ja lopusta }
Luokka Kirjasto
vielä kokonaisuudessaan.
public class Kirjasto { private HashMap<String, Kirja> hakemisto; private ArrayList<Kirja> kirjat; public Kirjasto() { hakemisto = new HashMap<String, Kirja>(); kirjat = new ArrayList<Kirja>(); } public void lisaaKirja(Kirja kirja) { String avainNimi = kirja.getNimi(); avainNimi = siisti(avainNimi); if(hakemisto.containsKey(avainNimi)) { System.out.println("Kirja on jo kirjastossa!"); } else { hakemisto.put(avainNimi, kirja); kirjat.add(kirja); } } public void poistaKirja(String kirjanNimi) { kirjanNimi = siisti(kirjanNimi); if(hakemisto.containsKey(kirjanNimi)) { Kirja poistettava = hakemisto.get(kirjanNimi); kirjat.remove(poistettava); hakemisto.remove(kirjanNimi); } else { System.out.println("Kirjaa ei löydy, ei voida poistaa!"); } } // kirjojen läpi käyminen yksi kerrallaan jätettiin pois Kirjasto-toteutuksesta public Kirja haeKirja(String kirjanNimi) { kirjanNimi = siisti(kirjanNimi); return hakemisto.get(kirjanNimi); } private String siisti(String siistittava) { siistittava = siistittava.toLowerCase(); // muunnetaan nimi pieniksi kirjaimiksi return siistittava.trim(); // poistetaan tyhjät merkit alusta ja lopusta } }
Monimutkaisen algoritmin suunnittelussa kynä ja paperi ovat usein hyödyllisiä. Aluksi kannattaa käydä läpi yksinkertaisia tapauksia ja yrittää löytää niistä säännöllisyyksiä, joiden avulla voidaan laatia yleisessä tapauksessa toimiva algoritmi. Sitten kun algoritmin peruslogiikka on selvillä, on mukavaa siirtyä tietokoneelle ja aloittaa koodaaminen.
Ohjelmoinnin perusteet -kurssilla oli tehtävänä laatia ohjelma, joka tulostaa annetun kokoisen kuusen seuraavien esimerkkien mukaisesti.
Anna korkeus: 3 * *** ***** *
Anna korkeus: 5 * *** ***** ******* ********* *
Tällaista ongelmaa on järkevää lähestyä piirtämällä muutamia kuusia ja katsomalla, miten ne muodostuvat tähdistä. Seuraavassa on piirretty tapaukset, joissa korkeus on 3 ja 4. Lisäksi on piirretty tarkemmin tapaus, jossa korkeus on 4. Kuvista on jätetty pois viimeiselle riville tuleva kuusen juuri.
Havaitaan seuraavat asiat, kun kuusen korkeus on 4:
Hieman miettimällä yleinen säännöllisyys on seuraava:
Tästä saadaan seuraava koodin runko:
valilyonnit = korkeus - 1; tahdet = 1; for (int i = 0; i < korkeus; i++) { // tulosta "valilyonnit" välilyöntiä // tulosta "tahdet" tähteä valilyonnit = valilyonnit - 1; tahdet = tahdet + 2; }
Lopullinen koodi voisi olla seuraavanlainen:
import java.util.Scanner; public class KuusenTulostus { private static Scanner lukija = new Scanner(System.in); private static void tulostaMonta(int maara, char merkki) { for (int i = 0; i < maara; i++) { System.out.print(merkki); } } public static void main(String[] args) { System.out.print("Anna korkeus: "); int korkeus = Integer.parseInt(lukija.nextLine()); int valilyonnit = korkeus - 1; int tahdet = 1; for (int i = 0; i < korkeus; i++) { tulostaMonta(valilyonnit, ' '); tulostaMonta(tahdet, '*'); System.out.println(); valilyonnit = valilyonnit - 1; tahdet = tahdet + 2; } tulostaMonta(korkeus - 1, ' '); tulostaMonta(1, '*'); System.out.println(); } }
Abstrahoinnilla tarkoitetaan asioiden määrittelyä tarpeellisten toimintojen ja ominaisuuksien kautta, ottamatta kantaa siihen miten ne on toteutettu. Abstrahointi on perusteltua ohjelmistoja suunniteltaessa kun ei haluta ottaa kantaa siihen, miten yksittäiset toiminnot toteutetaan. Yksi abstrahoinnin välineistä on rajapinta, johon tutustutaan seuraavaksi.
Rajapinta (engl. interface) kuvaa käyttäytymistä määrittelemällä listan metodeja, joita rajapinnan kuvaava käyttäytyminen toteuttaa. Rajapinta määritellään omassa tiedostossa, jonka nimi on Rajapinnannimi.java. Rajapinnan tyyppi on interface, ja se sisältää määrittelyn käyttäytymiseen liittyvistä metodeista, mutta ei niiden toteutusta. Rajapinta määrittelee siis vain metodien nimet ja paluuarvot, mutta ei sitä, miten ne on toteutettu. Tällöin eri luokat voivat toteuttaa rajapinnan määrittelemän käyttäytymisen niihin sopivalla tavalla. Katsotaan puhumiskäyttäytymistä määrittelevää rajapintaa Puhuva.
public interface Puhuva { public String puhu(); // vain metodin nimi, ei toteutusta }
Rajapinta Puhuva
määrittelee
metodin puhu()
, joka palauttaa String-tyyppisen
olion. Metodin puhu()
toteutuksen pitää siis
palauttaa merkkijono, joka kuvaa puhetta. Puhuva-rajapintaa
voidaan ajatella sopimuksena puhumiskäyttäytymisestä.
Kaikkien rajapinnan määrittelevien metodien näkyvyysmääre on automaattisesti public
. Tämän takia näkyvyysmääre jätetään usein kokonaan merkkaamatta:
public interface Puhuva { String puhu(); // sama kuin public String puhu() }
Rajapinta on vain sopimus käyttäytymisestä. Jotta
käyttäytyminen toteutuu, täytyy luokan toteuttaa rajapinta. Luokka
toteuttaa rajapinnan avainsanalla implements, joka kertoo
luokan toteuttavan kaikki annetun rajapinnan metodit. Rajapinnan
toteuttaminen tarkoittaa sopimuksen tekemistä siitä, että luokka
tarjoaa kaikki rajapinnan määrittelemät toiminnot, eli
metodit. Luokkaa, joka toteuttaa rajapinnan, mutta ei toteuta
rajapinnan metodeja, ei voi olla olemassa. Seuraava
luokka Luennoitsija toteuttaa
rajapinnan Puhuva. Huomaa että Luennoitsija-luokan on
pakko toteuttaa rajapinnan Puhuva määrittelevä metodi public
String puhu()
.
public class Luennoitsija implements Puhuva { ... public String puhu() { String[] aiheet = {"kapselointi", "periytyminen", "polymorfismi", "abstrahointi"}; Random arpoja = new Random(); int indeksi = arpoja.nextInt(aiheet.length); return "Yksi olio-ohjelmoinnin peruskäsitteistä, " + aiheet[indeksi] + "."; } }
Luennoitsija-luokka valitsee satunnaisesti yhden neljästä
olio-ohjelmoinnin peruskäsitteestä, ja puhuu siitä. Satunnaisuutta
varten luodaan uusi Random-olio, jolta voidaan kätevästi pyytää satunnaista
indeksiä taulukkoon. Lopuksi palautetaan merkkijono "Yksi olio-ohjelmoinnin
peruskäsitteistä, " katenoituna indeksin osoittamaan
aiheeseen. Yllä olevan Luennoitsija-luokan
metodi puhu()
palauttaa siis satunnaisesti yhden
seuraavasta neljästä erilaisesta lauseesta.
Yksi olio-ohjelmoinnin peruskäsitteistä, kapselointi. Yksi olio-ohjelmoinnin peruskäsitteistä, periytyminen. Yksi olio-ohjelmoinnin peruskäsitteistä, polymorfismi. Yksi olio-ohjelmoinnin peruskäsitteistä, abstrahointi.
Toteutetaan vielä luokka Opiskelija, joka myös toteuttaa rajapinnan Puhuva.
public class Opiskelija implements Puhuva { ... public String puhu() { return "Mikä toi on?"; } }
Yllä oleva luokka Opiskelija toteuttaa rajapinnan Puhuva ja sen määrittelemän
metodin puhu()
. Metodi puhu()
palauttaa aina
tekstin "Mikä toi on?".
Luodaan vielä ohjelma, joka luo kolme rajapinnan Puhuva toteuttavaa luokkaa. Kaksi luennoitsijaa ja yhden opiskelijan.
Luennoitsija mattiL = new Luennoitsija(); Luennoitsija mattiP = new Luennoitsija(); Opiskelija mattiV = new Opiskelija(); System.out.println(mattiV.puhu()); System.out.println(mattiL.puhu()); System.out.println(mattiV.puhu()); System.out.println(mattiP.puhu()); System.out.println(mattiV.puhu()); System.out.println(mattiP.puhu());
Yllä olevan ohjelman tulostus voisi olla esimerkiksi seuraavaa.
Mikä toi on? Yksi olio-ohjelmoinnin peruskäsitteistä, kapselointi. Mikä toi on? Yksi olio-ohjelmoinnin peruskäsitteistä, polymorfismi. Mikä toi on? Yksi olio-ohjelmoinnin peruskäsitteistä, kapselointi.
Koska rajapinta Puhuva määrittelee metodin puhu()
,
voidaan kaikilta Puhuva-rajapinnan toteuttavien luokkien olioilta
kutsua metodia puhu()
. Rajapinta toimii kuten
viitetyyppiset muuttujat, eli rajapinta-tyyppinen muuttuja
sisältää viitteen olioon. Jos olion tyyppinä käytetään rajapintaa,
voidaan siltä kutsua vain rajapinnan määrittelemiä
metodeja. Rajapintojen ja luokkien suurin ero on se, että
rajapinnasta ei voi tehdä ilmentymää. Tietyn rajapinnan
toteuttavan luokan voi kuitenkin asettaa rajapinta-tyyppiseen
muuttujaan. Seuraavassa esimerkissä luodaan kaksi Puhuva-tyyppistä
oliota, toinen Luennoija luokasta, toinen luokasta
Opiskelija. Kummallekin kutsutaan myös metodia puhu()
Puhuva mattiP = new Luennoitsija(); Puhuva mattiV = new Opiskelija(); System.out.println(mattiV.puhu()); System.out.println(mattiP.puhu());
Yllä olevassa esimerkissä kutsutaan siis viitteiden mattiP ja
mattiV takana oleville olioille metodia puhu()
. Koska
luokat Luennoitsija ja Opiskelija toteuttavat rajapinnan Puhuva, on
niillä myös metodi puhu()
.
Edellä nähdyn dialogin
olioiden mattiL
, mattiP
ja mattiV
välillä voi kirjoittaa seuraavasti.
Puhuva mattiL = new Luennoitsija(); Puhuva mattiP = new Luennoitsija(); Puhuva mattiV = new Opiskelija(); System.out.println(mattiV.puhu()); System.out.println(mattiL.puhu()); System.out.println(mattiV.puhu()); System.out.println(mattiP.puhu()); System.out.println(mattiV.puhu()); System.out.println(mattiP.puhu());
Myös taulukkoon tai ArrayListille voidaan tallettaa olioita rajapinnan tyyppisenä.
Kuvataan pajaohjaustilannetta muutaman Puhuva-rajapinnan toteuttavan olion avulla. Ensin määritellään Pajaohjaaja:
public class PajaOhjaaja implements Puhuva { public void juoKahvia() { Sysytem.out.println("poistun nyt paikalta muutamaksi minuutiksi"); } public String puhu() { String[] aiheet = {"kirjoittanut kaikkea koodia mainiin", "tehnyt liian pitkää metodia"}; Random arpoja = new Random(); int indeksi = arpoja.nextInt(aiheet.length); return "Et kai " + aiheet[indeksi] + "?"; } }
Pajassa on 25 opiskelijaa ja 3 pajaohjaajaa, kaikkia käsitellään Puhuva-tyyppisinä:
ArrayList>Puhuva< osallistujat = new ArrayList>Puhuva<(); osallistujat.add( PajaOhjaaja() ); osallistujat.add( PajaOhjaaja() ); osallistujat.add( PajaOhjaaja() ); for ( int i=0; i < 25; i++ ) { osallistujat.add( new Opiskelija() ); } while ( true ) { for ( Puhuva p : osallistujat ) { System.out.println( p.puhu() ); } osallistujat.get(0).juoKahvia(); // ei toimi! osallistujat käsitellään Puhuva:na }
Tämä piinallinen pajaohjaustapahtuma kestää ikuisesti.
Huom: Kun rajapintaa käytetään muuttajan tyyppinä, ei
tiedetä rajapinnan toteuttavan luokan muista metodeista. Voidaan käyttää
ainoastaan rajapinnan määrittelemiä metodeja.
Koska osallistujat tunnetaan ainoastaan Puhuva
-rajapinnan kautta, ei viimeisessä rivillä oleva ohjaajan kahvinjuontiyritys onnistu.
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ää myös metodikutsuissa parametrin tyyppinä.
Luodaan
uusi käyttäytymistyyppi, eli rajapinta, Liikkuva. Rajapinta
liikkuva määrittelee metodit liiku()
,
ja paljonkoLiikuttu()
. Metodi liiku()
määrittelee liikkumistoiminnon, ja
metodi paljonkoLiikuttu()
taas antaa tiedon
kuljetusta matkasta.
public interface Liikkuva { public void liiku(); public int paljonkoLiikuttu(); }
Toteutetaan myös kaksi luokkaa, Auto ja Formula, jotka kummatkin toteuttavat rajapinnan Liikkuva. Luokilla Auto ja Formula on attribuutti kilometrit, joka pitää kirjaa liikutuista kilometreistä.
public class Auto implements Liikkuva { private int kilometrit; ... public Auto() { this.kilometrit = 0; } ... public void liiku() { this.kilometrit++; } public int paljonkoLiikuttu() { return this.kilometrit; } }
public class Formula implements Liikkuva { private int kilometrit; ... public Formula() { this.kilometrit = 0; } ... public void liiku() { this.kilometrit += 3; } public int paljonkoLiikuttu() { return this.kilometrit; } }
Sekä luokka Auto, että luokka Formula toteuttavat rajapinnan liikkuva. Luodaan seuraavaksi pääohjelma, jossa on metodi Liikkuva-tyyppisten olioiden liikuttamiseen.
public static void main(String[] komentoriviParametrit) { Auto volga = new Auto(); Formula f1 = new Formula(); liikuta(volga); liikuta(f1); System.out.println("Autolla ajettu " + volga.paljonkoLiikuttu() + " km."); System.out.println("Formulalla ajettu " + f1.paljonkoLiikuttu() + " km."); } public static void liikuta(Liikkuva liikkuva) { liikkuva.liiku(); }
Esimerkki tulostaa seuraavan tulosteen
Autolla ajettu 1 km. Formulalla ajettu 3 km.
Koska luokat Auto ja Formula toteuttavat rajapinnan Liikkuva, voidaan ne antaa parametrina metodille joka ottaa Liikkuva-tyyppisiä olioita. Metodin sisällä voi tietysti kutsua vain rajapinnan Liikkuva määrittelemiä metodeja.
Rajapinta voi olla myös metodin paluuarvo. Seuraava metodi luo uusia Liikkuva-rajapinnan toteuttavia olioita. Huomaa että rajapinnalla ei ole konstruktoria, vaan metodi palauttaa rajapinnan toteuttavista luokista tehtyjä ilmentymiä.
public static Liikkuva luoLiikkuva() { Random arpoja = new Random(); if(arpoja.nextBoolean()) { return new Auto(); } return new Formula(); }
Yllä oleva metodi luo 50% todennäköisyydellä auto-olion, ja 50% todennäköisyydellä formulaolion. Luotu Auto tai Formula palautetaan rajapinnan Liikkuva toteuttavana oliona.
Javan API tarjoaa ison määrän valmiita rajapintoja. Muunmuassa tuttu ArrayList-luokka toteuttaa rajapinnan List, joka määrittelee listan perustoiminnot. ArrayList-luokkaa voisikin käyttää myös seuraavasti:
List<String> merkkijonot = new ArrayList<String>(); merkkijonot.add("Moi taas!");
Koska luokka ArrayList toteuttaa rajapinnan List, ja rajapinta
List määrittelee metodin add()
, voidaan
metodia add()
kutsua yllä luodulle List-tyyppiselle
oliolle.
Kuten Java API:sta huomaamme Rajapinnan List
toteuttavia luokkia on muitakin, esim. LinkedList
:
List<String> merkkijonot = new LinkedList<String>(); merkkijonot.add("Moi taas!");
Molemmat rajapinnan toteutukset ArrayList ja LinkedList toimivat käyttäjän näkökulmasta samoin. Rajapinta siis abstrahoi niiden sisäisen toiminnallisuuden täysin. Sisäinen rakenne ArrayList:illä ja LinkedListillä on kuitenkin huomattavan erilainen. Jos on kyse suurista syötteistä on tietyissä toiminnoissa suuri suorituskykyero. LinkedList:iin voi lisätä tai poistaa mistä kohtaa tahansa alkioita nopeasti, toisin kuin ArrayList:istä.
Seuraavassa lisätään listan alkuun eli add(0, "Moi taas"!)
komennolla paikkaan 0 miljoona merkkijonoa:
List<String> merkkijonot = new LinkedList<String>(); for (int i=0; i < 1000000; i++ ) { merkkijonot.add(0,"Moi taas!"); }
LinkedList toimii nopeasti. Jos listana käytettäisiin ArrayList:iä, olisi suorituskyky katastrofaalinen! ArrayList:iin lisääminen kannattaa tehdä aina loppuun eli add(0, "Moi taas"!)
sijaan add("Moi taas")
, sillon suorituskykyhaittaa ei ole.
ArrayList:iä taas toimii nopeasti jos on tarve käydä läpi listaa "hyppimällä" eli jossain muussa järjestyksessä kuin alusta loppuun.
Seuraavassa lisätään ensin listalle miljoona merkkijonoa ja sen jälkeen tulostetaan satunnaisia merkkijonoja miljoona kappaletta:
List<String> merkkijonot = new ArrayList<String>(); // lisätään listalle miljoona merkkijonoa Random arpa = new Random(); for (int i=0; i < 1000000; i++ ) { System.out.println( merkkijonot.get( arpa(1000000) ) ); }
Tähön tarkoitukseen ArrayList on tehokas, mutta LinkedList surkea.
Yksi Javan valmiiksi tarjoamista rajapinnoista
on Comparable. Rajapinta
Comparable määrittelee metodin compareTo()
, joka
palauttaa this
-olion paikan vertailujärjestyksessä verrattuna parametrina
annettuun olioon (negatiivinen luku, 0 tai positiivinen luku). Jos this
-olio on vertailujärjestyksessä ennen parametrina saatavaa olioa, tulee metodin palauttaa negatiivinen luku, jos taas parametrina saatava olio on järjestyksessä ennen, tulee metodin palauttaa positiivinen luku. Vertailujärjestyksellä tarkoitetaan tässä ohjelmoijan määrittelemää olioiden "suuruusjärjestystä", eli jos oliot järjestetään sort-metodilla, mikä on niiden järjestys.
Muutetaan aiemmin luotua luokkaa Opiskelija siten,
että sillä on nimi ja se toteuttaa Comparable-rajapinnan. Lisätään
opiskelijalle myös attribuutit pituus, jota
käytetään compareTo()
-metodissa. Comparable-rajapinta
ottaa tyyppiparametrina myös luokan, johon sitä verrataan.
public class Opiskelija implements Puhuva, Comparable<Opiskelija> { private String nimi; private int pituus; public Opiskelija(String nimi, int pituus) { this.nimi = nimi; this.pituus = pituus; } public String annaNimi() { return this.nimi; } public int annaPituus() { return this.pituus; } // metodi saa parametrikseen Opiskelija-tyyppisen olion, sillä Comparable-rajapinnalle on annettu tyypiksi Opiskelija public int compareTo(Opiskelija toinen) { if(this.annaPituus() == toinen.annaPituus()) { return 0; } else if (this.annaPituus() > toinen.annaPituus()) { return 1; } else { return -1; } } public String puhu() { return "Mikä toi on?"; } }
Koska compareTo()
-metodista riittää palauttaa negatiivinen luku, jos this
-olio
on pienempi kuin parametrina annettu olio, voitaisiin edellä esitelty metodi toteuttaa myös
seuraavasti:
public int compareTo(Opiskelija toinen) { return this.annaPituus() - toinen.annaPituus(); }
Tässä on hiukan kertausta sekä joitakin uusia asioita Javan Collections
-luokasta.
Java tarjoaa suuren määrän valmiiksi tehtyjä toimintoja
erilaisten tietorakenteiden
käsittelyyn. Luokkakirjasto Collections
on eräs kirjasto, jota tullaan käyttämään tällä
kurssilla. Collections löytyy Javan
pakkauksesta java.util.Collections, eli sen saa käyttöön
kutsulla import java.util.Collections;
, joka
asetetaan lähdekooditiedoston alkuun.
Luokkakirjasto collections tarjoaa valmiiksi toteutetun järjestämisalgoritmin, jolla voidaan järjestää Comparable-rajapinnan toteuttavia olioita. Katsotaan seuraavaksi Opiskelija-olioiden järjestämistä pituusjärjestykseen.
ArrayList<Opiskelija> opiskelijat = new ArrayList<Opiskelija>(); opiskelijat.add(new Opiskelija("Matti L", 187)); opiskelijat.add(new Opiskelija("Robert W", 272)); opiskelijat.add(new Opiskelija("Aditya D", 56)); for(Opiskelija l: opiskelijat) { System.out.println(l.annaNimi()); } System.out.println(); Collections.sort(opiskelijat); for(Opiskelija l: opiskelijat) { System.out.println(l.annaNimi()); }
Esimerkin tulostus on seuraavanlainen
Matti L Robert W Aditya D Robert W Matti L Aditya D
Collections-luokkakirjasto tarjoaa myös valmiiksi toteutetun binäärihaun. Metodi binarySearch()
palauttaa haetun alkion indeksin listasta, jos se löytyy. Jos alkioita ei löydy, hakualgoritmi palauttaa negatiivisen arvon. Metodi binarySearch()
käyttää Comparable-rajapintaa haetun olion löytämiseen. Jos olion compareTo()
-metodi palauttaa arvon 0, eli olio on sama, ajatellaan arvon löytyneen.
Opiskelija-luokkamme vertaa pituuksia compareTo()
-metodissaan, eli listasta etsiessä etsisimme samanpituista opiskelijaa.
ArrayList<Opiskelija> opiskelijat = new ArrayList<Opiskelija>(); opiskelijat.add(new Opiskelija("Matti L", 187)); opiskelijat.add(new Opiskelija("Robert W", 272)); opiskelijat.add(new Opiskelija("Aditya D", 56)); Collections.sort(opiskelijat); Opiskelija haettava = new Opiskelija("Nimi", 180); int indeksi = Collections.binarySearch(opiskelijat, haettava); if(indeksi >= 0) { System.out.println("180 senttiä pitkä löytyi indeksistä " + indeksi); System.out.println("Nimi: " + opiskelijat.get(indeksi).annaNimi()); } haettava = new Opiskelija("Nimi", 187); int indeksi = Collections.binarySearch(opiskelijat, haettava); if(indeksi >= 0) { System.out.println("187 senttiä pitkä löytyi indeksistä " + indeksi); System.out.println("Nimi: " + opiskelijat.get(indeksi).annaNimi()); }
Esimerkkimme tulostaa seuraavaa
187 senttiä pitkä löytyi indeksistä 1 Nimi: Matti L
Huomaa että esimerkissä kutsuttiin myös
metodia Collections.sort()
. Tämä tehdään sen takia,
että binäärihakua ei voida tehdä jos taulukko tai lista ei ole
valmiiksi järjestyksessä.
Kurssilla ollaan usein törmätty tilanteeseen, missä ohjelman suoritus antaa virheen, eikä paikkaa josta virhe johtuu löydy helposti. Tässä kappaleessa esitellään muutama hyvä keino virheiden löytämiseksi.
Ensimmäinen tapa virheiden löytämiseen on muuttujien tilan
tulostaminen System.out.println()
-komentojen
avulla. Jos metodi tai ohjelma toimii väärin, voi siihen lisätä
välitulostuksia, jotka tulostavat arvot aina sopivissa
kohdissa. Näitä arvoja tutkimalla voi katsoa, onko ohjelman
toiminta halutunlaista.
NetBeans tarjoaa myös niinsanotun debuggerin, eli työkalun jolla ohjelmaa voidaan suorittaa askeleittain. Lähdekoodi-ikkunan vasenta laitaa painamalla voidaan asettaa pysähdyspisteitä (engl. breakpoint), joissa ohjelman suoritus pysähtyy automaattisesti. Debugger näyttää pysähdyskohdissa muuttujat ja niiden arvot. Debuggerin saa valitsemalla Run-valikosta "Debug Main Project". Myös komento "Ctrl+F5" ajaa debuggerin. Seuraavaan pysähdyskohtaan pääsee etenemällä painamalla "F5". Huomaa että pysähdyskohdat täytyy määritellä sellaisille riveille, joissa on komento.
Youtube-video NetBeansin bebuggerin käytön perusteista.
Javassa puhutaan usein gettereistä ja settereistä, eli metodeista jotka joko palauttavat tai asettavat arvoja. Kirjoitetaan tästä eteenpäin arvon asettavat metodit muodossa setMuuttujanNimi()
, ja arvon palauttavat metodit muodossa getMuuttujanNimi()
. Alla olevassa luokassa Elain
on oikein nimetyt getterit ja setterit.
public class Elain { private String nimi; private double pituus; public Elain(String nimi, double pituus) { this.nimi = nimi; this.pituus = pituus; } public double getPituus() { return this.pituus; } public void setPituus(double pituus) { this.pituus = pituus; } public String getNimi() { return this.nimi; } public void setNimi(String nimi) { this.nimi = nimi; } }
Getterit ja Setterit voidaan luoda automaattisesti useimmissa IDEissä, esimerkiksi NetBeanssissa niiden luominen tapahtuu valitsemalla Source -> Insert Code -> Getter and Setter... ja valitsemalla muuttujat joille getterit ja setterit tehdään.
Final on samanlainen määre kuin static, eli sitä käytetään muuttujien määrittelyssä. Final-määre lukitsee muuttujan arvon siten, että sitä ei voi enää muuttaa. Alkeistyyppisille muuttujille tämä tarkoittaa sitä, että muuttujan arvo pysyy samana. Viitetyyppisille muuttujille lukitseminen tarkoittaa sitä, että viitettä ei voi muuttaa, mutta viitteen takana olevan olion sisäistä tilaa voi toki muuttaa.
Ohjelmoinnin perusteissa nähtiin viikon 6 Kassapääte-tehtävässä kuta kuinkin seuraanvanlaista koodia.
public class Kassapaate { public static double EDULLISESTI = 2.40; public static double MAUKKAASTI = 4.10; }
Yllä olevan toteutuksen heikkous on se, että muuttujien arvoa voi muuttaa myös mistä tahansa. Final-määre lukitsee arvot paikalleen, eli määrittelemällä luokkakirjaston seuraavasti muuttujien arvoa ei enää voi muuttaa.
public class Kassapaate { public static final double EDULLISESTI = 2.40; public static final double MAUKKAASTI = 4.10; }
Hyvä suunnittelutapa on lukita kaikkialle näkyvät staattiset
muuttujat, eli määreen public static
omaavat
muuttujat, pysyviksi määreellä final.
Periytymisellä tarkoitetaan sitä kun luokka saa, eli perii, jo olemassa olevan luokan ominaisuudet. Periytymisen hyötynä on ohjelmointityön väheneminen, kun jo valmiiksi olemassaolevia toteutuksia voidaan käyttää pohjana uusille toteutuksille. Luodaan luokka Tehdas
, jolla on metodi soitaPillia()
.
public class Tehdas { public void soitaPillia() { System.out.println("Piip!"); } }
Periytyminen tapahtuu avainsanalla extends, jota seuraa perittävän luokan nimi. Luokka voi periä aina vain yhden luokan. Luodaan seuraavaksi luokka OhjelmistoTehdas
, joka perii luokan Tehdas
toiminnot. Ohjelmistotehtaalla on myös metodi tuotaKoodia()
, joka palauttaa lähdekoodia merkkijonona.
public class OhjelmistoTehdas extends Tehdas { public String tuotaKoodia() { return "System.out.println(\"HelloWorld!\");"; } }
Koska luokka OhjelmistoTehdas
perii luokan Tehdas
, on sillä käytössä myös Tehtaan metodi soitaPillia()
. Voimme siis käyttää yllä olevaa ohjelmistotehdasta seuraavasti.
OhjelmistoTehdas t = new OhjelmistoTehdas(); t.soitaPillia(); System.out.println(t.tuotaKoodia());
Konstruktorikutsun new OhjelmistoTehdas()
voi tehdä koska luokka Tehdas
sisältää parametrittoman oletuskonstruktorin. Perivän luokan konstruktoria kutsuttaessa kutsutaan automaattisesti perityn luokan parametritonta konstruktoria. Saamma ylläolevasta esimerkistä seuraavan tulosteen.
Piip! System.out.println("HelloWorld!");
Perittyä luokkaa kutsutaan yläluokaksi, ja perivää luokkaa aliluokaksi. Kukin luokka voi periä vain yhden luokan, mutta moni luokka voi periä saman luokan. Yllä olevassa esimerkissä luokka Tehdas
on yläluokka, luokka OhjelmistoTehdas
aliluokka.
Luodaan vielä toinen tehdas, luokka KarkkiTehdas
.
public class KarkkiTehdas extends Tehdas { private String karkkimerkki; public KarkkiTehdas(String karkkimerkki) { this.karkkimerkki = karkkimerkki; } public String getKarkkimerkki() { return this.karkkimerkki; } }
Myös luokalla KarkkiTehdas
on käytössä metodi soitaPillia()
. Karkkitehtaan voi ottaa käyttöön seuraavasti.
KarkkiTehdas tehdas = new KarkkiTehdas("Kater"); tehdas.soitaPillia();
Karkkitehtaan soitaPillia()
metodikutsu tuottaa myös tulosteen Piip!
.
Jos yläluokalla on määritelty parametrillinen konstruktori, voi sitä kutsua määreen super
-avulla. Määre super
on kuin this
, mutta viittaa perityn luokan ominaisuuksiin, kun taas this
viittaa kyseiseen olioon. Luodaan parametrillisen konstruktorin omaava luokka Elain
, jolla on nimi ja pituus.
public class Elain { private String nimi; private double pituus; public Elain(String nimi, double pituus) { this.nimi = nimi; this.pituus = pituus; } public double getPituus() { return this.pituus; } public String getNimi() { return this.nimi; } }
Seuraavaksi luodaan luokka Vesieläin (Vesielain
), joka laajentaa luokkaa Elain
ja osaa uida. Luokan Elain
konstruktorissa viitataan yläluokkaan avainsanalla super. Jos kutsua super()
käytetään konstruktorissa, täytyy sen olla konstruktorin ensimmäinen komento.
public class Vesielain extends Elain { // vesieläimillä lienee myös kidukset, mutta unohdetaan ne hetkeksi public double nopeus; public Vesielain(String nimi, double pituus, double nopeus) { super(nimi, pituus); this.nopeus = nopeus; } public void ui() { System.out.println("Viuh!"); } }
Koska luokalla Elain
on ohjelmoijan kirjoittama konstruktori, ei luokalla ole enää Javan automaattisesti generoimaa parametrotonta oletuskonstruktoria. Aliluokan konstruktorista on pakko kutsua yliluokan konstruktoria. Sen takia seuraava konstruktori ei kelpaisi luokalle Vesieläin:
public Vesielain(String nimi, double pituus, double nopeus) { this.nimi = nimi; this.pituus = pituus; this.nopeus = nopeus; }
Jos taas luokalle Elain
lisättäisiin parametriton konstruktori, edelleinen kävisi luokan Vesieläin
konstruktoriksi, Java nimittäin lisää aliluokaan kutsun yliluokan parametrittomaan konstruktoriin jos konstruktori ei sisällä super
-kutsua yliluokan konstruktoriin.
Tehdään vielä luokat Riikinkukkoahven ja Siipisimppu, jotka perivät luokan Vesielain
ominaisuudet.
public class Riikinkukkoahven extends Vesielain { public Riikinkukkoahven(String nimi, double pituus, double nopeus) { super(nimi, pituus, nopeus); } }
public class Siipisimppu extends Vesielain { public Siipisimppu(String nimi, double pituus, double nopeus) { super(nimi, pituus, nopeus); } }
Koska Riikinkukkoahven ja Siipisimppu perivät kummatkin luokan Vesielain
, voivat ne käyttää Vesieläin luokassa toteutettuja valmiita toimintoja. Seuraavassa esimerkissä luodaan "Matti" - niminen siipisimppu, ja kutsutaan sen metodia ui()
. Huomaa että metodi ui()
on toteutettu luokassa Vesielain
. Koska Siipisimppu perii luokan Vesielain, saa se käyttää sen ja sen yläluokkien metodeja.
Siipisimppu mattiL = new Siipisimppu("Matti", 187, 22.5); mattiL.ui();
Esimerkki luo mattiL
-nimisellä viitteellä varustetun Siipisimppu-tyyppisen olion ja kutsuu sen ui()
-metodia. Metodi ui()
tulostaa merkkijonon Viuh!
.
Yläluokan metodit ovat suoraan alaluokan käytettävissä jos niille ei ole asetettu näkyvyysmäärettä private.. Lisätään luokalle Siipisimppu
toString()
-metodi, joka kutsuu luokassa Elain
määriteltyä getNimi()
-metodia.
public class Siipisimppu extends Vesielain { public Siipisimppu(String nimi, double pituus, double nopeus) { super(nimi, pituus, nopeus); } public String toString() { return "Hei, olen Siipisimppu, ja nimeni on " + getNimi(); } }
Luodaan vielä Siipisimppu-olio ja tulostetaan sen tila.
Siipisimppu mattiL = new Siipisimppu("Matti", 187, 22.5); System.out.println(mattiL);
Esimerkki tulostaa merkkijonon Hei, olen Siipisimppu, ja nimeni on Matti
.
Olemme tähän mennessä nähneet kaksi erilaista näkyvyysmäärettä. Määre public
asettaa metodit ja muuttujat kaikille näkyviksi, kun taas määrettä private
käytetään luokan ominaisuuksien kapselointiin. Periytymisessä voidaan käyttää kolmatta määrettä protected, joka tarkoittaa sitä, että metodi tai oliomuuttuja on aliluokille näkyvissä. Määrettä protected
käytetään esimerkiksi silloin, kun tarvitaan luokan sisäisiä apuvälineitä, joita ei kuitenkaan haluta kaikkien näkyville.
Jos luokan Elainn
oliomuuttujien näkyvyys olisi protected:
public class Elain { protected String nimi; protected double pituus; // ... }
Voitaisiin eläimestä periytyvissä luokissa käyttää suoraan oliomuuttujia nimi
ja pituus
. Eli sensijaan että olisi käytettävä edellä olleen esimerkin tapaan getteriä:
public class Siipisimppu extends Vesielain { // ... public String toString() { return "Hei, olen Siipisimppu, ja nimeni on " + getNimi(); } }
Voitaisiin oliomuuttujaan nimi
viitata suoraan:
public class Siipisimppu extends Vesielain { // ... public String toString() { return "Hei, olen Siipisimppu, ja nimeni on " + nimi; } }
Metodien ylikirjoituksella tarkoitetaan sitä, että yläluokassa oleva metodi toteutetaan uudestaan aliluokassa. Tällöin kun aliluokasta luodulle oliolle kutsutaan kyseistä metodia, kutsutaan aliluokan toteutusta. Yläluokasta luodulle oliolle kutsutaan yläluokan metoditoteutusta.
Toteutetaan Vesielain
-luokan periva luokka Hummeri
. Hummerit eivät osaa uida, joten niitä varten Vesielain
-luokassa oleva metodi ui()
täytyy ylikirjoittaa.
public class Hummeri extends Vesielain { public Hummeri(String nimi, double pituus, double nopeus) { super(nimi, pituus, nopeus); } public void ui() { System.out.println("Uisin jos osaisin :,("); } }
Luodaan vielä kaksi vesieläintä, Hummeri ja Siipisimppu, ja kutsutaan kummallekin metodia ui()
.
Siipisimppu mattiL = new Siipisimppu("Matti", 187, 22.5); System.out.println("Siipisimppu ui:"); mattiL.ui(); Hummeri mattiV = new Hummeri("Matti", 180, 3.5); System.out.println("Hummeri ui:"); mattiV.ui();
Esimerkin tulostus on seuraavanlainen
Siipisimppu ui: Viuh! Hummeri ui: Uisin jos osaisin :,(
Yllä olevan esimerkin kautta huomaamme suunnittelemassamme Eläin->Vesieläin->Hummeri periytymishierarkiassamme piilevän ongelman. Olemme luoneet Vesielain-luokan, jolla on ui()
-metodi, vaikka kaikki vesieläimet eivät osaa uida. Parempi ratkaisu olisikin luoda luokasta Vesielain
luokat Kala
ja Äyriäinen
. Hummeri laajentaisi, eli perisi, luokkaa Äyriäinen, kun taas Riikinkukkoahven ja Siipisimppu perisivät luokan Kala.
Jos aliluokka ylikirjoittaa yliluokan metodin, on aliluokan sisältä mahdollista tarvittaessa kutsua yliluokan metodia viittaamalla siihen super.metodinNimi();
Seuraavassa luokan Muikku
hyödyntää omassa ui()
-metodin toteutuksessa yliluokan ui()
-metodia, johon siis viitataan super.ui()
.
public class Muikku extends Vesielain { public Muikku(String nimi, double pituus, double nopeus) { super(nimi, pituus, nopeus); } public void ui() { for (int i = 0; i < 10; i++) { super.ui(); } } }
Vielä käyttöesimerkki:
Muikku mattiP = new Muikku("Matti", 12, 7.5); System.out.println("Muikku ui:"); mattiP.ui();
Esimerkin tulostus on seuraavanlainen
Muikku ui: Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!Viuh!
Javan kaikki luokat periytyvät luokasta Object
, jota voidaan ajatella kaikkien luokkien peruspalikkana. Luokka Object
määrittelee muunmuassa metodin toString()
, joka tulostaa olion sisäisen tilan. Jos oma luokkamme ei toteuta metodia toString()
, kutsumme oliota tulostettaessa luokan Object
määrittelemää toString()
-metodia. Perimistä voidaan ajatella seuraavanlaisena puuna, missä jokainen solmu, eli laatikko, perii yläpuolellaan olevan solmun.
Javaan kuuluu myös valmis tiedostoa kuvaava luokka File, jonka sisältö voidaan lukea kurssilla jo tutuksi tulleen Scanner-luokan avulla. Tiedosto saa konstruktorissaan parametriksi tiedostopolun, joka kuvaa tiedoston sijaintia tietokoneen levyjärjestelmässä.
NetBeans-ohjelmassa tiedostoille on oma välilehti nimeltä Files. Files-välilehdellä on määritelty kaikki projektiin liittyvät tiedostot. Jos projektin juureen, eli ei yhdenkään kansion sisälle, lisätään tiedosto, voidaan siihen viitata projektin sisältä suoraan tiedoston nimellä. Tiedosto-olion luominen tapahtuu antamalla sille parametrina polku tiedostoon, esimerkiksi seuraavasti
File tiedosto = new File("tiedoston-nimi.txt");
Koska Scanner-luokan konstruktori on kuormitettu, voi
lukemislähde olla näppäimistön lisäksi myös tiedosto. Käytössämme
on siis samat metodit tiedoston lukemiseen kuin käyttäjän syötteen
lukemiseen. Seuraavassa esimerkissä avataan tiedosto, tarkistetaan
onko siellä tekstiriviä, ja luetaan se jos on. Lopuksi luettu rivi
tulostetaan ja lukijan avaama tiedosto suljetaan. Huomaa että
olemme lisänneet määreen throws Exception
main()
-metodiin. Tutustumme poikkeuksiin ja niiden
hallintaan myöhemmin.
import java.io.File; import java.util.Scanner; public class TiedostonLuku { public static void main(String[] komentoriviParametrit) throws Exception { // tiedosto mistä luetaan File tiedosto = new File("tiedosto.txt"); Scanner lukija = new Scanner(tiedosto); if(lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); } lukija.close(); } }
Yllä oleva esimerkki avaa tiedoston tiedosto.txt
,
joka sijaitsee samassa sijainnissa ohjelman kanssa, ja lukee sen
ensimmäisen rivin. Lopuksi lukija suljetaan, jolloin tiedosto myös
suljetaan. Määre throws Exception
kertoo että metodi
saattaa heittää poikkeuksen. Samanlaisen määreen voi laittaa
kaikkiin metodeihin jotka käsittelevät tiedostoja.
Useampia rivejä voi lukea esimerkiksi seuraavanlaisen toistorakenteen avulla.
while(lukija.hasNextLine()) { String rivi = lukija.nextLine(); System.out.println(rivi); }
Luokan Scanner
metodi hasNextLine()
palauttaa totuusarvon true
jos tiedostossa on
luettava rivi.
Koska käytämme luokkaa Scanner
tiedoston
lukemiseen, voimme käyttää myös sen muita metodeja. Seuraavassa
esimerkissä luetaan tiedosto sana
kerrallaan. Metodi hasNext()
palauttaa
totuusarvon true
, jos tiedostossa on vielä luettava
sana, ja metodi next()
lukee
sen String
-olioon, jonka se palauttaa.
Esimerkkitiedostomme "tiedosto.txt" sisältää seuraavan tekstin: (voit copy-pasteta seuraavan tekstin oman tiedoston sisään kokeillessasi esimerkkiä itse!)
Poikkeukset (exceptions) ovat "poikkeuksellisia tilanteita" kesken normaalin ohjelmansuorituksen: tiedosto loppuu, merkkijono ei kelpaa kokonaisluvuksi, odotetun olion tilalla onkin null-arvo, taulukon indeksi menee ohjelmointivirheen takia sopimattomaksi, ...
Seuraava ohjelma luo Scanner
-olion, joka avaa
tiedoston tiedosto.txt
. Sen jälkeen se tulostaa joka
viidennen sanan tiedostosta.
File tiedosto = new File("tiedosto.txt"); Scanner lukija = new Scanner(tiedosto); int monesko = 0; while (lukija.hasNext()) { monesko++; String sana = lukija.next(); if (monesko % 5 == 0) { System.out.println(sana); } }
Ohjelman tulostus on seuraavanlainen
tilanteita" loppuu, odotetun taulukon sopimattomaksi,
Tiedostoon kirjoittamiseen on myös apuvälineet. Java
tarjoaa luokan FileWriter
,
jolla voi kirjoittaa tekstiä
tiedostoon. Luokan FileWriter
konstruktori on
kuormitettu, eli sillä on monta eri konstruktoria. Käytetään
tiedoston nimen parametrina saavaa konstruktoria.
FileWriter kirjoittaja = new FileWriter("tiedosto.txt"); kirjoittaja.write("Hei tiedosto!\n"); // huomaa, että myös rivinvaihto \n n kirjoitettava tiedostoon kirjoittaja.write("Lisää tekstiä\n"); // jos se sinne halutaan kirjoittaja.write("Ja vielä lisää"); kirjoittaja.close(); // sulkemiskutsu sulkee tiedoston ja varmistaa kirjoitettu teksti menee tiedostoon
Esimerkissä kirjoitetaan tiedostoon "tiedosto.txt" merkkijono
"Hei tiedosto!". Tiedosto löytyy Files-välilehdeltä
projektin tiedostoista. Tiedostoon kirjoittaminen voi aiheuttaa
poikkeustilanteen, eli heittää poikkeuksen. Emme
edelleenkään varaudu poikkeuksiin, vaan annamme metodille
määreen throws Exception
. Metodi, jolle annetaan
parametrina kirjoitettavan tiedoston nimi ja kirjoitettava sisältö
voisi näyttää seuraavalta.
public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception { FileWriter kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.write(teksti); kirjoittaja.close(); }
Metodin (myös main()
), joka kutsuu
metodia kirjoitaTiedostoon()
täytyy joko varautua
poikkeukseen, tai heittää poikkeus. Luodaan
vielä main()
-metodi jossa
kutsutaan kirjoitaTiedostoon()
-metodia. Huomaa että
myös main()
-metodille on määritelty throws
Exception
-määre.
public static void main(String[] args) throws Exception { kirjoitaTiedostoon("paivakirja.txt", "Rakas päiväkirja, tänään oli kiva päivä."); }
Yllä olevaa metodia kutsuttaessa luodaan tiedosto
"paivakirja.txt", ja kirjoitetaan siihen teksti "Rakas päiväkirja,
tänään oli kiva päivä.". Jos tiedosto on jo olemassa, pyyhkiytyy
vanhan tiedoston sisältö uutta
kirjoittaessa. Metodilla append()
voidaan lisätä
olemassaolevan tiedoston perään tekstiä, jolloin olemassaolevaa
tekstiä ei poisteta. Seuraava metodi lisaaTiedostoon()
lisää annetun tekstin tiedoston loppuun.
public static void lisaaTiedostoon(String tiedostonNimi, String teksti) throws Exception { FileWriter kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.append(teksti); kirjoittaja.close(); }
Joskus tiedoston perään metodilla append
kirjoittamisen sijasta on helpompi kirjoittaa koko tiedosto uudelleen.
Tehdään vielä pieni yhteenveto kirjoituksesta ja
lukemisesta. Tiedoston voi lukea Scanner
-luokan avulla
jolle annetaan parametrina File
-tyyppinen
olio. Tiedoston kirjoitus taas
tapahtuu FileWriter
-luokan avulla. Tehdään vielä
esimerkki jossa kirjoitetaan ensiksi tiedostoon, ja sitten luetaan
tiedoston sisältö. Käytetään lukijan
luonnissa FileWriter
-luokan konstruktoria, joka saa
parametrikseen File
-olion.
File tiedosto = new File("tiedosto.txt"); FileWriter kirjoittaja = new FileWriter(tiedosto); kirjoittaja.write("Heippa vaan!"); kirjoittaja.close(); Scanner lukija = new Scanner(tiedosto); System.out.println(lukija.nextLine());
Esimerkin tulostus on Heippa vaan!
Poikkeustilanteet ovat tilanteita joissa ohjelman suoritus ei
ole edennyt toivotusti. Edellä jätettiin
poikkeustilanteet käsittelemättä määrittelemällä metodit ja
pääohjelman sellaisiksi, että ne heittävät poikkeuksen
(throws Exception
). Katsotaan esimerkiksi
metodia lueTiedosto()
, joka lukee kokonaisen
tiedoston sisällön merkkijonoon.
public static String lueTiedosto(String tiedostonNimi) throws Exception { File tiedosto = new File(tiedostonNimi); Scanner lukija = new Scanner(tiedosto); String teksti = ""; while(lukija.hasNextLine()) { teksti += lukija.nextLine() + "\n"; } return teksti; }
Metodissa lueTiedosto()
luokan Scanner
tiedoston parametrina ottava
konstruktori saattaa heittää poikkeuksen tilanteessa, jossa
tiedoston lukeminen ei onnistu.
Javan APIsta
näkee poikkeuksen heittävät metodit ja konstruktorit
(esim. tämä).
Voimme myös itse hoitaa poikkeustenkäsittelyn, jolloin poikkeusta ei siirretä eteenpäin. Tällöin poikkeuksen käsittelyvastuu pysyy metodilla, jossa poikkeus tapahtuu.
Poikkeukset käsitellään try { } catch () { }
-
lohkorakenteella, jossa try { }
- lohkon sisällä on
ohjelmakoodi joka halutaan suorittaa. Osio catch () { }
kertoo poikkeuksen mihin varaudutaan, ja siihen
liittyvässä lohkossa on ohjelmakoodi, mikä suoritetaan
poikkeustilanteessa.
try { // mahdollisesti poikkeuksen heittävä ohjelmakoodi } catch (Exception e) { // poikkeus johon varaudutaan // ohjelmakoodi, joka suoritetaan poikkeustilanteessa }
Muutetaan metodia lueTiedosto()
siten, että se
varautuu itse mahdollisen poikkeustilanteen. Jos
Scanner
-konstruktori heittää poikkeuksen, palautetaan
null
-viite luettuna merkkijonona ja tulostetaan
virheilmoitus. System
-luokan staattinen muuttuja
err
tarjoaa samat tulostusmetodit kuin
out
, mutta niitä käytetään virhetapahtumien
tulostamiseen.
public static String lueTiedosto(String tiedostonNimi) { // ei heitetä poikkeusta File tiedosto = new File(tiedostonNimi); Scanner lukija = null; try { lukija = new Scanner(tiedosto); } catch (Exception e) { System.err.println("Tiedoston avaaminen ei onnistunut!"); return null; } String teksti = ""; while(lukija.hasNextLine()) { teksti += lukija.nextLine() + "\n"; } return teksti; }
Metodi siis varautuu Scanner
-luokan konstruktorin
mahdolliseen poikkeukseen. Jos poikkeus tapahtuu, tulostetaan
virheviesti ja palautetaan null
-viite.
Lisätietoa: Katenointi, eli kahden merkkijonon
yhdistäminen, luo aina uuden String
-olion, jolloin
tarvitaan myös roskienkeruuta vanhoille merkkijonoille. Luokkaa
StringBuilder
käyttämällä voimme yhdistää merkkijonoja ilman uusien
String
-olioiden luomista. Yllä oleva tiedoston lukeva metodi on paljon
tehokkaampi seuraavanlaisena.
public static String lueTiedosto(String tiedostonNimi) { // ei heitetä poikkeusta File tiedosto = new File(tiedostonNimi); Scanner lukija = null; try { lukija = new Scanner(tiedosto); } catch (Exception e) { System.err.println("Tiedoston avaaminen ei onnistunut!"); return null; } StringBuilder teksti = new StringBuilder(); while(lukija.hasNextLine()) { teksti.append(lukija.nextLine()); teksti.append("\n"); } return teksti.toString(); }
Abstrakti luokka on periytymiseen ja abstrahointiin liittyvä
apuväline. Abstraktista luokasta ei voida tehdä ilmentymiä, eli
olioita, mutta sen voi periä. Abstraktin luokan tunnistaa
määreestä abstract
sen määrittelyssä. Luodaan
abstrakti luokka Korjaaja
.
public abstract class Korjaaja { protected String korjattava; public Korjaaja(String korjattava) { this.korjattava = korjattava; } public void setKorjattava(String korjattava) { this.korjattava = korjattava; } public String getKorjattava() { return korjattava; } }
Ilmentymän luonti luokasta Korjattava
ei ole mahdollista, eli seuraava konstruktorikutsu ja viitteen asetus ei toimi.
Korjaaja mattiV = new Korjaaja("abstraktinen, sellainen, joka ei ole kouriintuntuva eikä konkreettinen");
Abstrakteilla luokilla määritellään yleiskäyttöinen runko runko
sen periville luokille. Esimerkiksi abstraktista luokasta
Korjaaja
voidaan periä luokka
KokeenKorjaaja
. Luokka KokeenKorjaaja
korjaa kokeita, ja sillä on abstraktin luokan
Korjaaja
määrittelemien metodien lisäksi metodi
korjaa()
. Abstraktin luokan periminen seuraa
normaalin periytymisen sääntöjä, eli kutsumme yläluokan
parametrillista konstruktoria super()
-kutsulla. Myös
yläluokan muuttujat ja metodit olisivat saatavilla.
public class KokeenKorjaaja extends Korjaaja { public KokeenKorjaaja(String korjattava) { super(korjattava); } public void korjaa() { this.korjattava += "\n - Pisteet " + Math.min(this.korjattava.length / 4, 60); } }
Metodi korjaa()
korjaa kokeen, eli tässä
tapauksessa määrittelee siitä saatavat pisteet. Luokkakirjaston
Math
metodi min()
palauttaa pienemmän
kahdesta parametrista, eli kokeesta voi saada maksimissaan 60
pistettä. Luokasta KokeenKorjaaja
voi tehdä
ilmentymän normaalilla olion luontikutsulla.
String teksti = "An abstract class is designed only as a parent class " + "from which child classes may be derived."; KokeenKorjaaja mattiP = new KokeenKorjaaja(teksti); mattiP.korjaa(); System.out.println(mattiP.getKorjattava());
Yllä oleva esimerkki luo ilmentymän luokasta KokeenKorjaaja
ja kutsuu sen korjaa()
-metodia. Esimerkin tulostus on seuraavanlainen:
An abstract class is designed only as a parent class from which child classes may be derived. - Pisteet 23
Abstrakteja luokkia käytetään tapauksissa joissa useat luokat tarvitsevat samaa rakennuspiirrustusta, mutta rakennuspiirrustukset itsessään eivät kuitenkaan ole sellaiset, joista haluttaisiin luoda konkreettinen ilmentymä.
Abstraktin luokan metodit voidaan määritellä abstrakteiksi,
jolloin niihin ei määritellä toteutusta. Abstrakti metodi
määritellään avainsanalla abstract
ja antamalla
metodin paluuarvo, nimi ja parametrit. Esimerkiksi abstrakti
metodi piirra()
.
public abstract void piirra();
Abstraktin metodin määrittely on lähes kuin rajapintametodin
määrittely. Sillä ei ole aaltosuluilla rajattavaa metodirunkoa, ja
metodimäärittely päättyy puolipisteeseen. Luodaan abstrakti luokka
Hahmo
, jolla on abstrakti metodi
piirra()
. Jokainen luokkaa Hahmo
laajentava ei abstrakti luokka joutuu toteuttamaan oman
piirra()
-metodinsa.
public abstract class Hahmo { protected String nimi; public Hahmo(String nimi) { this.nimi = nimi; } public String getNimi() { return this.nimi; } public void setNimi(String nimi) { this.nimi = nimi; } public abstract void piirra(); }
Abstraktin metodin määrittelyssä ei siis määritellä metodin
toteutusta, vaan vain tyyppi, nimi ja parametrit. Abstraktin
luokan perivät ei abstraktit luokat joutuvat toteuttamaan
abstraktit metodin. Jos abstrakti luokka perii abstraktin luokan,
ei sen tarvitse toteuttaa yläluokan abstraktia
metodia. Esimerkiksi seuraava luokka Elvis
määrittelee myös Elviksen piirtämisen.
public class Elvis extends Hahmo { public Elvis(String nimi) { super(nimi); } public void piirra() { System.out.println("G __"); System.out.println("\\\\ ,,)_"); System.out.println(" \\'-\\( /"); System.out.println(" \\ | ,\\"); System.out.println(" \\|_/\\\\"); System.out.println(" / _ '.D"); System.out.println(" / / \\ |"); System.out.println(" /_\\ /_\\"); System.out.println("'- '-"); System.out.println("\"" + this.nimi + "\""); } }
Ylimääräiset kenoviivat \ ennen kenoviivoja ja
lainausmerkkejä " johtuu siitä, että osa merkeistä
toimii erikoismerkkeinä. Esimerkiksi lainausmerkki aloittaa ja
lopettaa merkkijonon. Asettamalla kenoviivan ennen erikoismerkkiä,
esimerkiksi lainausmerkkiä, tulostetaan kenoviivaa seuraava
erikoismerkki normaalina merkkinä. Luodessamme luokan
Elvis
ilmentymän ja kutsuessamme sen
piirra()
-metodia seuraavasti saamme luontikappaleen
alla olevan tulosteen.
Elvis e = new Elvis("Matti L"); e.piirra();
G __ \\ ,,)_ \'-\( / \ | ,\ \|_/\\ / _ '.D / / \ | /_\ /_\ '- '- "Matti L"
Voimme vastaavasti luoda luokan Janis
(Jänis), jolla on oma piirra()
-metodinsa.
public class Janis extends Hahmo { public Janis(String nimi) { super(nimi); } public void piirra() { System.out.println("(\\___/)"); System.out.println("(='.'=)"); System.out.println("(\")_(\")"); } }
Jäniksen piirra()
-metodikutsu luo seuraavanlaisen tulosteen:
(\___/) (='.'=) (")_(")
Lisätään jänikselle vielä rajapinta Liikkuva
.
public interface Liikkuva { public void liiku(); public void liiku(int montaKertaa); }
Ja muutetaan jäniksen toteutusta siten, että piirretty jänis oikeasti liikkuu.
public class Janis extends Hahmo implements Liikkuva { private int sijainti; public Janis(String nimi) { super(nimi); this.sijainti = 0; } public void piirra() { tulostaTyhjaa(); System.out.println("(\\___/)"); tulostaTyhjaa(); System.out.println("(='.'=)"); tulostaTyhjaa(); System.out.println("(\")_(\")"); } private void tulostaTyhjaa() { for(int i = 0; i < sijainti; i++) { System.out.print(" "); } } public void liiku() { this.sijainti++; } public void liiku(int montaKertaa) { for(int i = 0; i < montaKertaa; i++) { liiku(); } } }
Kokeile minkälaisella koodilla saat jäniksen liikkumaan seuraavasti!
(\___/) (='.'=) (")_(") (\___/) (='.'=) (")_(") (\___/) (='.'=) (")_(")
Abstraktille luokalle voidaan myös tehdä sopimus rajapintojen
toteuttamisesta, aivan kuten kaikille muille luokille. Jos
abstraktille luokalle määritellään rajapinta, ei rajapinnan
metodeja ole kuitenkaan pakko toteuttaa abstraktin luokan
sisällä. Tällöin abstraktin luokan perivä (ei abstrakti) luokka
joutuu toteuttamaan rajapinnan määrittelemät metodit. Siirretään
yllä määritelty Liikkuva
-rajapinta abstraktiin
luokkaan Hahmo
.
public abstract class Hahmo implements Liikkuva { protected String nimi; public Hahmo(String nimi) { this.nimi = nimi; } public String getNimi() { return this.nimi; } public void setNimi(String nimi) { this.nimi = nimi; } public abstract void piirra(); }
Koska Liikkuva
-rajapinnan metodit ovat vain
sopimuksia metodien toteuttamisesta, aivan kuten abstraktit
metoditkin, ei rajapinnan metodeja tarvitse määritellä erikseen
Hahmo
-luokassa. Tällöin, samoin kuin abstraktien
metodien tapauksessa, perivät luokat joutuvat toteuttamaan
määritellyt metodit.
Esimerkiksi Hahmo
-luokan toteuttava luokka
Elvis
jouduttaisiin joko muuttamaan
abstraktiksi tai määrittelemään rajapinnan vaatimat
metodit. Abstraktin luokan ei tarvitse sisältää sovittujen
metodien toteutuksia, kun taas normaalissa luokassa niiden on
oltava joko kyseisessä luokassa tai periytymisen kautta. Lisätään
Elvikselle myös liikkumismetodi.
public class Elvis extends Hahmo { private boolean vasen; public Elvis(String nimi) { super(nimi); this.vasen = true; } public void piirra() { if (vasen) { piirraVasen(); } else { piirraOikea(); } System.out.println("\"" + this.nimi + "\""); } private void piirraVasen() { System.out.println("G __"); System.out.println("\\\\ ,,)_"); System.out.println(" \\'-\\( /"); System.out.println(" \\ | ,\\"); System.out.println(" \\|_/\\\\"); System.out.println(" / _ '.D"); System.out.println(" / / \\ |"); System.out.println(" /_\\ /_\\"); System.out.println("'- '-"); } private void piirraOikea() { System.out.println(" __ G"); System.out.println(" _(,, //"); System.out.println(" \\ )/-'/"); System.out.println(" /, | /"); System.out.println(" //\\_|/"); System.out.println(" D.' _ \\"); System.out.println(" | / \\ \\"); System.out.println(" / \\ / \\"); System.out.println(" -' -'"); } public void liiku() { this.vasen = !this.vasen; // käänteinen totuusarvo huutomerkin avulla! } public void liiku(int montaKertaa) { System.out.println("Sori, boogie toimii askel kerrallaan."); } }
Kuten seuraavasta esimerkistä ja tulosteesta huomaamme, Elviksemme osaa diskotanssin alkeet (jo ennen diskoa!).
Elvis e = new Elvis("Matti L"); e.piirra(); e.liiku(); e.piirra(); e.liiku(); e.piirra();
G __ \\ ,,)_ \'-\( / \ | ,\ \|_/\\ / _ '.D / / \ | /_\ /_\ '- '- "Matti L" __ D _(,, // \ )/-'/ /, | / //\_|/ G.' _ \ | / \ \ / \ / \ -' -' "Matti L" G __ \\ ,,)_ \'-\( / \ | ,\ \|_/\\ / _ '.D / / \ | /_\ /_\ '- '- "Matti L"
Polymorfismi on neljäs olio-ohjelmoinnin peruskäsitteistä kapseloinnin, abstrahoinnin ja periytymisen lisäksi. Polymorfismilla tarkoitetaan olioiden monimuotoisuutta, eli oliolla voi olla monta eri tyyppiä. Eri tyypit, joita olio voi edustaa, koostuvat sen perintöhierarkiasta ja toteutetuista rajapinnoista.
Yllä olevalla Elvis
-luokallamme on neljä eri tyyppiä:
Elvis
, eli sen oma luokkaHahmo
, eli Elviksen yläluokkaLiikkuva
, eli Hahmon sopima rajapinta, sekäObject
, eli kaikkien luokkien yläluokkaLuokasta Elvis
luodun olion voi siis antaa neljänä erityyppisenä parametrina. Elvis-olion voisi siis antaa kaikille neljästä seuraavasta metodista.
public void metodi(Elvis elvis) { ... } public void metodi(Hahmo hahmo) { ... } public void metodi(Liikkuva liikkuva) { ... } public void metodi(Object object) { ... }
Kussakin tapauksessa annetulle oliolle voi kutsua vain siihen
tyyppiin liittyviä metodeja, vaikka itse annettu viite oikeasti
viittaisikin Elvis
-tyyppiseen olioon. Esimerkiksi
metodikutsu public void metodi(Liikkuva l)
, jolle
annetaan parametrina Elvis
-tyyppinen olio, tietää
vain Liikkuva
-rajapinnan määrittelevät metodit
liiku()
ja liiku(int
montaKertaa)
. Seuraava metodikutsu yrittää liikuttaa
Liikkuva
-tyyppistä oliota yhteensä 5 kertaa.
public void liikuta(Liikkuva liikkuva) { // mahdollisia kutsuja siis vain liikkuva.liiku(); // ja liikkuva.liiku(4); // jossa parametrina annettu luku voi olla mikä tahansa kokonaisluku }
Toisaalta, koska Liikkuva
-rajapinta ei tiedä
Elvis
-luokan muista metodeista, ei esimerkiksi
seuraava olisi mahdollista (vaikka parametrina annettaisiin
Elvis
-luokasta luotu olio!).
public void liikuta(Liikkuva liikkuva) { liikkuva.piirra(); // ei mahdollinen kutsu, sillä Liikkuva rajapinnassa ei määritelty piirra()-metodia }
Luokalle Hahmo
taas metodin piirra()
kutsuminen olisi mahdollista. Huomaa että koska Hahmo
on abstrakti luokka, kutsutaan oikeasti sen perivän luokan
määrittelemää metodia piirra()
. Esimerkiksi
seuraavalle metodille voisi antaa minkä tahansa
Hahmo
-luokan perivän luokan ilmentymän.
public void piirraHahmo(Hahmo hahmo) { hahmo.piirra(); }
Voisimme antaa yllä olevalle metodille sekä
Elvis
-tyyppisiä olioita, että
Jänis
-tyyppisiä olioita, sillä ne molemmat perivät
Hahmo
-luokan, jolloin Hahmo
on yksi
niiden mahdollisista muodoista. Yllä olevaa metodia kutsuttaessa
seuraavasti saisimme alla olevan esimerkin jälkeen olevan
tulosteen.
Elvis elmeri = new Elvis("Matti L"); Janis jano = new Janis("Matti P"); piirraHahmo(elmeri); System.out.println(); piirraHahmo(jano);
G __ \\ ,,)_ \'-\( / \ | ,\ \|_/\\ / _ '.D / / \ | /_\ /_\ '- '- "Matti L" (\___/) (='.'=) (")_(")
Viitetyyppisen muuttujan tyyppi kertoo siis sen mitä metodeja ja attribuutteja muuttujalla on käytössä. Monimuotoisuutensa takia viite voi osoittaa oikeasti erityyppiseen olioon, kuin mikä tyyppi oliolle on annettu.
On kuitenkin oleellista huomata, että olipa olioon viittaavan muuttujan tyyppi mikä tahansa, kutsuttaessa olion metodia suoritetaan aina olion todellisen tyypin versio metodista.
Eli jos esim. peritään Elviksestä seuraava luokka:
public class ValeElvis extends Elvis { public Elvis(String nimi) { super(nimi); } public void piirra() { System.out.println(" o "); System.out.println(" /|\\"); System.out.println(" / \\"); } }
Luodaan vale-elviksiä ja piirretään ne:
ValeElvis e1 = new ValeElvis("Pekka"); Elvis e2 = new ValeElvis("Martin"); Hahmo e3 = new ValeElvis("Antti"); e1.piirra(); e2.piirra(); e3.piirra();
Luodaan siis kolme vale-elvistä, mutta sijoitetaan vale-elvikset kaikki erityyppisiin oliomuuttujiin.
Kaikissa tapauksissa tulostuu samanlainen tikku-ukko, eli vaikka esim. on määritelty Elvis e2 = new ValeElvis("Martin")
eli muuttujan tyyppinä on Elvis, on "langan päässä" kuitenkin vale-elvis ja metodikutsu e2.piirra()
tuottaa vale-elvismäisen tikku-ukon.
Abstrakti luokka Kappale
mallintaa "pohjaltaan ja kanneltaan"
yhtä suuria kappaleita, joilla kaikilla on ominaisuus korkeus
.
Kaikkien tälläisten kappaleiden tilavuus lasketaan samalla tavalla: korkeus * pohjan pinta-ala
Abstrakti luokka siis tietää miten miten tilavuus lasketaan:
public abstract class Kappale { // ... public double tilavuus() { return korkeus*pintaAla(); } // ... }
Mutta pinta-alaa se ei osaa laskea. Pinta-ala riippuu konkreettisesta Kuviosta, esim. neliön pohjan pinta-ala on sivu*sivu, kun taas lieriöllä pinta-ala on pohjan säde toiseen kertaa pii.
Lisätäänkin luokalla Kappale abstrakti metodi pintaAla()
, johon voidaan "ripustaa"
erityisen kappaleen pohjan (ja samalla "kannen") pinta-alan laskentataito.
public abstract class Kappale { private double korkeus; public Kappale(double korkeus) { this.korkeus = korkeus; } public abstract double pintaAla(); // perivän luokan on ylikirjoitettava tämä public double tilavuus() { return korkeus*pintaAla(); } }
Kuutio
ylikirjoittaa metodiin pintaAla()
sillä sillä on kyky laskea kuution pohjan (ja "kannen") pinta-ala:
public class Kuutio extends Kappale { private double sivu; public Kuutio(double sivu) { super(sivu); this.sivu = sivu; } public double pintaAla() { return sivu*sivu; } }
Lierio
ylikirjoittaa metodiin pintaAla()
sillä sillä on kyky laskea lieriön pohjan (ja "kannen") pinta-ala:
public class Lierio extends Kappale { private double sade; public Lierio(double korkeus, double sade) { super(korkeus); this.sade = sade; } public double pintaAla() { return sade*sade*Math.PI; } }
Näin jokaiseen Kappaleen perivään luokkaan saadaan valmiiksi taito laskea tilavuus, tarvitaan ainoastaan konkreettinen toteutus pinta-alan laskemiselle.
Yliluokassa Kappale
määriteltyä metodia tilavuus()
nimitetään "template-metodiksi", se määrittelee abstraktilla tavalla kaavan (engl. template) miten tilavuus lasketaan. Metodi kuitenkin käyttä abstraktia metodia pintaAla()
, eli ei osaa itse konkreettisesti laskutoimitusta suorittaa.
Aliluokka ylikirjoittaa abstraktin metodin pintaAla()
joka tulee kutsutuksi kun aliluokan tilavuutta selvitetään.
Tämäntyylisestä tekniikasta, jossa aliluokka joutuu määrittelemään metodin jota kutsutaan muualta nimitetään joskus "Hollywood-periaatteeksi: 'Don't call us, we call you'".
Tarkastellaan ensin kuinka ArrayListin
alkiot voidaan käydä läpi.
ArrayList <String> nimet = new ArrayList <String>(); nimet.add("Antti"); nimet.add("Juhana"); nimet.add("Martin"); nimet.add("Matti"); nimet.add("Pekka"); // käsittelemällä suoraan ArrayListin paikkoja for(int i = 0; i < nimet.size(); i++) { System.out.println(nimet.get(i)); } // tai ns. for-each silmukalla for(String nimi : nimet) { System.out.println(nimi); }
Javan mekanismi for-each-silmukan toteuttamiseen on Iterable
-rajapinta.
Kaikki oliot, jotka toteuttavat Iterable
-rajapinnan voidaan askeltaa (iteroida)
läpi käyttäen for-each-silmukkaa. Iterable
rajapinta
on itsessään varsin yksinkertainen. Se edellyttää seuraavan metodin toteuttamista.
public Iterator<T> iterator()<T>;
Siis, jotta olion läpi pystyttäisiin askeltamaan for-each-silmukalla, pitää sillä olla metodi
iterator()
, joka palauttaa Iterator-tyyppisen olion. Iterator-oliot ovat
indeksimuuttujien yleistyksiä. Ohessa on kolmas erilainen tapa iteroida ArrayListin alkiot läpi.
ArrayList<String> nimet = new ArrayList<String>(); ... Iterator<String> iteraattori = nimet.iterator(); while(iteraattori.hasNext()) { System.out.println(iteraattori.next()); }
Ylläoleva tapa käyttää hyväkseen Iterator-rajapintaa. Iteraattori-olio viittaa aina tiettyyn kohtaan
sitä vastaavassa joukossa. Jos iteraattori on iteroinut joukon läpi, niin sen metodi
hasNext()
palauttaa arvon false
. Mikäli joukossa on vielä jäljellä läpikäytäviä
alkioita, niin hasNext()
palauttaa arvon true
. Iteratorin metodi
next()
edistää iteraattorin seuraavan elementtiin ja palauttaa nykyisen elementin
paluuarvonaan. Paluuarvon tyyppi määräytyy Iterator
-olion tyyppiparametrista (edellä String).
Iterator<E>
-rajapinta edellyttää seuraavien metodien toteuttamista. Tässä
E on ns. tyyppiparametri. Iteraattorin tapauksessa tyyppiparametri kertoo, minkä
tyyppisiä objekteja next()
-metodi palauttaa (eli minkä tyyppisten objektien yli
iteroidaan).
public E next(); //edistää iteraattorin seuraavaan elementtiin ja palauttaa nykyisen arvon public boolean hasNext(); //kertoo onko iteraattorilla vielä seuraavaa elementtiä public void remove(); //poistaa nykyisen elementin
Seuraavassa esimerkissä luodaan oma iteraattori-luokka Hyppija
, joka
käy läpi annetun Integer
-tyyppisen ArrayList
in joka
n:nnen alkion läpi, missä n konstruktorissa annettu parametri
pompunPituus
.
class Hyppija implements Iterator<Integer> { private int pompunPituus; private int kohta; private ArrayList<Integer> lista; public Hyppija(ArrayList<Integer> lista, int pompunPituus) { this.kohta = 0; this.lista = lista; this.pompunPituus = pompunPituus; } public void remove() { lista.remove(this.kohta); } public boolean hasNext() { return this.kohta < lista.size(); } public Integer next() { Integer nykyinen = this.lista.get(kohta); kohta += pompunPituus; return nykyinen; } }
Seuraavassa ohjelmassa Hyppija
-olio luodaan listalle, joka sisältää luvut 0..99.
public static void main(String[] args) { ArrayList<Integer> lista = new ArrayList<Integer>(); for(int i = 0; i < 100; ++i) lista.add(i); Hyppija hyppija = new Hyppija(lista, 7); while(hyppija.hasNext()) { System.out.print(hyppija.next() + " "); } System.out.println(); }Hyppyjen pituudeksi määriteltiin 7, joten ohjelma tulostaa seitsemällä jaolliset luvut.
0 7 14 21 28 35 42 49 56 63 70 77 84 91 98
Luokan muuttaminen yleiseksi ns. geneeriseksi käy helposti. Seuraavassa on näytetty ainoastaan edellisestä muuttuneet kohdat.
class Hyppija<Tyyppi> implements Iterator<Tyyppi> { private int pompunPituus; private int kohta; private ArrayList<Tyyppi> lista; public Hyppija(ArrayList<Tyyppi> lista, int pompunPituus) { this.kohta = 0; this.lista = lista; this.pompunPituus = pompunPituus; } ... public Tyyppi next() { Tyyppi nykyinen = this.lista.get(kohta); kohta += pompunPituus; return nykyinen; } }
Edellä <Tyyppi>
on tyyppiparametri. Nyt luokka on paljon monikäyttöisempi.
Sitä voidaan käyttää esimerkiksi merkkijonoja sisältävän ArrayList
in yhteydessä.
public static void main(String[] args) { ArrayList<String> lista = new ArrayList<String>(); lista.add("eka"); lista.add("toka"); lista.add("kolmas"); lista.add("neljäs"); lista.add("viides"); lista.add("kuudes"); lista.add("seitsemäs"); lista.add("kahdeksas"); lista.add("yhdeksäs"); Hyppija<String> hyppija = new Hyppija(lista, 2); while(hyppija.hasNext()) { System.out.print(hyppija.next() + " "); } System.out.println(); }
Huomaa, että edellä joudumme parametrisoimaan Hyppija
-luokan ilmentymän
samaan tapaan kuin jo tuttujen ArrayList
ien yhteydessä.
Ohjelma tulostaa listan joka toisen alkion:
eka kolmas viides seitsemäs yhdeksäs
Olemme tähän mennessä rakentaneet ohjelmamme kahdella eri tavalla. Ohjelmat ovat joko suorittaneet laskutoimituksia, jonka jälkeen ne ovat lopettaneet toimintansa, tai niitä on ohjattu tekstipohjaisen käyttöliittymän kautta. Tutustutaan tässä kappaleessa graafisen käyttöliittymän luomiseen Javalla.
Käyttöliittymät koostuvat pohjakerroksesta, niinkutsutusta containerista, ja siihen asetetuista komponenteista. Komponentteja ovat napit, tekstit, ym. Container voi sisältää myös toisen containerin. Seuraava esimerkki luo dialogityylisen ikkunan, ja asettaa siihen napin (luokka JButton
) jossa on teksti "Ok!".
JFrame dlg = new JFrame(); JButton ok = new JButton("Ok!"); // lisätään dialogin containeriin nappi "ok" Container container = dlg.getContentPane(); container.add(ok); // asetetaan koko 240*120 pikseliä dlg.setSize(240, 120); // sulje ikkuna kun käyttäjä painaa X dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // näytä ikkuna dlg.setVisible(true);
Napin painaminen ei vielä tee yhtään mitään. Jos haluamme lisätä dialogiin uuden komponentin, esimerkiksi tekstielementin, joudumme päättämään sille sijainnin. Containerille on määritelty myös ulkoasu, eli jokaisella sen sisältämällä komponentilla on myös sijainti. Luokka BorderLayout
on eräs ulkoasun määrittelyyn käytetty luokka. Sen avulla voidaan määritellä komponenttien sijainti ilmansuuntia käyttäen (NORTH, EAST, SOUTH, WEST
). Muutetaan dialogia siten, että ikkunassa on pohjoisessa nappi "Ok!", ja etelässä teksti (luokka JLabel
) "Eipäs!".
// luodaan ikkuna JFrame dlg = new JFrame(); // luodaan komponentit JButton ok = new JButton("Ok!"); JLabel eipas = new JLabel("Eipäs!"); // lisätään komponentit containeriin Container container = dlg.getContentPane(); container.add(ok, BorderLayout.NORTH); container.add(eipas, BorderLayout.SOUTH); // ikkunan ulkoasun ja käyttäytymisen asettelua dlg.setSize(240, 120); dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); dlg.setVisible(true);
Tässäkään esimerkissä napin painaminen ei vielä tee mitään.
Käyttöliittymiä rakennettaessa jokainen komponentti on oma osansa käyttöliittymää. Tällöin jokaiselle komponentille täytyy myös tehdä oma tapahtumanhallinta. Java tarjoaa joukon erilaisia tapahtumankuuntelijoita, jotka voidaan liittää komponentteihin. Jos komponentilla (esimerkiksi nappi) on tapahtumankuuntelija, ja komponentissa tapahtuu toiminto (esimerkiksi nappia painetaan), kuulee tapahtumankuuntelija tapahtuman, ja suorittaa tapahtumankäsittelyyn ohjelmoidun ohjelman.
Tapahtumien kuuntelijat on määritelty rajapintoina. Esimerkiksi rajapinta ActionListener
määrittelee metodin actionPerformed()
, joka saa käyttöliittymästä parametrina ActionEvent
-olion. Olio ActionEvent
sisältää tapahtumaan liittyvät tiedot. Luodaan luokka, joka toteuttaa tapahtumankuuntelijan ActionListener
.
public class NapinKuuntelija implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("Nappia painettu!"); } }
Tapahtumankuuntelijan voi rekisteröidä, eli liittää komponenttiin, komponenttiin liittyvillä metodeilla. Esimerkiksi luokalla JButton
on metodi addActionListener()
, jolle voidaan antaa parametrina ActionListener
-rajapinnan toteuttava luokka. Esimerkiksi ylläolevaan ohjelmaan voidaan liittää luokka Napinkuuntelija
seuraavasti.
// luodaan ikkuna JFrame dlg = new JFrame(); // luodaan komponentit JButton ok = new JButton("Ok!"); JLabel eipas = new JLabel("Eipäs!"); // lisätään napille tapahtumankuuntelija ok.addActionListener(new NapinKuuntelija()); // lisätään komponentit containeriin Container container = dlg.getContentPane(); container.add(ok, BorderLayout.NORTH); container.add(eipas, BorderLayout.SOUTH); // ikkunan ulkoasun ja käyttäytymisen asettelua dlg.setSize(240, 120); dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); dlg.setVisible(true);
Nyt jos nappia painetaan kolmesti saadaan konsoliin seuraavanlainen tuloste:
Nappia painettu! Nappia painettu! Nappia painettu!
Muutetaan luokkaa NapinKuuntelija
siten, että se saa parametrina JLabel
-tyyppisen tekstielementin. Kun nappia painetaan, tekstielementin tekstiksi asetetaan teksti "Nappia painettu!"
public class NapinKuuntelija implements ActionListener { private JLabel tekstielementti; public NapinKuuntelija(JLabel teksti) { this.tekstielementti = teksti; } public void actionPerformed(ActionEvent e) { teksti.setText("Nappia painettu!"); } }
Muutetaan dialoginluontia vielä siten, että NapinKuuntelija
saa parametrikseen JLabel
-olion
// lisätään napille tapahtumankuuntelija ok.addActionListener(new NapinKuuntelija(eipas));
Napin painalluksen jälkeen ikkuna näyttää seuraavalta.
Voimme myös periä JFrame
luokan ja rakentaa oman ikkunamme sen avulla. Seuraava esimerkki NapinPainallus
vastaa yllä olevaa esimerkkiä, mutta olemme käyttäneet perittyä luokkaa suoraan. Myös tapahtumankäsittely hoidetaan luokassa NapinPainallus
.
public class NapinPainallus extends JFrame implements ActionListener { // käyttöliittymän komponentit private JButton okButton; private JLabel eipasLabel; public NapinPainallus() { super(); okButton = new JButton("Ok!"); eipasLabel = new JLabel("Eipäs!"); okButton.addActionListener(this); Container container = getContentPane(); container.add(okButton, BorderLayout.NORTH); container.add(eipasLabel, BorderLayout.SOUTH); setSize(240, 120); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setVisible(true); } public void actionPerformed(ActionEvent e) { eipasLabel.setText("Nappia painettu!"); } }
Nyt voimme luoda uuden ikkunan main()
-metodissa kutsumalla new NapinPainallus()
.
public class Main { public static void main(String[] args) { new NapinPainallus(); } }
Luokkaa JPanel
käytetään muunmuassa piirtoalustana. Luokalla JPanel
on metodi paint()
, jota kutsutaan aina kun paneeli halutaan piirtää. Metodi paint()
saa käyttöliittymältä Graphics
-tyyppisen olion, jolla voidaan piirtää paneelille. Laajennetaan JPanel
luokkaa luokalla Piirturi
, ja muutetaan sen piirtotoiminnallisuutta.
public class Piirturi extends JPanel { public void paint(Graphics g) { super.paint(g); } }
Luokkamme Piirturi
ei vielä tee mitään muuta, kuin kutsuu yläluokkansa (eli JPanel
-luokan) paint()
-metodia. Luodaan myös pääohjelma, jossa luodaan JDialog
-tyyppinen ikkuna, ja asetetaan sen containeriin luokan Piirturi
ilmentymä.
// luodaan ikkuna JFrame dlg = new JFrame(); // luodaan piirturi Piirturi piirturi = new Piirturi(); // otetaan viite ikkunan containeriin ja asetetaan piirturi siihen Container container = dlg.getContentPane(); container.add(piirturi); // ikkunan koko, sulkemistoiminnallisuus ja näkyväksi asetus dlg.setSize(480, 360); dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); dlg.setVisible(true);
Ohjelmaa ajettaessa ikkuna näyttää seuraavalta.
Piirretään piirturiin ympyrä. Graphics-oliolla piirrettäessä iso osa metodeista määrittelee piirrettävän objektin siten, että ensiksi annetaan sen vasemman yläkulman koordinaatit, jonka jälkeen tulee piirrettävän kuvion leveys ja korkeus. Esimerkiksi seuraava kutsu luo kohdasta (30, 30) alkavan ympyrän, jonka halkaisija on 100. Huomaa että kohta 30, 30 lasketaan paneelin vasemmasta ylälaidasta!
g.fillOval(30, 30, 100, 100);
Ikkuna näyttää nyt seuraavanlaiselta.
Lisätään vielä toinen ja kolmas ympyrä. Kolmas ympyrä ei ole täytetty.
g.fillOval(250, 40, 80, 80); g.drawOval(40, 200, 300, 100);
Ikkuna näyttää nyt kokonaisuudessaan seuraavanlaiselta.
Piirturi luokan sisältämä lähdekoodi on seuraavanlainen.
public class Piirturi extends JPanel { public void paint(Graphics g) { super.paint(g); g.fillOval(30, 30, 100, 100); g.fillOval(250, 40, 80, 80); g.drawOval(40, 200, 300, 100); } }
Tutustutaan näppäimistön kuunteluun tarkoitettuun rajapintaan, eli rajapintaan KeyListener
. Rajapinta KeyListener
tarjoaa metodit näppäimistön tapahtumien kuunteluun. Rajapinta määrittelee kolme erilaista näppäimistöltä tulevaa tapahtumaa, kirjaimen kirjoitus (keyTyped()
), paino (keyPressed()
) ja nosto (keyReleased()
. Toteutetaan rajapinta KeyListener
luokassa Piirturi
.
public class Piirturi extends JPanel implements KeyListener { public void paint(Graphics g) { super.paint(g); g.fillOval(30, 30, 100, 100); g.fillOval(250, 40, 80, 80); g.drawOval(40, 200, 300, 100); } public void keyPressed(KeyEvent e) { } public void keyTyped(KeyEvent e) { } public void keyReleased(KeyEvent e) { } }
KeyListener
-rajapinnan tarjoavat metodit saavat parametrina KeyEvent
-tyyppisen olion käyttöliittymältä. KeyEvent
-oliolta voimme esimerkiksi kysyä, mitä nappia on painettu (metodi getKeyCode()
). Muutetaan piirturi-luokkaa siten, että nuoli ylös pienentää hahmon suuta, nuoli alas suurentaa sitä (eli sen korkeus muuttuu). Käytetään tähän metodia keyPressed()
, sillä jos näppäimistön nappia pidetään pohjassa metodia keyPressed()
kutsutaan jatkuvasti.
public class Piirturi extends JPanel implements KeyListener { int y = 0; public void paint(Graphics g) { super.paint(g); g.fillOval(30, 30, 100, 100); g.fillOval(250, 40, 80, 80); g.drawOval(40, 200, 300, 100 + y); } public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_UP) { this.y--; repaint(); } if (e.getKeyCode() == KeyEvent.VK_DOWN) { this.y++; repaint(); } } public void keyTyped(KeyEvent e) { } public void keyReleased(KeyEvent e) { } }
Tapahtumankäsittelijä pitää rekisteröidä ikkunaan, sillä se on uloin käyttöliittymän kerros. Voimme rekisteröidä näppäimistönkuuntelijan pääohjelmalle seuraavasti.
// luodaan ikkuna JFrame dlg = new JFrame(); // luodaan piirturi Piirturi piirturi = new Piirturi(); // otetaan viite ikkunan containeriin ja asetetaan piirturi siihen Container container = dlg.getContentPane(); container.add(piirturi); // lisätään näppäimistönkuuntelija dlg.addKeyListener(piirturi); // ikkunan koko, sulkemistoiminnallisuus ja näkyväksi asetus dlg.setSize(480, 360); dlg.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); dlg.setVisible(true);
Alla oleva kuva on kuva hahmosta kun ylöspäin nappia on pidetty muutama hetki pohjassa.
Paraskaan ohjelmoija tekee virheitä. Ohjelmia on siis testattava. Testauksen tekeminen antamalla ohjelmalle syötteitä käsin ja varmistamalla visuaalisesti että ohjelma toimii oikein on vaivalloista. Testaus onkin syytä automatisoida.
Olemme jo muutamassa tehtävässä "automatisoineet" syötteen, eli antaneet Scanner-oliolle parametriksi merkkijonon, jonka se tulkitesee käyttäjän näppäimistöltä antamaksi syötteeksi. Asia voidaan kuitenkin viedä vielä pitemmälle ja laittaa kone myös tarkistamaan että ohjelman tuottama vastaus on odotettu.
Poikkeuksia voi heittää throw
-komennolla.
throw
-komennolle annetaan virheolio, joka on
Exception
luokan tai sen aliluokan ilmentymä.
Tähän mennessä ollaan jo törmätty ainakin virheisiin NullPointerException
ja IndexOutOfBoundsException
.
Katsotaan seuraavaksi esimerkkiä virheen heittämisestä. Seuraava metodi
etsii int
-taulukosta sen pienimmän alkio ja heittää virheen,
jos tätä ei voida tehdä (jos annettu taulukko olikin tyhjä).
public static int minimi(int[] taulu) { if (taulu == null) throw new NullPointerException(); if (taulu.length == 0) throw new IllegalArgumentException("ei voida laskea minimiä tyhjälle taulukolle"); int min = taulu[0]; for (int luku : taulu) { min = Math.min(min, luku); } return min; }
Esimerkissä heitetään NullPointerException
, jos parametrina annettiin
null
ja IllegalArgumentException
jos taulukko olikin tyhjä.
Virheluokkien konstruktoreille voi antaa String
- parametrin, joka kuvaa
virhetilannetta. Tämä parametri ei kuitenkaan ole välttämätön.
Luokka Exception
on poikkeusten yläluokka, eli
kaikki poikkeukset perivät luokan
Exception
. Polymorfismin ansiosta voimme varautua
kaikkiin poikkeuksiin varautumalla poikkeustyyppiin
Exception
, eli kaikkien poikkeusten yläluokkaan. Tämä
ei kuitenkaan aina ole tarpeellista tai toivottua, koska erilaiset
poikkeustilanteet saattavat tarvita erilaista käsittelyä.
Yksi eniten nähdyistä poikkeuksista on
NullPointerException
, eli poikkeustilanne, joka
johtuu null
-viitteen käyttämisestä esimerkiksi
metodikutsussa. Voimme nähdä NullPointerException
poikkeuksen esimerkiksi seuraavan lähdekoodin avulla.
String merkkijono = null; System.out.println("Merkkijonon pituus on " + merkkijono.length());
Koska viite merkkijono
on null, ei
String
-olioon liittyvän length()
metodin
suoritus onnistu ja ohjelma heittää poikkeuksen
NullPointerException
.
Aiemmin luomamme metodi lueTiedosto()
varautuu
poikkeustyyppiin Exception
, vaikka
Scanner
-luokan konstruktori oikeasti heittää
FileNotFoundException
-tyyppisen
poikkeuksen. Poikkeukset ovat yleensä hyvin nimettyjä, esimerkiksi
FileNotFoundException kertoo sen, että haettua tiedostoa ei
löytynyt tai sen avaamisessa oli ongelmia. Voimme muuttaa
lueTiedosto()
-metodia varautumaan
FileNotFound
-tyyppiseen poikkeukseen seuraavasti.
public static String lueTiedosto(String tiedostonNimi) { // ei heitetä poikkeusta File tiedosto = new File(tiedostonNimi); Scanner lukija = null; try { lukija = new Scanner(tiedosto); } catch (FileNotFoundException e) { System.err.println("Tiedoston avaaminen ei onnistunut!"); return null; } String teksti = ""; while(lukija.hasNextLine()) { teksti += lukija.nextLine() + "\n"; } return teksti; }
Lohko catch() {}
määrittelee varauduttavan
poikkeuksen lisäksi muuttujan, mihin poikkeuksen tiedot
tallennetaan. Esimerkiksi seuraava rakenne määrittelee poikkeuksen
FileNotFoundException
tallentumisen muuttujaan
e
. Koska poikkeukset perivät yläluokan
Exception
, voimme käyttää sen metodeja poikkeuksen
tietojen tulostamiseen.
try { // ohjelmakoodi, joka saattaa heittää poikkeuksen } catch (FileNotFoundException e) { // poikkeuksen tiedot ovat tallessa muuttujassa e }
Luokka Exception
tarjoaa muutamia hyödyllisiä
metodeja. Esimerkiksi metodi printStackTrace()
tulostaa polun, joka johti poikkeukseen. Tutkitaan seuraavaa
metodin printStactTrace()
tulostamaa virhettä.
Exception in thread "main" java.lang.NullPointerException at pakkaus.Luokka.tulosta(Luokka.java:43) at pakkaus.Luokka.main(Luokka.java:29)
Poikkeuspolun lukeminen tapahtuu alhaalta ylöspäin. Alimpana on
ensimmäinen kutsu, eli ohjelman suoritus on alkanut luokan
Luokka
metodista main()
. Rivillä 29 on
kutsuttu metodia tulosta()
. Metodissa
tulosta()
on tapahtunut rivillä 43 poikkeus,
NullPointerException
. Poikkeuksen tiedot ovatkin
hyvin hyödyllisiä virhekohdan selvittämisessä.
Poikkeuksiin varauduttaessa on mahdollista määritellä myös
lohko, joka suoritetaan riippumatta siitä, tapahtuiko poikkeus vai
ei. Lohko finally { }
tulee poikkeuslohkon jälkeen,
ja se sisältää joka tapauksessa suoritettavan koodin. Finally
lohkon lähdekoodi suoritetaan sen jälkeen, kun try {}
ja catch {}
-lohkot on suoritettu. Katsotaan jo
nähtyä esimerkkiä tiedostoon kirjoittamisesta.
public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception { FileWriter kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.write(teksti); kirjoittaja.close(); }
Luokan FileWriter
konstruktori siis avaa
parametrina annetun tiedoston kirjoittamista varten, ja yrittää
kirjoittaa siihen. Yllä olevassa esimerkissä on kaksi erillistä
poikkeuskohtaa. Luokka FileWriter
voi heittää
poikkeuksen IOException
jos tiedostoa ei jostain
syystä saada avattua kirjoittamista varten. Metodi
write()
voi myös heittää poikkeuksen jos tiedostoon
kirjoittaminen ei onnistu, vaikka tiedoston avaaminen olisikin jo
onnistunut. Muutetaan metodia siten, että poikkeustenkäsittely
tapahtuu metodin sisällä.
public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) { // <- enää ei tarvitse heittää poikkeusta FileWriter kirjoittaja = null; try { kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.write(teksti); kirjoittaja.close(); } catch (Exception e) { System.err.println("Tiedostoon kirjoittaminen epäonnistui."); e.printStackTrace(); } }
Poikkeusten käsittelyyn siirrytään heti poikkeuksen
tapahtuessa, esimerkiksi jos ylläolevassa esimerkissä
konstruktorikutsu new FileWriter()
heittää
poikkeuksen, ei sitä seuraavaa kahta kutsua suoriteta
ollenkaan. Jos poikkeus tapahtuu metodissa write()
,
jää metodikutsu close()
suorittamatta, ja tiedosto
jää auki.
Lohko finally {}
on erinomainen tapauksiin, joissa
täytyy sulkea resursseja lopuksi, riippumatta suorituksen
kulusta. Siirretään metodikutsu close
lohkoon
finally {}
, jotta sen suoritus tapahtuu aina.
public static void kirjoitaTiedostoon(String tiedostonNimi, String teksti) { FileWriter kirjoittaja = null; try { kirjoittaja = new FileWriter(tiedostonNimi); kirjoittaja.write(teksti); } catch (Exception e) { System.err.println("Tiedostoon kirjoittaminen epäonnistui."); e.printStackTrace(); } finally { if(kirjoittaja != null) { kirjoittaja.close(); } else { System.err.println("Kirjoittajaa ei koskaan luotu."); } } }
Jos metodi palauttaa arvon try {}
tai
catch
-lohkossa, lohkon finally
sisältö
suoritetaan silti. Tutkitaan metodia vitonen()
, joka
palauttaa arvon 5 try
-lohkossa, ja tulostaa merkkijonon
Moi
finally lohkossa.
public static int vitonen() { try { return 5; } catch (Exception e) { } finally { System.out.println("Moi!"); } return 0; }
System.out.println(vitonen());
Moi! 5
Esimerkin tulostuksesta huomataan että lohko finally
suoritetaan ennen arvon palauttamista, jolloin myös
merkkijono Moi!
tulostuu ennen
lukua 5
.
Huomaamme suurempia ohjelmia suunniteltaessa ja toteuttaessa luokkamäärän kasvavan suureksi. Pakkauksilla (package
) voidaan jakaa luokat eri sijainteihin niiden toiminnallisuuden perusteella, ja parantaa ohjelmiston ylläpidettävyyttä ja hallittavuutta. Pakkausten käyttö tarkoittaa käytännössä sitä, että luokat sijaitsevat kansioissa projektin sisällä.
Luokka määrittelee oman pakkauksensa määreellä package
. Esimerkiksi seuraava luokka Kirja
sijaitsee pakkauksessa kirjasto
.
package kirjasto; public class Kirja { private String nimi; private String sisalto; public Kirja() { } ... }
Huomaa että pakkausten nimet kirjoitetaan aina pienellä!
Jos luokan pakkauksena on kirjasto, sijaitsee se ohjelman lähdekoodikansion sisällä kansiossa kirjasto
. Vastaavasti voimme luoda pakkauksia siten, että luokat on jaettu useammalle tasolle. Esimerkiksi seuraava luokka KirjanLainaaja
sijaitsisi kansion kirjasto sisällä olevassa kansiossa lainaus.
package kirjasto.lainaaja; public class KirjanLainaaja { private String sijainti; public KirjanLainaaja() { } ... }
Kaikki hyvät ohjelmointiympäristöt, kuten NetBeans ja Eclipse, tarjoavat valmiit pakkausten hallintaan, jolloin ohjelmoijan ei tarvitse huolehtia kansioista. Uuden pakkauksen voi luoda NetBeansissa projektin Source Packages
-osiossa oikeaa hiirennappia painamalla ja valitsemalla New -> Java Package...
. Luodun pakkauksen sisälle voidaan luoda luokkia kuten oletuspakkaukseenkin (default package
).
Pakkausten käyttö mahdollistaa myös lähdekoodin jakamisen siten, että eri projekteissa toteutetut lähdekoodit eivät sekoitu toisiinsa. Yksi nimeämiskäytäntö paketeille on maa.organisaation-webosoite.projekti....
, esimerkiksi fi.visiojapojat.supertuote
, jotka alla on supertuote-projektiin liittyvät lähdekoodit (ja pakkaukset!).
Javan API:n pakkaukset ovat yleensä pakkauksissa java.alue.
. Esimerkiksi Javan luokka ArrayList
sijaitsee pakkauksessa java.util
, eli javan työkalut. Ottaessamme luokan ArrayList
käyttöömme komennolla import java.util.ArrayList
kerromme käytännössä halutun luokan nimen, sekä pakkauksen jonka sisällä luokka sijaitsee.
Jos pakkauksessa kirjasto.lainaaja
sijaitseva luokka KirjanLainaaja
haluaa käyttää pakkauksessa kirjasto
sijaitsevaa luokkaa Kirja
, täytyy sen myös tuoda kirja käyttöön komennolla import kirjasto.Kirja
package kirjasto.lainaaja; import kirjasto.Kirja; public class KirjanLainaaja { private String sijainti; ... public KirjanLainaaja() { } public void lainaa(Kirja kirja) {..} ... }
Samassa pakkauksessa sijaitsevia luokkia ei tarvitse erikseen tuoda käyttöön, eli saman pakkauksen sisällä olevat luokat ovat oletuksena käytössä pakkauksessa oleville luokille.
Pakkausnäkyvyydellä tarkoitetaan sitä, että metodeille ja luokille ei määritellä erikseen näkyvyyttä. Näkyvyysmääreitähän ovat public
, protected
ja private
. Jos näkyvyysmääreet jätetään pois metodeilta, voi metodeja kutsua pakkauksen sisällä olevista luokista. Vastaavasti konstruktorilla ja muuttujilla.
Jos luokka Kirja
olisi määritelty ilman näkyvyysmäärettä public, voi sitä käyttää siis vain saman pakkauksen sisällä.
package kirjasto; class Kirja { private String nimi; private String sisalto; ... }
Nyt luokka KirjanLainaaja
ei voi käyttää luokkaa Kirja
, koska ne sijaitsevat eri pakkauksissa. Näkyvyysmääre protected
määrittelee näkyvyyden siten, että muuttujat ja metodit ovat näkyvissä vain luokan periville luokille sekä samassa pakkauksessa oleville luokille. Määrettä protected
ei kuitenkaan voi käyttää määrittelemään luokan näkyvyyttä.
Olemme jo jonkin aikaa käyttäneet luokkia kuten ArrayList
ja
HashMap
, joille kerrotaan minkä tyyppistä dataa niihin säilötään.
Esimerkiksi ArrayList<Integer>
on ArrayList
,
johon voi tallettaa kokonaislukuja.
Tutustutaan nyt kuinka tällaisia luokkia voi tehdä itse. Seuraavassa
esimerkissä tehdään luokka Lokero
, johon tallettaa yhden olion
ja hakea lokerosta sen nykyisen olion.
public class Lokero<Tyyppi> { private Tyyppi alkio; public void asetaArvo(Tyyppi alkio) { this.alkio = alkio; } public Tyyppi haeArvo() { return alkio; } }
Tässä geneeriselle tyypille annetaan nimi Tyyppi
, jota voidaan
käyttää luokan määrittelyssä. Luokkaa voidaan nyt käyttää samaan tapaan kuin
esimerkiksi geneeristä ArrayList
iä.
Lokero<Integer> lukulokero = new Lokero<Integer>(); Lokero<String> merkkijonolokero = new Lokero<String>(); lukulokero.asetaArvo(5); merkkijonolokero.asetaArvo(":)"); int luku = lukulokero.haeArvo();
Geneeristen staattisten metodien yhteydessä tyyppiparametri on laitettava metodinmäärittelyn yhteyteen, static-määreen jälkeen. Seuraavassa lokerotehdas, eli staattinen metodi, joka muodostaa ja palauttaa halutun tyyppisen lokeron ja asettaa sille sisällön:
public class Lokerotehdas { public static <Tyyppi> Lokero<Tyyppi> lokeroi(Tyyppi lokeroitava) { Lokero <Tyyppi> uusiLokero = new Lokero<Tyyppi>(); uusiLokero.asetaArvo(lokeroitava); return uusiLokero; } }
Jos metodi lokeroi
olisi erillisen luokan sijasta luokalla Lokero
, tulisi se määritellä samalla tavallasillä luokan tyyppiparametreihin ei voi viitata staattisessa metodissa.
Käyttöesimerkki:
Lokero<String> merkkilokero = Lokerotehdas.lokeroi("Ohjelmoinnin jatkokurssi, viikko 6"); System.out.println( merkkilokero.haeArvo() );