Helsingin yliopisto / tietojenkäsittelytieteen laitos / Ohjelmointitekniikka (Scala) / © Arto Wikla 2010

4 Luokista ja olioista

Muutettu viimeksi 23.3.2010 / Sivu luotu 12.3.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Luokat, kentät, metodit

Johdannossa nähtiin "Javamainen" Scala-versio Ohjelmoinnin perusteet -kurssin Pikkuvarasto-esimerkistä:

class Pikkuvarasto (private var määrä: Double, val tuote: String) {
  def this() = this(0.0, "(nimetön)")  // parametriton konstruktori oletusarvoin
  def paljonkoOn = määrä // aksessori on tehtävä koska määrä on private!
  def vieVarastoon(paljonko: Double) = if (paljonko > 0) määrä += paljonko
  def otaVarastosta(paljonko: Double) =
    if (paljonko <= 0)  0.0
    else if (paljonko <= määrä) {määrä -= paljonko;  paljonko;}
    else {val saatiin = määrä; määrä = 0; saatiin;}
  override def toString =  "("+tuote+": "+määrä+")"
  def summa(toinen: Pikkuvarasto) =
    new Pikkuvarasto(määrä + toinen.määrä, tuote + toinen.tuote);
}

val mehua =  new Pikkuvarasto(10, "Mehu")
val olutta = new Pikkuvarasto(123.4, "Olut")
val bensaa = new Pikkuvarasto(90.1, "Bensa")
println(mehua); println(olutta); println(bensaa)

val a = new Pikkuvarasto(); println(a)

val b = mehua; println("" + b + mehua)
b vieVarastoon(25); println("" + b + mehua)

println("Muuttuja bensaa:" + bensaa + ", määrä=" + bensaa.paljonkoOn +
        ", nimi=" + bensaa.tuote)
//...
Luvun 1 huomioita:

Nyt aletaan askarrella luokkien ja olioiden kanssa "Scala-henkisemmin".

"Kapseloidaan" aluksi kokonaisluku. (Eipä tässä kyllä oikeastaan mitään kapseloida, kun mitään ei piiloteta eikä edes operaatioita lisätä kapseliin.)

  class Intti1 {
    var arvo = 0
  }

  val a = new Intti1
  val b = new Intti1
Syntyy jotakin kovin tuttua, mutta on tässä jotakin uuttakin: Myös nolla on olio, johon sekä a:n että b:n kenttä arvo aluksi viittaa! (Kuva luennolla!)

Käyttöä:

  a.arvo = 7
  b.arvo = a.arvo * 3
  // tai myös:
  a arvo = 7
  b arvo = (a arvo) * 3

  println(a.arvo)  //  7
  println(b arvo)  // 21
Näin saatiin siis rakenne, joka muistuttaa eräiden kielten tietuetta (record, struct). Jos kenttä olisi val, saataisiin muuttumaton vakiotietue.

Vieläkin vaivattomammin saadaan aikaan monipuolisempi versio äskeisestä kirjoittamalla konstruktorille var-parametri:

  class Intti2 (var arvo: Int) { }

  val a = new Intti2(7)
  val b = new Intti2(0)
  b.arvo = a.arvo * 3
  println(a.arvo)  //  7
  println(b arvo)  // 21
Ja jos halutaan parametritonkin konstruktori, se käy helposti kirjoittamalla pääkostruktoria käyttävä lisäkonstruktori:
  class Intti2 (var arvo: Int) {
   def this() = this(0)          // HUOM: Toki lisäkonstruktorilla voi olla myös
  }                              // oma lohko, jossa on vaikka millainen algoritmi!

  val a = new Intti2(7)
  val b = new Intti2
  b.arvo = a.arvo * 3
Laaditaan sitten klassiseen tyyliin aidosti kapseliin laitettu positiivinen kokonaisluku:
 class InttiPos (private var piiloarvo: Int) {
   def this() = this(0)

   def aseta(a: Int) {  // *** setteri ***
     piiloarvo = if (a>0) a else 666 // virhetilanteen hoito
   }

   def arvo = piiloarvo  // *** getteri ***
 }

 val a = new InttiPos(7)
 val b = new InttiPos

 b aseta a.arvo * 3
 a aseta a.arvo - 1000

 println(a.arvo)  // 666
 println(b arvo)  //  21
Tässä esimerkissä metodi aseta ei palauta mitään arvoa. Sen tyyppi on Unit, mikä vastaa eräiden kielten "void-metodia". Kirjoitustyyli def aseta(a: Int) {...} sopii tähän tilanteeseen. Parametriton funktio arvo taas palauttaa Int-arvon. Siksi se kirjoitetaan yhtäsuuruusmerkkisyntaksilla.

Puolipiste

Puolipisteen saa yleensä jättää pois. Jos samalle riville haluaa useampia lauseita/lausekkeita, puolipiste tarvitaan.

Knoppologiaa: Puolipistepäättelijä ymmärtää rakenteen

  x
  + y
kahdeksi lauseeksi/lausekkeeksi! Avun saa joko suluttamalla tai muistamalla säännön: Jos rivin lopussa on infix-operaatio, lause/lauseke jatkuu seuraavalla rivillä. Rivinvaihtojen tyyli on siis:
  x +
  y +
  z

Ainokaiset ja factory-metodi

Ainokainen eli "singleton" on suunnittelumalli (design pattern) "ainutkertainen olio". Scalassa ainokaisilla on keskeinen rooli, koska kielessä ei ole mitään Javan "staticin" kaltaista.

Kuten jo aeimmin nähtiin, Bytecodeksi käännettävä sovellus voidaan sisällyttää ainokaiseen olioon. Esimerkkinä vaikka pääohjelman sisältävä sovellus:

object tervehdi {
  def main(args: Array[String]) = {
    println("Montako tervehdystä?")
    var lkm = readInt
    while (lkm>0) {
      println("Hoi maailma!")
      lkm = lkm-1
    }
  }
}
Myös vaikkapa joukko kirjastometodeja voidaan koota ainokaiseen, vrt. Javan kirjastoluokka staattisine metodeineen. Ainokainen myös alustetaan samaan tapaan kuin Javan staattinen kalusto: ensimmäinen ohjelman suorittama viittaus ainokaiseen luo ja alustaa tuon olion.

Vaikka ainokaiset vaikuttavat vain naamioiduilta staticeilta, ne ovat itse asiassa aivan jotakin muuta: nillä voi olla yliluokka, jolta ne perivät ominaisuuksia, niihin voidaan liittää piirteitä ("mixata traitteja"), olio on tyypiltään yliluokkansa ja piirteidensä tyyppinen - kaikkine seurauksineen ...

Luokkamäärittelyn kanssa samassa käännösyksikössä voidaan määritellä luokan kanssa saman niminen olio! Tuollainen olio on luokan oliokumppani (companion object) ja luokka tuon olion luokkakumppani (companion class). Näin voidaan ohjelmoida esim staattisten metodien ja muuttujien Scala-vastineita ja erityisesti Scala-idiomiin kuuluvia factory-metodeita:

  class InttiOb(alkuarvo: Int) {
    var arvo = alkuarvo
  }

  object InttiOb {
    def apply(alkuarvo: Int) = new InttiOb(alkuarvo) // factory-metodi!
    def huu {println("BÖÖ")} // kirjastometodi
  }

  val a = InttiOb(7)  // factory-metodin kutsuja
  val b = InttiOb(14)
  val c = InttiOb.apply(42) // näinkin toki saa sanoa...

  println(a.arvo)
  println(b arvo)
  println(c arvo)
  InttiOb.huu         // kirjastometodin kutsu

Juuri tähän tapaan esim. edellä nähty Map-kummastelun aihe on toteutettu. Ja tuo "kaarisulkeiden semantiikka" tosiaankin eräitä poikkeuksia lukuunottamatta todellakin on apply-metodin kutsun lyhennysmerkintä... Ja oliokumppaneita voi olla myös abstrakteilla otuksilla kuten piirteillä (trait). Ja aivan erityisesti niillä...

Sovellus

Scala-sovelluksen voi luoda edellä nähdyn main-metodi-ainokaisen lisäksi myös "miksaamalla Application-traitin" ainokaiseen:
object tervehdi extends Application {
  println("Montako tervehdystä?")
  var lkm = readInt
  while (lkm>0) {
    println("Hoi maailma!")
    lkm = lkm-1
  }
}
Hieman tämä säästää kirjoitusvaivoja. Rajoituksena on, ettei komentoriviparametreja voi antaa.

Valmistellaan vähän korvaa/silmää tulevaan. Mitä tässä oikeastaan tapahtuu?
Olioon "mixataan" siis "traitti" eli liitetään piirre Application. Olio perii piirteestä oikean muotoisen main-metodin, joten saadaan aikaan suoritettava ohjelma. Aaltosulkeissa oleva koodi laitetaan syntyvän ainokaisen ensisijaiseen konstruktoriin ja suoritetaan, kun olio alustetaan. Pian ehkä selviää, mitä tämä kaikki tarkoittaa...