Muutettu viimeksi 16.4.2010 / Sivu luotu 9.4.2010 / [oppikirjan esimerkit] / [Scala]
Sivun sisältöä:
Olio-ohjelmoinnissa luokkia rakennellaan perimällä muita luokkia (inheritance), asettamalla muiden luokkien ilmentymiä luokan jäseniksi (composition) ja ohjelmoimalla luokkiin omia ominaisuuksia, jotka täydentävät ja muuttavat muualta saatuja ominaisuuksia.
Scalan olio-ohjelmointitekniikat muistuttavat kovasti Javan vastaavia; tärkein lisäys ovat piirteet, "traitit", Javan hyvin rajoitettua vähemmän rajoitettu - mutta kuitenkin rajoitettu - moniperintä. Kaikki eivät valttämättä kutsu esim. Javan interface-tekniikkaa "moniperinnäksi". Minä kutsun...;-)
Piirteet esitellään omassa luvussaan. Tässä luvussa käydään tiiviisti läpi Scalan "javamaiset" periytymisjutut.
abstract class Moottori {
def annaKierrosluku(): Double
def asetaPolttoaineensyottomaara(maara: Double): Unit
// ... Täällä voisi vallan mainiosti olla myos ei-abstrakteja metodeita!
// ... Ja jopa sellaisia, jotka kutsuvat abstrakteja metodeita!
}
Ohjelmoidaan sitten luokka, joka sisältää abstraktin
Moottori-tyyppisen kentän. Idea (kuten pitäisi jo olla
tuttua) on siis siinä, että voidaan ohjelmoida välineitä,
jotka eivät ole riippuvaisia minkään erityisen moottorin
ominaisuuksista. Yleiskäyttöisyyttä, koodin uudelleenkäyttöä,
varmistettua riippumattomuutta...
class Auto(m: Moottori) { // Moottori on abstrakti luokka!
// rakennellaan loputkin autosta ...
def mitenLujaaMennaan() = {
// nopeus riippuu tavalla tai toisella kierrosluvusta:
m.annaKierrosluku()/30
}
def kaasuta(bensaa: Double) {
m.asetaPolttoaineensyottomaara(bensaa)
// ...
}
// ties mitä muita metodeita ...
}
Tuttuun tapaan abstraktista luokasta ei tietenkään voi luoda
ilmentymiä, joten jotta
Autosta, jonka konstruktorin parametrin tyyppi
on abstrakti, voisi luoda ilmentymän, tarvitaan jokin
abstraktin Moottori-luokan ei-abstrakti aliluokka.
Cosworth toteuttaa perimänsä abstraktit metodit:
class Cosworth extends Moottori {
private var kierrosluku = 0.0
// muut tietorakenteet ...
def annaKierrosluku() = kierrosluku // getteri
def asetaPolttoaineensyottomaara(maara: Double) {
kierrosluku =
if (maara < 0) 0.0
else if (maara > 100)
9300.0
else
(maara/100)*9300 // tms...
}
// muita aksessoreita ...
}
Ja jo alkaa syntyä kilpa-autoja:
val brrrmmm = new Cosworth() // "oikea moottori", joka val lotus49 = new Auto(brrrmmm) // kelpaa auton luontiin lotus49.kaasuta(96) println(lotus49.mitenLujaaMennaan()) // 297.6 lotus49.kaasuta(-23); println(lotus49.mitenLujaaMennaan()) // 0.0 lotus49.kaasuta(1100) println(lotus49.mitenLujaaMennaan()) // 310.0Vastaavalla tavalla voitaisiin luoda ja käyttää Auto-olioita milloin milläkin moottorilla... Auton moottoriksi kelpaa mikä tahansa olio, jonka luokka on Moottori-luokan ei-abstrakti aliluokka!
Terminologinen huomautus: Ohjelmointikielistä puhuttaessa esitteleminen (declare) ja määritteleminen (define) tarkoittavat erityisiä asioita: Nimet eli tunnukset esitellään ja nimien eli tunnuksien merkitys määritellään. Yllä luokka Moottori vain esitteli tunnuksen annaKierrosluku. Luokka Cosworth sitten myöskin esittelyn lisäksi määritteli tuon tunnuksen omalla tavallaan.
Edellä kirjoitettiin tyhjät sulkeet näkyviin:
def annaKierrosluku(): Double // abstraktina ... m.annaKierrosluku() // käytössä ... def annaKierrosluku() = kierrosluku // toteutuksessaSulkeet voi myös jättää pois
def annaKierrosluku: Double // abstraktina ... m.annaKierrosluku // käytössä ... def annaKierrosluku = kierrosluku // toteutuksessaKumpikin tapa kelpaa kääntäjälle, mutta kaikki sekoitukset eivät. [Englanniksi (vai Scala-englanniksi?) sanapari on "parameterless method" ja "empty-paren method".]
"Tapana on", "tyyliin kuuluu", kirjoittaa tyhjä parametrilista näkyviin silloin, kun metodi muuttaa olion kenttiä, kun metodi siis on "setteri" tai sillä on muita sivuvaikutuksia. Tyhjä parametrilista on tapana kirjoittaa myös silloin, kun kyseessä on "getteri", jonka arvo riippuu muuttuvista kentistä.
Mutta sivuvaikutukseton "getteri" immutaabeleista kentistä on tyylin mukaista ohjelmoida ilman tyhjiä sulkeita.
Tässä tyylittelyssä ei ole todellakaan kyse pelkästä "ohjelmointiestetiikasta"! Kun parametriton metodi kirjoitetaan ilman tyhjiä sulkeita, tarvittaessa metodi voidaan korvata kentällä tyyliin;
// metodit height ja width
abstract class Element {
def contents: Array[String]
def height: Int = contents.length
def width: Int = if (height == 0) 0 else contents(0).length
}
// kentät height ja width
abstract class Element {
def contents: Array[String]
val height = contents.length
val width = if (height == 0) 0 else contents(0).length
}
Tämän luokan käyttäjä (aliluokan ohjelmoija tai aliluokan
ilmentymiä käyttävä ohjelmoija) viittaa nimiin height
ja width ihan samalla tavalla riippumatta siitä, ovatko
ne metodeita vai kenttiä!
Yksi tärkeä ero metodi- ja kenttäratkaisussa on se, että kenttä vie tilaa ja metodin kutsu puolestaan laskenta-aikaa. Valinta on luokan toteuttajan. Ja oleellista on, että tämä valinta on kapseloitu tuohon luokkaan! Sovelluksen kannalta toiminnallisuus on sama.
"Eikä tässä vielä kaikki": Vaikka abstrakti luokka määrittelisi parametrittoman metodin, toteutusluokka voi toteuttaa sen kenttänä!
abstract class Moottori {
def annaKierrosluku: Double
def asetaPolttoaineensyottomaara(maara: Double): Unit
}
class Auto(m: Moottori) { // Moottori on abstrakti luokka!
// rakennellaan loputkin autosta ...
def mitenLujaaMennaan() = {
// nopeus riippuu tavalla tai toisella kierrosluvusta:
m.annaKierrosluku/30
}
def kaasuta(bensaa: Double) {
m.asetaPolttoaineensyottomaara(bensaa)
}
}
class TasaKone extends Moottori {
val annaKierrosluku = 1000.0 // vakiogetteri
def asetaPolttoaineensyottomaara(maara: Double) { }
}
val surrur = new TasaKone() // "oikea moottori", joka
val trabi = new Auto(surrur) // kelpaa auton luontiin
trabi.kaasuta(96)
println(trabi.mitenLujaaMennaan()) // 33.333333333333336
trabi.kaasuta(-23);
println(trabi.mitenLujaaMennaan()) // 33.333333333333336
trabi.kaasuta(1100)
println(trabi.mitenLujaaMennaan()) // 33.333333333333336
Kuten nähtiin, Scalassa kuitenkin myös kentät voivat korvata metodeja. Tästä seuraa tärkeä ero Javaan: kenttien ja metodien nimet kuuluvat samaan nimiavaruuteen, joten niiltä vaaditaan yksikäsitteisyyttä.
Ja todellakin, kenttä voi korvata perityn metodin! Mutta ei toisin päin...
Javassa samannimisyys sallitaan kentille, metodeille, luokille (tyypeille), pakkauksille ja osoitteille (nimetyille lauseille break- ja contunue-lauseita varten). Scalassa tällaisia "nimiavaruuksia" on vain kaksi: arvot (kentät, metodit, pakkaukset, ainokaiset) ja tyypit (luokkien ja piirteiden (trait) nimet).
Ja vastaavasti var tekee kentästä julkisen muuttujan.. Myös määreet private, protected ja override ovat käytettävissä. Perityn kentän voi korvata myös konstruktorin parametrilistassa:
class Cat {
val dangerous = false
}
class Tiger(
override val dangerous: Boolean,
private var age: Int
) extends Cat
Tämä vastaa sitä, että kirjoitettaisiin:
class Tiger(param1: Boolean, param2: Int) extends Cat {
override val dangerous = param1
private var age = param2
}
class Piste(private var x: Int, private var y: Int) {
def this() = this(0, 0)
def siirry(dx: Int, dy: Int) {x += dx; y += dy;}
override def toString = "("+x+","+y+")"
}
val a = new Piste()
val b = new Piste(7, 14)
println(a) // (0,0)
println(b) // (7,14)
a.siirry(2, 3)
b.siirry(4, 5)
println(a) // (2,3)
println(b) // (11,19)
Java-taustaiselle uutta on kenttien määrittelymahdollisuus jo
pääkonstruktorin parametrilistassa, konstruktoreiden kuormittamisen
tapa ja korvaavan metodin pakollinen override-määre.
Laaditaan sitten tälle luokalle aliluokka:
class VariPiste(x: Int, y: Int, private var vari: Int)
extends Piste(x, y) {
def this() = this(0, 0, 0)
def this(vari: Int) = this(0, 0, vari)
//-- lisämetodi:
def uusiVari(vari: Int) {this.vari = vari}
override def toString = super.toString + " väri: " + vari
}
val c = new VariPiste()
val d = new VariPiste(7)
val e = new VariPiste(4,5,9)
println(c)) // (0,0) väri: 0
println(d)) // (0,0) väri: 7
println(e)) // (4,5) väri: 9
e.siirry(1, 1) // yliluokan operaation käyttö!
println(e)) // (5,6) väri: 9
e.uusiVari(14) // oman operaation käyttö
println(e)) // (5,6) väri: 14
Siis yliluokan konstruktoria kutsutaan extends-ilmauksen
yhteydessä!. Korvattuun perittyyn metodiin viitataan tutulla
tavalla ilmauksella super.
Huom: Korvattuihin kenttiin ei superilla
voi viitata! Miksiköhän ei?
Esimerkki:
abstract class Yli {
val a = 1
private val b = 2
def c = 3
def d: Int
}
class Ali extends Yli {
override val a = 10 // pakollinen
private val b = 20 // kielletty
override def c = 30 // pakollinen
def d = 40 // sallittu muttei pakollinen
val e = 50 // kielletty
}
Sitten paljastuu, ettei
asia kuitenkaan ole ihan niin suoraviivainen:
abstract class Yli {
def a = 1
val b = 2
var c = 3
val d = 4
}
class Ali extends Yli {
override def a = 10 + super.a // korvattu metodi, viittaus korvattuun metod$
override val b = 20 // ok
// val xx = 10 + super.b // error: super may be not be used on value b
// override var c = 30 // variable c cannot override a mutable variable
// val yy = 10 + super.c // error: super may be not be used on variable c
// override var d = 40 // error overriding value d in class Yli of type Int;
// variable d is not stable
}
val x = new Ali
println(x.a) // 11
println(x.b) // 20
Huom: Scalassa vaikuttaa olevan override-epäjohdonmukaisuuksia:
Versiossa 2.8 muutellaan joitakin 2.7:n sääntöjä. Tarkkana siis!
"Arton polymorfismisääntöjä":
Olkoon TyyppiB luokka, joka on luokan TyyppiA välitön tai välillinen aliluokka. Ja olkoon muuttuja x tuon yliluokan tyyppiä: x: TyyppiA
Tällaisen muuttujan arvoksi voi sijoittaa viitteen mihin tahansa olioon, joka on tyyppiä TyyppiA. Koska "jokainen kissa on eläin" eli aliluokan jokainen ilmentymä on myös yliluokan tyyppiä, on luvallista sijoittaa
x = new TyyppiB()"Aksessoidaan muuttujaa": x.met()
val a: VariPiste = new VariPiste(7) val b: Piste = a val c: Any = a println(a) // (0,0) väri: 7 println(b) // (0,0) väri: 7 println(c) // (0,0) väri: 7Siis olion tyyppi -- ei muuttujan tyyppi -- määrää, mikä versio toString-metodista valitaan.