Aliluokka täydentää, erikoistaa, yliluokan määrittelyitä. Edellä jo nähtiin esimerkkiluokat Elain ja Kissa, joista jälkimmäinen peri kaikki edellisen ominaisuudet ja lisäksi täydensi niitä kissojen erityisillä ominaisuuksilla.
Aliluokalla voi itselläänkin olla aliluokkia. Näin periytymisellä voidaan rakentaa puumainen luokkien hierarkia. (Tällaista periytymismekanismia kutsutaan yksittäisperiytymiseksi. Jotkin kielet (esim. C++) sallivat ominaisuuksien perimisen useammasta luokasta, ns. moniperiytymisen. Javassa moniperiytymisen edut sanotaan saatavan ilman haittoja rajapintaluokan avulla, kts. luku 4.5)
Javassa on erityinen luokka Object, joka on kaikkien luokkien yliluokka. Se mm. määrittelee kaikille luokille yhteisiä metodeita. Object-luokan avulla voidaan toteuttaa myös omia yleiskäyttöisiä työkaluja: Jos muuttujan tai muodollisen parametrin tyyppi on Object, sen arvoksi voidaan asettaa mikä olio tahansa!
Luokan ilmentymällä, oliolla, on siis oman luokan määrittelemien ominaisuuksien lisäksi kaikkien oman luokan yliluokkien ominaisuudet! Kun luokkien hierarkia hahmottuu puuna, olion ominaisuudet voi nähdä kokoelmana piirteitä, jotka on saatu kaikista luokista matkalla Object-luokasta omaan luokkaan. (Periytymismekanismissa on välineitä, joilla jotkin piirteet voidaan peittää tai korvata! Niistä myöhemmin.)
Periytymismekanismia voidaan käyttää kahdella erilaisella tavalla ohjelmointityön jäsentämisessä (sovellan Kai Koskimiehen teosta Pieni oliokirja, 1997) (Kirjan uudempi versio on Kai Koskimies: Oliokirja, satku.fi, 2000.)
public class Yli { private int ylempi; public Yli() {ylempi=100;} public int getYlempi() {return ylempi;} public void setYlempi(int ylempi) {this.ylempi=ylempi;} }Luokan ilmentymiä käytetään yhtä tuttuun tapaan:
Yli y = new Yli(); System.out.println(y.getYlempi()); // 100 y.setYlempi(35); System.out.println(y.getYlempi()); // 35Laaditaan sitten luokalle aliluokka, joka täydentää perittyjä ominaisuuksia uusilla:
public class Ali extends Yli { private int alempi; public Ali() {alempi=1000;} public int getAlempi() {return alempi;} public void setAlempi(int alempi) {this.alempi=alempi;} }Aliluokan ilmentymällä on yliluokan ominaisuudet:
Ali a = new Ali(); System.out.println(a.getYlempi()); // 100 a.setYlempi(765); System.out.println(a.getYlempi()); // 765Ja tietenkin sillä on myös oman luokan lisäämät ominaisuudet:
System.out.println(a.getAlempi()); // 1000 a.setAlempi(9900); System.out.println(a.getAlempi()); // 9900
Aliluokan ilmentymä on myös yliluokan ilmentymä:
Yli aa = new Ali();mutta aa:n tyypistä johtuen käytössä ovat vain yliluokan operaatiot:
System.out.println(aa.getYlempi()); // 100 aa.setYlempi(19367); System.out.println(aa.getYlempi()); // 19367Vaikka aa:n arvona olevalla Ali-tyyppisellä oliolla on kaikki aliluokankin ominaisuudet, yritys käyttää niitä Yli-tyyppisen muuttujan kautta:
aa.setAlempi(9876);johtaa kääntäjän havaitsemaan virheeseen:
TestaaYliAli.java:31: cannot find symbol symbol : method setAlempi(int) location: class Yli aa.setAlempi(9876); ^
Vaikka aliluokan ilmentymä kelpaakin myös yliluokan ilmentymäksi, päinvastainen ei ole mahdollista. Yritys
Ali yy = new Yli();tuottaa kääntäjän virheilmoituksen
TestaaYliAli.java:48: incompatible types found : Yli required: Ali Ali yy = new Yli(); ^Onnistuisiko esplisiittinen tyyppimuunnos:
Ali yy = (Ali)(new Yli());Kelpaa kääntäjälle! Mutta suorituksessa käy huonosti:
Exception in thread "main" java.lang.ClassCastException: Yli at TestaaYliAli.main(TestaaYliAli.java:61)
Suorituksen aikana havaitaan, että Yli-olio ei ole muutettavissa Ali-olioksi!
Mutta jos Yli-tyyppinen muuttuja saa arvokseen Ali-tyyppisen olion, muunnos onnistuukin:
Yli q = new Ali(); Ali w = (Ali)q;Vaikka q ja w osoittavat samaan olioon, vain w:tä käyttäen olioon on mahdollista soveltaa aliluokassa määriteltyjä metodeita!
w.setAlempi(4321); // OK! q.setAlempi(4321); käännösvirhe
(Yllä nähdyt esimerkit on koottu ohjelmaan TestaaYliAli.)
public class Piste { //-- kentät: private int x, y; // vain omaan käyttöön! //-- konstruktorit: public Piste() {x=0; y=0;} public Piste(int x, int y) {this.x = x; this.y = y;} //-- metodit: public String toString() { // korvaa Object-luokan metodin! return "("+x+","+y+")"; } public void siirry(int dx, int dy) { x += dx; y += dy; } }Tätä luokkaa voi jo opittuun tapaan käyttää vaikkapa seuraavasti:
Piste a = new Piste(); Piste b = new Piste(7, 14); System.out.println(a); //eli: System.out.println(a.toString()); System.out.println(b); a.siirry(2, 3); b.siirry(4, 5); System.out.println(a); System.out.println(b);Lauseet tulostavat:
(0,0) (7,14) (2,3) (11,19)Jos halutaan toteuttaa värillinen piste, jolla on pisteen ominaisuuksien lisäksi väri ja väriin liittyviä operaatioita, on luontevaa toteuttaa värillinen piste pisteen aliluokkana.
Tämän voi nähdä pelkästään ohjelmointiteknisenä menettelynä: uudelleenkäytetään jo ohjelmoidut pisteen määrittelyt.
Toisaalta asian voi nähdä myös teoreettisemmin mallinnuksena: Värillinen piste on pisteen erikoistapaus. Se on ns. "IsA"-relaatiossa pisteeseen, so. jokainen värillinen piste on piste ja kaikki, mikä voidaan tehdä pisteelle, voidaan tehdä värilliselle pisteelle. Päivastainen ei ole mahdollista, koska värillinen piste sisältää joitakin uusia ominaisuuksia, joita pisteellä ei ole.
Määritellään luokka VariPiste seuraavasti:
public class VariPiste extends Piste { //-- lisäkenttä: private int vari; // vain omaan käyttöön //-- konstruktorit: public VariPiste() {super();} public VariPiste(int vari) {this.vari = vari;} public VariPiste(int x, int y, int vari) { super(x, y); this.vari = vari; } //-- lisämetodi: public void uusiVari(int vari) { this.vari = vari; } //-- korvaava metodi ("overriding"): // peittää Piste-luokan public String toString() { // metodin! return super.toString()+" väri: "+vari; } }Nyt voi ohjelmoida vaikkapa:
VariPiste c = new VariPiste(); VariPiste d = new VariPiste(7); VariPiste e = new VariPiste(4,5,9); System.out.println(c); System.out.println(d); System.out.println(e); e.siirry(1, 1); // yliluokan operaation käyttö! System.out.println(e); e.uusiVari(14); // oman operaation käyttö System.out.println(e);Lauseet tulostavat:
(0,0) väri: 0 (0,0) väri: 7 (4,5) väri: 9 (5,6) väri: 9 (5,6) väri: 14
Kun aliluokan ilmentymä luodaan, tapahtuu seuraavaa ("super(...)" on Javan ilmaus yliluokan konstruktorin kutsulle):
Huom: On järkevää ohjelmoida siten, että luotavan olion kaikkien kenttien asetukset kirjoitetaan aina näkyviin konstruktoreihin! Jopa oletusalkuarvot on syytä asettaa uudelleen. Miksi?
Tarkastellaan vielä edellistä esimerkkiä:
public class VariPiste extends Piste { //-- lisäkenttä: private int vari; // vain omaan käyttöön //-- konstruktorit: public VariPiste() {super();} // Pelkkä VariPiste() { } tarkoittaa samaa!Jos VariPiste ei määrittelisi parametritonta konstruktoria, oliota ei voisi myöskään luoda parametreitta, koska luokalla on automaattisesti parametriton oletuskonstruktori vain, jos muita konstruktoreita ei ole ohjelmoitu.
Koska yliluokan parametritonta konstruktoria kutsutaan joka tapauksessa automaattisesti, määrittely:
public VariPiste() { }riittäisi. Piste käydään asettamassa (0,0):ksi, väri tulee oletusalkuarvon takia 0:ksi.
public VariPiste(int vari) { // Kutsuu ensin automaattisesti this.vari = vari; // super(); ! }Tässä käydään automaattisesti asettamassa piste (0,0):ksi ennen värin asettamista. Ilmausta this on käytetty vain mukavuudenhalusta: näin muodollinen parametri saa olla samanniminen kuin asetettava kenttä. (this on - kuten jo aiemmin nähtiin - olion kenttä, joka viittaa olioon itseensä!)
public VariPiste(int x, int y, int vari) { super(x, y); this.vari = vari; }VariPisteen kolmiparametrinen konstruktori asettaa ensin pisteen koordinaatit Piste-luokan kaksiparametrisella konstruktorilla.
Huom: Piste-luokan private-kentät x ja y eivät näy VariPisteelle. Konstruktorilla ne silti voidaan käydä asettamassa. Aksessoreilla niitä päästään käsittelemään muutenkin.
Huom: (yleisemmin): Mikään yliluokassa private-määreellä määritelty kenttä tai metodi ei näy minnekään luokkamäärittelyn ulkopuolelle, ei edes aliluokkaan. Perittyjä private-kenttiä pääsee aliluokassakin käsittelemään vain perityillä aksessoreilla, ei mielin määrin. Siispä kapselointi kapseloi yliluokan yksityisen kaluston myös aliluokalta!
Jos luokkaan ei ohjelmoida lainkaan omia konstruktoreita, luokka saa automaattisesti parametrittoman oletuskonstruktorin, joka puolestaan ei muuta tee kuin kutsuu yliluokan parametritonta konstruktoria.
Laaditaan VariPisteelle sisarus NimiVariPiste:
public class NimiVariPiste extends Piste { private String vari="EiVäriä"; public void uusiVari(String vari) { this.vari = vari; } public String toString() { return super.toString()+" väri: "+vari; } }Lauseet:
Piste a = new Piste(); VariPiste b = new VariPiste(1,2,3); NimiVariPiste c = new NimiVariPiste(); System.out.println(a); System.out.println(b); System.out.println(c); c.uusiVari("Vihreä"); System.out.println(c); c.siirry(7,8); System.out.println(c);tulostavat:
(0,0) (1,2) väri: 3 (0,0) väri: EiVäriä (0,0) väri: Vihreä (7,8) väri: Vihreä
Olemme jo aiemmin tutustuneet metodien kuormittamiseen (overloading): samannimiset eriparametriset metodit ovat luvallisia samassa näkyvyysalueessa. Tässäkin luvussa konstruktoreita on kovasti kuormitettu.
Kuormittaminen ei rajoitu vain yhden luokkamäärittelyn sisään, vaan aliluokkakin voi vallan mainiosti täydentää perittyjä ominaisuuksia lisäämällä metodeita, jotka kuormittavat perittyjä metodeita.
Metodin korvaaminen (overriding) tarkoittaa puolestaan perityn metodin korvaamista uudella samantyyppisellä, samannimisellä ja samaparametrisella metodilla. [Ilmentymämetodi voi korvata vain ilmentymämetodin, luokkametodi voi korvata vain luokkametodin, sekoittaa ei saa!]
Kun luokan muut metodit kutsuvat tällaista metodia, kutsu tarkoittaa juuri tätä korvaavaa metodia. Korvattuun perittyyn metodiin voi viitata käyttämällä ilmausta super, joka tarkoittaa "minun yliluokkaani".
Edellä Piste määritteli:
public String toString() { return "("+x+","+y+")"; }ja VariPiste:
public String toString() { return super.toString()+" väri: "+vari; }Tässä siis VariPiste antaa uuden merkityksen metodille toString(). Itse asiassa myös Piste on antanut tälle metodille uuden merkityksen: kaikkien muiden luokkien yliluokka Object jo määrittelee metodille toString() erään merkityksen! VariPiste myös käyttää hyväkseen Piste-yliluokkansa toString() metodia.
Korvaavan metodin valinta liittyy dynaamisesti (siis ohjelman suoritusaikana) olion tyyppiin, ei muuttujan tyyppiin! Tätä kutsutaan polymorfismiksi eli monimuotoisuudeksi.
Esimerkiksi lauseet:
VariPiste a = new VariPiste(7); Piste b; Object c; b = a; c = a; System.out.println(a); System.out.println(b); System.out.println(c);tulostavat:
(0,0) väri: 7 (0,0) väri: 7 (0,0) väri: 7vaikka muuttujan b tyyppi on Piste ja muuttujan c tyyppi on Object, koska itse olion tyyppi on VariPiste!
Kääntäjä on tarkistanut, että muuttujille b (tyyppiä Piste) ja c (tyyppiä Object) on käytettävissä toString()-metodi (println tarvitsee sitä!) ja generoinut tuon metodin kutsun. Suoritusaikana tulkki havaitsee b:n ja c:n arvona olevan olion tyypiksi VariPiste ja kutsuu toString()-metodia olion tyypin perusteella, ei muuttujan tyypin perusteella. Polymorfismi siis perustuu korvattujen metodien ketjuun perintähierarkiassa ja siihen, että ohjelman suoritusaikana ketjusta valitaan olion omaa luokkaa lähinnä oleva versio toisiaan kuormittavista metodeista.
Yliluokan kentän voi peittää (hide) näkyvistä, kun aliluokassa määrittelee perityn kentän nimelle uuden merkityksen. Jos edellä nähtyjen esimerkkien tapaan ohjelmoidaan luokan kentät yksityisinä ja käytetään niitä vain aksessoreiden kautta, kentän peittämismahdollisuudesta saatava ilo on siinä, että omia kenttiä nimetessä ei tarvitse vältellä perittyjen kenttien nimien käyttämistä. (Tai runollisemmin: Kun ostaa hienon luokan ja erikoistaa siitä aliluokkana oman sovelluksen, ei tarvitse saada pitkää luetteloa kaikista perityistä tunnuksista, joita ei saa käyttää... ;-)
Esimerkki kenttien peittämisestä sekä aksessorien korvaamisesta ja kuormittamisesta:
public class Yli { private int prix = 1; private int priy = 2; public int pubx = 3; public int puby = 4; public int annaPrix() {return prix;} public int priyPlus() {return priy + 1;} public String toString() { return prix + "/" + priy + "/" + pubx + "/" + puby; } } public class Ali extends Yli { private int prix = 100; // peittää perityn private-kentän private int priy = 200; // -"- public int pubx = 300; // peittää perityn public-kentän public int annaPrix() {return prix;} // korvaa perityn metodin public int priyPlus(int i) // kuormittaa perittyä metodia {return priy + i;} public String toString() { return super.toString() + "\n" + prix + "/" + priy + "/" + pubx + "/" + puby; }Lauseet
Ali a = new Ali(); System.out.println(a); System.out.println(a.annaPrix()); System.out.println(a.priyPlus(1000)); System.out.println(a.priyPlus()); System.out.println(a.pubx); System.out.println(a.puby);tulostavat:
1/2/3/4 100/200/300/4 100 1200 3 300 4
Vakio this viittaa juuri siihen olioon, jota ollaan luomassa tai käsittelemässä. Vakio super viittaa aivan samaan olioon, mutta olion luokan yliluokan ilmentymänä. Näin super-ilmauksella päästään käsiksi yliluokasta perittyihin, aliluokassa korvattuhin metodeihin ja peitettyihin muuttujiin, elleivät ne ole private.
this(...) puolestaan tarkoittaa luokan muiden konstruktoreiden kutsuja, super(...) luokan yliluokan konstruktoreiden kutsuja. Tällaista saa käyttää ainoastaan jonkin konstruktorin ensimmäisenä lauseena. Konstruktorin alussa voi siis käyttää vain jompaa kumpaa. Tavallisissa metodeissa kumpaakaan ei voi milloinkaan kutsua.
Nämä konstruktorin kutsut ovat sikäli erikoisia, että ne johtavat vain kutsutun konstruktorin algoritmin suorittamiseen, eivät uuden olion luontiin. Tavallinen konstruktorin kutsu new-ilmauksella luo aina uuden olion
this(...)-kutsun avulla esimerkiksi monimutkaisessa kontruktorissa voidaan käyttää hyväksi yksinkertaisempia konstruktoreita.
Kutsu super(...) puolestaan tarjoaa mahdollisuuden valita jokin muu kuin yliluokan parametriton konstruktori suoritettavaksi. Tällaisella kutsulla voidaan esimerkiksi alustaa perittyjä kenttiä. Se tarjoaa myös mahdollisuuden asettaa alkuarvoja yliluokalta perittyihin yksityisiin (private) kenttiin.
Huom: (16.11.2007) Peräti itse Java-spesifikaatio sanoo, ettei yksityisiä (private) kenttiä peritä aliluokassa. Mielestäni siellä sanotaan "väärin"! Kirjoittajat tarkoittanevat, ettei aliluokkaa ohjelmoitaessa ole pääsyä perittyihin yksityisiin kenttiin? Mielestäni kuitenkin se, että aliluokan ilmentymät väistämättä sisältävät kaikki luokan ominaisuudet - niin perityt yksityiset kentät kuin kaiken muunkin - oikeuttaa sanomaan, että myöskin private-kentät periytyvät!
Esimerkki:
public class Yliluokka { protected int i; // i näkyy aliluokkiin ja pakkaukseen! // ks. seuraava kappale public Yliluokka() { i = 34; } } public class Aliluokka extends Yliluokka { private int i = 12; // peittää perityn i:n private int p, q, r; public Aliluokka() { super(); p = super.i; q = this.i; } public Aliluokka(int r) { this(); this.r = r; } public void tulostaIit(int i) { System.out.println(i); // metodin parametri System.out.println(this.i); // olion luokan oma i System.out.println(super.i); // olion luokan yliluokan i } public static void main(String[] args) { Aliluokka a = new Aliluokka(); System.out.println(a.p + " " + a.q + " "+ a.r); Aliluokka b = new Aliluokka(56); System.out.println(b.p + " " + b.q + " "+ b.r); b.tulostaIit(78); } }Ohjelma Aliluokka tulostaa
34 12 0 34 12 56 78 12 34
Määrellä final tehdään muuttujasta tai muodollisesta parametrista vakio, ts. muuttujan arvoa ei voi muuttaa.
Java-kielessä - monista muista kielistä poiketen - vakiolle voi kerran sijoittaa arvon! Tuo sijoitus on tietenkin tavallisesti muuttujan määrittelyn yhteydessä. Metodin muuttujalle se voidaan tehdä määrittelyn jälkeenkin metodin algoritmissa, ilmentymämuuttujalle konstruktorissa ja luokkamuuttujalle staattisessa alustuslohkossa.
Jos metodi on final, sitä ei voi aliluokissa korvata. Kun luokka määritellään final-määreellä, luokalle ei voi tehdä aliluokkia.
Määreellä abstract ilmaistaan luokan olevan abstrakti. Myös metodi voidaan määritellä abstraktiksi. Abstraktin metodin lohko on pelkkä puolipiste:
public abstract void metodi();Vain abstraktilla luokalla voi olla abstrakteja metodeita. Luokka voidaan määritellä abstraktiksi vaikka se implementoisi kaikki metodinsa. Tällaisesta luokasta ei voida luoda ilmentymiä.
Kuten luvussa 4.3 opittiin, kirjastoluokan ilmentymien luominen
on kuitenkin tapana estää yksityisellä konstruktorilla:
public final class Kirjasto {
private Kirjasto() { } // ilmentymien esto!!
... vakioiden ja metodien määrittelyt ...
}
Abstraktista luokasta ei siis voi luoda ilmentymää, mutta aliluokkia sille voidaan laatia. Ja juuri tämä on abstraktin luokan olemassaolon tarkoitus:
Esimerkki abstraktin luokan yhdestä käyttötavasta
Halutaan toteuttaa luokka Auto, jossa ei oteta kantaa moottorin rakenteen yksityiskohtiin, riittää että moottori osaa tietyt operaatiot. Esitetään nuo vaatimukset abstraktina luokkana Moottori:
public abstract class Moottori { public abstract double annaKierrosluku(); public abstract void asetaPolttoaineensyöttömäärä(double määrä); // ... // ... Täällä voisi vallan mainiosti olla myös ei-abstrakteja metodeita! // ... Ja jopa sellaisia, jotka kutsuvat abstrakteja metodeita! }Tästä luokasta ei tietenkään voida luoda ilmentymiä kuten ei mistään muustakaan abstraktista luokasta.
Sitten ohjelmoidaan luokka Auto. Moottoriksi kelpaa mikä tahansa Moottori-luokan ei-abstraktin aliluokan ilmentymä:
public class Auto { private Moottori m; // ties mitä muita tietorakenteita ... public Auto(Moottori m) { // Moottori on abstrakti luokka! this.m = m; // rakennellaan loputkin autosta ... } public double mitenLujaaMennään() { // nopeus riippuu tavalla tai toisella kierrosluvusta: return /* ... */ m.annaKierrosluku()/30 /* ? tms...*/; } public void kaasuta(double bensaa) { m.asetaPolttoaineensyöttömäärä(bensaa); // ... } // ties mitä muita metodeita ... }Autoja voidaan valmistaa vain antamalla konstruktorille todellisena parametrina jokin konkreettinen Moottori-olio. Abstraktista Moottori-luokastahan ei ole mahdollista luoda ilmentymiä. Laaditaan siis Moottori-luokalle jokin ei-abstrakti aliluokka, joka toteuttaa kaikki perityt abstraktit metodit:
public class Cosworth extends Moottori { private double kierrosluku; // muut tietorakenteet ... public Cosworth() { // ... } public double annaKierrosluku() { return kierrosluku; } public void asetaPolttoaineensyöttömäärä(double määrä) { if (määrä < 0) kierrosluku = 0.0; else if (määrä > 100) kierrosluku = 9300.0; else kierrosluku = (määrä/100)*9300; // tms... } // muita aksessoreita ... }Nyt voidaan ohjelmoida jokin Auto-luokkaa käyttävä sovellus. Auton moottoriksi asetetaan tässä esimerkissä Cosworth-luokan ilmentymä:
public class AutoSovellus { public static void main(String[] args) { Cosworth brrrmmm = new Cosworth(); // "oikea moottori", joka Auto lotus49 = new Auto(brrrmmm); // kelpaa auton luontiin lotus49.kaasuta(96); System.out.println(lotus49.mitenLujaaMennään()); // 297.6 lotus49.kaasuta(-23); System.out.println(lotus49.mitenLujaaMennään()); // 0.0 lotus49.kaasuta(1100); System.out.println(lotus49.mitenLujaaMennään()); // 310.0 // ... } }Vastaavalla tavalla voitaisiin luoda ja käyttää Auto-olioita milloin milläkin moottorilla... Auton moottoriksi kelpaa mikä tahansa olio, jonka luokka on Moottori-luokan ei-abstrakti aliluokka!
Ajatusta voidaan jatkaa toiseenkin suuntaan: Ehkäpä haluttaisiin ohjelmoida luokka Kilpa_ajaja:
public class Kilpa_ajaja { private Auto auto; // ... kilpa-ajajan muut tarvikkeet ... public Kilpa_ajaja(Auto auto) { this.auto = auto; // ... } // ... aksessorit ... }Nyt Jim Clark saataisiin Lotus 49:ää käyttäväksi kilpa-ajajaksi vaikkapa seuraavasti:
Moottori m = new Cosworth(); Auto lotus49 = new Auto(m); Kilpa_ajaja jimClark = new Kilpa_ajaja(lotus49);Tämän tapaisia rakenteita esiintyy melko usein Javan valmiin kaluston käytössä ja yleisemminkin isoissa ohjelmistoissa. Edellinen esimerkki voidaan kirjoittaa tiiviimminkin - ja juuri näin usein olio-ohjelmaa kirjoitetaan:
Kilpa_ajaja jimClark = new Kilpa_ajaja(new Auto(new Cosworth()));
Tällä kurssilla varsinaiseen ohjelmiston suunnittelemiseen ei ole mahdollista perehtyä, mutta jo yksittäisiä luokkia suunnitellessa on hyvä muistaa kaksi näkökulmaa: