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

4 Luokista ja olioista

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

Sivun sisältöä:

Luokat, kentät, metodit

Scalassa nimettyjä tai nimeämättömiä alirutiineita, aliohjelmia, on tapana nimittää funktioiksi, vaikka ne eivät palauttaisi mitään varsinaista arvoa (eli niiden tyyppi on Unit, vrt. eräiden kielten void-metodit).

Kun funktiota käytetään olio-ohjelmoinnissa olion aksessorina, sitä on tapana kutsua metodiksi. [Java-ohjelmoijilla on paha tapa kutsua mitä tahansa nimettyä aliohjelmaa metodiksi...]

// Luokan otsikko on pääkonstruktorin otsikko.
// Sen muodollisista parametreista tulee ilmentymämuuttujia.

class Esim(piilovakio: Int,
           val julkivakio: Int,
           var julkimuuttuja: Int,
           private var piilomuuttuja: Int)  {

  // pääkonstruktorin algoritmi:
  piilomuuttuja *= julkimuuttuja

  // toissijainen konstruktori kutsuu pääkonstruktoria:
  def this(arvo: Int) {
    this(1, arvo, 2, 3)
  }

  // ... tai toista apukonstruktoria:
  def this() {
    this(666)
  }

  // aksessorit:
  def annaPiilomuuttujanArvo = piilomuuttuja
  // eli
  // def annaPiilomuuttujanArvo(): Int = {return piilomuuttuja}

  override def toString = "piilossa on " + piilomuuttuja

}

val a = new Esim(1,2,3,4)
val b = new Esim(4321)
val c = new Esim

println(a.annaPiilomuuttujanArvo)  // 12
println(b.annaPiilomuuttujanArvo)  //  6

println(a)  // piilossa on 12
println(b)  // piilossa on 6
println(c)  // piilossa on 6

Aletaan tutustua asiaan yksityiskohtaisemmin:

"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!

Käyttöä:

  a.arvo = 7
  b.arvo = a.arvo * 3

  // tai myös toisin kunhan sallitaan:
  import scala.language.postfixOps    // nykyään pitää antaa lupa!
                                      // (ilman lupaa "deprecated)
  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 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

Konstruktorien mahdolliset algoritmit kirjoitetaan siis seuraavaan tyyliin:

  class Intti3 (var arvo: Int) {

    println("Luotiin Intti3-olio: " + arvo) // pääkonstruktorin algoritmi

    def this() = {     // apukonstruktorin algoritmi
      this(0)          // Javan tapaan this pitää olla alussa!
      println("Kutsuttiin parametritonta konstruktoria.")
    }
  }

  val a = new Intti3(7)
  val b = new Intti3
  b.arvo = a.arvo * 3
Tulostus:
Luotiin Intti3-olio: 7
Luotiin Intti3-olio: 0
Kutsuttiin parametritonta konstruktoria.

Laaditaan sitten klassiseen tyyliin aidosti kapseliin laitettu ei-negatiivinen kokonaisluku. Kostruktorille ja setterille annettu negatiivinen parametri hoidellaan petomaisesti:

 class InttiPos (private var piiloarvo: Int) {

   if (piiloarvo < 0) piiloarvo = 666

   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
 val c = new InttiPos(-13)

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

 println(a.arvo)  // 666
 println(b.arvo)  //  21
 println(c.arvo)  // 666
Tässä esimerkissä metodi aseta ei palauta mitään varsinaista 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.

Jos edellistä esimerkkiä jatketaan vaikkapa lauseella:

  println(b aseta a.arvo * 3)
eli jos tulostetaan Unit-tyyppisen funktion arvo, saadaan tulostus ().

Huom: Jos funktio määritellään ilman yhtäsuuruusmerkkiä tyyliin def aseta(a: Int) {...}, sen tyyppi on Unit, vaikka suoritus päättyisi return-lauseeseen! Kääntäjä osaa onneksi opastaa: Jos yllä getteri korvataan seuraavasti

 def arvo {return piiloarvo}  // *** getteri ***
saadan varoitus: "warning: enclosing method arvo has result type Unit: return value discarded".

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

Ainokainen – singleton

Ainokainen eli "singleton" on suunnittelumalli (design pattern) "ainutkertainen olio". Scalassa ainokaisilla on keskeinen rooli mm. siitä syystä, ettei kielessä ei ole lainkaan Javan kaltaista static-maailmaa.

Kuten jo aeimmin nähtiin, Javan tavukoodiksi käännettävä sovellus voidaan ohjelmoida ainokaiseen olioon. Esimerkkinä pelkän pääohjelman sisältävä sovellus:

object tervehdi {
  def main(args: Array[String]) = {
    println("Montako tervehdystä?")
    var lkm = scala.io.StdIn.readInt  // (näikin siis voi tehdä)
    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.

Oliokumppani, luokkakumppani ja factory-metodi

Vaikka ainokaiset vaikuttavat vain naamioiduilta "staticeilta", ne ovat itse asiassa paljon muutakin: nillä voi olla yliluokka, jolta ne perivät ominaisuuksia, niihin voidaan liittää piirretyyppejä ("mixata traitteja"), olio on tyypiltään yliluokkansa ja piirretyyppiensä tyyppinen, ...

Luokkamäärittelyn kanssa samassa käännösyksikössä voidaan määritellä luokan kanssa saman niminen ainokainen olio! Tällainen 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) {  // luokkakumppani
    var arvo = alkuarvo
  }

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

  val a = InttiOb(7)  // factory-metodin kutsuja, siis ei new-operaatioita!
  val b = InttiOb(14)

  val c = InttiOb.apply(42) // näinkin toki saa sanoa...

  val d = new InttiOb(49)   // ja toki näikin ...

  println(a.arvo)
  println(b.arvo)
  println(c.arvo)
  println(d.arvo)
  InttiOb.huu         // kirjastometodin kutsu
Tulostus:
Tehdas valmistaa InttiObin 7
Tehdas valmistaa InttiObin 14
Tehdas valmistaa InttiObin 42
7
14
42
49
BÖÖ
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 piirretyypeillä (trait). Ja erityisesti juuri niillä...

Sovellus

Scala-sovelluksen voi luoda edellä nähdyn main-metodi-ainokaisen lisäksi myös "miksaamalla App-traitin" ainokaiseen. Säästyypähän main-metodin kirjoittamiselta.
object tervehdi extends App {
  println("Montako tervehdystä?")
  var lkm = scala.io.StdIn.readInt
  while (lkm>0) {
    println("Hoi maailma!")
    lkm = lkm-1
  }
}

Valmistellaan vähän korvaa ja silmää tulevaan. Mitä tuossa oikeastaan tapahtuu?

Olioon "mixataan traitti" eli liitetään piirretyyppi App. Olio perii piirretyypistä oikean muotoisen main-metodin, joten saadaan aikaan suoritettava ohjelma. Aaltosulkeissa oleva koodi laitetaan synnytettävän ainokaisen konstruktoriin ja suoritetaan, kun olio alustetaan. Pian ehkä selviää, mitä tämä kaikki tarkoittaa...

Myös komentoriviparametrit löytyvät piirretyypin mukanaan tuomasta muuttujasta args:

  object komentoriviparametrit extends App {
    println("Parametrit olivat: " + (args mkString ", "))
  }