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.5 Rajapintaluokka

(Muutettu viimeksi 24.11.2009)

Jotkin erilaiset luokat voivat ominaisuuksiltaan ja käyttötavaltaan olla osittain samanlaisia. Joskus voi olla tarkoituksenmukaista käyttää toisistaan eroavien luokkien - tai niiden ilmentymien - yhteisiä ominaisuuksia ottamatta kantaa siihen, mikä erityinen luokka on kyseessä.

Yhteiset piirteet voidaan periä yhteisestä yliluokasta. Tällöin piirteet ovat täsmälleen samat. Jos ne sen sijaan ovat operaatioita, joiden käyttötarkoitus on sama, mutta toteutustapa on erilainen, metodit laaditaan kuhunkin luokkaan erikseen.

Javan rajapintaluokka (interface) on keino kerätä yhteen luokkia, joilla on joitakin samankaltaiseen tarkoitukseen laadittuja samaotsikkoisia metodeita. Rajapintaluokka pelkästään luettelee noiden yhteisten metodien otsikot - toteutustavasta ei sanota mitään.

Jatketaan kappaleen 4.1 Eläin-esimerkkiä: Kissan lisäksi Elain-luokalla voisi olla mm. aliluokat Nauta, Hevonen, Vuohi ja Lammas, jotka täydentävät Elain-luokalta perittyjä ominaisuuksia lajikohtaisilla ominaisuuksilla. Luokat Lehma, Tamma, Kuttu ja Uuhi puolestaan edelleen täydentävät lajikohtaista määrittelyä kyseisten lajien naaraspuolisten yksilöiden erityisellä ominaisuudella: ne voivat tuottaa maitoa.

Luokkiin Lehma, Tamma, Kuttu ja Uuhi voidaan luontevasti määritellä mm. lypsämistoiminto. Lypsytekniikat eri luokissa toki voivat olla erilaisia.

Kaikille eläimille yhteisiä metodeita voidaan laatia seuraavaan tapaan:

     public static void elele(Elain e, int päivienLuku) {
       for (int i=0; i<päivienLuku; ++i) {
         double painoa = e.syö(ruokaa);
         e.kasva(painoa);
         
         // ... mitä eläin sitten tekeekään ...
         // ... metodit on määritelty luokassa Elain ...
         // ... siksi niitä (ja vain niitä!) voidaan
         //     soveltaa muuttujaan e

         e.nuku();
       }
     }

Tälle metodille voitaisiin antaa parametriksi niin Kissaolio, Vuohiolio kuin vaikkapa Uuhioliokin, koska ne kaikki ovat Eläimen ilmentymiä ja siksi ovat perintönä saaneet tarvittavat operaatiot yliluokasta Elain.

Sitä vastoin lypsämisoperaatiota kaikille Eläimille ei ole määritelty. Sellainen löytyy vain luokista Lehma, Tamma, Kuttu ja Uuhi.

Jos haluttaisiin laatia metodi, joka lypsää parametrioliotaan, edelläoleva tapa ei onnistuisikaan: Toki Elain-luokkaan voitaisiin ohjelmoida lypsä-metodi, mutta silloin se olisi kaikkien eläinten yhteinen ominaisuus, niin kanojen, Maranellon korskeiden orhien, sonnien kuin hämähäkkienkin.

Apu löytyy ohjelmoimalla lypsäville eläimille yhteinen rajapintaluokka Lypsava:

   public interface Lypsava {
     public double lypsä(); // ei algoritmia, vain otsikko!
   } 

Luokan sanotaan toteuttavan (implement) rajapintaluokan kun se toteuttaa kaikki rajapintaluokan luettelemat metodit.

Eräät Elain-luokan aliluokat sisältävät lypsä-metodin, joten niiden voidaan ilmaista toteuttavan rajapintaluokan Lypsava. Algoritmeissa voi toki olla lajikohtaisia eroja. Ja toteuttaminen on nimenomaan sanottava luokkamäärittelyn otsikossa:

    public class Lehma extends Nauta
                       implements Lypsava {
      ...
    }
Luokan alussa oleva implements-ilmaus on lupaus: luokassa on määritelty eli toteutettu kaikki kyseisen rajapintaluokan luettelemat metodit!

Rajapintaluokka on tyyppi siinä kuin mikä tahansa muukin Javan tyyppi! Se kelpaa muodollisen parametrin tyypiksi, muuttujan tyypiksi, taulukon komponenttityypiksi, metodin arvotyypiksi, ...

Rajapintaluokkaa Lypsava voitaisiin käyttää vaikkapa juustotehtaan toteutuksessa:

public class Juustotehdas {

  private Lypsava lypsettävä;
  // ... tehtaan muu kalusto ...

  public Juustotehdas(Lypsava tuotantoelain) {
    this.lypsettävä = tuotantoelain;
    // muun kaluston konstruointi
  }

  public double teeJuustoa() {
     double maitomäärä = lypsettävä.lypsä();
     // ... juuston tuotanto maidosta ...
     return ...
  }

  // ... muita aksessoreita

}
Tehdasta voitaisiin käyttää vaikkapa seuraavasti:
  ...
  Lehma mansikki = new Lehma();
  Juustotehdas emmental = new Juustotehdas(mansikki);
  double juustoa = emmental.teeJuustoa();
  ...
  Kuttu bää = new Kuttu();
  Juustotehdas vuohenjuusto = new Juustotehdas(bää);
  double juustoa = vuohenjuusto.teeJuustoa();
  ...
  Juustotehdas lampaanjuusto = new Juustotehdas(new Uuhi());
  ...
Huomautuksia:

Abstrakti luokka ja rajapintaluokka

Luvun 4.4 esimerkki abstraktista luokasta olisi luontevampaa toteuttaa rajapintaluokan avulla, koska Moottori-luokassa ei ollut mitään ei-abstraktia, ei kenttiä, ei metodeita.
public interface Moottori {
  public abstract double annaKierrosluku();
  public abstract void asetaPolttoaineensyöttömäärä(double määrä);
}
//-----------------------------
public class Auto {
  private Moottori m;
  // ties mitä muita tietorakenteita ...
  public Auto(Moottori m) {   // Moottori on rajapintaluokka!
    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 ...
}
//-----------------------------
public class Cosworth implements 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 ...
}
//-----------------------------
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
    // ...
  }
}

Jos luokkaan Moottori haluttaisiin ohjelmoida jotakin ei-abstraktia toiminnallisuutta - esimerkiksi sarjanumeron generointi - ei rajapintaluokkaa voisi käyttää:
public abstract class XMoottori {

  // ei-abstrakti osuus:

  private static int sarjanumerolaskuri = 0;
  private final int omaSarjanumero;

  public XMoottori() {
    ++sarjanumerolaskuri;                // HUOM: abstraktinkin yliluokan
    omaSarjanumero = sarjanumerolaskuri; // konstruktori käydään suorittamassa,
  }                                      // kun aliluokasta luodaan ilmentymä!

  public int getSarjanumero() {return omaSarjanumero;}

  // abstraktit metodit:
  public abstract double annaKierrosluku();
  public abstract void asetaPolttoaineensyöttömäärä(double määrä);
}
//-----------------------------
public class XAuto {
  private XMoottori m;
  // ties mitä muita tietorakenteita ...
  public XAuto(XMoottori 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 ...
}
//-----------------------------
public class XCosworth extends XMoottori {
                    // HUOM: luokka perii metodin getSarjanumero() sellaisenaan!
  private double kierrosluku;
  // muut tietorakenteet ...
  public XCosworth() {
    // ...
  }
  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 ...
}
//-----------------------------
public class XAutoSovellus {
  public static void main(String[] args) {

    XCosworth apumoottori = new XCosworth();
    System.out.println(apumoottori.getSarjanumero()); // 1  <----!

    XCosworth brrrmmm = new XCosworth();
    System.out.println(brrrmmm.getSarjanumero());     // 2  <----!

    XAuto lotus49 = new XAuto(brrrmmm);
    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
    // ...
  }
}

"Arton polymorfismisääntöjä":

Olkoon TyyppiB luokka, joka on luokan TyyppiA välitön tai välillinen aliluokka. Tai olkoon TyyppiA rajapintaluokka, jonka TyyppiB toteuttaa.
Olkoon sitten muuttuja x tuon yliluokan tai rajapintaluokan tyyppiä: TyyppiA x;

Tällaisen muuttujan arvoksi voi sijoittaa viitteen mihin tahansa olioon, joka on tyyppiä TyyppiA. Koska "jokainen kissa on eläin" eli aliluokan jokainen ilmentymä on myös yliluokan tyyppiä, on luvallista sijoittaa

  x = new TyyppiB();
"Aksessoidaan muuttujaa": x.met();



Takaisin luvun 4 sisällysluetteloon.