Arto Wikla 2011. Materiaalia saa vapaasti käyttää itseopiskeluun. Muu käyttö vaatii luvan.

9 Abstraktit luokat ja rajapintaluokat

(Muutettu viimeksi 17.11.2011, sivu perustettu 12.11.2010. Arto Wikla)

Tässä luvussa tutustutaan kahteen tärkeään olio-ohjelmoinnin työkaluun, abstraktiin luokkaan (abstract class) ja rajapintaluokkaan (interface). Molemmat ovat siitä merkillisiä tapauksia, ettei niistä voi tavallisten luokkien tapaan luoda lainkaan ilmentymiä! Mikä siis on niiden olemassaolon syy?

Ensinnäkin molemmat ovat tyyppejä, toisin sanoen muuttujat, kentät, parametrit, paluuarvot, jne. voivat olla tyypiltään tällaisia. Ja tuollaiset muuttujat yms. sitten voivat saada arvokseen vaikkapa abstraktin luokan aliluokkien ilmentymiä. Rajapintaluokkien käyttötekniikka perustuu samantapaiseen tekniikkaan.

Muista tyyppisääntö: Jokainen kissa on eläin, mutta jokainen eläin ei ole kissa! Siispä abstraktinkin luokan aliluokan ilmentymä on abstraktin luokan tyyppiä. Ja niinpä tuollainen ilmentymä sopii sijoitettavaksi muuttujaan, jonka tyyppi on tuo abstrakti luokka. Kissa-olion voi sijoittaa Elain-tyyppiseen muuttujaan.

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. Metodilta siis puuttuu algoritmi kokonaan:

public abstract void metodi();

Vain abstraktilla luokalla voi olla abstrakteja metodeita.

Abstraktista luokasta ei siis voi luoda ilmentymää, mutta aliluokkia sille voidaan laatia. Ja juuri tämä on abstraktin luokan olemassaolon tarkoitus:

Esimerkki:

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 int getKierrosluku();
  public abstract void   setPolttoainetta(int maara);

  // ...
  // Muita metodeita, myös ei-abstrakteja!
  // Ja jopa sellaisia, jotka kutsuvat abstrakteja metodeita!
  // Esim:

  public String toString() {
    return "RPM = " + getKierrosluku();  // kutsutaan abstraktia metodia!
  }
}

Tästä luokasta ei tietenkään voida luoda ilmentymiä kuten ei mistään muustakaan abstraktista luokasta.

Sitten ohjelmoidaan se pyydetty luokka Auto. Kirjoitetaan rohkeasti auton moottorin tyypiksi abstrakti luokka Moottori. Konstruktorille todelliseksi parametriksi 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 int getNopeus() {
    // nopeus riippuu tavalla tai toisella kierrosluvusta:
    return /* ... */ m.getKierrosluku()/30 /* ? tms...*/;
  }

  public void kaasuta(int bensaa) {
    m.setPolttoainetta(bensaa);

    // ...
  }
  // ties mitä muita metodeita ...
}

Autoja voidaan siis valmistaa vain antamalla konstruktorille todellisena parametrina jokin konkreettinen Moottori-olio. Abstraktista Moottori-luokasta 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 int kierrosluku;
  // muut tietorakenteet ...

  public Cosworth() {
    // ...
  }

  public int getKierrosluku() {
    return kierrosluku;
  }

  public void setPolttoainetta(int maara) {
    if (maara < 0)
      kierrosluku = 0;
    else if (maara > 100)
      kierrosluku = 9300;
    else
      kierrosluku = maara*93;  // 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.getNopeus()); // 297

    lotus49.kaasuta(-23);
    System.out.println(lotus49.getNopeus()); // 0

    lotus49.kaasuta(1100);
    System.out.println(lotus49.getNopeus()); // 310

    // ...
  }
}

Luokan Auto konstruktorin muodollisen parametrin tyyppi on abstrakti luokka Moottori, Cosworth on Moottori-luokan ei-abstrakti aliluokka. Ja nimenomaan koska Cosworth on Moottorin aliluokka, se on tyypiltään myös Moottori ja siten kelpaa annettavaksi todellisena parametrina Auton konstruktorille!

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

public class KilpaAjaja {

  private Auto auto;
  // ... kilpa-ajajan muut tarvikkeet ...

  public KilpaAjaja(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);
KilpaAjaja jimClark = new KilpaAjaja(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 olio-ohjelmaa usein kirjoitetaankin:

KilpaAjaja jimClark = new KilpaAjaja(new Auto(new Cosworth()));

Abstrakti luokka ja koukkumetodit

Moottori-luokkaan edellä ohjelmoitiin myös yksi ei-abstrakti metodi, toString. Koska sitä ei syrjäytetty (eli korvattu) aliluokassa Cosworth, se ihan tavalliseen tapaan periytyi tähän aliluokkaan.

Muokataan edellä nähtyä esimerkkiä:

public class AutoSovellus {

  public static void main(String[] args) {

    Cosworth brrrmmm = new Cosworth(); // "oikea moottori", joka
    Auto lotus49 = new Auto(brrrmmm);  // kelpaa auton luontiin

    System.out.println(brrrmmm);             // RPM = 0   // Moottorin koukussa on
                                                          // Cosworthin getKierrosluku!
    lotus49.kaasuta(96);
    System.out.println(lotus49.getNopeus()); // 297

    System.out.println(brrrmmm);             // RPM = 8928

    lotus49.kaasuta(-23);
    System.out.println(lotus49.getNopeus()); // 0

    System.out.println(brrrmmm);             // RPM = 0

    lotus49.kaasuta(1100);
    System.out.println(lotus49.getNopeus()); // 310

    System.out.println(brrrmmm);             // RPM = 9300

    // ...
  }
}

Tällainen tekniikka – ei-abstraktit metodit kutsuvat abstrakteja metodeja – tarjoaa erään melko tavallisen tavan laatia helposti muokattavia sovelluksia:

Laaditaan palveluiltaan rikas luokka, luokka, joka toteuttaa monia metodeja ei-abstrakteina. Vain joitakin metodeja jätetään abstrakteiksi. Ja juuri nämä abstarktit metodit ovat niitä, jotka aliluokassa toteuttamalla sovellus erikoistetaan omiin tarkoituksiin sopivaksi.

Ohjelmoidaan esimerkkinä "monitaitoinen" Tervehtija:

public abstract class Tervehtija {

  public abstract String kuka();  // koukkumetodi

  // ei-abstrakteja metodeita, jotka kutsuvat abstraktia metodia kuka()

  public void aamulla() {
    System.out.println("Huomenta " + kuka() + "!");
  }

  public void paivalla() {
    System.out.println("Päivää" + kuka() + "!");
  }

  public void illalla() {
    System.out.println("Iltaa" + kuka() + "!");
  }

  public void yolla() {
    System.out.println("Hyvää yötä" + kuka() + "!");
  }
}

Nyt Tervehtijan aliluokkina voidaan toteuttaa erilaisia tervehdittäviä:

public class PekanTervehtija extends Tervehtija {

   public String kuka() {
     return "Pekka";
   }
}

public class LiisanTervehtija extends Tervehtija {

   public String kuka() {
     return "Liisa";
   }
}

Käyttöesimerkki:

public class TervehtijaEsimerkkeja {
  public static void main(String[] args) {

    PekanTervehtija  pekka   = new PekanTervehtija();
    LiisanTervehtija liisa = new LiisanTervehtija();

    pekka.aamulla();  // Huomenta Pekka!
    liisa.aamulla();  // Huomenta Liisa!

    pekka.paivalla(); // Päivää Pekka!
    liisa.illalla();  // Iltaa Liisa!

    pekka.yolla();    // Hyvää yötä Pekka!
    liisa.aamulla();  // Huomenta Liisa!

    pekka.illalla();  // Iltaa Pekka!
    liisa.paivalla(); // Päivää Liisa!
  }
}

Joskus yllä nähtyyn tekniikkaan – toteutetaan jokin/muutama peritty abstrakti metodi ja saadaan "ilmaiseksi" kaikenlaista valmiiksi ohjelmoitua lisätoiminnallisuutta – liitetään ilmauksia "Hollywood-periaate: 'Don't call us, we call you'", "koukkumetodi" (hook method) ja "template-metodi". Tekniikkaa voidaan käyttää esimerkiksi ns. sovelluskehysten laadinnassa.

Laaditaan edellistä hieman realistisempi esimerkki tästä tärkeästä tekniikasta :

Abstrakti luokka Kappale mallintaa "pohjaltaan ja kanneltaan" yhtä suuria kappaleita, joilla kaikilla on ominaisuus korkeus. Se sisältää "koukun" pintaAla(), johon voidaan ripustaa erityisen kappaleen pohjan (ja samalla "kannen") pinta-alan laskentataito. Valmiiksi ohjelmoituna eli ei-abstraktina luokka sisältää tilavuuden laskenta-algoritmin:

public abstract class Kappale {
  private double korkeus;

  public Kappale(double korkeus) {
    this.korkeus = korkeus;
  }

  public abstract double pintaAla();  // KOUKKU!

  public double tilavuus() {
    return pintaAla()*korkeus;
  }
} 

Kuutio ripustaa koukkuun taidon laskea kuution pohjan (ja "kannen") pinta-alan:

public class Kuutio extends Kappale {
  private double sarma;

  public Kuutio(double sarma) {
    super(sarma);
    this.sarma = sarma;
  }

  public double pintaAla() {
    return sarma*sarma;
  }
} 

Lierio ripustaa koukkuun taidon laskea lieriön pohjan (ja "kannen") pinta-alan:

public class Lierio extends Kappale {
  private double sade;

  public Lierio(double korkeus, double sade) {
    super(korkeus);
    this.sade = sade;
  }

  public double pintaAla() {
    return 2*sade*Math.PI;
  }
} 

Abstrakti luokka sovelluskehyksenä

Tarkastellaan esimerkkinä pienen "sovelluskehyksen" määrittelyä ja käyttöä. Laaditaan luokka Pelikehys, josta voi erikoistaa yksinkertaisia pelejä, jotka perustuvat kahden pelaajan String-muotoisiin vastauksiin. Oletuksena on myös, että kaikissa pelitilanteissa on voittaja. Tasapelejä kehys ei tunne.

Sovelluskehys tarjoaa pelin toteuttajalle palveluita, kunhan sovelluksen luoja vain ohjelmoi itse päätössäännön, jolla yhden pelin (pelikierroksen) voittaja ratkaistaan.

Teknisesti päätössäännön vaatiminen on toteutettu pelikehyksen abstraktina metodina:

public abstract class Pelikehys {

  // Tähän koukkuun ripustetaan todellinen päätössääntö, jolla voittajan valitaan:

  public abstract boolean ekaVoittaa(String eka, String toka);

  // Kehyksen tarjoamat valmiit palvelut:

  public void tulostaTulos(String eka, String toka) {
    if (ekaVoittaa(eka, toka))
      System.out.println("Ensimmäinen voittaa!");
    else
      System.out.println("Toinen voittaa!");
  }
}

Esimerkkinä kahden pelaajan peli, jossa pidemmän merkkijonon antaja voittaa. Jos merkkijonot ovat yhtä pitkät, toinen voittaa. Sovellus ohjelmoidaan abstraktin Pelikehys-luokan aliluokkana. Voittajan valitsevalle päätössäännölle, metodille ekaVoittaa annetaan toteutus:

import java.util.Scanner;

public class PidempiVoittaa extends Pelikehys {
  private static Scanner lukija = new Scanner(System.in);

  public boolean ekaVoittaa(String eka, String toka) {
     return eka.length() > toka.length();
  }

  public static void main(String[] args) {

    PidempiVoittaa peli = new PidempiVoittaa();
    String eka, toka;

    System.out.print("1. pelaajan vastaus: ");
    eka = lukija.nextLine();
    System.out.print("2. pelaajan vastaus: ");
    toka = lukija.nextLine();

    peli.tulostaTulos(eka, toka);
  }
}

Huomaa miten pääohjelmassa luodaan oman luokan ilmentymä, jolla sitten pelataan.

Toinen esimerkki, puhdas onnenpeli:

import java.util.Scanner;

public class Onnetar extends Pelikehys {
  private static Scanner lukija = new Scanner(System.in);

  public boolean ekaVoittaa(String eka, String toka) {
     return Math.random() < 0.5;
  }

  public static void main(String[] args) {

    Onnetar peli = new Onnetar();

    System.out.print("1. pelaajan vastaus: ");
    lukija.nextLine();   // vastauksella ei väliä!
    System.out.print("2. pelaajan vastaus: ");
    lukija.nextLine();

    peli.tulostaTulos("", "");  // vastauksilla ei väliä!
  }
}

Tätä esimerkkiä kehitellään moneen suuntaan harjoituksissa.

Abstrakti luokka tyyppinä

Abstraktissa luokassa toki kaikki metodit voivat olla abstrakteja. Esimerkiksi Moottori voisi olla seuraavaan tapaan ohjelmoitu:

public abstract class Moottori {

  public abstract int getKierrosluku();
  public abstract void setPolttoainetta(int maara);

  // ... mahdollisesti muita abstrakteja metodeita...
}

Kokonaan abstrakti luokka ei toteuta ainuttakaan metodia, kunhan vain antaa luettelon metodien otsikoista.

Mitä ihmeen tehtävää tällaisella pelkällä metodiluettelolla voisi olla? Itse asiassa paljonkin! Kun muistetaan, että aliluokan ilmentymä on aina myöskin yliluokan tyyppiä ja siten voidaan sijoittaa yliluokkansa tyyppisen muuttujan arvoksi, vastaus löytyy:

Jos halutaan käyttää mitä tahansa tietyt operaatiot omaavaa oliota kyseisten operaatioiden sallimiin tehtäviin, olio voidaan sijoittaa yhteisen yliluokan tyyppiseen muuttujaan.

Tällä tavoin voidaan tehdä yleiskäyttöisiä sovelluksia: Sovellus ohjelmoidaan ottamatta kantaa, minkä aliluokan ilmentymää käytetään – kutsutaan vain olioiden yhteisen (abstraktin) yliluokan (abstrakteja) metodeja ja polymorfismi pitää huolen, että todellisuudessa käynnistyvät kulloinkin kyseessä olevan olion omat metodit.

Auto-luokan moottoriksi kelpaa mikä tahansa Moottori-olio eli minkä tahansa Moottori-luokan ei-abstraktin aliluokan ilmentymä. Näinhän asiat olivat toki edelläkin, vaikka Moottori oli vain osittain abstrakti.

Kokonaan abstraktien luokkien idea perustuu täysin siihen, että luokka on tyyppi. Kaikki luokat itse asiassa ovat myös tyyppejä, mutta monet luokat myös toteuttavat joitakin ominaisuuksia, kenttiä ja metodeita.

Esimerkki kokonaan abstraktista luokasta tyyppinä:

Abstrakti eläimen mallinnus:

public abstract class Elain {
  public abstract void syo(double paljonko);
  public abstract double getPaino();
  public abstract String aantele(); 
  // ... jne.
}

Luokka luettelee, millä metodein kaikkia eläimiä voidaan käsitellä.

Toteutetaan eräitä erityisiä eläinluokkia:

public class Kissa extends Elain {
  private double paino;
  // ...
  public Kissa() {
    this.paino = 0.02; 
  }
  public void syo(double paljonko) {
    this.paino += 0.4 * paljonko;
  }
  public double getPaino() {
    return this.paino;
  }
  public String aantele() {
    return "Miau";
  }
}

public class Nauta extends Elain {
  private double paino;
  // ...
  public Nauta() {
    this.paino = 18.0; 
  }
  public void syo(double paljonko) {
    this.paino += 0.3 * paljonko;
  }
  public double getPaino() {
    return this.paino;
  }
  public String aantele() {
    return "Ammuu";
  }
}

public class Hevonen extends Elain {
  private double paino;
  // ...
  public Hevonen() {
    this.paino = 15.0;
  }
  public void syo(double paljonko) {
    this.paino += 0.1 * paljonko; 
  }
  public double getPaino() {
    return this.paino;
  }
  public String aantele() {
    return "Ihahaa";
  }
}  

Laaditaan sitten hyvin yksinkertainen eläinhoitola, jossa osataan hoitaa mitä tahansa eläintä. Ja "hoitaminen" tässä nyt tarkoittakoon pelkkää ruokkimista:

public class Elainhoitola {
  private Elain hoidettava;
  public Elainhoitola(Elain hoidettava) {
    this.hoidettava = hoidettava;
  }

  public void hoida(double maara) {
    hoidettava.syo(maara);
    System.out.println(hoidettava.aantele()
                    + ". Painan nyt " + hoidettava.getPaino());
  }
}

Tärkeä huomio: Koska Elainhoitolassa hoidettava eläin on Elain-tyyppisen muuttujan arvona, kääntäjä sallii eläimelle sovellettavaksi kaikkia luokassa Elain määriteltyjä metodeita, mutta vain näitä. Ohjelman suorituksen aikana valitaan kunkin eläimen oman luokan versio metodista. Se on taas se polymorfismi...

Ja lopuksi käyttöesimerkki. Mikä tahansa eläin kelpaa hoidettavaksi ja kutakin hoidellaan omaan luokkaan toteutetuin metodein.

public class Hesy {
  public static void main(String[] args) {

    Kissa missu = new Kissa();
    Nauta mansikki = new Nauta();
    Hevonen vedonVieteri = new Hevonen();

    Elainhoitola missunKoti = new Elainhoitola(missu);
    missunKoti.hoida(0.01);   // Miau. Painan nyt 0.024

    Elainhoitola navetta = new Elainhoitola(mansikki);
    navetta.hoida(4.0);       // Ammuu. Painan nyt 19.2

    Elainhoitola talli = new Elainhoitola(vedonVieteri);
    talli.hoida(5.0);         // Ihahaa. Painan nyt 15.5

    Elainhoitola tehotuotanto = new Elainhoitola(new Nauta());
    tehotuotanto.hoida(15.0); // Ammuu. Painan nyt 22.5

  }
}

Tärkeä huomio: Kun todelliseksi eläimeksi hoitolaan annetaan Kissa-olio, kissaa hoidetaan Kissa-luokan toteuttamin metodein, kun eläimeksi annetaan Nauta-olio, sitä hoidetaan naudan tapaan, jne. Oliolle sovellettava metodi mahdollisesti toisiaan syrjäyttävistä metodeista valitaan ohjelman suoritusaikana olion omaa luokkaa lähinnä olevasta luokasta. Tässsä esimerkissä metodit löytyivät jo heti olioiden omista luokista.

Ja taas siis polymorfismi rulaa...

Rajapintaluokka

Äskeisessä esimerkissä kaikki eläimet kelpaavat hoidettaviksi, koska kaikilla Elain luokan ei-abstrakteilla aliluokilla on oltava toteutus kaikista hoitamiseen tarvittavista metodeista syo, getPaino ja aantele.

Joskus voi kuitenkin eteen tulla tilanne, että periytymishierarkiassa siellä täällä on luokkia, jotka jakavat yhteisiä ominaisuuksia (= samannimisiä metodeita), joita ei kuitenkaan ole tarkoituksenmukaista periä yhteiseltä yliluokalta.

[Agraariterminologinen huomautus: Tässä esimerkkisarjassa "lypsettäviä" eläimiä – kuten lehmiä ja tammoja – kutsutaan "lypsäviksi". Taustalla on vanha kielenkäyttö tyyliin: "Mitenkäs paljon talossa on lypsäviä". Nykykorvaan Hilja Maitotyttö tai lypsykone kuulostaisi paremmin ansaitsevan adjektiivin "lypsävä", mutta historiallisista syistä kutsumme "lypsettävissä olevia" eläimiä "lypsäviksi".]

Esimerkki:

Eläinten darwinistisessa sukupuussa imettäväisten alipuun naaraspuoliset oliot ovat "lypsäviä". Olkoon tavoitteenamme juustotehtaan toteuttaminen. Yritetään ensin ohjelmoida Lehmä ja Tamma luokkien Nauta ja Hevonen aliluokkina:

public class Lehma extends Nauta {
  public double lypsa() {
    return 3.14;
  }
}

public class Tamma extends Hevonen {
  public double lypsa() {
    return 2.72;
  }
}

Vastaavaan tapaan voisimme ohjelmoida muitakin lypsävien olioiden luokkia, esimerkiksi: Lammas—Uuhi, Vuohi—Kuttu, jne. Todellisuudessa lypsäminen toki olisi monimutkaisempaa: ainakin eläimen paino vähenisi lypsettäessa yms. Esimerkin kannalta tällä ei kuitenkaan ole merkitystä.

Nyt herää kysymys, mikä tyyppi juustotehtaan tuotantoeläimelle annetaan. Jos se on Lehma, miten tammanjuustotehdas toteutetaan. Ja yleiskäyttöistä tehdastahan tässä tavoitellaan.

Yksi ratkaisu olisi ohjelmoida yhteiseen yliluokkaan Elain abstrakti metodi lypsa() ja vaatia siis kaikkia eläinluokkia tavalla tai toisella toteuttamaan tuo metodi. Kömpelöä!

Ja jos juustotehtaan tuotantoeläimen tyyppi sitten olisi Elain, miten kävisi kun tuotantoeläimeksi annettaisiin Maranellon korskea orhi tai Saku Sammakko. Molemmathan ovat eläimiä.

Ohjelmoitikielissä joissa on ns. moniperintä eli luokalla voi olla useita yliluokkia, asia on on helppo hoitaa: Ohjelmoidaan vain yksi uusi (abstrakti tai ei) luokka Lypsava ja asetetaan Lehma ja Tamma myös Lypsavan aliluokaksi:

public class Lehma extends Nauta and extends Lypsava ...
public class Tamma extends Hevonen and extends Lypsava ...

Tässä luokkaan Lypsava voitaisiin ohjelmoida myös kenttiä ja ei-abstrakteja metodeita, jotka aliluokat perisivät.

Nyt tullaan Javan erityiseen piirteeseen: Javassa kaikilla luokilla Object-luokkaa lukuunottamatta on täsmälleen yksi yliluokka eli Javassa on käytössä yksittäisperintä.

Javassa tällainen tilanne hoidetaan rajapintaluokan avulla. Rajapintaluokka (interface, "rajapinta") on nimetty kokoelma abstrakteja metodeita. Se kelpaa luokan tapaan tyypiksi.

Ohjelmoidaan rajapintaluokka lypsäville:

public interface Lypsava {
  public double lypsa();
} 

Ja nyt ohjelmoidaan Lehma ja Tamma siten, että ne yhden yliluokan perimisen lisäksi toteuttavat (implement) rajapintaluokan Lypsava:

public class Lehma extends Nauta implements Lypsava {
  public double lypsa() {
    return 3.14;
  }
}

public class Tamma extends Hevonen implements Lypsava {
  public double lypsa() {
    return 2.72;
  }
}

Tärkeä huomio: Kun luokan alussa luvataan toteuttaa jokin rajapintaluokka, kääntäjä tarkistaa, että lupaus pidetään eli metodit toteutetaan. Kääntäjä siis tietää, että kaikki oliot, joiden luokka toteuttaa Lypsavan, todellakin ovat lypsettävissä. Ja juuri siksi kääntäjä sallii rajapintaluokkaa Lypsava käytettävän tyyppinä. Tuon tyyppisen muuttujan arvolle voi soveltaa vain rajapintaluokan luettelemia metotodeita. (Luokka voi toteuttaa myös useita rajapintaluokkia.)

Viimein päästään juustotehtaan toteuttamiseen:

public class Juustotehdas {

  private Lypsava tuotantoelain;

  public Juustotehdas(Lypsava tuotantoelain) {
    this.tuotantoelain = tuotantoelain;
  }

  public double tuotaJuustoa(double pyydettyMaara) {
    double maara = 0.3 * tuotantoelain.lypsa(); // oletus syntyvän juuston määrästä
    while (maara < pyydettyMaara)
      maara += 0.3 * tuotantoelain.lypsa();
    return maara;
  }
}

Juustotehtaan tuotantoeläimen tyyppi on siis Lypsava. Tämä johtaa siihen, että kääntäjä sallii tuotantoeläimelle sovellettavaksi vain Lypsavassä lueteltujen metodien soveltamisen. Toisaalta samainen kääntäjä pitää huolen siitä, että Juustotehdas-konstruktorin kutsujalla on oltava olio, jonka luokka on toteuttanut kaikki Lypsavan vaatimat metodit. Ja siis taas polymorfismi rulaa...

Lopuksi esimerkki juustotuotannosta parissa tehtaassa:

public class Juustotuotanto {
  public static void main(String[] args) {

    Lehma mansikki = new Lehma();
    Juustotehdas edam = new Juustotehdas(mansikki);
    double juustoa = edam.tuotaJuustoa(10.0);
    System.out.println("Edamia: pyyntö 10.0, saatiin " + juustoa);

    Tamma valma = new Tamma();
    Juustotehdas mongol = new Juustotehdas(valma);
    juustoa = mongol.tuotaJuustoa(10.0);
    System.out.println("Mongolia: pyyntö 10.0, saatiin " + juustoa);
  }
}

Ohjelman tulostus:

Edamia: pyyntö 10.0, saatiin 10.362
Mongolia: pyyntö 10.0, saatiin 10.608000000000002

Huomautuksia:

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

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