Aliluokka täydentää, erikoistaa, yliluokan määrittelyitä. Edellä jo nähtiin esimerkkiluokat Eläin 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 omasta luokasta luokkaan Object. (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) (kirjasta on uusi versio: Oliokirja, 2000):
public class Piste { //-- kentät: private int x, y; // vain omaan käyttöön! //-- konstruktorit: Piste() {x=0; y=0;} Piste(int x, int y) {this.x = x; this.y = y;} //-- metodit: public String toString() { // peittää Object-luokan metodin! return "("+x+","+y+")"; } public void siirry(int dx, int dy) { x += dx; y += dy; } }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 Piste { //-- lisäkenttä: private int vari; // vain omaan käyttöön //-- konstruktorit: VariPiste() {super();} VariPiste(int vari) {this.vari = vari;} 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):
public class VariPiste extends Piste { //-- lisäkenttä: private int vari; // vain omaan käyttöön //-- konstruktorit: VariPiste() {super();} // Ilman tätä VariPiste-oliota // ei voi luoda ()-konstruktorilla! // Pelkkä VariPiste() { } riittäisi!Jos VariPiste ei määrittelisi parametritonta konstruktoria, oliota ei voisi myöskään luoda parametreitta. Koska yliluokan parametritonta konstruktoria kutsutaan joka tapauksessa automaattisesti, määrittely:
VariPiste() { }riittäisi. Piste käydään asettamassa (0,0):ksi, väri tulee oletusalkuarvon takia 0:ksi.
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 viite itseensä!)
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.
Jos aliluokalla ei ole lainkaan omia konstruktoreita, käytetään 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.
Metodin korvaaminen eli syrjäyttäminen (overriding) tarkoittaa perityn metodin korvaamista uudella samantyyppisellä, samannimisellä ja samaparametrisella metodilla. Ilmentymämetodi voi korvata vain ilmentymämetodin, luokkametodi voi korvata vain luokkametodin, sekoittaa ei saa!
Korvaava metodi on luokassa käytössä automaattisesti, mutta myös korvattuun 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 luokkien (paitsi itsensä!) 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 olion tyyppiin, ei muuttujan tyyppiin! Tätä kutsutaan polymorfismiksi eli monimuotoisuudeksi.
Esimerkiksi lauseet:
VariPiste a = new VariPiste(7); Piste b; b = a; System.out.println(a); System.out.println(b);tulostavat:
(0,0) väri: 7 (0,0) väri: 7vaikka muuttujan b tyyppi on Piste!
Yliluokan kentän voi peittää (hide) näkyvistä, kun aliluokassa määrittelee nimelle uuden merkityksen. Jos edelläolleiden esimerkkien tapaan ohjelmoidaan luokan kentät yksityisinä (ja käytetään niitä vain aksessoreiden kautta), kentän peittämismahdollisuudesta saatava ilo on siinä, että kenttänimiä laatiessa ei tarvitse muistaa yli- ja aliluokkien kenttänimiä.
Esimerkki kenttien peittämisestä sekä aksessorien korvaamisesta ja kuormittamisesta (21.11.2001)
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.
this(...) puolestaan tarkoittaa luokan muiden konstruktoreiden kutsuja, super(...) luokan yliluokan konstruktoreiden kutsuja. Näitä voi käyttää vain jonkin konstruktorin ensimmäisenä lauseena. (Konstruktorin alussa voi siis käyttää vain jompaa kumpaa. Tavallisissa metodeissa niitä ei voi käyttää. Tavallinen metodi voi toki kutsua konstruktoria nimellä, mutta silloin on aina kyseessä uuden olion luonti.)
Nämä konstruktorin kutsut ovat sikäli erikoisia, että ne johtavat kutsutun suorittamiseen siten, että sen algoritmia sovelletaan kutsuvan konstruktorin jo luoman olion kenttiin - tavallinen konstruktorin kutsuhan luo aina uuden olion
this(...)-kutsun avulla monimutkaisessa kontruktorissa voidaan käyttää hyväksi yksinkertaisempia konstruktoreita.
Kutsu super(...) puolestaan tarjoaa yliluokan konstruktorit suoritettaviksi. Kutsulla voidaan esimerkiksi alustaa perittyjä kenttiä. Se tarjoaa epäsuoran pääsyn myös yliluokalta perittyihin yksityisiin kenttiin.
Esimerkki:
public class Yliluokka { int i; Yliluokka() { i = 34; } } public class Aliluokka extends Yliluokka { int i = 12; // peittää perityn i:n int p, q, r; public Aliluokka() { super(); p = super.i; q = this.i; } public Aliluokka(int r) { this(); this.r = r; } 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); } }Ohjelma Aliluokka tulostaa
34 12 0 34 12 56
Määrellä final tehdään kentästä vakio, ts. alkuarvolausekkeen sijoittamisen jälkeen kentän arvoa ei voi muuttaa. Jos metodi on final, sitä ei voi aliluokissa korvata. Kun luokka määritellään final-määreellä, luokalle ei voi tehdä aliluokkia.
Luokkaa kutsutaan abstraktiksi, jos se ei toteuta (eli implementoi) kaikkia metodeitaan. Tällainen menettely on perusteltua, kun halutaan laatia yleiskäyttöinen luokka, jonka jotkin metodit ohjelmoidaan sovelluskohtaisesti. Määreellä abstract ilmaistaan luokan olevan abstrakti. Myös metodi voidaan määritellä abstraktiksi. Abstraktin metodin lohko on pelkkä puolipiste:
abstract void testipenkki();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, kirjastoluokka on kuitenkin tapana
määritellä:
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 tämä on itse asiassa abstraktin luokan olemassaolon syy:
Abstrakteja luokkia voidaan käyttää ns. public interfacen toteuttamiseen. Jonkin luokan julkinen, käyttäjälle tarjottu kalusto määritellään erillisenä abstraktina luokkana ilman metodien toteutusta. Luokka, joka metodit toteuttaa, jää käyttäjältä piiloon. Javassa myös rajapintaluokalla saa toteuttettua vastaavanlaisen abstraktion.
Pieni 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ä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 ... }Kun sitten halutaan luoda ilmentymä luokasta Auto, konstruktorille on annettava todelliseksi parametriksi jokin Moottori-tyyppinen arvo. Abstraktista Moottori-luokasta ei kuitenkaan voida luoda ilmentymiä. Laaditaan luokalle ei-abstrakti aliluokka, joka toteuttaa perimänsä abstraktit metodit:
public class RepcoBrabham extends Moottori { private double kierrosluku; // muut tietorakenteet ... 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ä RepcoBrabham-luokan ilmentymä:
public class AutoSovellus { public static void main(String[] args) { RepcoBrabham brrrmmm = new RepcoBrabham(); // "oikea moottori", joka Auto lotus = new Auto(brrrmmm); // kelpaa auton luontiin lotus.kaasuta(96); System.out.println(lotus.mitenLujaaMennään()); // 297.6 lotus.kaasuta(-23); System.out.println(lotus.mitenLujaaMennään()); // 0.0 lotus.kaasuta(1100); System.out.println(lotus.mitenLujaaMennään()); // 310.0 // jne..., tms... } }Vastaavalla tavalla voitaisiin luoda ja käyttää Auto-olioita milloin milläkin moottorilla... Ainoa edellytys on, että kyseisen moottorin luokka on Moottori-luokan ei-abstrakti aliluokka.
Tällä kurssilla varsinaiseen ohjelmiston suunnittelemiseen ei ole mahdollista perehtyä, mutta jo yksittäisiä luokkia suunnitellessa on hyvä muistaa kaksi näkökulmaa: