Matti Paksula, Arto Vihavainen, Matti Luukkainen
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.
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
.
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!
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 muuttuja 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. Esimerkiksi seuraava Kassa
-toteutus sisältää Laskuri
-tyyppiä olevan olion, joka näkyy kaikille luokan periville luokille.
public class Kassa { protected Laskuri laskuri; public Kassa() { this.laskuri = new Laskuri(); } public void lisaaKassaan(int montako) { for(int i = 0; i < montako; i++) { laskuri.kasvataArvoa(); } } // muita metodeja }
Toinen luokka, MatinKassa
tekee oman toteutuksen lisaaKassaan()
-metodista.
public class MatinKassa extends Kassa { private Laskuri jemma; public MatinKassa() { jemma = new Laskuri(); } @Override public void lisaaKassaan(int montako) { for(int i = 0; i < montako; i++) { if(i % 5 == 0) { jemma.kasvataArvoa(); } else { laskuri.kasvataArvoa(); } } } }
Luokka MatinKassa
jemmaa siis aina joka viidennen asian, eikä lisää sitä alkuperäisen kassan laskuriin. Alkuperäisen kassan laskuri
-attribuutti on käytettävissä, koska sille on annettu määre protected
. Javassa on public
, protected
ja private
-määreiden lisäksi käsite pakkausnäkyvyys, jolla tarkoitetaan metodien ja attribuuttien näkymistä saman pakkauksen sisällä. Palataan pakkausmääreeseen ensi viikolla.
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.
Rajapinnat käyttäytyvät kuin luokat periytymisessä, eli niitä voi periä kuten luokkia. Katsotaan kahta erilaista periytymistilannetta.
Jos yläluokka toteuttaa rajapinnan, on sen toteutus olemassa myös rajapinnalle. Tällöin myös alaluokkaa voidaan käyttää esimerkiksi parametrina metodille, joka ottaa parametrikseen yläluokan toteuttaman rajapinnan tyyppiä olevan olion. Esimerkiksi luokka KahviLaskuri
, joka toteuttaa rajapinnan Laskuri
.
public class KahviLaskuri implements Laskuri { protected int arvo; public KahviLaskuri() { this.arvo = 0; } public void kasvataArvoa() { arvo = arvo + 1; } public void vahennaArvoa() { arvo = arvo - 1; } public int getArvo() { return arvo; } public void lisaaArvoon(Laskuri laskuri) { this.arvo += laskuri.getArvo(); } }
Luokka KahviLaskuri
toteuttaa rajapinnan Laskuri
. Kahvilaskurilla on lisäksi metodi lisaaArvoon()
, joka lisää arvoon parametrina annetun arvon. Toteutetaan luokka EspressoLaskuri
, joka perii luokan KahviLaskuri
.
public class EspressoLaskuri extends KahviLaskuri { // ei mitään tällä hetkellä }
Luokan EspressoLaskuri
ilmentymää voi käyttää KahviLaskuri
-luokan metodin lisaaArvoon()
parametrina koska sen yläluokka toteuttaa rajapinnan Laskuri
.
EspressoLaskuri espressoLaskuri = new EspressoLaskuri(); // juodaan espressoa espressoLaskuri.kasvataArvoa(); espressoLaskuri.kasvataArvoa(); espressoLaskuri.kasvataArvoa(); espressoLaskuri.kasvataArvoa(); // siirretään saldot kahviin KahviLaskuri kahviLaskuri = new KahviLaskuri(); kahviLaskuri.lisaaArvoon(espressoLaskuri); System.out.println("Kahvilaskurissa arvona " + kahviLaskuri.getArvo());
Esimerkin tulostus tulostaa merkkijonon Kahvilaskurissa arvona 4
.
Rajapinnan voi periä samalla tavalla kuin luokan. Tällöin alirajapinta saa ylärajapinnan kaikki metodit ja määrittelyt käyttöönsä. Esimerkiksi rajapinta Laskuri
ja sen periva rajapinta AsettavaLaskuri
.
public interface Laskuri { public void kasvataArvoa(); public void vahennaArvoa(); public int getArvo(); }
public interface AsettavaLaskuri extends Laskuri { public void setArvo(int arvo); }
Rajapinnan AsettavaLaskuri
toteuttavan luokan pitää toteuttaa neljä metodia, kasvataArvoa()
, vahennaArvoa()
, getArvo()
ja asetaArvo()
. Toteutetaan vielä luokka MokkaLaskuri
, joka toteuttaa rajapinnan AsettavaLaskuri
. Koska olemme oppineet perimään luokkien metodeja ja attribuutteja, laajennamme luokkaa KahviLaskuri
.
public class MokkaLaskuri extends KahviLaskuri implements AsettavaLaskuri { public void setArvo(int uusiArvo) { arvo = uusiArvo; } }
Huomaa että arvon asetus toimii vain, koska luokan KahviLaskuri
toteutuksessa on määritelty muuttuja arvo
aliluokille näkyviksi.
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 tietoston 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 paremmin myöhemmin tällä kurssilla.
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,