Luku 7.5: Yli- ja aliluokat
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 niin, että toiset luokat periytyvät siitä.
Lisää tasokuvioita
Luvussa 7.3 määrittelimme piirreluokan Shape
ja sen perivän 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, joka perii Shape
-piirteen näin:
class Square(val sideLength: Double) extends Shape:
def area = this.sideLength * this.sideLength
Kalvamaan kuitenkin jää, että koodissa on nyt selvästi toistoa: neliön pinta-ala-algoritmi
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: voimme 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. Aliluokka
Square
periytyy yliluokastaan; Square
-oliot ovat nyt myös
Rectangle
-tyyppisiä.
Luokalla Square
on vain yksi luontiparametri, joka kertoo
kunkin sivun mitan.
Kun aliluokasta luodaan ilmentymä, tehdään myös yliluokassa
määritellyt alustustoimenpiteet. Aliluokka voi välittää
luontiparametreja 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. Tämä tehdään niin, että
kummaksikin suorakaiteen luontiparametriksi (eli kummaksikin
sivunpituudeksi) tulee neliöolion saaman luontiparametrin 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
Yliluokasta periytyminen näyttää kovasti samanlaiselta kuin luvussa 7.3 nähty piirreluokasta periytyminen. 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 kyseessä on piirreluokka ( |
Esimerkki |
---|---|
Yläkäsitettä kuvaa piirreluokka. |
|
Alakäsite perii tuon piirreluokan. |
|
Piirreluokassa saa olla abstrakteja metodeita ja muuttujia. |
|
Piirreluokasta ei voi luoda suoraan ilmentymää. |
Pelkkä |
Luokka voi periä useita piirreluokkia. |
|
Piirreluokka ei voi välittää luontiparametreja ylätyyp(e)illeen. |
|
Ja tässä vastaavasti yliluokkien ominaisuuksia. Esimerkkinä on yliluokka Rectangle
,
joka kuvaa Square
-luokan yläkäsitteen:
Kun kyseessä on yliluokka ( |
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 noista vaihtoehdoista 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 paluuarvossa 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ä luontiparametri.Älä määrittele ilmentymämuuttujaa
name
uudelleenval
-sanaa käyttäen luokassaContainer
. Tuo muuttuja on jo määritelty yliluokassa.Container
-luokan luontiparametrin 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.
Voit itse päättää, millaista kokoelmaa käytät
Container
-luokan sisäisessä toteutuksessa (vektori? puskuri?). Joka tapauksessacontents
-metodin on palautettava sisältö vektorissa. Jos käytät puskuria, lisää alkuunimport scala.collection.mutable.Buffer
.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Vähän abstrakteista luokista
Tavallisten luokkien ja piirreluokkien lisäksi on mahdollista määritellä niin sanottuja abstrakteja luokkia (abstract class). Abstraktit luokat muistuttavat monin tavoin piirreluokkia, mutta tiettyjä erojakin on.
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; se on syytä tietää, koska ne eivät ole harvinaisuus Scalassa tai ohjelmointikielissä muutenkaan. Kaikissa ohjelmointikielissä ei ole abstraktin luokan ja piirreluokan 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ää luontiparametreja yläkäsitteilleen? |
Ei voi. |
Voi. |
Voi. |
Voiko sellaisia periä useita (luetella |
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 niistä voi periytyä 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 abstrakteja ja konkreettisia yliluokkia voi käyttää käsitehierarkioissa. 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[Any](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 tyyppi on 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
kirjoittamasi — 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
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ällä kurssilla 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ä.
AnyVal
ia ja AnyRef
iä voi kokeilla REPLissä näin:
val sekalaisia2 = Vector[AnyVal](123, true)sekalaisia2: Vector[AnyVal] = Vector(123, true) val sekalaisia3 = Vector[AnyRef]("laama", Vector(123, 456), Square(10))sekalaisia3: Vector[AnyRef] = Vector(laama, Vector(123, 456), o1.shapes.Square@667113)
Int
- ja Boolean
-tyypit periytyvät molemmat AnyVal
ista.
Näitä arvoja voi laitta AnyVal
eja sisältävään kokoelmaan.
String
, Vector[Int]
ja Square
periytyvät AnyRef
istä.
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
-piirteet perivät
Matchable
-piirteen, 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ä.)
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
ien on oltava tilaltaan muuttumattomia
ja täyttää muitakin tiukkoja ehtoja. Oikeissa paikoissa
käytettyinä niillä voi parantaa suoritustehokkuutta.
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ä olio perii piirreluokan "lennosta", mikä määritellään olionluomiskä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 joka perii
myös Sylkeva
-piirteen. Luomme tuosta luokasta saman tien
yhden ilmentymän.
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,joka perii 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 on useimmiten hyvä kirjata koodiin "laajoiksi" eli käyttää yliluokkaa tai piirreluokkaa muuttujien tyyppinä. Tämä koskee eritoten parametrimuuttujia. 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 perivä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 kiinnostavaa lisäluettavaa (lähinnä Javaa ja sen rajapintaluokkia ennestään tunteville): 'Interface' Considered Harmful.
Jos luokka periytyy useasta piirreluokasta, 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:
open 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. Tällöin voidaan 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.
Yhdistetyypit (kuten Int|String
)
Aiemmin totesimme, että jos staattisena tyyppinä on Any
, voivat
arvot olla mitä vain, kuten tässä:
val sekalaisia: Vector[Any] = Vector(-123, "laama")sekalaisia: Vector[Any] = Vector(-123, laama)
Vaan mitä jos tuosta jättää tyyppimäärittelyn Vector[Any]
pois?
val sekalaisia = Vector(-123, "laama")sekalaisia: Vector[Int | String] = Vector(-123, laama) val ekaAlkio = sekalaisia.headval ekaAlkio: Int | String = -123
Scala päättelee tyypiksi Vector[Int|String]
eli
"vektorillinen alkioita, joista kukin on joko kokonaisluku
tai merkkijono".
Myös paljas Int|String
on tyyppi. Tämän vektorin
head
-metodin palauttaa arvon, jonka staattinen tyyppi on
"joko kokonaisluku tai merkkijono". Tällaista tyyppiä sanotaan
yhdistelmätyypiksi (union type).
Mitä arvolla voi tehdä, jos sen staattinen tyyppi on Int|String
?
Vain sellaisia asioita, joita voi tehdä sekä kokonaisluvuilla että
merkkijonoilla:
ekaAlkio.length-- [E008] Not Found Error: value length is not a member of Int | String ekaAlkio.abs-- [E008] Not Found Error: value abs is not a member of Int | String
length
on määritelty merkkijonoille ja abs
kokonaisluvuille, ...
... mutta kumpaakaan metodeista ei ole sekä merkkijonoilla että kokonaisluvuilla, joten tällaiset käskyt tuottavat käännösaikaisen virheen.
Mutta jos haluat tehdä valinnan dynaamisen tyypin perusteella,
match
-käsky toimii hyvin yhdistetyyppienkin kanssa:
ekaAlkio match case luku: Int => luku.abs case jono: String => jono.lengthres13: Int = 123
Mitään kolmatta case
a ei tarvita sen varalta, että kyseessä
olisi jokin muu arvo, koska Int|String
-tyyppinen arvo on
väistämättä joko Int
tai String
.
Yhdistetyypin voi myös kirjata koodiin. Esimerkiksi muuttujan tyypiksi voi merkitä yhdistetyypin, kuten alla on tehty.
def laske(lukuTaiJono: Int | String) =
lukuTaiJono match
case luku: Int => luku.abs
case jono: String => jono.lengthdef laske(lukuTaiJono: Int | String): String
Vector(-123, "laama").map(laske)res14: Vector[Int] = Vector(123, 5)
Yhteenvetoa
Piirreluokkien lisäksi myös tavalliselle luokalle voi määritellä alakäsitteitä: yläkäsitettä kuvaavan luokan eli yliluokan ominaisuudet periytyvät sen aliluokille.
Luokka voi olla abstrakti, jolloin siinä voi olla abstrakteja metodeita ja muuttujia kuten piirreluokassakin. Abstraktista luokasta ei voi luoda ilmentymiä suoraan 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, 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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, 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, Juha Sorva ja Jaakko Nakaza. 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; sitä 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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
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 eli siitä sopii periyttää toisia luokkia.