Johdatus Javan perintään

Perintä on mekanismi, jonka avulla luokka voidaan määrittää joidenkin muiden luokkien laajennokseksi tai mukautetuksi versioksi. Perivää luokkaa sanotaan aliluokaksi ja perittyjä luokkia yliluokiksi. Perivän luokan olioiden voi ajatella koostuvan perittyjen yliluokkien (ali)olioista, joiden oheen perivä luokka voi määrittää uusia tai korvaavia ominaisuuksia.

Javassa luokkien perintä on yksinkertaisuuden vuoksi rajoitettu niin, että luokka voi periä suoraan vain yhden luokan. Luokka voi toki tällöinkin välillisesti periä useitakin luokkia, mutta luokkien perintähierarkia on lineaarinen esimerkiksi tapaan “luokka Teacher perii luokan Employee, joka perii luokan Person. Tässä luokka Teacher perisi suoraan luokan Employee ja epäsuorasti Employee:n kautta myös luokan Person. Luokan Teacher olioilla on tällöin myös luokkien Employee ja Person ominaisuudet.

Javassa luokan perintä tehdään lisäämällä luokkamäärityksessä luokan nimen perään avainsana extends ja perittävän luokan nimi. C++-kielen perintään tutustuneiden kannalta on oleellista mainita, ettei Javassa voi erikseen määrittää perinnälle näkyvyyttä: Javan perintä vastaa C++:n julkista perintää. Perivän luokan luokkamäärityksessä voi viitata perityn luokan kaikkiin muihin paitsi yksityisiin jäseniin. Perivä luokka voi siten manipuloida perityn luokan yksityisiä jäseniä ainoastaan sen puitteissa, millaisia niitä manipuloivia ei-yksityisiä jäsenfunktioita peritty luokka sisältää.

Tarkastellaan esimerkkiä, missä edelläkuvatun perintäketjun luokat olisivat seuraavan kuvan mukaisia:

Luokkien Person, Employee ja Teacher määrittelyt.

Hahmotelma luokkien Person, Employee ja Teacher määrittelyistä.

Seuraava kuva näyttää, millainen rakenne näiden luokkien olioilla olisi Javassa.

Person-, Employee- ja Teacher-olioiden rakenteet.

Hahmotelma Person-, Employee- ja Teacher-olioiden rakenteista.

Huomaa, kuinka Teacher-olio sisältää ensin luokan Person, sitten luokan Employee ja lopuksi luokan Teacher ei-staattiset jäsenmuuttujat. Vastaavasti Employee-oliossa on ensin luokan Person ja sitten luokan Employee ei-staattiset jäsenmuuttujat. Tämä kuvastaa sitä, kuinka kukin aliluokka täydentää yliluokkaansa. Osa jäsenmuuttujista on esitetty harmaalla värillä. Tämä kuvastaa sitä, että ne ovat yliluokan yksityisiä jäseniä eikä perivällä luokalla ole oikeutta käsitellä niitä suoraan, vaikka nekin ovat osa samaa oliota. Kuvasta on jätetty pois esim. sellainen tekninen detalji, että Javassa kullakin oliolla on lisäksi viite oman luokkansa ns. virtuaalitauluun, joka sisältää tietoa olion luokan jäsenfunktioista.

Seuraava kuva listaa luokkien Person, Employee ja Teacher rakentimet ja julkiset funktiot. Kuva havainnollistaa sitä seikkaa, että rakentimet eivät periydy, mutta epäyksityiset funktiot periytyvät. Tässä periytyminen ei sinänsä tarkoita, että perivä luokka sisältäisi kopiot yliluokkansa funktioista, vaan että perivän luokan kautta voi kutsua yliluokassa määritettyjä epäyksityisiä funktioita.

Luokkien Person, Employee ja Teacher rakentimet ja julkiset jäsenfunktiot.

Luokkien Person, Employee ja Teacher rakentimet ja julkiset jäsenfunktiot.

Perinnän yhteydessä on tärkeä huomata, että perivän luokan rakentimet ovat vastuussa myös olion yliluokilta perittyjen ominaisuuksien asianmukaisesta alustamisesta. Tämän vuoksi perivän luokan rakentimen tulee tarvittaessa välittää yliluokan rakentimelle sen tarvitsemat parametrit. Edellinen esimerkki havainnollisti tätä seikkaa siinä mielessä, että aliluokkien rakentimet ottivat “omien” jäsenmuuttujiensa alustusarvojen lisäksi myös yliluokkien jäsenmuuttujien alustusarvot. Parametrit välitetään yliluokan rakentimelle kutsumalla yliluokan rakenninta avainsanan super kautta, ja tämä täytyy tehdä aliluokan rakentimen ensimmäisenä toimenpiteenä.

Seuraava esimerkkikoodi esittää kokonaiset Java-kieliset toteutukset luokille Person, Employee ja Teacher. Luokat on esitetty yksinkertaisuuden vuoksi suoraan peräkkäin; ne täytyisi asettaa erillisiin tiedostoihin.

public class Person {
  private String name;
  private String personId;

  public Person(String name, String personId) {
    this.name = name;
    this.personId = personId;
  }

  public String getName() {
    return name;
  }

  public String getPersonId() {
    return personId;
  }
}

public class Employee extends Person {  // Peritään Person: lisätään perään "extends Person".
  private String title;
  private float salary;

  public Employee(String name, String personId, String title, float salary) {
    super(name, personId);  // Alustetaan ensin yliluokka Person.
    this.title = title;     // Yliluokan alustamisen jälkeen omat alustukset yms.
    this.salary = salary;
  }

  public String getTitle() {
    return title;
  }

  public float getSalary() {
    return salary;
  }
}

import java.util.List;  // Luokalla Teacher on jäsenenä List-viite.

public class Teacher extends Employee { // Peritään Employee: lisätään perään "extends Employee".
  private List<String> classes;

  public Teacher(String name, String personId, String title, float salary,
          List<String> classes) {
    super(name, personId, title, salary); // Alustetaan ensin yliluokka Employee.
    this.classes = classes;
  }

  public List<String> getClasses() {
    return classes;
  }
}

Perivään luokkaan voi toteuttaa perityn funktion kanssa samanlaisen (sama nimi ja yhteensopivat parametrien sekä palautusarvon tyypit) jäsenfunktion. Tällöin perivän luokan oma toteutus korvaa kyseisen perityn funktion.

Ali- ja yliluokan oliot ovat keskenään “yhteensopivia” perintähierarkiassa alhaalta ylöspäin edeten: aliluokan oliota on laillista käsitellä aivan kuin se olisi yliluokkansa olio. Aliluokan olio voidaan esimerkiksi viitata sen yliluokan tyyppiä vastaavalla viitemuuttujalla. Tämä kuvastaa sitä, kuinka aliluokka periaatteessa omaa kaikki yliluokkansa ominaisuudet. Perintää usein kuvataankin “is a”-suhteeksi: aliluokan oliot ovat (myös) perimänsä yliluokan olioita. Aliluokan oliota voidaan siis ainakin ohjelmointikielen näkökulmasta käyttää jokseenkin kaikissa konteksteissa, missä sen yliluokan olio olisi laillinen. Käytännön kannalta tämä tosin edellyttää, ettei aliluokka ole määrittänyt jotain sellaisia korvaavia omia ominaisuuksia, jotka johtavat yliluokalle asetettujen odotusten suhteen virheelliseen lopputulokseen. Näin ei toki pitäisi koskaan käydä: yliluokalta odotettujen ominaisuuksien rikkominen aliluokassa on merkki huonosta ohjelmistosuunnittelusta.

Javassa on erityinen kattoluokka Object, jonka jokainen viitetyyppi (myös taulukot) oletusarvoisesti perii. Kaikilla olioilla on siten olemassa mm. seuraavat Object-luokan määrittämät julkiset jäsenfunktiot:

  • toString(): palauttaa oliota kuvaavan String-merkkijonoesityksen.

    • Object-luokasta peritty versio tulostaa ainoastaan olion identiteettiä koskevaa tietoa.

    • Kun tulostat jonkin olion x, esim. tapaan System.out.println(x), muunnetaan x automaattisesti merkkijonoksi kutsulla x.toString.

  • equals(Object b): palauttaa tiedon (true/false), onko olio samanlainen kuin olio b.

    • Object-luokasta peritty versio vertailee olioiden identiteettejä eikä samanlaisuutta.

  • hashCode(): palauttaa oliolle lasketun hajautusarvon (int-arvo).

    • Object-luokasta peritty versio tyypillisesti laskee hajautuskoodin olion identiteetin (muistiosoitteen) eikä arvon pohjalta.

Koska näiden funktioiden Object-luokalta perityt versiot pohjautuvat (yleensä) vain olion identiteettiin eikä arvoon, tulisi kaikkien muiden luokkien tarvittaessa toteuttaa omat korvaavat versionsa niistä.

Alla on esimerkkinä ArrayList<Object>-luokan perivä luokka ObjectList, jonka ainoa itse määrittämä jäsen on oma jäsenfunktion toString toteutus. Varsin moni luokka toteuttaa siitä oman korvaavan version esimerkiksi helpottaakseen luokan olioiden tulostamista. Esimerkissä samalla tuodaan esiin, että Javan tarjoamilla valmiilla listaluokilla on yhteinen kattotyyppi List. Siten esimerkiksi ArrayList<Object>-, ObjectList- ja LinkedList<Object>-olioita voidaan käsitellä yhdenmukaisesti List<Object>-viitteiden kautta. Tässä on huomattava, että viitteen kautta voi tehdä vain viitteen tyypin puitteissa laillisia jäsenviittauksia. Esimerkiksi esimerkkikoodissa ei voisi yrittää kutsua ArrayList-olion jäsenfuktiota trimToSize List-viitteen kautta, koska List-tyypillä ei ole sellaista jäsenfunktiota.

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ObjectList extends ArrayList<Object> {
  // Korvaava toteutus toString-funktiolle. Tässä on samalla esimerkki @Override-
  // annotaatiosta. Javassa voi (ja kannattaa, vaikkei ole pakko) asettaa korvaavaksi
  // funktioksi tarkoitetun toteutuksen eteen annotaation "@Override". Tämä ohjaa
  // Java-kääntäjää varmistamaan, että toteutus todella korvaa on korvaus eli että sen
  // muoto täsmää jonkin yliluokasta perityn funktion kanssa.
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder(this.size() + " values:\n");
    for(int i = 0; i < this.size(); i++) {
      sb.append("  ").append(Integer.toString(i)).append(": ")
              .append(this.get(i).toString());
    }
    return sb.toString();
  }

  public static void main(String[] args) {
    // Luodaan ObjectList, johon asetetaan arvot "one", 2, 3.0 ja "four".
    // Tässä käytetään ArrayList-luokalta perittyä add-funktiota.
    ObjectList myList = new ObjectList();
    myList.add("one");
    myList.add(2);
    myList.add(3.0);
    myList.add("four");

    // Luodaan ja tulostetaan vertailun vuoksi sekä ArrayList<Object> että
    // LinkedList<Object>, jotka alustetaan ObjectList-listan myList sisällöllä.
    // Tämä onnistuu, koska Javan säiliöt voi yleensä alustaa toisella säiliöllä ja
    // ObjectList-lista kelpaa säiliöksi, koska se on säiliöluokan ArrayList aliluokka.
    ArrayList<Object> arrList = new ArrayList<>(myList);
    LinkedList<Object> linkedList = new LinkedList<>(myList);
    System.out.format("myList:%n%s%n%n", myList);
    System.out.format("arrList:%n%s%n%n", arrList);
    System.out.format("linkedList:%n%s%n%n", linkedList);

    // ObjectList-oliota voi käsitellä sen yliluokan ArraList<ObjectA> viitteen kautta.
    ArrayList<Object> myList2 = myList;

    // Lisäksi kaikkia Javan listoja voi käsitellä List-tyyppisinä. Alla tulostetaan kukin
    // edellä luotu lista alempana määritetyllä funktiolla printList, joka ottaa parametrikseen
    // tyyppiä List<Object> olevan olion. Myös esim. ObjectList käy sellaiseksi, koska ObjectList
    // on ArrayList-yliluokkansa välityksellä myös List.
    printList("myList", myList);
    printList("arrList", arrList);
    printList("linkedList", linkedList);
  }

  // Tulostaa List<Object>-tyyppisen listan alkiot.
  // Tulostukseen lisätään parametrin title ilmaisema listan nimi.
  private static void printList(String title, List<Object> list) {
    System.out.format("printList(%s):%n", title);
    for(int i = 0; i < list.size(); i++) {
      System.out.format(" %s[%d]: %s", title, i, list.get(i));
    }
    System.out.format("%n%n");
  }
}

Esimerkin suorittaminen tuottaa tulostuksen:

myList:
4 values:
  0: one  1: 2  2: 3.0  3: four

arrList:
[one, 2, 3.0, four]

linkedList:
[one, 2, 3.0, four]

printList(myList):
myList[0]: one myList[1]: 2 myList[2]: 3.0 myList[3]: four

printList(arrList):
arrList[0]: one arrList[1]: 2 arrList[2]: 3.0 arrList[3]: four

printList(linkedList):
linkedList[0]: one linkedList[1]: 2 linkedList[2]: 3.0 linkedList[3]: four

Eräs edellisessä esimerkissä merkillepantava seikka on, että funktiossa printList voitiin suorittaa kutsut list.size() ja list.get(i). Tyypillä List<Object> on kyseiset julkiset jäsenfunktiot, ja siten myös kaikilla sen alityypeillä, kuten ArrayList<Object> ja ObjectList, on oltava tällaiset julkiset funktiot. Emme toteuttaneet luokalle ObjectList niistä omia versiota, mutta se peri kyseiset funktiot yliluokaltaan ArrayList<Object>.

Toinen, ja erittäin tärkeä, kutsuja list.size() ja list.get(i) koskeva huomio on, että kutsut sidottiin ajonaikaisesti olion oman tyypin toteuttamiin funktioihin: vaikka list oli List<Object>-viite, ohjautui esimerkiksi olion linkedList tapauksessa kutsu list.get(i) olion oman luokan LinkedList<Object> eikä viitteen tyypin List<Object> mukaiseen get-funktioon. Tämä käyttäytyminen ilmentää laajemmin ottaen sitä, että Javassa kaikki jäsenfunktiot ovat oletusarvoisesti “virtuaalisia” eli olioviitteen kautta tehtävä jäsenviittaus sidotaan ajonaikana olion konkreettisen luokan mukaan. Tässä “ei-virtuaalinen” vaihtoehto, joka on esimerkiksi C++-kielessä oletusarvoinen käyttäytymismalli, tarkoittaisi kutsun sitomista jo käännösaikana viitteen tyypin mukaan (jolloin kutsut list.size() ja list.get(i) olisivat ohjautuneet tyypin List<Object> funktioihin size ja get).

Perinnän yhdeksi päämotivaatioksi mainitaan toiminnallisuuden uudelleenkäyttäminen: ei tarvitse toteuttaa kaikkea toiminnallisuutta tyhjästä vaan tuodaan luokkaan perinnän kautta sen yliluokkien jo valmiiksi toteuttamia ominaisuuksia. Perinnän kaikkein tärkeimmäksi ominaisuudeksi voisi kuitenkin sanoa mahdollisuutta käsitellä monentyyppisiä olioita yhdenmukaisesti, yhteisen kattotyypin avulla. Edellä funktio printList oli yksi esimerkki tästä: se toimii kaikkien sellaisten listojen kanssa, joiden kattotyyppi on List<Object>. Perinnän ansiosta saimme tulostettua kolme erityyppistä listaa samalla funktiolla sen sijaan, että olisimme joutuneet toteuttamaan kolme erillistä funktiota printList(ArrayList<ObjectList>), printList(ArrayList<Object>) ja printList(LinkedList<Object>).

Jos olit tarkkaavainen ehkä huomasit, ettei edellä puhuttu luokasta List vaan ylimalkaisemmin “tyypistä” List. Tämä johtuu siitä, että List ei ole luokka vaan rajapinta. Rajapintoja käsitellään pian tarkemmin.

Koodausdemo (kesto 1:05:34)

Javassa luokan perintä tehdään lisäämällä luokkamäärityksessä luokan nimen perään

Perivä luokka voi

Javassa on erityinen kattoluokka Object, jonka jokainen viitetyyppi (myös taulukot) oletusarvoisesti perii.