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.
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.
Ali- ja yliluokat
Määritelläänkin Square
-luokka näin:
class Square(size: Double) extends Rectangle(size, size) {
}
Square
on vain yksi konstruktoriparametri, joka kertoo
kunkin sivun mitan.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.)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.) | Rectangle n 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
}
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.Puhelu
ja Tekstiviesti
on useampia
konstruktoriparametreja, joista muut liittyvät näihin
nimenomaisiin aliluokkiin, mutta ...alvLisatty
arvo välitetään sellaisenaan
yliluokan konstruktoriparametriksi. Sen käsittely hoituu
kokonaan yliluokassa Tapahtuma
.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
}
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ä
new llä? |
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 trait
iin. 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
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.
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)
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 Option
in nimenomainen tarkoitus. Emme voi
määritellä Option
ille 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)
Int
, String
, Boolean
, Vector[Int]
ja Square
.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 ^
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.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
:
Jako AnyRef
iin ja AnyVal
iin 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.
Lisää AnyRef
istä ja AnyVal
ista
Kiinnostuneille (JVM:stä/Javasta jotain tietäville) tiedoksi,
että JVM-pohjaisessa Scala-toteutuksessa AnyVal
-aliluokkia
on toteutettu JVM:n alkeistyypeillä kuten int
ja double
,
kun taas AnyRef
-luokasta periytyvät luokat on toteutettu
JVM-tasollakin luokilla.
Nimet AnyRef
ja AnyVal
heijastelevat tätä jakoa. Edellisen
kategorian toteutuksessa käytetään viittauksia (reference)
mutta jälkimmäisessä vain yksinkertaisia arvoja (value).
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. AnyVal
ista 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 AnyRef
istä.
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 AnyRef
istä 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 AnyVal
ista.String
, Vector
ja Square
periytyvät AnyRef
istä eli
Object
ista.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): laatimammetoString
-toteutukset korvaavat oletusarvoisen toteutuksen (joka tuottaa kuvauksia kuteno1.shapes.Square@ecfb83
). Tuo korvattu oletusarvoinen toteutus on määriteltyAnyRef
-luokassa.View
-luokan tapahtumankäsittelijöissä (kutenonClick
; 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:
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).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.
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.")
}
}
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 Item
ille 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 paitsiItem
-luokka myös alkuaContainer
-luokalle sinun täydennettäväksesi. - Älä unohda, että yliluokalle
Item
on välitettävä konstruktoriparametri. - Älä määrittele ilmentymämuuttujaa
name
uudelleenval
-sanaa käyttäen luokassaContainer
. Tuo muuttuja on jo määritelty yliluokassa.Container
-luokan konstruktoriparametrin nimi voi silti hyvin ollaname
. 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
toteutuksentoString
-metodille etkä Scala-olioiden oletustoteutusta kuten aiemmissa yhteyksissä. - Osaatko toteuttaa aliluokan
toString
-metodin siten, että kutsut sen sisältä yliluokantoString
-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.
Suositellut vaiheet ja vinkkejä
Voit edetä esimerkiksi seuraavasti.
Selaa aluksi ainakin luokkien
CourtCase
,Entity
,NaturalPerson
jaJuridicalPerson
dokumentaatio, niin saat kokonaiskuvan luokista.Laadi luokka
CourtCase
. Huomaa, että oikeusjuttuun liittyy kaksiEntity
-yliluokan tyyppistä muuttujaa: kyseiset oliot ovat jonkinlaisia oikeushenkilöitä, muttaCourtCase
ei ota kantaa siihen, millaisia.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.
- Luokka on abstrakti, mikä lukee dokumentaatiossakin.
Käytä sanaa
Laadi luokka
NaturalPerson
samannimiseen tiedostoon.- Yliluokka on ilmoitettu
extends
-sanalla dokumentaatiossa. Muista lisäksi, että aliluokan koodissa on ilmoitettavaextends
-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.
- Yliluokka on ilmoitettu
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
NaturalPerson
in aliluokat samaan tiedostoon tuon yliluokkansa kanssa.
- Varsinkin, kun luokkamme ovat pieniä,
voimme hyvin tehdä niin, että sijoitamme
Ota esiin
Restriction.scala
, jota käytetään kohta apuna vajaavaltaisten henkilöiden kuvaamisessa. Abstrakti luokkaRestriction
on jo tehty, samoin sen perivä yksittäisolioIllness
. Lisää vastaava yksittäisolioUnderage
.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
Toteuta
JuridicalPerson
. Yksikin rivi riittää (koska lisämetodeita ei tarvita ja tyhjät aaltosulut voi jättää pois).Toteuta
HumanOrganization
jaGeographicalFeature
.- Huomaa, että yliluokan abstraktiksi jättämän
parametrittoman
def
in voi korvata myös muuttujamäärittelyllä. MääritteleHumanOrganization
iincontact
-muuttuja jaGeographicalFeature
enkind
-muuttuja.
- Huomaa, että yliluokan abstraktiksi jättämän
parametrittoman
Sinun ei tarvitse vaivautua kirjoittamaan
Nation
-,Municipality
- jaCorporation
-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.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
jaSylkeva
, ja - joka toteuttaa
Kyttyrallinen
-piirreluokan abstraktinkyttyroidenMaara
-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
"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 var
eiksi?
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össuper
-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 def
in 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.
extends
-sanaa. Tällä kertaa sen perässä on mainittu tavallinen luokka eikä piirreluokka. Sanotaan: luokkaSquare
perii (inherits) luokanRectangle
. Perivää luokkaa sanotaan aliluokaksi (subclass), perittyä luokkaa yliluokaksi (superclass).Square
-oliot ovat nyt myösRectangle
-tyyppisiä.