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:
Seuraava kuva näyttää, millainen rakenne näiden luokkien olioilla olisi Javassa.
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.
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 kuvaavanString
-merkkijonoesityksen.Object
-luokasta peritty versio tulostaa ainoastaan olion identiteettiä koskevaa tietoa.Kun tulostat jonkin olion
x
, esim. tapaanSystem.out.println(x)
, muunnetaanx
automaattisesti merkkijonoksi kutsullax.toString
.
equals(Object b)
: palauttaa tiedon (true
/false
), onko olio samanlainen kuin oliob
.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.