Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 7.5: Periytyminen
Tästä sivusta:
Pääkysymyksiä: Miten määrittelen alakäsitteen luokalle, joka ei ole piirreluokka? Miten Scalan valmiit luokat muodostavat tyyppihierarkian? Kuulin jonkun puhuvan "abstraktista luokasta"; mikä se on?
Mitä käsitellään? Luokan periytyminen toisesta. Scala APIn tyyppihierarkioita. Abstraktit luokat.
Mitä tehdään? Luettavaa on jonkin verran, mistä osa on ihan vapaaehtoista mutta ehkä kiinnostavaa. Ohjelmointitehtäviä on yksi pieni.
Suuntaa antava työläysarvio:? Ehkä tunti.
Pistearvo: B20.
Oheismoduulit: Traits.
Johdanto
Edellisissä luvuissa on opittu, miten yläkäsitteitä voi mallintaa piirreluokilla. Tämä luku täydentää edellisiä: tutustumme siihen, miten myös tavallisen luokan voi merkitä toisten luokkien yläkäsitteeksi. Tätä perinteistä olio-ohjelmointitekniikkaa kutsutaan periytymiseksi tai perinnäksi (inheritance).
Lisää tasokuvioita
Luvussa 7.3 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
Mitä 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 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 ilahduta käsitteellisen mallinnuksen kannaltakaan, 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 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
Tehdään pieni lisäys Rectangle
-luokkaan:
open class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape:
def area = this.sideLength * this.anotherSideLength
Square
sta tehdään Rectangle
n aliluokka (subclass):
class Square(size: Double) extends Rectangle(size, size)
Käytämme jälleen extends
-sanaa. Tällä kertaa sen perässä on
mainittu tavallinen luokka eikä piirreluokka. Sanotaan: luokka
Square
perii (inherits) luokan Rectangle
. 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. Aliluokka voi välittää
konstruktoriparametreja yliluokalleen; idea on sama kuin
piirreluokille luvussa 7.3. Tässä määritellään, että kun
Square
-oliota luodaan, tehdään samat alustustoimenpiteet
kuin Rectangle
-oliolle, kuitenkin niin, että molemmiksi
suorakaiteiden konstruktoriparametreiksi (eli molemmiksi
sivunpituuksiksi) laitetaan neliöolion saaman
konstruktoriparametrin arvo.
Kuinka välttämätön tuo open
on?
On syynsä sille, miksi ei ole hyvä ajatus periyttää aliluokkia ihan mistä tahansa luokasta, joka ei ole moiseen tarkoitettu. Nuo syyt on havaittu todellisiksi isoissa ohjelmaprojekteissa eivätkä tule kunnolla esille tällaisella alkeiskurssilla. Pientä osviittaa niistä saa lisälukemistosta tämän luvun lopussa.
Scalassa siis luokka merkitään avoimeksi open
-sanalla. Näin
ohjelmoija ilmoittaa, että kyseinen luokka on suunniteltu käytettäväksi
periytymisessä ja sille sopii määritellä aliluokkia vapaasti. Ellei
luokkaa noin avaa, ei sille pidä määritellä aliluokkia muualla
kuin samassa kooditiedostossa. open
-sanan poisjättö ei teknisesti
tee mahdottomaksi määritellä aliluokkia muuallakin, mutta Scala-kääntäjä
varoittaa moisesta epäilyttävästä koodista, ja tuollaista periytymistä
onkin parempi välttää. (Vrt. sulkeminen sealed
-sanalla, joka täysin
estää suorien alakäsitteiden määrittelemisen muissa tiedostoissa;
luku 7.4.)
Joissakin poikkeustilanteissa on perusteltua laatia aliluokka
sellaisellekin toisaalla määritellylle luokalle, joka ei ole avoin.
Kääntäjän varoitusilmoituksen voi tällöin poistaa antamalla käskyn
import scala.language.adhocExtensions
. O1-kurssilla tälle
ei ole tarvetta.
Periytyminen vs. piirreluokat
Luokan määritteleminen toisen luokan aliluokaksi näyttää kovasti samanlaiselta kuin luvussa 7.3 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. |
|
Alakäsitteeseen liitetään tuo piirre. |
|
Piirreluokassa saa olla abstrakteja metodeita ja muuttujia. |
|
Piirreluokasta ei voi luoda suoraan ilmentymää. |
Pelkkä |
Luokkaan voi liittää useita piirreluokkia. |
|
Piirreluokka ei voi välittää konstruktoriparametreja ylityyp(e)illeen. |
|
Ja tässä vastaavasti periytymisen ominaisuuksia. Esimerkkinä on yliluokka Rectangle
, joka
kuvaa Square
-luokan yläkäsitteen:
Kun käytetään periytymistä: |
Esimerkki |
---|---|
Yläkäsitettä kuvaa tavallinen luokka (joka on hyvä määritellä avoimeksi, jos tarkoitettu periytymiseen). |
|
Aliluokka periytyy yliluokasta. |
|
Tavallisessa luokassa ei saa olla abstrakteja metodeja tai muuttujia. (Paitsi, että... lisää aiheesta kohta.) |
|
Yliluokasta voi luoda ilmentymän suoraan. |
|
Luokalla saa (mm. Scalassa) olla vain yksi välitön yliluokka. |
class X extends Yli1, Yli2 ei toimi. (Muttaclass X extends Yli1, Piirre1, Piirre2 on sallittu.) |
Mikä vain tavallinen luokka ( |
|
On monia tilanteita, joissa kumpi tahansa näistä tekniikoista kelpaa.
Ohjelmointiharjoitus: esineitä esineiden sisällä
Johdanto
Oletetaan, että laaditaan ohjelmaa, jossa on tarkoitus kuvata erilaisia esineitä.
Käytössämme on yksinkertainen luokka Item
:
open 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ä
contents
-metodi, joka palauttaa lisätyt esineet vektorissa.Container
-esineidentoString
-metodi poikkeaa tavallisten esineiden vastaavasta.
Laatimasi luokan tulisi toimia tähän tapaan:
val container1 = Container("box")container1: o1.items.Container = box containing 0 item(s)
container1.addContent(Item("ring"))
container1res0: o1.items.Container = box containing 1 item(s)
val container2 = Container("bag")container2: o1.items.Container = bag containing 0 item(s)
container2.addContent(Item("book"))
container2.addContent(container1)
container2res1: o1.items.Container = bag containing 2 item(s)
container1.contentsres2: Vector[o1.items.Item] = Vector(ring)
container2.contentsres3: Vector[o1.items.Item] = Vector(book, box containing 1 item(s))
toString
in 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 laukun sisältämän laatikon sisällä onkin vielä sormus.
(Miten saataisiin kaikki "sisällön sisällötkin"? Palataan siihen luvussa 12.2.)
Ohjeita ja vinkkejä
Traits-moduulin pakkauksesta
o1.items
löytyy tuoItem
-luokka, josta tosin puuttuu alustaopen
. Lisää se.Samasta pakkauksesta löytyy myös alku
Container
-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
.Huomaa, että tässä tehtävässä korvaat (override) yliluokan
Item
toteutuksentoString
-metodille etkä Scala-olioiden oletustoteutusta kuten aiemmissa ohjelmissa.Osaatko toteuttaa aliluokan
toString
-metodin niin, että kutsut sen sisältä yliluokantoString
-metodia sen sijaan, että katsoisit esineen nimen suoraan? (Tämä ei ole välttämätöntä.)Älä lisää luokkaan muita julkisia jäseniä kuin pyydetyt. Yksityisiä jäseniä voit lisätä harkintasi mukaan.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Abstraktit luokat
Eroa piirreluokkien ja periytymisen välillä hämärtää se, että on mahdollista määritellä niin sanottuja abstrakteja luokkia (abstract class). Abstraktit luokat muistuttavat monin tavoin piirreluokkia, mutta eroavat niistä tietyin tavoin.
O1-kurssin kannalta abstraktit luokat eivät ole keskeisiä; suosimme ohjelmissamme piirreluokkia. Ohjelmointiopintojesi edetessä opit valitsemaan abstraktien luokkien ja piirreluokkien välillä itsenäisemmin. Nyt riittää tietää yleissivistävästi, että abstrakteja luokkia on olemassa, koska ne eivät ole harvinaisuus Scalassa tai ohjelmointikielissä muutenkaan. Kaikissa ohjelmointikielissä ei ole näitä kahta käsitettä erikseen.
Katsotaan esimerkki. Luvun 7.3 ohjelmassa meillä oli piirreluokka Entity
:
trait Entity(val name: String):
def contact: NaturalPerson
def kind: String
override def toString = s"$name ($kind)"
Kaksi metodeista on abstrakteja. Tällaisia alatyyppien toteutettavaksi jätettyjä metodeita ei voi ihan tavallisessa luokassa olla, mutta seuraava vaihtoehtoinen määrittely on mahdollinen:
abstract class Entity(val name: String):
def contact: NaturalPerson
def kind: String
override def toString = s"$name ($kind)"
Sana abstract
alussa tekee luokasta abstraktin. Tällaiseen luokkaan saa kirjata
abstrakteja metodeita kuten piirreluokkaankin. Ja samoin kuin piirreluokasta ei voi
luoda suoraan ilmentymiä, ei voi abstraktista luokastakaan. Abstraktit luokat ovat
avoimia ilman erillistä mainintaa, eli open
-sanaa ei lisäksi tarvita.
Luokkaa, joka ei ole abstrakti eikä piirreluokka, voi vertailun vuoksi sanoa konkreettiseksi luokaksi (concrete class).
Abstrakti luokka muistuttaa eräin tavoin piirreluokkaa ja eräin tavoin konkreettista yliluokkaa. Verrataan kootusti:
Piirreluokka |
Abstrakti
yliluokka
|
Konkreettinen
yliluokka
|
|
---|---|---|---|
Voiko se sisältää abstrakteja metodeja? |
Voi. |
Voi. |
Ei voi. |
Voiko siitä luoda suoraan ilmentymiä? |
Ei voi. |
Ei voi. |
Voi. |
Voiko se välittää konstruktoriparametreja yläkäsitteilleen? |
Ei voi. |
Voi. |
Voi. |
Voiko sellaisia luetella yläkäsitteinä useita ( |
Voi. |
Ei voi. |
Ei voi. |
Käyttäisinkö piirreluokkaa vai abstraktia luokkaa, noin yleisemmin?
Nyrkkisääntö: Ellei ole erityistä syytä käyttää abstraktia luokkaa, käytä piirreluokkaa, koska niitä voi liittää luokkiin joustavammin.
Valinta piirreluokan ja abstraktin yliluokan välillä voi tuntua hankalalta. O1-kurssilla yleensä annetaan luokkatason spesifikaatiot valmiina, ja valinnat on tältä osin tehty puolestasi. Aihe on ajankohtaisempi kurssilla Ohjelmointistudio 2. Vähän lisää aiheesta löytyy myös Kirjoja ja linkkejä -sivulla mainituista kirjoissa Scala Cookbook ja Programming in Scala.
Luokkahierarkiat Scala APIssa
Kuten piirreluokkia, myös periytymistä voi käyttää käsitehierarkioiden muodostamiseen. Eräitä esimerkkejä hierarkioista löytyy Scala APIsta. Katsotaan muutama.
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.4 on johdanto käyttöliittymiin Swing-kirjastolla. Tuo valinnainen luku on sijoitettu kurssin ja tämän kurssimateriaalin loppupäähän. Jos aihe kovasti 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.3 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.4 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.3 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
itse määritellä Option
ille muita aliluokkia, ja hyvä niin.
Kaikkien luokkien äiti: Any
Tutkitaan Scala-olioita REPLissä:
val sekalaisia = Vector(123, "laama", true, Vector(123, 456), Square(10), IArray(1, 2, 3))sekalaisia: Vector[Any] = Vector(123, laama, true, Vector(123, 456), o1.shapes.Square@114c3c7, Array(1, 2, 3))
Esimerkissä luodaan vektori, jossa on keskenään aivan erilaisia
olioita: Int
, String
, Boolean
, Vector[Int]
, Square
ja
IArray[Int]
-tyyppinen alkiokokoelma.
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]res4: Boolean = true jokuOlio.isInstanceOf[String]res5: Boolean = true jokuOlio.isInstanceOf[Square]res6: Boolean = false jokuOlio = Square(10)jokuOlio: Any = o1.shapes.Square@ecfb83 jokuOlio.isInstanceOf[Any]res7: Boolean = true jokuOlio.isInstanceOf[String]res8: Boolean = false jokuOlio.area-- Error: ... value area is not a member of Any
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ä.)
Melkein kaikkien luokkien äiti: AnyRef
eli Object
Scalan piällystyyppi Any
jakautuu kahteen "pää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.
Näistä tyypeistä on silti hyvä tietää sen verran, 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. 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 Item
periytyvät
AnyRef
istä.
Tilannetta mutkistaa hieman se, että silloin, kun käytetään Scalaa Java-virtuaalikoneen
"päällä" (kuten usein tehdään; luku 5.4), 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), 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.
Vielä yksi: Matchable
Mainittujen "ylätyyppien" lisäksi voit törmätä vielä yhteen: Matchable
on piirreluokka, joka kattaa yläkäsitteenä kaikki sellaiset Scala-
tyypit, joita on luvallista käyttää hahmontunnistuksessa — siis
match
-käskyssä. Sekä AnyRef
että AnyVal
piirteisiin on liitetty
Matchable
-piirre, joten Matchable
on käsitteenä lähes yhtä laaja
kuin Any
. (match
-käskyä voi soveltaa melkein kaikkiin Scala-arvoihin,
mutta on joitakin aivan poikkeuksellisia tyyppejä, joilla ei
Matchable
-piirrettä ole. Lisätietoja on saatavilla verkosta,
mutta ne eivät ole kurssin kannalta merkityksellisiä.)
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:
object terasmies extends Henkilo("Clark"): def lenna = "WOOSH!" end terasmies// defined object terasmies
Olemme sittemmin käyttäneet samaa tekniikkaa View
-luokan kanssa määritellessämme
yksittäisiä View
-olioita, 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
periyttää Henkilo
-yliluokasta yksittäisen olion. Vastaavasti olemme toteuttaneet
View
-yläkäsitteen abstraktiksi jättämän makePic
-metodin erilaisilla tavoilla.
Yksittäistä olioita määritellessä voi yliluokkia ja piirreluokkia yhdistellä joustavasti muillakin tavoin. Voit halutessasi lukea siitä alta lisää.
Alakäsitteen määritteleminen "lennosta"
Scala mahdollistaa olioiden luomisen niin, että olioon liitetään piirreluokka "lennosta", luomiskäskyn yhteydessä.
Määritellään pohjustuksena pari piirreluokkaa ja yksi tavallinen luokka. 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
Kokeillaan uutta tapaa tehdä olio:
val lemmikki = new Elain("laama") with Sylkevalemmikki: Elain & Sylkeva = eläin, tarkemmin sanoen laama
Määrittelemme "lennosta" uuden, tarkemmin nimeämättömän
alatyypin, joka on Elain
-luokan aliluokka ja johon on
liitetty Sylkeva
-piirre. Samalla luodaan tuosta luokasta
saman tien yksi ilmentymä.
Sana new
on pakollinen tällaisissa käskyissä, joissa
ei ainoastaan luoda uutta ilmentymää vaan samalla
määritellään sille uusi tyyppi. Huomaa myös with
.
Tällä Elain & Sylkeva
-yhdistelmätyyppiä olevalla oliolla voi
siis tehdä asioita, joita on määritelty joko Elain
-luokassa tai
Sylkeva
-piirreluokassa:
lemmikki.lajires9: String = laama lemmikki.syljeres10: 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"), Sylkeva// defined 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
, jajoka toteuttaa
Kyttyrallinen
-piirreluokan abstraktinkyttyroidenMaara
-metodin tietyllä tavalla.
val seSeOn = new Elain("dromedaari") with Kyttyrallinen with Sylkeva: def kyttyroidenMaara = 1seSeOn: Elain & Kyttyrallinen & Sylkeva = eläin, tarkemmin sanoen dromedaari
Katsotaan lopuksi, miten samalla tekniikalla voi luoda olion, jolla
on Sylkeva
-piirre ja omanlaisensa toString
-metodi:
val tunnettuKalamies = new Sylkeva: val nimi = "Eemeli" override def toString = this.nimitunnettuKalamies: Sylkeva = Eemeli
Tässä kirjoitettiin new
-sanan perään piirreluokan eikä
tavallisen luokan nimi. Käsky ei kuitenkaan luo ilmentymää
suoraan piirreluokasta (mitä ei voikaan tehdä). Se määrittelee
uuden nimettömän alatyypin, jolla on tuo piirre ja mainitut
lisä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, jos metodin todella on
järkevää toimia ainoastaan ympyräolioille (esimerkiksi 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 tulisi 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.
Scalassa, kuten joissakin muissakin kielissä, on käytettävissä myös
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, B class BA extends B, A// defined class AB // defined class BA AB().metodires11: String = b:n metodi BA().metodires12: String = a:n metodi
Lisätietoja hakusanoilla scala linearization tai Kirjoja ja linkkejä -sivun lähteistä.
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.3 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
niin, 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 tiedostossa määritellyt.)
Sopivasti käytettynä final
-määre voi parantaa ohjelman
ymmärrettävyyttä tai ehkäistä luokkien epätarkoituksenmukaista
käyttöä. Joissain tapauksissa myös ohjelman tehokkuus paranee,
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.
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.
Piirreluokilla ja abstrakteilla yliluokilla on paljon yhteistä mutta myös eroja. Erot riippuvat ohjelmointikielestä (ja kaikissa kielissä ei ole kumpaakin käsitettä erikseen).
O1-kurssilla laadimme näistä lähinnä piirreluokkia. Kurssin tarpeisiin abstrakteista luokista ei tarvitse tietää juuri muuta kuin että ne muistuttavat piirreluokkia.
Kaikki Scala-luokat kuuluvat hierarkiaan, jonka kantaluokkana on valmis luokka
Any
.Lukuun liittyviä termejä sanastosivulla: periytyminen eli perintä, aliluokka, yliluokka, tyyppihierarkia,
Any
; abstrakti luokka; staattinen tyyppi, dynaaminen tyyppi; avoin luokka, suljettu luokka;final
.
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!
Materiaalin luvut tehtävineen ja viikkokoosteineen on laatinut Juha Sorva.
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: (aakkosjärjestyksessä) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista suunnittelivat Juha Sorva ja Teemu Sirkiä. Teemu Sirkiä ja Riku Autio toteuttivat ne apunaan Teemun aiemmin rakentamat työkalut Jsvee ja Kelmu.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset laati Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Tapa, jolla 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+ luotiin alun perin Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Nykyään tätä avoimen lähdekoodin projektia kehittää Tietotekniikan laitoksen opetusteknologiatiimi ja tarjoaa palveluna laitoksen IT-tuki. Pääkehittäjänä on nyt Markku Riekkinen, jonka lisäksi A+:aa ovat kehittäneet kymmenet Aallon opiskelijat ja muut.
A+ Courses -lisäosa, joka tukee A+:aa ja O1-kurssia IntelliJ-ohjelmointiympäristössä, on toinen avoin projekti. Sen suunnitteluun ja toteutukseen on osallistunut useita opiskelijoita yhteistyössä O1-kurssin opettajien kanssa.
Kurssin tämänhetkinen henkilökunta löytyy luvusta 1.1.
Vaikka kyseessä on tavallinen luokka (
class
), josta voi luoda ilmentymiä, niin tämä luokka on avoin. Kirjoittamalla avainsananopen
ilmoitamme, ettäRectangle
saa vapaasti toimia yliluokkana (superclass) mille vain toisille luokille.