Tämä kurssi on jo päättynyt.

Kurssin viimeisimmän version löydät täältä: O1: 2024

Luku 7.3: Periytyminen ja luokkahierarkiat

Tästä sivusta:

Pääkysymyksiä: Miten määrittelen alakäsitteen luokalle, joka ei ole piirreluokka? Miten Scalan valmiit luokat muodostavat tietotyyppien sukupuun? Miten muodostan itse omista luokistani sukupuun?

Mitä käsitellään? Luokan periytyminen toisesta. Scalan luokkahierarkia. Abstraktit luokat.

Mitä tehdään? Luetaan ja ohjelmoidaan.

Suuntaa antava työläysarvio:? Kolme tuntia.

Pistearvo: B70.

Oheisprojektit: Subtypes.

../_images/person04.png

Johdanto

Tässä luvussa jatkamme edellisen aiheesta eli ylä- ja alakäsitteiden määrittelemisestä.

Periytymiseksi tai perinnäksi (inheritance) kutsutulla tekniikalla myös tavallisia luokkia voi käyttää toisten luokkien yläkäsitteinä.

Lisää tasokuvioita

Luvussa 7.2 määrittelimme piirreluokan Shape ja sille toteuttavan luokan Rectangle:

trait Shape {

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double

}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {

  def area = this.sideLength * this.anotherSideLength

}

Entä jos haluamme lisätä ohjelman tyyppivalikoimaan neliöt: sellaiset suorakaiteet, joiden jokaisen sivun pituus on aina täsmälleen sama kuin muidenkin sivujen? Neliöolion voisi luoda käskyllä kuten new Square(10).

Yksi tapa olisi tietysti luoda Square-luokka, johon piirre Shape liitetään näin:

class Square(val sideLength: Double) extends Shape {

  def area = this.sideLength * this.sideLength

}

Kalvamaan jää, että koodissa on nyt selvästi toistoa: neliön pinta-alanlaskemisalgoritmi on aivan sama kuin suorakaiteenkin; sattuu vain olemaan niin, että sivut ovat saman mittaiset. Toteutus ei ole myöskään käsitteellisen mallinnuksen näkökulmasta ilahduttava, sillä se asettaa neliöt suorakaiteiden rinnalle Shape-tyypin alakäsitteeksi. Ihmisinä miellämme, että neliöt ovat erikoistapaus suorakaiteista: kukin neliö on myös suorakaide (ja kuvio).

Ongelma kuitenkin ratkeaa helposti: voidaan määritellä, että neliö on suorakaiteen alakäsite. Alla kuvana esitetyn käsitehierarkian voi muodostaa, vaikka Rectangle onkin tavallinen luokka eikä piirreluokka.

../_images/inheritance_shape_square.png

Ali- ja yliluokat

Määritelläänkin Square-luokka näin:

class Square(size: Double) extends Rectangle(size, size) {

}
Käytetään jälleen extends-sanaa. Tällä kertaa sen perässä on mainittu tavallinen luokka eikä piirreluokka. Sanotaan: luokka Square perii (inherits) luokan Rectangle. Perivää luokkaa sanotaan aliluokaksi (subclass), perittyä luokkaa yliluokaksi (superclass). Square-oliot ovat nyt myös Rectangle-tyyppisiä.
Luokalla Square on vain yksi konstruktoriparametri, joka kertoo kunkin sivun mitan.
Kun aliluokasta luodaan ilmentymä, tehdään myös yliluokassa määritellyt alustustoimenpiteet (esim. asetetaan yliluokassa määritellyille ilmentymämuuttujille arvoja). Aliluokan määrittelystä usein välitetään konstruktoriparametreja yliluokan ohjelmakoodille. Esimerkiksi tässä määritellään, että kun Square-oliota luodaan, niin tehdään samat alustustoimenpiteet kuin Rectangle-oliolle, kuitenkin siten, että molemmiksi suorakaiteiden konstruktoriparametreiksi (eli molemmiksi sivunpituuksiksi) laitetaan neliöolion saaman konstruktoriparametrin arvo. (Ks. animaatio alla.)
Aaltosulut saisi tässä itse asiassa jättää kokonaan poiskin, kun niiden sisään ei tule mitään. (Neliöille ei ole tässä määritelty mitään sellaisia metodeita tai ilmentymämuuttujia, joita ei suorakaiteillakin olisi.)

Periytyminen vs. piirreluokat

Luokan määritteleminen toisen luokan aliluokaksi näyttää kovasti samanlaiselta kuin luvussa 7.2 nähty piirreluokan liittäminen luokkaan. Samankaltaisesta asiasta onkin kyse.

Erojakin on; vertaillaan. Tässä ensin piirreluokkien ominaisuuksia käyttäen esimerkkinä piirreluokkaa Shape, joka kuvaa Rectangle-luokan yläkäsitteen:

Kun käytetään piirreluokkaa: Esimerkki
Yläkäsitettä kuvaa piirreluokka. trait Shape
Alakäsitteeseen liitetään tuo piirre. class Rectangle extends Shape
Piirreluokassa saa olla abstrakteja metodeita ja muuttujia. def area: Double
Piirreluokasta ei voi luoda suoraan ilmentymää. Pelkkä new Shape ei toimi.
Piirreluokalla ei voi olla konstruktoriparametreja. trait Shape(...) ei toimi.
On mahdollista liittää luokkaan useita piirreluokkia. class X extends Piirre1 with Piirre2 with Piirre3 toimii.

Ja tässä vastaavasti periytymisen ominaisuuksia käyttäen esimerkkinä yliluokkaa Rectangle, joka kuvaa Square-luokan yläkäsitteen:

Kun käytetään periytymistä: Esimerkki
Yläkäsitettä kuvaa tavallinen luokka. class Rectangle
Aliluokka periytyy yliluokasta. class Square extends Rectangle
Tavallisessa luokassa ei saa olla abstrakteja metodeja tai muuttujia. (Paitsi, että... lisää aiheesta kohta.) Rectanglen kaikilla metodeilla on toteutus.
Yliluokasta voi luoda ilmentymän suoraan. new Rectangle toimii.
Yliluokalla voi olla konstruktoriparametreja. class Rectangle(val x: Int, val y: Int) extends Shape toimii.
Luokalla saa (mm. Scalassa) olla vain yksi välitön yliluokka. class X extends Yli1 with Yli2 ei toimi. (Mutta class X extends Yli1 with Piirre1 with Piirre2 on sallittu.)

On monia tilanteita, joissa kumpi tahansa näistä tekniikoista kelpaa.

Abstraktit luokat

Eroa piirreluokkien ja periytymisen välillä hämmentää se, että on mahdollista määritellä niin sanottuja abstrakteja luokkia. Tutustutaan tähänkin aiheeseen esimerkin kautta.

Palataan puhelinlaskuteemaan, joka oli esillä luvun 2.3 esimerkissä. Tuolloin käytit luokkaa Puhelu, joka kuvasi yksittäisen puhelun laskutuksen kannalta oleellisia ominaisuuksia. Alla on yksi toteutus tämänkaltaiselle luokalle. (Tästä on esimerkin yksinkertaistamisen vuoksi jätetty pois paikallisverkkomaksu, joka oli mukana luvun 2.3 versiossa.)

class Puhelu(val alkuhinta: Double, val minuuttihinta: Double, val kesto: Double) {
  def kokonaishinta = this.alkuhinta + this.minuuttihinta * this.kesto
}

Entäpä, jos haluamme laskuihin mukaan myös tekstiviestit? Lisäksi haluaisimme merkitä kustakin laskutettavasta puhelusta tai viestistä, onko siinä jo mukana 24 prosentin arvonlisävero vai ei, ja tarjota metodin, joka laskee verottoman hinnan.

Halutaan siis, että lasku koostuisi "laskutettavista tapahtumista", joita ovat puhelut ja tekstiviestit. Ensimmäinen luonnos voisi olla seuraava:

class Tapahtuma(val alvLisatty: Boolean) {
  def verotonHinta = if (this.alvLisatty) this.kokonaishinta / 1.24 else this.kokonaishinta
}
class Puhelu(val kesto: Double,
             val alkuhinta: Double,
             val minuuttihinta: Double,
             alvLisatty: Boolean) extends Tapahtuma(alvLisatty) {
  def kokonaishinta = this.alkuhinta + this.minuuttihinta * this.kesto
}
class Tekstiviesti(val hinta: Double, alvLisatty: Boolean) extends Tapahtuma(alvLisatty) {
  def kokonaishinta = this.hinta
}
Haluamme, että Tapahtuma-oliota luotaessa konstruktoriparametri alvLisatty kertoo, onko kyseisen tapahtuman hinnassa mukana 24 %:n arvonlisäveroa. Mikäli on, niin metodi verotonHinta palauttaa hinnan, josta veron määrä on vähennetty.
Aliluokilla Puhelu ja Tekstiviesti on useampia konstruktoriparametreja, joista muut liittyvät näihin nimenomaisiin aliluokkiin, mutta ...
... parametrin alvLisatty arvo välitetään sellaisenaan yliluokan konstruktoriparametriksi. Sen käsittely hoituu kokonaan yliluokassa Tapahtuma.
Tässä esimerkkikoodissa on ongelma. Tapahtuma-luokan metodissa verotonHinta nimittäin kutsutaan tapahtumaolion kokonaishinta-metodia, mutta missään ei ole määritelty, että tällainen metodi todella on kaikilla tapahtumaolioilla. (Molemmilla tämän esimerkin aliluokilla sellainen kyllä on, mutta yleispätevästi ei voida sanoa, että kaikilla Tapahtuma-olioilla aina tällainen metodi olisi.) Scala-kääntäjä parahtaa.

Tarvittaisiin kaikille Tapahtuma-tyyppisille olioille kokonaishinta-metodi, jonka toteutus jätetään aliluokkien huoleksi. Jotenkin näin:

class Tapahtuma(val alvLisatty: Boolean) {

  def kokonaishinta: Double

  def verotonHinta = if (this.alvLisatty) this.kokonaishinta / 1.24 else this.kokonaishinta

}

Tässä siis kokonaishinta-metodi on abstrakti (eli toteutukseton). Sen määrittely kuitenkin takaa tällaisen metodin löytyvän tapahtumaolioilta, joten verotonHinta-metodi voidaan toteuttaa yleisesti mille tahansa tapahtumaoliolle Tapahtuma-luokassa.

Mutta ei kai muissa kuin piirreluokissa saanut olla abstrakteja metodeita!? Ja tosiaan: yllä oleva versio Tapahtuma-luokasta ei mene Scala-kääntäjästä läpi. Sen sijaan tämä menee:

abstract class Tapahtuma(val alvLisatty: Boolean) {

  def kokonaishinta: Double

  def verotonHinta = if (this.alvLisatty) this.kokonaishinta / 1.24 else this.kokonaishinta

}
Jos tavalliseen class-sanalla määriteltyyn luokkaan halutaan abstrakteja metodeja, se onnistuu kyllä, kunhan kirjaamme luokkamäärittelyyn sanan abstract.

Tällaista luokkaa sanotaan abstraktiksi luokaksi. Luokkaa, joka ei ole abstrakti eikä piirreluokka, voi vertailun vuoksi sanoa konkreettiseksi luokaksi (concrete class).

Abstrakti luokka muistuttaa siis piirreluokkaa. Otetaan se mukaan vertailuun:

  Piirreluokka Abstrakti yliluokka Konkreettinen yliluokka
Voiko sisältää abstrakteja metodeita? Voi. Voi. Ei voi.
Voiko luoda suoraan ilmentymiä newllä? Ei voi. Ei voi. Voi.
Voiko olla konstruktoriparametreja? Ei voi. Voi. Voi.
Voiko käyttää useita yläkäsitteinä (with-sanojen perässä)? Voi. Ei voi. Ei voi.

Toinen toteutus Tekstiviesti-luokalle

Yllä tekstiviestiluokka toteutettiin näin:

class Tekstiviesti(val hinta: Double, alvLisatty: Boolean) extends Tapahtuma(alvLisatty) {
  def kokonaishinta = this.hinta
}

Tuntuu ehkä tarpeettomalta käyttää kahta nimeä hinta ja kokonaishinta, joiden kautta pääsee käsiksi täsmälleen samaan arvoon. Eikä tarvitsekaan. Tämäkin toimii:

class Tekstiviesti(val kokonaishinta: Double, alvLisatty: Boolean) extends Tapahtuma(alvLisatty)

Ihmetystä voi aiheuttaa se, että nythän kokonaishinta onkin muuttuja eikä sennimistä metodia ole erikseen def-sanalla määritelty lainkaan toteuttamaan yliluokan abstrakti metodi. Mutta ei se mitään: abstraktin metodin voi toteuttaa näinkin. Oleellista on, että lausekkeella olio.kokonaishinta on Double-tyyppinen arvo myös tekstiviesteille. On luokan käyttäjän näkökulmasta merkityksetöntä, onko kyseessä val-muuttuja vai vaikutukseton, parametriton metodi, joka palauttaa aina saman arvon. (Lisää aiheesta Wikipediassa: uniform access principle.)

Eikö äsken olisi voitu käyttää piirreluokkaa?

Yksi ratkaisutapa olisi ollut vaihtaa Tapahtuma piirreluokaksi eli class-sana traitiin. Tämänsuuntaisella ratkaisulla on mahdollista saada aikaan toimiva ohjelma. Kuitenkin ratkaisu on sikäli epätyydyttävä, että Scala kieltää piirreluokilta konstruktoriparametrit, joten muitakin muutoksia olisi tarvittu.

Vapaaehtoinen lisätehtävä: mieti, millainen ratkaisusta tulisi, jos Tapahtuma olisikin piirreluokka.

Käyttäisinkö piirreluokkaa vai yliluokkaa, noin yleisemmin?

Nyrkkisääntö: Ellei ole erityistä syytä käyttää abstraktia luokkaa, niin kannattaa käyttää piirreluokkaa, koska niitä voi liittää luokkiin joustavammin.

Yliluokan käytön syyksi voi sopia esimerkiksi se, että halutaan luokalle konstruktoriparametreja.

Valinta piirreluokan, abstraktin yliluokan ja konkreettisen yliluokan välillä voi tuntua hankalalta. Ohjelmointi 1 -kurssilla luokkatason spesifikaatiot ohjelmiin on pääsääntöisesti annettu valmiina, ja valinnat tältä osin tehty puolestasi. Aihe on ajankohtaisempi kurssilla Ohjelmointistudio 2. Lisäksi voit lukea esimerkiksi Kirjoja ja linkkejä -sivulla mainitun kirjan Programming in Scala (Third Edition) kappaleen 12.7.

Luokkahierarkiat ja Scala API

../_images/inheritance_animals-fi.png

Luku 7.2 osoitti, että piirreluokilla voi muodostaa käsitehierarkioita. Myös periytymistä voi käyttää tällaisten hierarkioiden muodostamisessa. Vaikka luokalla voi olla vain yksi välitön yliluokka (direct superclass), voi sillä epäsuorasti olla useita yliluokkia; esimerkiksi yllä olevassa kaaviossa hämähäkin välitön yliluokka on niveljalkainen, mutta myös eläin on sen yliluokka.

Scala API:n valmiit luokat muodostavat hierarkioita. Katsotaan muutama esimerkki.

Käyttöliittymäelementtien hierarkia

Pakkaus scala.swing tarjoaa luokkia, jotka kuvaavat käyttöliittymien rakennuspalikoita eli GUI-elementtejä. Alla on osittain kuvattu se hierarkia, jonka nämä luokat muodostavat.

../_images/inheritance_swing.png

Osa näistä pakkauksen scala.swing luokista on piirreluokkia, osa tavallisia luokkia. Kukin alakäsite saa ominaisuuksia yläkäsitteittensä kautta. Ylin käsite UIElement määrittelee kaikille elementeille yhteisiä ominaisuuksia kuten taustaväri ja koko.

Swing-kirjastosta

Luvussa 12.3 on johdanto Swing-GUI-kirjaston käytöön. Tuo valinnainen luku on sijoitettu kurssin ja tämän kurssimateriaalin loppupäähän. Jos aihe erityisesti kutkuttaa, voit lukea sen aiemminkin. Riittävät esitiedot siihen sinulla on nyt periytymisestä opittuasi.

Option-hierarkia (ja suljetut yliluokat)

../_images/inheritance_option1.png

Pieni hierarkia liittyy tuttuun Option-luokkaankin.

Jo luvussa 4.2 näit, että Option-tyyppisiä olioita on kahdenlaisia. Jokainen Option-tyyppinen olio on joko Some-olio jonkinlaisella sisällöllä tai None. Tässäkin on kyse periytymisestä: Option-tyyppi on abstrakti luokka, josta periytyvät konkreettinen luokka Some ja yksittäisolio None.

Luvussa 7.2 mainittiin, että piirreluokan voi sulkea sealed-sanalla, jolloin sille ei voi määritellä muita välittömiä alakäsitteitä kuin ne, jotka on kirjattu samaan tiedostoon. Yliluokankin voi sulkea, ja Option on juuri tällainen suljettu yliluokka: kuten luvussa 4.2 todettiin, Option voi olla joko Some tai None (jotka on määritelty samassa tiedostossa) mutta ei koskaan mikään muu. Se onkin Optionin nimenomainen tarkoitus. Emme voi määritellä Optionille itse muita aliluokkia ja hyvä niin.

Kaikkien luokkien äiti: Any

Tutkitaan Scala-olioita REPLissä:

val sekalaisia = Vector(123, "laama", true, Vector(123, 456), new Square(10))sekalaisia: Vector[Any] = Vector(123, laama, true, Vector(123, 456), o1.shapes.Square@114c3c7)
Esimerkissä luodaan vektori, jossa on keskenään aivan erilaisia olioita: Int, String, Boolean, Vector[Int] ja Square.
Vektorin alkioiden tyypiksi päätellään Any. Kyseessä on "vektorillinen mitä tahansa olioita". Ilmeisesti siis myös kokonaisluvut, vektorit, neliöt, jne. ovat Any-tyyppisiä?

Piirreluokkia lukuun ottamatta kaikki Scala-luokat ja yksittäisoliot — myös itse kirjoitetut — periytyvät automaattisesti Any-nimisestä luokasta, vaikka tätä ei erikseen koodiin normaalisti kirjatakaan. Aivan kaikki Scala-ohjelmissa käytetyt oliot ovat siis Any-tyyppisiä muiden tyyppiensä lisäksi.

Tätä kaikkien luokkien kantaluokkaa voi käyttää myös vaikkapa muuttujan tyyppinä, kuten seuraavassa REPL-esimerkissä:

var jokuOlio: Any = "kumkvatti"jokuOlio: Any = kumkvatti
jokuOlio.isInstanceOf[Any]res0: Boolean = true
jokuOlio.isInstanceOf[String]res1: Boolean = true
jokuOlio.isInstanceOf[Square]res2: Boolean = false
jokuOlio = new Square(10)jokuOlio: Any = o1.shapes.Square@ecfb83
jokuOlio.isInstanceOf[Any]res3: Boolean = true
jokuOlio.isInstanceOf[String]res4: Boolean = false
jokuOlio.area<console>:12: error: value area is not a member of Any
            jokuOlio.area
                     ^
Kuten tyypin nimikin antaa ymmärtää, tällaiseen muuttujaan voi sijoittaa arvoksi viittauksen millaiseen tahansa olioon, vaikkapa merkkijonoon tai neliöön, kuten tässä.
Muuttujan jokuOlio staattinen tyyppi on siis Any, ja kutsu jokuOlio.isInstanceOf on luvallinen koska (ja vain koska) kyseinen metodi isInstanceOf on määritelty luokassa Any ja on näin käytettävissä mille tahansa Scala-oliolle.
Sen sijaan kutsu jokuOlio.area epäonnistuu, vaikka muuttujaan sattuukin olemaan tallennettuna Square-tyyppinen olio, jolla area-metodi on. Muuttujan staattinen tyyppi rajoittaa sitä, millaiset metodikutsut ovat sallittuja.

Useimmiten Any-tyyppinen muuttuja ei ole sovelias valinta, koska se rajoittaa muuttujan arvon käyttämistä liiaksi. Kun staattisena tyyppinä on Any, voi arvolla tehdä vain sellaisia asioita, jotka on määritelty Any-luokassa. Näitä kaikille Scala-oliolle yhteisiä metodeita ovat vain isInstanceOf, toString, ==, != sekä kourallinen muita. Muuttujille kannattaa yleensä valita jokin spesifisempi tyyppi, kuten olet kurssilla tähänkin mennessä tehnyt. (Vähän lisää aiheesta jäljempänä.)

Havainto dokumentaatiosta

Monien kohtaamiesi luokkien Scaladoc-dokumentaatiossa lukee extends AnyRef. Näin lukee vaikkapa tässä tutussa District-luokkaa kuvaavassa dokumentaatiossa:

Kuitenkaan mitään extends AnyRef-ilmaisua ei koodissa ole näkynyt; mistä on kysymys? Ja miksi AnyRef eikä Any?

Melkein kaikkien luokkien äiti: AnyRef eli Object

Scalan piällystyyppi Any jakautuu kahteen "haaraan". Sillä on välittömät aliluokat AnyVal ja AnyRef:

../_images/inheritance_any.png

Jako AnyRefiin ja AnyValiin liittyy Scala-kielen toteutukseen eikä ole erityisen keskeinen aloittelevan Scala-ohjelmoijan tai yleisemmin ohjelmoinnin perusteiden kannalta. Kuitenkin näistä tyypeistä kannattaa olla ainakin sen verran tietoinen, että hahmotat, miksi niiden nimet esiintyvät joskus Scaladoc-dokumenteissa, REPL-tulosteissa ja virheilmoituksissa.

AnyVal on yliluokka eräille sellaisille valmiille luokille, jotka edustavat tietynlaisia suhteellisen yksinkertaisia perustietotyyppejä ja joiden käyttö on tietyillä tavoin tehokkaampaa kuin muiden tietotyyppien. AnyValista periytyvät tutut tietotyypit Int, Double, Boolean, Char, Unit, ja muutama muu. AnyVal-luokalle on suhteellisen harvoin järkevää itse tehdä aliluokkia, tämän kurssin puitteissa ei koskaan.

AnyRef puolestaan on yliluokka kaikille muille (ei-piirre-)luokille ja yksittäisolioille. Esimerkiksi luokat String ja Vector sekä yllä itse laadittu luokka Tapahtuma periytyvät AnyRefistä.

Tilannetta mutkistaa hieman se, että silloin, kun käytetään Scalaa Java-virtuaalikoneen "päällä" (kuten tänä päivänä useimmiten tehdään; luku 5.2), niin AnyRefistä käytetään toteutusteknisistä syistä myös nimeä Object.

AnyVal sekä AnyRef eli Object näkyvät myös REPLissä:

val sekalaisia2 = Vector(123, true)sekalaisia2: Vector[AnyVal] = Vector(123, true)
val sekalaisia3 = Vector("laama", Vector(123, 456), new Square(10))sekalaisia3: Vector[Object] = Vector(laama, Vector(123, 456), o1.shapes.Square@667113)
Int- ja Boolean-tyypit periytyvät molemmat AnyValista.
String, Vector ja Square periytyvät AnyRefistä eli Objectista.

Yläkäsitteet ja metodien korvaaminen

Kakkoskierrokselta alkaen olemme käyttäneet sanaa override metoditoteutusten korvaamiseen uusilla. Erityisesti olemme käyttäneet sitä:

  • toString-metodeissa (luku 2.5): laatimamme toString-toteutukset korvaavat oletusarvoisen toteutuksen (joka tuottaa kuvauksia kuten o1.shapes.Square@ecfb83). Tuo korvattu oletusarvoinen toteutus on määritelty AnyRef-luokassa.
  • View-luokan tapahtumankäsittelijöissä (kuten onClick; luku 2.8): View-luokan tarjoamat oletustoteutukset reagoivat tapahtumiin olemalla jouten, mutta voimme korvata ne sovellukseen sopivilla reaktioilla.

Metodeita voi korvata tyyppihierarkioissa yleisemminkin. Tehdään kokeeksi muutama miniluokka:

../_images/inheritance_a.png
class A {
  def test() = {
    println("Terveisiä luokasta A.")
  }
}
class B extends A {
}
class C extends A {
  override def test() = {
    println("Terveisiä luokasta C.")
  }
}
class D extends C {
}
class E extends D {
  override def test() = {
    println("Terveisiä luokasta E.")
  }
}

Kokeillaan REPLissä:

(new A).test()Terveisiä luokasta A.
(new B).test()Terveisiä luokasta A.
(new C).test()Terveisiä luokasta C.
(new D).test()Terveisiä luokasta C.
(new E).test()Terveisiä luokasta E.
B-luokka ei määrittele korvaavaa toteutusta, joten B-olion test-metodi on peritty luokasta A.
C-luokka korvaa testimetodin uudella versiolla.
D-luokassa ei ole korvaavaa toteutusta. D-olio käyttää välittömässä yliluokassa C olevaa toteutusta (joka korvaa luokan A määrittelemän version).
Luokassa E taas on metoditoteutus, joka korvaa sekä luokan C että luokan A versiot.

Tutkitaan lisää:

var olio = new Aolio: A = A@e1ee21
olio.test()Terveisiä luokasta A.
olio = new Colio: A = C@c081a6
olio.test()Terveisiä luokasta C.
Huomaa: Muuttujan staattinen tyyppi on A. Sen arvon dynaaminen tyyppi on C.
test-metodia voi kutsua mille tahansa lausekkeelle, jonka staattinen tyyppi on A (tai jokin A:n alatyyppi), so. mille tahansa oliolle, jolla taatusti on tämä metodi. Se, mitä kutsuttaessa tapahtuu, puolestaan riippuu siitä, mikä on viestin vastaanottavan olion dynaaminen tyyppi. Tässä siis suoritetaan nimenomaan C-tyyppisille oliolle määritelty korvaava testimetodi, vaikka muuttujan tyyppi on A.

Tehdään vielä yksi kokeiluluokka:

class F extends E {
  override def test() = {
    super.test()
    println("Terveisiä luokasta F.")
  }
}
Avainsanaa super käyttäen voi viitata yläkäsitteen yhteydessä olevaan määrittelyyn. Tässä kutsutaan yliluokan versiota test-metodista. F-tyyppisen olion test-metodi siis tekee ensin sen, mitä yliluokan E samanniminen metodikin tekee, ja sitten lisäksi tuottaa luokalle F ominaisen tulosteen. Esimerkki alla.
(new F).test()Terveisiä luokasta E.
Terveisiä luokasta F.

Scalassa sana override on kirjoitettava metodin määrittelyyn aina, kun korvaa yliluokassa olevan metoditoteutuksen.

Miksi override-pakko?

Kun kirjoitat koodiin override, niin kuittaat tietäväsi, että "tässä korvaan yläkäsitteelle määritellyn toteutuksen toisella". Ellei override-sanaa vaadittaisi, saattaisit hyvinkin sattumalta ja huomaamattasi antaa metodille sellaisen nimen, joka on jo muussa käytössä jossakin yliluokista, mistä voisi seurata erikoisiakin bugeja.

Samoin kuin esimerkiksi staattinen tyypitys tämä on siis käytäntö, joka pienentää virheiden tekemisen riskiä.

Lisäetu on, että korvaaminen tulee näin dokumentoitua myös koodin lukijalle.

Kokoava koodinlukutehtävä

Seuraava vähän hölmö ohjelma kokoaa yhteen edellä esiteltyjä asioita. Voi käyttää sitä tietojesi tarkistukseen. Jos ymmärrät ohjelman toiminnan yksityiskohtaisesti, niin ymmärrät myös keskeisimmät periytymiseen liittyvät ohjelmarakenteet.

Autoilutarina

Lue alla oleva koodi. Mieti perusteellisesti, mitkä tekstit se tulostaa ja missä järjestyksessä. Kirjoita mieluiten tuloste itsellesi muistiin!

object Cruising extends App {
  val car = new Car
  car.receivePassenger(new Schoolkid("P. Pupil"))
  car.receivePassenger(new ChemicalEngineer)
  car.receivePassenger(new MechanicalEngineer)
  car.receivePassenger(new ElectricalEngineer)
  car.receivePassenger(new ComputerScientist)
  car.start()
}
class Car {
  private val passengers = Buffer[Passenger]()

  def receivePassenger(passenger: Passenger) = {
    passenger.sitDown()
    this.passengers += passenger
  }

  def start() = {
    println("(The car won't start.)")
    for (passenger <- this.passengers) {
      passenger.remark()
    }
  }
}
abstract class Passenger(val name: String) {
  def sitDown() = {
    println(this.name + " finds a seat.")
  }

  def speak(sentence: String) = {
    println(this.name + ": " + sentence)
  }

  def diagnosis: String

  def remark() = {
    this.speak(this.diagnosis)
  }
}
abstract class Student(name: String) extends Passenger(name) {
  def diagnosis = "No clue what's wrong."
}
class Schoolkid(name: String) extends Student(name)
abstract class TechStudent(name: String) extends Student(name) {
  override def remark() = {
    super.remark()
    this.speak("Clear as day.")
  }
}
class ChemicalEngineer extends TechStudent("C. Chemist") {
  override def diagnosis = "It's the wrong octane. Next time, I'll do the refueling."
}
class MechanicalEngineer extends TechStudent("M. Machine") {

  override def diagnosis = "Nothing wrong with the gas. It must be the pistons."

  override def speak(sentence: String) = {
    super.speak(sentence.replace(".", "!"))
  }
}
class ElectricalEngineer extends TechStudent("E. Electra") {
  override def sitDown() = {
    println(this.name + " claims a front seat.")
  }

  override def diagnosis = "Hogwash. The spark plugs are faulty."
}
class ComputerScientist extends TechStudent("C.S. Student") {
  override def remark() = {
    this.speak("No clue what's wrong.")
    this.speak(this.diagnosis)
  }

  override def diagnosis = "Let's all get out of the car, close the doors, reopen, and try again."
}

Kävithän koodin ajatuksella läpi? Kirjoititko odottamasi tulosteen muistiin?

Avaa nyt projekti Subtypes ja aja pakkauksesta o1.cruising löytyvä ohjelma (jonka koodi on yllä). Vastasiko tuloste täsmälleen sitä, mitä odotit? Jos ei, selvitä mistä erot johtuivat. Voit käyttää debuggeria apuna.

Ohjelmointiharjoitus: esineitä esineiden sisällä

Johdanto

Oletetaan, että laaditaan ohjelmaa, jossa on tarkoitus kuvata erilaisia esineitä. Käytössämme on yksinkertainen luokka Item:

class Item(val name: String) {
  override def toString = this.name
}

Otetaan tässä pienessä ohjelmointitehtävässä tavoitteeksi, että tällaisten "tavallisten esineiden" lisäksi ohjelmassamme olisi sellaisia esineitä, joiden sisällä voi olla toisia esineitä. Esimerkiksi laukun sisällä voisi olla kirja ja laatikko, joista laatikon sisällä olisi sormus.

Tehtävänanto

Laadi Itemille aliluokka Container, joka kuvaa säkkien ja laatikoiden kaltaisia esineitä, jotka voivat sisältää toisia esineitä. Tällaisilla säiliöesineillä on nimi kuten muillakin esineillä. Lisäksi niillä on addContent-metodi, jolla sisältöä lisätään, sekä tavallisista esineistä poikkeava toString-metodi.

Laatimasi luokan tulisi toimia tähän tapaan:

val container1 = new Container("box")container1: o1.items.Container = box containing 0 item(s)
container1.addContent(new Item("ring"))
container1res5: o1.items.Container = box containing 1 item(s)
val container2 = new Container("bag")container2: o1.items.Container = bag containing 0 item(s)
container2.addContent(new Item("book"))
container2.addContent(container1)
container2res6: o1.items.Container = bag containing 2 item(s)

Ohjeita ja vinkkejä

  • Subtypes-projektin pakkauksesta o1.items löytyy paitsi Item-luokka myös alkua Container-luokalle sinun täydennettäväksesi.
  • Älä unohda, että yliluokalle Item on välitettävä konstruktoriparametri.
  • Älä määrittele ilmentymämuuttujaa name uudelleen val-sanaa käyttäen luokassa Container. Tuo muuttuja on jo määritelty yliluokassa. Container-luokan konstruktoriparametrin nimi voi silti hyvin olla name.
  • toString-metodin palautusarvossa sisällettyjen esineiden lukumäärään otetaan mukaan vain välittömästi sisällä olevat. Esimerkissämme laukun sisällä on siis kaksi esinettä, vaikka sisälletyn laatikon sisällä onkin vielä sormus. (Miten saataisiin kaikki "sisällön sisällötkin"? Palataan siihen luvussa 11.2.)
  • Huomaa, että tässä tehtävässä korvaat (override) yliluokan Item toteutuksen toString-metodille etkä Scala-olioiden oletustoteutusta kuten aiemmissa yhteyksissä.
  • Osaatko toteuttaa aliluokan toString-metodin siten, että kutsut sen sisältä yliluokan toString-metodia sen sijaan, että katsoisit esineen nimen suoraan? (Tämä ei ole välttämätöntä.)
  • Tässä ei tarvitse toteuttaa mitään muita toimintoja kuten sisällön tutkimista tai poistamista.

Palauttaminen

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Ohjelmointiharjoitus: oikeushenkilöitä

Tehtävänanto

Tutustu pakkauksen o1.legal dokumentaatioon projektissa Subtypes. Se kuvaa useita luokkia, joilla voi mallintaa oikeustapauksia ja niiden asianomistajina ja vastaajina toimivia erilaisia oikeushenkilöitä (legal entity tai legal person). Toteuta luokat projektin sisältämiin Scala-tiedostoihin.

Luokkia on monta, mutta ne ovat yksinkertaisia. Tehtävän keskiössä ovat näiden luokkien väliset suhteet. Ne on esitetty kuvana alla.

../_images/project_legal.png

Suositellut vaiheet ja vinkkejä

Voit edetä esimerkiksi seuraavasti.

  1. Selaa aluksi ainakin luokkien CourtCase, Entity, NaturalPerson ja JuridicalPerson dokumentaatio, niin saat kokonaiskuvan luokista.

  2. Laadi luokka CourtCase. Huomaa, että oikeusjuttuun liittyy kaksi Entity-yliluokan tyyppistä muuttujaa: kyseiset oliot ovat jonkinlaisia oikeushenkilöitä, mutta CourtCase ei ota kantaa siihen, millaisia.

  3. Laadi luokka Entity omaan tiedostoonsa.

    • Luokka on abstrakti, mikä lukee dokumentaatiossakin. Käytä sanaa abstract.
    • Joidenkin metodienkin kohdalla on dokumentaatiossa sana abstract. Tätä sanaa ei kuitenkaan tarvitse eikä pidä kirjoittaa metodien Scala-koodiin. Abstraktin metodin määrittelemiseksi riittää, että jätät metodin rungon pois, kuten mm. tässä luvussa on opetettu.
  4. Laadi luokka NaturalPerson samannimiseen tiedostoon.

    • Yliluokka on ilmoitettu extends-sanalla dokumentaatiossa. Muista lisäksi, että aliluokan koodissa on ilmoitettava extends-sanan yhteydessä, mitä välittömälle yliluokalle välitetään konstruktoriparametriksi aliluokan ilmentymää luodessa. (Vrt. Container edellä.)
    • Yksi tämän luokan konstruktoriparametreista välitetään yliluokalle, toista ei. Huomaa myös, että nimiparametria vastaava muuttuja on jo määritelty yliluokassa, joten sitä val-sanaa ei pidä toistaa täällä.
    • Dokumentaatio kertoo, mitkä metodit tulevat yläkäsitteiltä ja mitkä ovat kussakin aliluokassa uusia. Kunkin Scaladoc-sivun alkupäässä on harmaalla pohjalla kohta Inherited, jossa olevia nappuloita painamalla voit säädellä, näkyvätkö sivulla myös yläkäsitteiltä saadut metodit. Kokeile.
  5. Laadi luokka FullCapacityPerson (eli täysvaltainen luonnollinen henkilö). Se alkaa kenties mennä jo rutiinilla.

    • Varsinkin, kun luokkamme ovat pieniä, voimme hyvin tehdä niin, että sijoitamme NaturalPersonin aliluokat samaan tiedostoon tuon yliluokkansa kanssa.
  6. Ota esiin Restriction.scala, jota käytetään kohta apuna vajaavaltaisten henkilöiden kuvaamisessa. Abstrakti luokka Restriction on jo tehty, samoin sen perivä yksittäisolio Illness. Lisää vastaava yksittäisolio Underage.

  7. Toteuta ReducedCapacityPerson.

    • Jos olet toteuttanut aiemmat metodit oikein, niin tämän pitäisi toimia kind-metodin toteutuksena:

      override def kind = super.kind + " with " + this.restriction
      
  8. Toteuta JuridicalPerson. Yksikin rivi riittää (koska lisämetodeita ei tarvita ja tyhjät aaltosulut voi jättää pois).

  9. Toteuta HumanOrganization ja GeographicalFeature.

    • Huomaa, että yliluokan abstraktiksi jättämän parametrittoman defin voi korvata myös muuttujamäärittelyllä. Määrittele HumanOrganizationiin contact-muuttuja ja GeographicalFeatureen kind-muuttuja.
  10. Sinun ei tarvitse vaivautua kirjoittamaan Nation-, Municipality- ja Corporation-luokkia, joissa ei ole mitään uutta asiaa. Voit yksinkertaisesti poistaa kommentit annettujen toteutusten ympäriltä. Jos olet laatinut yliluokat oikein, näiden aliluokkien pitäisi toimia sellaisenaan.

  11. Toteuta luokka Group.

    • Huomaa, ettei ryhmillä ole tässä ohjelmassa nimiä. Anna yliluokalle konstruktoriparametriksi merkkijonoliteraali.

Palauttaminen

A+ esittää tässä kohdassa tehtävän palautuslomakkeen.

Ilmentymien räätälöintiä yläkäsitteistä

Luvussa 2.4 opimme luomaan olion, jolle on ilmentymäkohtaisesti räätälöity metodi luokan määrittelemien metodien lisäksi:

val terasmies = new Henkilo("Clark") {
 def lenna = "WOOSH!"
}terasmies: Henkilo{def lenna: String} = $anon$1@25ba32e0

Olemme sittemmin käyttäneet samaa tekniikkaa erityisesti View-luokan kanssa luodessamme View-luokan ilmentymiä, joille olemme räätälöineet omia metodeita.

Tässä vaiheessa kurssia voimme todeta, että itse asiassa tämä ilmentymäkohtainen räätälöinti on esimerkki periytymisestä. Esimerkiksi yllä REPLissä annettu käsky luo "lennosta" Henkilo-luokalle nimettömän aliluokan ja saman tien tuosta aliluokasta yhden ainoan ilmentymän. Vastaavasti olemme luoneet toteuttaneet View-yliluokan abstraktin makePic-metodin erilaisilla tavoilla.

Sama toimii yleisemminkin sekä piirreluokille että yliluokille. Voit halutessasi lukea siitä alta lisää.

Alakäsitteen määritteleminen "lennosta"

Scala mahdollistaa olioiden luomisen siten, että olioon liitetään piirreluokka "lennosta", olion luomiskäskyn yhteydessä.

Määritellään pohjustuksena pari piirreluokkaa ja yksi tavallinen luokka. Huomaa, että nämä kolme ovat toisistaan täysin erilliset:

class Elain(val laji: String) {
  override def toString = "eläin, tarkemmin sanoen " + this.laji
}
trait Sylkeva {
  def sylje = "pthyi"
}
trait Kyttyrallinen {
  def kyttyroidenMaara: Int
}defined class Elain
defined trait Sylkeva
defined trait Kyttyrallinen

Tehdään uusi Elain-luokan aliluokka, joka on nimetön ja jolla on piirre Sylkeva. Samalla luodaan tuosta luokasta saman tien ilmentymä, joka on kyseistä "lennosta määriteltyä" Elain with Sylkeva -yhdistelmätyyppiä:

val lemmikki = new Elain("laama") with Sylkevalemmikki: Elain with Sylkeva = eläin, tarkemmin sanoen laama

Tällä oliolla voi siis tehdä asioita, joita on määritelty joko Elain-luokassa tai Sylkeva-piirreluokassa:

lemmikki.lajires7: String = laama
lemmikki.syljeres8: String = pthyi

Toki saman lopputuloksen saa myös määrittelemällä erikseen nimetyn luokan seuraavaan tapaan ja luomalla sitten tuosta luokasta ilmentymän.

class Laama extends Elain("laama") with Sylkevadefined class Laama

Metodien lisääminen "lennosta" määriteltyyn tyyppiin

Jatketaan esimerkkiä. Luodaan olio, joka on sellaista tyyppiä,

  • joka on Elain-luokan aliluokka,
  • jolla on piirteet Kyttyrallinen ja Sylkeva, ja
  • joka toteuttaa Kyttyrallinen-piirreluokan abstraktin kyttyroidenMaara-metodin tietyllä tavalla.
val seSeOn = new Elain("dromedaari") with Kyttyrallinen with Sylkeva {
  def kyttyroidenMaara = 1
}seSeOn: Elain with Kyttyrallinen with Sylkeva = eläin, tarkemmin sanoen dromedaari

Tehdään lopuksi olio, joka on nimetöntä tyyppiä ja jolla on Sylkeva-piirre sekä pari lisäominaisuutta (nimi-muuttuja ja omanlaisensa toString-metodi):

val tunnettuKalamies = new Sylkeva {
  val nimi = "Eemeli"
  override def toString = this.nimi
}tunnettuKalamies: Sylkeva{val nimi: String} = Eemeli

Huomaa, että tässä kirjoitettiin new-sanan perään piirreluokan eikä tavallisen luokan nimi. Käsky ei kuitenkaan luo ilmentymää suoraan piirreluokasta (mitä ei voikaan tehdä; luku 7.2). Se määrittelee uuden tyypin, jolla on tuo piirre ja aaltosulkeiden sisäiset ominaisuudet, ja luo ilmentymän tästä uudesta tyypistä.

Hakusanoja: scala trait mixin, scala anonymous subclass.

Lisälukemisto

Staattisen tyypin valitsemisesta

Staattiset tyypit yleensäkin, ja parametrimuuttujien staattiset tyypit eritoten, on useimmiten hyvä kirjata koodiin mahdollisimman "laajoiksi" eli käyttää yliluokkaa tai piirreluokkaa muuttujien tyyppinä. Näin metodeista ja luokista tulee yleiskäyttöisempiä ja helpommin muokattavia.

Vertaa:

def doSomething(circle: Circle) = {
  // ...
}

vs.

def doSomething(shape: Shape) = {
  // ...
}

Ensimmäinen määrittely on perusteltu, mikäli metodin todella on järkevää toimia ainoastaan ympyräolioille (esim. siksi, että se tarvitsee toimiakseen parametriksi saamansa ympyräolion sädettä, jollaista ei ole muilla kuvioilla). Muutoin jälkimmäinen määrittely on yleensä parempi, koska metodi toimii nyt erilaisille kuvioille, myös mahdollisille Shape-piirreluokan vielä luomattomille alatyypeille.

Aina ei voi yleistää; muutenhan kaiken tyypiksi kannattaisi laittaa Any. Mutta yleistä kun voit.

Julkisen ja yksityisen väliltä: protected

Aliluokan olioillakin on yliluokan private-muuttujat osana tietojaan. Yliluokalta perityt metodit käyttävät noita muuttujia. Mutta aliluokan koodista ei voi suoraan viitata yliluokan private-osiin. Vastaavasti myöskään piirreluokan liittävät luokat eivät pääse suoraan käsiksi piirreluokan yksityisiin osiin.

Kuitenkin Scalassa, kuten joissakin muissakin kielissä, on käytettävissä näkyvyysmääre protected. Se sallii juuri tuon äsken mainitun, jota private ei salli: protected-muuttujaa tai metodia voi käyttää luokan itsensä lisäksi alakäsitteiden koodissa (mutta ei vapaasti mistä tahansa muualta, kuten julkisia osia). Lisätietoja löydät netistä.

Moniperintä

Ylempänä mainittiin, että luokalla voi kirjata vain yhden yliluokan. Miksi ei saisi periä kuin yhdestä välittömästä yliluokasta? Selvitä netitse, mitä on moniperintä (multiple inheritance). Voit myös selvittää, mikä on moniperintään liittyvä "tuomion timantti" ("deadly diamond of death") ja miksi jotkut pitävät sitä ongelmana ja toiset eivät.

Hieman provokatiivista mutta mielenkiintoista lisäluettavaa (lähinnä Javaa ja sen rajapintaluokkia ennestään tunteville):'Interface' Considered Harmful.

Jos liittää useita piirreluokkia, mitä metoditoteutusta käytetään?

Tässä pieni esimerkki:

trait X {
  def metodi: String
}
trait A extends X {
  override def metodi = "a:n metodi"
}
trait B extends X {
  override def metodi = "b:n metodi"
}defined trait X
defined trait A
defined trait B
class AB extends A with B
class BA extends B with Adefined class AB
defined class BA
(new AB).metodires9: String = b:n metodi
(new BA).metodires10: String = a:n metodi

Lisätietoja hakusanoilla scala linearization tai oheisesta linkistä.

Liskovin periaate

../_images/liskov-fi.png

"Neliö suorakaiteen aliluokkana" on klassinen esimerkki, kun tarkastellaan Liskovin periaatetta (Liskov substitution principle), josta on tässä annettu epätäsmällinen mukaelma. Tämän periaatteen noudattamista pidetään eräänä olio-ohjelman laadun kriteerinä.

Luokille, jotka on laadittu Liskovin periaatetta noudattaen, pätee:

Jos S on T:n aliluokka, niin on mahdollista ja mielekästä kohdistaa luokasta S luotuun ilmentymään mikä tahansa sellainen toimenpide, jonka voi kohdistaa luokasta T luotuun ilmentymään.

Toisin sanoen: T:n ilmentymän paikalle sopii yhtä hyvin myös S:n ilmentymä.

Palataan esimerkkiin. Aiemmin tässä luvussa määriteltiin tällaiset luokat:

class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
  def area = this.sideLength * this.anotherSideLength
}
class Square(size: Double) extends Rectangle(size, size)

Mieti, onko tämä ohjelmakoodi Liskovin periaatteen mukainen vai ei. Entä jos muutettaisiin Rectangle-luokasta val- sanat vareiksi? Muista: yllä määriteltiin luokan Square laatimisen tavoitteeksi, että kaikkien Square-olioiden tulee edustaa sellaisia suorakaiteita, joiden sivut ovat keskenään samanmittaiset.

Lue lisää aiheesta vaikkapa Wikipediasta:

Käsitteiden välisistä riippuvuuksista

Piirreluokkaluvussa 7.2 oli jo esillä ajatus siitä, että ylä- ja alakäsitteen välillä on epäsymmetrinen suhde: alakäsite määritellään yläkäsitteen avulla muttei toisin päin. Sama pätee periytymiseenkin:

  • Aliluokan koodiin kirjataan, mistä yliluokasta aliluokka periytyy ja voidaan siis luottaa siihen, että aliluokan olioillakin on tietyt perityt metodit käytettävissä. Aliluokan koodissa voidaan kutsua this.yliluokastaPerittyMetodi. Aliluokasta voi myös super-sanalla viitata nimenomaisesti yliluokan osiin.
  • Periytymistä ei merkitä yliluokkaan, ja yliluokan koodi onkin sikäli aliluokista riippumaton. Yliluokan koodissa ei voi this-oliolle kutsua mahdollisten aliluokkien erityismetodeita vaan vain metodeita, jotka koskevat koko yläkäsitettä. super-sanalla ei ole vastinetta, joka viittaisi periytymishierarkiassa alaspäin.

Jos yliluokat olisivat riippuvaisia aliluokistaan, niin muutokset aliluokkiin usein aiheuttaisivat muutoksia yliluokan toimintaan. Tämä olisi monesti harmillista. Esimerkiksi: Ei ole harvinaista periä jokin ohjelmakirjastossa määritelty luokka (vaikkapa View). Kirjaston laatija ei voi etukäteen tai muutenkaan tuntea kaikkia aliluokkia eikä reagoida niihin tuleviin muutoksiin.

Toisaalta on toivottavaa, että yliluokkaan voi lisätä uutta toiminnallisuutta metodeina, ja nuo metodit tulevat käyttöön kaikkiin aliluokkiin. Käytännössä tämä usein onnistuukin, mutta asiaan liittyy myös eräs periytymisen heikkous:

Yliluokkien hauraudesta

Ilmaisu hauraan yliluokan ongelma (fragile base class problem) viittaa tilanteisiin, joissa yliluokkaa ei voi muuttaa tuntematta aliluokkien yksityiskohtia.

Yksi esimerkki ongelmasta on metodin lisääminen yliluokkaan siten, että se "rikkoo" aliluokan, jonne oli satuttu tekemään samanniminen metodi. Scalan tapauksessa tällöin syntyy käännösaikainen virhe, joka valittaa override-määrittelyn puuttumisesta aliluokassa. Sellaisissa toisissa kielissä, jotka eivät vaadi override-merkintää, seurauksena voi olla yllättävä virheellinen toiminta ohjelma-ajon aikana.

Aiheeseen palataan olio-ohjelman suunnittelun yhteydessä kevään puolella. Voit hakea tietoa esimerkiksi Wikipedian artikkelista; ks. myös Composition over inheritance -periaate.

Korvaamisen ja alakäsitteiden estäminen: final

Jos kirjoitat defin eteen sanan final, ei tuota metodia voi korvata aliluokassa. Metodi periytyy alatyyppien olioillekin sellaisenaan. Korvausyritys tuottaa käännösaikaisen virheilmoituksen.

Sama sana final luokkamäärittelyn alussa ennen class-sanaa estää kokonaan aliluokkien määrittelemisen tuolle luokalle. (Vrt. luokan sulkeminen sanalla sealed, joka estää muut välittömät aliluokat kuin samassa tieddostossa määritellyt.)

Sopivasti käytettynä final-määre voi parantaa ohjelman ymmärrettävyyttä tai ehkäistä luokkien epätarkoituksenmukaista käyttöä. Sillä voi myös saavuttaa tehokkuusparannuksia, kun kääntäjän ei tarvitse huomioida korvaavien toteutusten mahdollisuutta. Scalan peruskirjastossa on monia final-luokkia.

Yhteenvetoa

  • Periytyminen on olio-ohjelmointitekniikka, jossa yläkäsitettä kuvaavan luokan eli yliluokan ominaisuudet periytyvät alakäsitteitä kuvaaville aliluokille.
  • Piirreluokilla ja yliluokilla on paljon yhteistä mutta myös eroja. Erityisesti:
    • Luokalla voi olla vain yksi välitön yliluokka, mutta siihen voi liittää useita piirreluokkia.
    • Yliluokalla, abstraktillakin, voi olla konstruktoriparametreja, toisin kuin piirreluokalla.
  • Luokka voi olla abstrakti, jolloin siinä voi olla abstrakteja metodeita ja muuttujia kuten piirreluokassakin. Abstraktista luokasta ei voi suoraan luoda ilmentymiä vaan vain aliluokkiensa kautta.
  • Luokista voi muodostaa käsitehierarkioita. Kaikki Scala-luokat kuuluvat hierarkiaan, jonka kantaluokkana on valmis luokka Any.
  • Aliluokassa voi korvata yliluokan metodin aliluokkakohtaisella toteutuksella.
  • Lukuun liittyviä termejä sanastosivulla: periytyminen eli perintä, aliluokka, yliluokka, luokkahierarkia, Any; abstrakti luokka; staattinen tyyppi, dynaaminen tyyppi; korvata (metodi); suljettu luokka.

Palaute

Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.

Tekijät

Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!

Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.

Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.

Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.

Tehtävien automaattisen arvioinnin ovat toteuttaneet Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.

Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.

Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.

Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.

O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.

Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic) yksinkertaiseen graafiseen ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.

Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.

Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.

Lisäkiitokset tähän lukuun

Kiitos autoilutarinan pohjana olleen vitsin keksijälle, kuka sitten onkin.

../_images/imho7.png
Palautusta lähetetään...