Helsingin yliopisto / Tietojenkäsittelytieteen laitos / 581258-1 Johdatus ohjelmointiin
Copyright © 2001 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 21.11.2001)

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

  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ä

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


Konstruktorit ja periytyminen

Yliluokan konstruktorit eivät periydy aliluokalle!

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

  1. Valitaan kuormitetuista konstruktoreista se, jonka parametrit vastaavat todellisia parametreja.

  2. Suoritetaan ensin yliluokan konstruktori:

  3. Luokan kentät alustetaan.

  4. Suoritetaan konstruktorin muut lauseet.
Tarkastellaan äskeistä esimerkkiä:
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ä


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ä. [Ominaisuuksien näkyminen ja peittyminen luokkahierarkiassa muistuttaa paljon lohkorakenteisten kielten tunnusten näkyvyyssääntöjä!]

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

this ja super, this() ja super()

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

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

Protected, final, abstract

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.

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.

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.