Helsingin yliopisto / Tietojenkäsittelytieteen laitos
Copyright © 2005 Arto Wikla. Tämän oppimateriaalin käyttö on sallittu vain yksityishenkilöille opiskelutarkoituksissa. Materiaalin käyttö muihin tarkoituksiin, kuten kaupallisilla tai muilla kursseilla, on kielletty.

4.4 Periytyminen

(Muutettu viimeksi 20.11.2009)

Käsitteet ja periaate

Periytyminen (inheritance) on luokan piirteiden - kenttien ja metodien - siirtymistä toiselle luokalle. Saava luokka on aliluokka (subclass), antava luokka on yliluokka (superclass). Aliluokkaa kutsutaan joskus myös laajennetuksi (extended) tai johdetuksi (derived) luokaksi.

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.)

  1. Käytännöllisenä ohjelmointitekniikkana:
    Periytymisellä voidaan toteuttaa aiemmin ohjelmoitujen välineiden uudelleenkäyttöä: välineitä täydennetään ja erityistetään uusien tarpeiden mukaisiksi. Yleisemmin voidaan puhua ns. inkrementaalisesta ohjelmankehityksestä: olemassa olevaa ohjelmistoa laajennetaan uusilla komponenteilla ilman, että joudutaan koskemaan jo toteutettuihin ohjelmiin. (Luokka ei koskaan tiedä mitään aliluokistaan!)

  2. Käsitteellisenä mallintamisena:
    Ongelmamaailmaa mallinnetaan peritymisen käsittein: yliluokka-aliluokka -suhde vastaa yleinen-erityinen -suhdetta. Eläin on kissan yleistys, lehmä on naudan erikoistapaus, jne.
Koskimies 1997: "Kyseessä ... [on] aito olioperustaisen ohjelmointiparadigman kaksikasvoisuus. Suunniteltaessa yksittäistä sovellusta korostuu käsitteellinen mallintaminen, kun taas erityisesti sovelluskehyksiä [eräänlainen peruspaketti erityisten sovellusten toteuttamisen lähtökohdaksi, esim. graafinen käyttöliittymä, kääntäjä, ... ] suunniteltaessa korostuu koodin yleisyys, so. uudelleenkäyttö."

Pieni esimerkki periytymisestä

Ohjelmoidaan tuttuun tapaan pieni luokka:
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());  // 35

Laaditaan 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());  // 765
Ja 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());  // 19367
Vaikka 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.)

Vaativa esimerkki periytymisestä

Määritellään luokka Piste seuraavasti:
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

Konstruktorit ja periytyminen

Yliluokan konstruktorit eivät periydy aliluokalle!

Kun aliluokan ilmentymä luodaan, tapahtuu seuraavaa ("super(...)" on Javan ilmaus yliluokan konstruktorin kutsulle):

  1. Varataan muistista tila luotavan olion kaikille kentille, niin olion omassa luokassa määritellyille kuin perityillekin. Kentät saavat tyyppinsä mukaisen oletusalkuarvon.
  2. Valitaan kuormitetuista konstruktoreista se, jonka parametrit vastaavat todellisia parametreja.
  3. Konstruktorin algoritmin ensi toimena aina kutsutaan yliluokan konstruktoria oli kutsu kirjoitettu näkyviin tai ei:
  4. Jos ilmentymämuuttujien määrittelyihin on kirjoitettu alkuarvojen asetuksia, arvot asetetaan muuttujiin.
  5. Vasta kaiken tämän jälkeen suoritetaan itse ohjelmoitu osuus konstruktorin algoritmista.

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ä


Korvaaminen ja peittäminen

Sen lisäksi, että aliluokka voi täydentää perimiään ominaisuuksia uusilla ominaisuuksilla, aliluokka voi myös antaa perimilleen ominaisuuksille uusia merkityksiä.

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: 7
vaikka 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

this ja super, this() ja super()

Ilmaukset this ja super ovat olioarvoisia vakioita, final-muuttujia, jotka ovat automaattisesti käytössä jokaisen luokan konstruktoreissa, ilmentymämetodeissa, ilmentymämuuttujien alustuslausekkeissa, ...

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

Protected, final

Näkyvyyssäännöt käsitellään kootusti luvussa 4.7, mutta lyhyt ennakkosilmäys: Luokan konstruktorilla on oletusarvoisesti sama näkyvyys kuin luokalla itsellään. Silti konstruktorin näkyvyys on syytä kirjoittaa näkyviin. Miksi?

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.

Abstrakti luokka

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 vaaditaan ohjelmoitavaksi sovelluskohtaisesti aliluokassa.

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()));

Luokan ja periytymisen suunnittelusta

Ohjelman ja ohjelmiston suunnittelussa luokat ja periytyminen ovat voimakkaita välineitä. Kuten jo edellä todettiin, niillä voidaan toisaalta pyrkiä jäsentämään ja mallintamaan ongelmaa, toisaalta niillä voidaan yrittää hallita ratkaisun laatimista, ongelman ratkaisevan ohjelmiston tuottamista.

Tällä kurssilla varsinaiseen ohjelmiston suunnittelemiseen ei ole mahdollista perehtyä, mutta jo yksittäisiä luokkia suunnitellessa on hyvä muistaa kaksi näkökulmaa:

  1. Luokka voi olla malli oliolle. Luokka siis määrää ilmentymänsä rakenteen ja käyttäytymisen.
  2. Luokka voi olla lähtökohta tarkennukselle, erikoistamiselle. Luokka siis määrää, mitä aliluokka perii.


Takaisin luvun 4 sisällysluetteloon.