Scalaa kootusti

Tämä sivu esittelee valikoituja Scala-kielen ominaisuuksia ja Scala-kielen oheiskirjastojen sisältämiä työkaluja. Sivulla on niistä pieniä, irrallisia esimerkkejä.

Voit oppimateriaalin varsinaisiin lukuihin jo tutustuttuasi käydä täältä kertaamassa yksityiskohtia "Miten se nyt kirjoitettiinkaan?" -hengessä.

Tämä on ainoastaan tekninen kooste eräistä Scalaan liittyvistä rakenteista ja työkaluista. Alla ei opeteta periaatteita, käsitteitä tai termejä eikä kerrota, mitä esitellyillä välineillä kannattaa tehdä; näistä asioista opit oppimateriaalin varsinaisissa luvuissa. Sivu ei etene nikamalleen samassa järjestyksessä kuin nuo luvut.

Tämä kooste ei kata koko Scala-kieltä. Koosteessa painottuvat rakenteet, joita Ohjelmointi 1 -kurssilla muutenkin käsitellään.

Etkö löydä etsimääsi?

Vastauksia voi löytyä myös näiden linkkien kautta:

Pidemmän päälle kannattaa opetella lukemaan Scala-kielen määrittelyä ja Scala API -dokumentaatiota, vaikka ne alkeiskurssilaiselle osin vaikeaselkoisia ovatkin.

Jos olisit kaivannut tälle sivulle jotakin, mitä täällä ei ole, voit kertoa asiasta kurssin palautekanavia pitkin tai suoraan sähköpostitse osoitteeseen juha.sorva@aalto.fi.

Tämän sivun osiot

Alkeita

Lukuja

Laskutoimituksia (luku 1.3):

100 + 1res0: Int = 101
1 + 100 * 2res1: Int = 201
(1 + 100) * 2res2: Int = 202

Int-tyyppisten kokonaislukujen jakolasku pyöristää kohti nollaa:

76 / 7res3: Int = 10

Modulo-operaattori % tuottaa jakojäännöksen (luku 1.7):

76 % 7res4: Int = 6

Double-arvoilla laskettaessa saadaan desimaaleja (laskentatarkkuuden puitteissa):

76.0 / 7.0res5: Double = 10.857142857142858

Katso myös lukutyyppien rajoitukset luvusta 5.2 ja lukutyyppien metodeita luvusta 4.4.

Merkkejä

String on merkkijonotyyppi (luku 1.3). Merkkijonoilla on operaattorit + ja *:

"maa" + "laama"res6: String = maalaama
"laama" * 3res7: String = laamalaamalaama

Yksittäisiä merkkejä voi kuvata tyypillä Char (luku 4.4). Char-literaali muodostetaan heittomerkeillä:

'a'res8: Char = a
'!'res9: Char = !

Lisää merkkijonojen käsittelyä alempana kohdissa Merkkijonojen metodeita, Kokoelmien alkeita ja Kokoelmien käsittely korkeamman asteen metodeilla.

Muuttujia

Muuttujan määritteleminen (luku 1.4):

val lukumuuttuja = 100lukumuuttuja: Int = 100

Myös tietotyypin voi erikseen kirjata, kuten alla, vaikka tyyppipäättelystä (luku 1.8) johtuen se on usein tarpeetonta:

val toinenMuuttuja: Int = 200toinenMuuttuja: Int = 100

Muuttujan nimeä voi käyttää lausekkeena tai suuremman lausekkeen osana:

lukumuuttujares10: Int = 100
lukumuuttuja + toinenMuuttuja + 1res11: Int = 301

val-muuttujan arvoa ei voi vaihtaa, mutta var-muuttujan voi:

var muutettavissa = 100muutettavissa: Int = 100
muutettavissa = 150muutettavissa: Int = 150
muutettavissa = muutettavissa + 1muutettavissa: Int = 151

Äskeisen kaltaiset sijoitukset, joissa uusi arvo saadaan yksinkertaisella laskutoimituksella saman muuttujan edellisestä arvosta, voi kirjoittaa myös lyhyemmin (luku 4.3). Tässä yhdistetään sijoitus ja aritmeettinen operaattori:

muutettavissa += 10muutettavissa: Int = 161
muutettavissa -= 100muutettavissa: Int = 61
muutettavissa *= 2muutettavissa: Int = 122

Kommentteja

Ohjelmakoodin kirjoitetut kommentit (luku 1.2) eivät vaikuta ohjelman suoritukseen.

// Tämä on yksirivinen kommentti. Se alkaa kahdella kauttaviivalla ja päättyy rivin loppuun.

val muuttuja = 100 // Kommentin voi kirjoittaa sen koodirivin perään, johon se liittyy.

/* Tällainen kommentti, joka alkaa kauttaviivalla
   ja tähdellä, voi olla monirivinenkin.
   Kommentti päättyy samoihin merkkeihin toisin päin. */

Aloitusmerkintää /** käytetään dokumentaatiokommenttien kirjoittamiseen (luku 2.7) :

/** Tämä seuraavan muuttujan kuvaus tulee dokumenttiin. */
val teksti = "Minut on dokumentoitu."

Dokumentaatiokommenttien perusteella voidaan automaattisesti tuottaa Scaladoc-sivuja.

Pakkaukset ja kirjastot

Pakkausten käyttö

Scalan peruskirjastojen (luku 2.7) ja muiden pakkausten työkaluja voi käyttää kirjaamalla pakkauksen nimen käytetyn funktion tai muun työkalun nimen eteen. Tässä käytetään abs-itseisarvofunktiota pakkauksesta scala.math:

scala.math.abs(-50)res12: Int = 50

Itse asiassa yleispakkauksen scala sisältö on aina automaattisesti käytössä, joten saman voi sanoa lyhyemminkin viittaamalla vain alipakkaukseen math:

math.abs(-50)res13: Int = 50

Yleispakkauksesta scala löytyvät mm. tietotyypit Int ja Double, kokoelmatyypit Vector ja List sekä tulostusfunktio println. Näitä työkaluja voi käyttää mainitsematta pakkauksen nimeä lainkaan. Ei siis tarvitse kirjoittaa esimerkiksi scala.Int, vaikka se sallittua onkin.

Usein pakkausten nimien toistuvan kirjoittamisen voi välttää import-käskyllä:

Käyttöönotto import-käskyllä

Pakkauksen sisältämän työkalun käyttöönotto (luku 1.5):

import scala.math.absimport scala.math.abs
abs(-50)res14: Int = 50
abs(100)res15: Int = 100
Alun scala voisi jättää poiskin yllä mainitusta syystä.
Nyt ei tarvita pakkauksen nimeä.

Näin otetaan käyttöön pakkauksen koko sisältö kerralla:

import scala.math._import scala.math._

import-käskyt kirjoitetaan usein kooditiedoston alkuun, jolloin mainitut työkalut ovat käytössä koko tiedoston sisältämässä koodissa. Käskyn voi sijoittaa myös muualle: esimerkiksi import funktion rungon alussa tuo työkalun käyttöön vain kyseiseen funktioon.

Pakkauksen määritteleminen

Omia työkaluja laadittaessa pakkaukset merkitään Scala-kooditiedostojen alkuun tällaisella määrittelyllä (luku 2.7):

package pakkauksen.mahdollisesti.moniosainen.nimi

Tiedostot tallennetaan pakkausten nimiä vastaavaan hakemistorakenteeseen.

Tässä mainitun tavan lisäksi Scalassa voi määritellä pakkauksina toimivia olioita, joista lisää alempana kohdassa Pakkausoliot.

Yleisiä funktioita scala.math-pakkauksesta

Muutama yleishyödyllinen funktio pakkauksesta scala.math:

import scala.math._import scala.math._
val itseisarvo = abs(-50)itseisarvo: Int = 50
val potenssi = pow(10, 3)potenssi: Double = 1000.0
val neliojuuri = sqrt(25)neliojuuri: Double = 5.0
val sini = sin(1)sini: Double = 0.8414709848078965
val kahdestaIsompi = max(2, 10)kahdestaIsompi: Int = 10
val kahdestaPienempi = min(2, 10)kahdestaPienempi: Int = 2

Samasta pakkauksesta löytyvät mm. muut trigonometriset funktiot (cos, atan, jne.), cbrt (kuutiojuuri), hypot (hypotenuusa; parametreiksi kaksi kateetinmittaa), floor (alaspäin pyöristys), ceil (ylöspäin pyöristys), round (lähimpään pyöristys), log ja log10 (logaritmeja). Koko luettelo löytyy pakkauksen dokumentaatiosta.

Osia muiden Scala API:n pakkausten sisällöstä on esitelty tämän sivun muissa kappaleissa aiheen mukaan.

Syötettä ja tulostetta: println, readLine

Tekstikonsoliin tulostaminen onnistuu println-käskyllä:

println(100 + 1)101
println("laama")laama

Alla on esimerkkejä näppäimistösyötteen lukemisesta tekstikonsolissa (luku 2.6). Esimerkeissä oletetaan, että käsky import scala.io.StdIn._ on annettu. Huomaa, että nämä syötteenkäsittelykäskyt eivät toistaiseksi toimi Scala IDE:n sisäänrakennetussa REPLissä.

println("Kirjoita jotain tätä kehotetta seuraavalle riville: ")
val kayttajanSyottamaTeksti = readLine()

Jos kehotteen ja syötteen väliin ei halua rivinvaihtoa, voi käyttää print-käskyä, joka ei vaihda riviä lopuksi:

print("Kirjoita jotain tämän kehotteen perään samalle riville: ")
val kayttajanSyottamaTeksti = readLine()

Sama lyhyemmin:

val kayttajanSyottamaTeksti = readLine("Kirjoita jotain tämän kehotteen perään samalle riville: ")

readLine tuottaa String-tyyppisen arvon. Käyttäjän syötteen voi myös tulkita lukuarvoksi:

val syotettyInt = readInt()
val syotettyDouble = readDouble()

Viimeksi mainitut käskyt käskeytyvät ajonaikaisen virhetilanteeseen, ellei syöte ole luku.

Funktioiden alkeita

Yksinkertainen funktio

Esimerkkifunktio luvusta 1.7:

def keskiarvo(eka: Double, toka: Double) = (eka + toka) / 2
Parametrien tyypit on kirjattava kaksoispisteiden perään.
Muista muutkin välimerkit.
Kun funktion rungon muodostaa vain yksi lauseke, funktion palautusarvo saadaan evaluoimalla tuo lauseke.

Funktion kutsuminen

Funktiokutsu:

keskiarvo(10.0, 12.5)res16: Double = 11.25
Funktiokutsu on lauseke, jonka arvo on funktion palauttama arvo.

Monirivinen funktio

Kun funktion runko koostuu useasta peräkkäisestä käskystä, ovat aaltosulkeet tarpeen. Tässä esimerkki luvusta 1.7:

def verot(tulot: Double, tuloraja: Double, peruspros: Double, lisapros: Double) = {
  val perusosa = min(tuloraja, tulot)
  val lisaosa = max(tulot - tuloraja, 0)
  perusosa * perusprosentti + lisaosa * lisaprosentti
}
Yhtäsuuruusmerkki tällöinkin aina mukaan.
Viimeiseksi evaluoitavan lausekkeen arvo on funktion palautusarvo.

Tilaa muuttavien funktioiden tapauksessa aaltosulkeita on tapana käyttää silloinkin, kun se ei ole pakollista (ks. tyyliopas).

Parametreista

Yllä olevilla funktioilla on yksi parametriluettelo (kaarisulkeissa funktion nimen perässä). Parametriluettelo voi olla tyhjäkin (luku 2.7):

def tulostaVakioteksti() = {
  println("Tämä tulostuu aina, kun kutsu tulostaVakioteksti() suoritetaan.")
}

Funktiolla ei välttämättä ole parametriluetteloa lainkaan. (Tämä on yleistä olioiden yhteydessä; luku 2.2.)

def palautaTeksti = "Funktiokutsu palautaTeksti tuottaa aina tämän merkkijonon."

Parametriluetteloja voi olla useita (luku 7.3):

def kokeilu(eka: Int, toka: String)(lisaparametri: Int) = eka * lisaparametri + toka
kokeilu(10, "laama")(100)res17: String = 1000laama

Palautusarvoista

Kaikissä yllä olevissa esimerkeissä palautusarvon tyyppi on jätetty kirjaamatta koodiin, mikä on sallittua tyyppipäättelyn vuoksi (luku 1.8). Palautusarvon tyypin saa erikseen kirjatakin, kuten näissä esimerkeissä:

def keskiarvo(eka: Double, toka: Double): Double = (eka + toka) / 2

def palautaTeksti: String = "Funktiokutsu palautaTeksti tuottaa aina tämän merkkijonon."

Tietyissä tilanteissa palautusarvon tyyppi on pakko kirjata. Näin on eritoten silloin, jos funktio kutsuu itsensä kanssa samannimistä funktiota eli joko

  • toista samannimistä mutta eriparametrista funktiota (kuormitettaessa; luku 3.5) tai
  • itseään (rekursiossa; luku 11.3).

Arvon palauttaminen return-käskyllä

Arvo on mahdollista (muttei Scalassa tapana) määrätä palautettavaksi myös return-käskyllä (luku 6.1), joka katkaisee funktion suorituksen:

def verot(tulot: Double, tuloraja: Double, peruspros: Double, lisapros: Double): Double = {
  val perusosa = min(tuloraja, tulot)
  val lisaosa = max(tulot - tuloraja, 0)
  return perusosa * peruspros + lisaosa * lisapros
}
return-käskyn perään kirjoitetaan lauseke, jonka arvo palautetaan.
Funktiolle, jossa return-käskyä käytetään, on kirjattava palautusarvon tyyppi.

Yksittäisoliot

Olion määritteleminen: metodit, muuttujat, this

Yksittäisen olion määrittely luvussa 2.2 tarkemmin kuvaillusta esimerkistä:

object tyontekija {
  var nimi = "Matti Mikälienen"
  val syntynyt = 1965
  var kkpalkka = 5000.0
  var tyoaika = 1.0

  def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt

  def kuukausikulut(kulukerroin: Double) = this.kkpalkka * this.tyoaika * kulukerroin

  def korotaPalkkaa(kerroin: Double) = {
    this.kkpalkka = this.kkpalkka * kerroin
  }

  def kuvaus =
    this.nimi + " (s. " + syntynyt + "), palkka " + this.tyoaika + " * " + this.kkpalkka + " euroa"

}
Avainsanan object perään kirjoitetaan oliolle valittu nimi.
Aaltosulkeet ovat tässä pakolliset. Ei yhtäsuuruusmerkkiä kuten funktioiden määrittelyissä.
Olion tietoja tallennettuna muuttujiin. Osa on tässä muuttumattomia (val), osa ei (var).
Olioon liitetyt funktiot eli metodit def-sanalla alkaen.
Olion metodia suoritettaessa this viittaa olioon itseensä. Esimerkiksi tässä lausekkeen this.nimi arvo on olion oman nimi-muuttujan arvo. (this-sana ei kuitenkaan ole aina pakollinen; ks. luku 2.2.)

Yksittäisolion käyttö: pistenotaatio

Olion muuttujien käyttö:

tyontekija.kkpalkkares18: Double = 5000.0
tyontekija.tyoaika = 0.6tyontekija.tyoaika: Double = 0.6

Metodikutsuja:

tyontekija.korotaPalkkaa(1.1)tyontekija.ikaVuonna(2017)res19: Int = 52

Sovelluksen käynnistäminen: käynnistysoliot

Käynnistysolio (luku 2.6) on yksittäisolio, joka toimii sovelluksen käynnistyskohtana:

object Testiohjelma extends App {
  println("Nämä rivit suoritetaan, kun sovellus ajetaan.")
  println("Tässä yksinkertaisessa sovelluksessa ei muuta olekaan kuin nämä tulostuskäskyt.")
  println("Monimutkaisemmassa ohjelmassa täältä kutsuttaisiin muiden ohjelmaan kuuluvien olioiden metodeita.")
}
extends App määrittelee, että kyseessä on käynnistysolio. Tarkemmin sanoen tässä liitetään olioon App-piirre; ks. kohta Piirreluokat alempaa.

Pakkausoliot

Yksittäisolio voi toimia pakkauksena, johon kootaan toisiinsa liittyviä työkaluja kuten funktioita, olioita ja luokkia (luku 4.4). Tällaisen pakkausolion voi määritellä ihan tavalliseksi yksittäisolioksi. Alla on määritelty pakkaus omat.kokeilu:

package omat

object kokeilu {
  def tuplaa(luku: Int) = luku * 2
  def triplaa(luku: Int) = luku * 3
}
Nämä funktiot on määritelty kokeilu-nimisen olion sisään, jota on tarkoitus käyttää pakkausoliona.

Yllä oleva koodi tulee tallentaa omat-nimisessä kansiossa olevaan tiedostoon, jonka nimi voi olla esimerkiksi kokeilu.scala. Oliosta voi nyt ottaa työkaluja käyttöön tavallisella import-käskyllä:

import omat.kokeilu._import omat.kokeilu._
tuplaa(10)res20: Int = 20
triplaa(10)res21: Int = 30

Toinen tapa määritellä pakkausolio

Pakkausolio on mahdollista määritellä myös näin package-avainsanaa kahdesti käyttäen:

package omat

package object kokeilu {
  def tuplaa(luku: Int) = luku * 2
  def triplaa(luku: Int) = luku * 3
}

Näin määriteltynä pakkausolio tulee sijoittaa haluttua pakkauksen nimeä vastaavaan kansioon, tiedostoon nimeltä package.scala, esimerkiksi omat/kokeilu/package.scala.

Luokat (ja lisää olioista)

Luokan määrittely

Tässä luvusta 2.4 esimerkkiluokka, jolla voi kuvata keskenään erilaisia työntekijöitä. Kukin tämän luokan ilmentymä on oma Tyontekija-tyyppinen olionsa, jolla on omat tiedot:

class Tyontekija(annettuNimi: String, annettuSyntymavuosi: Int, annettuPalkka: Double) {

  var nimi = annettuNimi
  val syntynyt = annettuSyntymavuosi
  var kkpalkka = annettuPalkka
  var tyoaika = 1.0

  def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt

  // Jne. Muita metodeita.
}
Avainsana class luokan nimen edessä. Pakolliset aaltosulkeet.
Konstruktoriparametrit: kun tästä luokasta luodaan ilmentymä new-käskyllä, on annettava nimi, syntymävuosi ja palkka.
Konstruktorina eli ilmentymän alustavana ohjelmakoodina toimii aaltosulkeiden sisään kirjoitettu koodi metodien määrittelyjä lukuunottamatta. Tässä alustetaan olion tiedot konstruktoriparametrien arvoilla sekä asetetaan työajaksi konstruktoriparametreista riippumatta aina alkuarvo 1.0.
Metodit määritellään aivan kuin yksittäisolioille. Sana this viittaa luokankin koodissa siihen olioon, jolle metodia kutsutaan. Esimerkiksi tässä lasketaan ikä juuri metodia suorittavan ilmentymän syntynyt-muuttujan arvon perusteella.

Yllä olevan luokkamäärittelyn voi kirjoittaa lyhyemminkin (luku 2.4):

class Tyontekija(var nimi: String, val syntynyt: Int, var kkpalkka: Double) {

  var tyoaika = 1.0

  def ikaVuonna(vuosi: Int) = vuosi - this.syntynyt

  // Jne. Muita metodeita.
}
Tässä on hyödynnetty mahdollisuutta yhdistää ilmentymämuuttujien ja niiden (alku)arvot määräävien konstruktoriparametrien määrittelyt.
Yksi ilmentymämuuttujista ei saa arvoaan suoraan konstruktoriparametrista. Se määritellään erikseen.

Ilmentymien luominen ja käyttö

Yllä kuvattua Tyontekija-luokkaa voi käyttää näin (luku 2.3):

new Tyontekija("Eugenia Enkeli", 1963, 5500)res22: o1.luokkia.Tyontekija = o1.luokkia.Tyontekija@1145e21
Luodaan ilmentymä new-sanalla, jonka perään kirjoitetaan luokan nimi ja sulkeissa arvot konstruktoriparametreille.
Lausekkeen arvo on viittaus uuteen olioon, joka on luokan ilmentymä.

Muuttujaan voi tallentaa viittauksen luotuun olioon. Tällöin oliota voi käyttää helposti muuttujan nimen kautta.

val juuriPalkattu = new Tyontekija("Teija Tonkeli", 1985, 3000)juuriPalkattu: o1.luokkia.Tyontekija = o1.luokkia.Tyontekija@704234
juuriPalkattu.ikaVuonna(2017)res23: Int = 32
println(juuriPalkattu.kuvaus)Teija Tonkeli (s. 1985), palkka 1.0 * 3000.0 euroa

Perustyypit olioina ja operaattorinotaatio

Yleiset perustyypit kuten Int, Double ja String ovat myös luokkia ja niiden toiminnot metodeita (luku 4.4). Esimerkiksi yhteenlaskun voi tehdä pistenotaatiota ja metodia nimeltä + käyttäen kuten tässä:

1.+(1)res24: Int = 2

Tutumpi lauseke 1 + 1 toimii myös, koska yksiparametrista metodia voi kutsua myös operaattorinotaatiolla, jossa piste ja sulut jätetään pois. Sama on mahdollista myös itse laadituille metodeille:

juuriPalkattu ikaVuonna 2017res25: Int = 32

Totuusarvot

Boolean-tyyppi

Totuusarvoja voi kuvata Boolean-tietotyypillä (luku 2.8). Tämän tyyppisiä arvoja on tasan kaksi, true ja false, joita vastaavat Scala-literaalit.

falseres26: Boolean = false
val tamanMuuttujanArvoOnTosi = truetamanMuuttujanArvoOnTosi: Boolean = true

Vertailuoperaattorit

Vertailuoperaattorit tuottavat totuusarvoja (luku 2.8):

10 <= 10res27: Boolean = true
20 < (10 + 10)res28: Boolean = false
val ikavuodet = 20ikavuodet: Int = 20
val onAikuinen = ikavuodet >= 18onAikuinen: Boolean = true
ikavuodet == 30res29: Boolean = false
20 != ikavuodetres30: Boolean = false
Yhtäsuuruutta vertaillessa on käytettävä kahta yhtäsuuruusmerkkiä.
Operaattorilla != vertaillaan erisuuruutta.

Logiikkaoperaattorit

Logiikkaoperaattoreita (luku 3.4):

Operaattori Nimi Esimerkki Vastaa tarpeeseen
&& ja (and) jokuVaite && toinenVaite "Ovatko molemmat totuusarvot true?"
|| tai (or) jokuVaite || toinenVaite "Onko ainakin toinen totuusarvoista true?"
^ poissulkeva tai (exclusive or eli xor) jokuVaite ^ toinenVaite "Onko tasan yksi totuusarvoista true?
! ei tai negaatio (not tai negation) !jokuVaite "Onko totuusarvo false?"

Esimerkkejä:

val jaettava = 50000jaettava: Int = 50000
var jakaja = 100jakaja: Int = 100
!(jakaja == 0)res31: Boolean = true
jakaja != 0 && jaettava / jakaja < 10res32: Boolean = false
jakaja == 0 || jaettava / jakaja >= 10res33: Boolean = true
jaettava / jakaja >= 10 || jakaja == 0res34: Boolean = true

Operaattorit && ja || ovat ehdollisia, eli jos vasemmanpuoleinen lauseke riittää määräämään koko lausekkeen totuusarvon, niin oikeanpuoleista ei evaluoida lainkaan:

jakaja = 0jakaja: Int = 0
jaettava / jakaja >= 10 || jakaja == 0java.lang.ArithmeticException: / by zero
...
jakaja == 0 || jaettava / jakaja >= 10res35: Boolean = true
jakaja != 0 && jaettava / jakaja < 10res36: Boolean = false

Valitseminen if-käskyllä

if-perusteet

if-käsky (luku 2.9) valitsee kahdesta vaihtoehdosta evaluoimalla ehtolausekkeen:

val luku = 100luku: Int = 100
if (luku > 0) luku * 2 else 10res37: Int = 200
if (luku < 0) luku * 2 else 10res38: Int = 10
Ehtolauseketta ympäröivät sulut ovat pakolliset. Ehtona voi olla mikä tahansa Boolean-tyyppinen lauseke.

if-käskyllä muodostettua lauseketta voi käyttää muiden lausekkeiden tapaan esimerkiksi muuttujaan sijoitettaessa tai funktion parametrina:

val valinnanTulos = if (luku > 100) 10 else 20valinnanTulos: Int = 20
println(if (luku > 100) 10 else 20)20

Jos valinnaisessa osassa on peräkkäisiä käskyjä, rivitetään ja käytetään aaltosulkeita (mikä on muutenkin tapana, jos käsky muuttaa tilaa; ks. tyyliopas):

if (luku > 0) {
  println("Luku on positiivinen.")
  println("Tarkemmin sanoen se on: " + luku)
} else {
  println("Kyseessä ei ole positiivinen luku.")
}Luku on positiivinen.
Tarkemmin sanoen se on: 100

Jos riittää, että ehdon ollessa totta suoritetaan tietty toimenpide ja muuten ei tehdä mitään, niin else-osion voi jättää pois:

if (luku != 0) {
  println("Osamäärä on: " + 1000 / luku)
}
println("Loppu.")Osamäärä on: 10
Loppu.
Viimeinen tulostuskäsky ei kuulu if-käskyyn vaan on sen perässä. Tämä koodinpätkä siis tulostaa lopuksi "Loppu." riippumatta siitä, onko luku-muuttujan arvo nolla vai ei. Mikäli olisi ollut, ei tämä koodi muuta tulostaisikaan.

if-käskyjen yhdisteleminen

Yksi tapa valita useasta vaihtoehdosta on kirjoittaa if-käsky toisen if-käskyn else-osioksi:

val luku = 100luku: Int = 100
if (luku < 0) "negatiivinen" else if (luku > 0) "positiivinen" else "nolla"res39: String = positiivinen
if (luku < 0) {
  println("Luku on negatiivinen.")
} else if (luku > 0) {
  println("Luku on positiivinen.")
} else {
  println("Luku on nolla.")
}Luku on positiivinen.

if-käskyt voi muutenkin laittaa sisäkkäin:

if (luku > 0) {
  println("On positiivinen.")
  if (luku > 1000) {
    println("On yli tuhat.")
  } else {
    println("On positiivinen muttei yli tuhat.")
  }
}  On positiivinen.
On positiivinen muttei yli tuhat.
Ulommassa käskyssä ei tässä esimerkissä ole else-osiota ollenkaan. Jos luku ei olisi ollut positiivinen, ei olisi tulostunut mitään.

Äskeisessä esimerkissä else-sana liittyi "lähimpään" if-käskyyn. Tuo else-osa suoritettiin nimenomaan siksi, että ulomman käskyn ehto toteutui mutta sisemmän ei. Seuraavassa esimerkissä, joka on aaltosulutettu toisin, sisemmällä if-käskyllä ei ole else-osiota, mutta ulommalla on:

if (luku > 0) {
  println("On positiivinen.")
  if (luku > 1000) {
    println("On yli tuhat.")
  }
} else {
  println("On nolla tai negatiivinen.")
}On positiivinen.

Näitäkin esimerkkejä on selostettu tarkemmin luvussa 2.9. Tuon luvun lopussa on myös esimerkkejä virhetilanteista, joita voi syntyä, kun käyttää if-käskyä funktion palautusarvon määrittämiseen.

Olemattomia arvoja

Some ja None

Tämä esimerkkifunktion palautusarvo on tyyppiä Option[Int] (luku 3.3). Funktio palauttaa joko jakolaskun lopputuloksen käärittynä Some-olioon tai None, jos osamäärää ei voi määrittää:

def osamaara(jaettava: Int, jakaja: Int) = if (jakaja == 0) None else Some(jaettava / jakaja)
osamaara(100, 5)res40: Option[Int] = Some(20)
osamaara(100, 0)res41: Option[Int] = None

Tässä Option-oliota käytetään merkkijonotyypin String kanssa:

var testi: Option[String] = Nonetesti: Option[String] = None
testi = Some("melkein kaikki ovat jo somessa")testi: Option[String] = Some(melkein kaikki ovat jo somessa)
Option[String]-tyyppinen muuttuja voi viitata joko None-yksittäisolioon — jolloin merkkijonoa ei ole — tai Some-olioon, jonka sisään on kääritty String-arvo.
Hakasulkuihin kirjoitetaan tyyppiparametri eli kääreen sisällä mahdollisesti olevan arvon tyyppi.
Jos tyyppimäärittely jätettäisiin tästä pois, ei sijoituskäskystä voisi päätellä, millainen Some-arvo testi-muuttujaan voitaisiin sijoittaa.

Option-tyypin sijaan on mahdollista käyttää null-arvoa, mutta se ei ole useimmissa Scala-ohjelmissa kannatettavaa (luku 3.3).

Option-olioiden metodeita

Metodeilla isDefined ja isEmpty voi tutkia, onko kääre tyhjä:

val kaarittyLuku = Some(100)kaarittyLuku: Option[Int] = Some(100)
kaarittyLuku.isDefinedres42: Boolean = true
kaarittyLuku.isEmptyres43: Boolean = false
None.isDefinedres44: Boolean = false
None.isEmptyres45: Boolean = true

Metodi getOrElse palauttaa arvon kääreen sisältä. Sille annetaan parametrilauseke, joka määrää, mitä metodi palauttaa kääreen ollessa tyhjä:

kaarittyLuku.getOrElse(12345)res46: Int = 100
None.getOrElse(12345)res47: Int = 12345

Samantapainen metodi orElse palauttaa Option-olion itsensä, jos kyseessä on Some, tai sille annetun parametrilausekkeen arvon, jos kyseessä on None. Se siis eroaa getOrElsestä siten, että se ei pura käärettä:

kaarittyLuku.orElse(Some(54321))res48: Option[Int] = Some(100)
None.getOrElse(Some(54321))res49: Option[Int] = Some(54321)

Metodi get yksinkertaisesti avaa kääreen ja palauttaa sisällön:

val luku = kaarittyLuku.getluku: Int = 100

Viimeksi mainitun metodin käyttöä kannattaa kuitenkin usein välttää (luku 3.3), koska se aiheuttaa ajonaikaisen virhetilanteen, mikäli kääre oli tyhjä eli None. Vaihtoehtoja get-metodille ja muutenkin Option-tyypistä lisää on kerrottu alempana kohdassa Option kokoelmatyyppinä.

Käyttöalue ja näkyvyysmääreet

Ohjelman osien — muuttujien, funktioiden, luokkien tai yksittäisolioiden — sallittu käyttöalue eli skooppi (luku 5.4) määräytyy sen mukaan, missä tuo osa on määritelty. Lisäksi käyttöalueeseen voi vaikuttaa näkyvyysmääreillä kuten private (luku 2.7).

Luokan ja sen osien käyttöalue

class Esimerkki(konstruktoriparametri: Int) {

  val julkinenIlmentymamuuttuja = konstruktoriparametri * 2
  private val yksityinenIlmentymamuuttuja = konstruktoriparametri * 3

  def julkinenMetodi(parametri: Int) = parametri * this.yksityinenMetodi(parametri)

  private def yksityinenMetodi(parametri: Int) = parametri + 1 + this.yksityinenIlmentymamuuttuja

}
Julkisen ilmentymämuuttujan käyttöalueeseen sisältyy koko luokka. Lisäksi siihen voi viitata luokan ulkopuolelta: olio.korkeus. Samoin julkista metodia voi käyttää mistä tahansa päin ohjelmaa. Ilmentymämuuttuja tai metodi on julkinen ellei toisin määritellä.
Yksityisen ilmentymämuuttujan ja yksityisen metodin käyttöalue on koko kyseinen luokka.
Tämä luokka itse on julkinen, joten sitä voi käyttää muualta ohjelmasta vapaasti.
Funktioiden rungot ovat aina yksityisiä. Niiden sisässä oleviin määrittelyihin ei pääse käsiksi mistään kyseisen funktion ulkopuolelta.

Paikallisten muuttujien käyttöalue

Kun siirrät hiiren kursorin laatikoiden päälle, korostuvat mainittujen muuttujien käyttöalueet.

def funktio(parametri: Int) = {
  var paikallinen = parametri + 1
  var toinenPaikallinen = paikallinen * 2
  if (paikallinen > toinenPaikallinen) {
    val vainIffissa = toinenPaikallinen
    toinenPaikallinen = paikallinen
    paikallinen = vainIffissa
  }
  toinenPaikallinen - paikallinen
}
Parametrimuuttuja kuten parametri on määritelty koko kyseisen funktion ohjelmakoodissa. Sitä voi käyttää sieltä mistä vain.
Metodin koodissa uloimmalla tasolla määritelty muuttuja, kuten paikallinen, on käytettävissä määrittelykohdasta alkaen metodin koodin loppuun.
Samoin toinenPaikallinen.
Kun ulompi käsky sisältää muuttujamäärittelyn, niin määritelty muuttuja on käytettävissä vain kyseisen ulomman käskyn sisällä. Esimerkiksi tässä muuttuja vainIffissa on määritelty vain if-käskyn sisällä.

Mutkikkaampia esimerkkejä luvusta 5.4.

Sisäkkäiset funktiot, oliot ja luokat

Yllä metodin sisällä oli paikallisia muuttujia. Funktioita, olioita ja luokkia voi muutenkin laittaa sisäkkäin. Muutama esimerkki löytyy kurssin loppupuolelta, mm. luvusta 11.3.

Kumppanioliot

Poikkeuksena yleisiin sääntöihin luokka ja sen kumppaniolio pääsevät käsiksi toistensa yksityisiin osiin. Tässä tiivistelmä luvun 4.5 esimerkistä:

object Asiakas {
  private var montakoLuotu = 0
}

class Asiakas(val nimi: String) {
  Asiakas.montakoLuotu += 1
  val numero = Asiakas.montakoLuotu

  override def toString = "#" + this.numero + " " + nimi
}
Kumppaniolio on yksittäisolio, jolle annetaan prikulleen sama nimi kuin luokalle itselleen ja jonka määrittely kirjoitetaan samaan tiedostoon.
Kumppaniolioon voidaan kirjata luokkaan yleisellä tasolla liittyviä tietoja (kuten tässä ilmentymälaskuri) tai metodeita. Muuttujasta montakoLuotu on muistissa vain yksi kopio, koska kumppanioliotakin on vain yksi. Vrt. asiakasolioiden nimet ja numerot, joita on yksi per asiakasolio.
Asiakas-luokka ja sen kumppaniolio ovat "kavereita", joilla ei ole salaisuuksia. Ne pääsevät poikkeuksellisesti käsiksi myös toistensa yksityisiin tietoihin.

Parit ja muut monikot

Monikko on tilaltaan muuttumaton rakenne, joka muodostuu kahdesta tai useammasta keskenään mahdollisesti eri tyyppisestä arvosta (luku 9.1). Monikon voi määritellä käyttäen sulkuja ja pilkkuja:

val nelikko = ("Tässä monikossa on neljä erilaista jäsentä.", 100, 3.14159, false)nelikko: (String, Int, Double, Boolean) = (Tässä monikossa on neljä erilaista jäsentä.,100,3.14159,false)

Monikon jäsenet on numeroitu ykkösestä(!) alkaen, ja niihin pääsee käsiksi alaviivaa hyödyntäen:

nelikko._1res50: String = Tässä monikossa on neljä erilaista jäsentä.
nelikko._3res51: Double = 3.14159

Pari on yleinen erikoistapaus monikosta. Tässä parissa molemmat jäsenet ovat merkkijonoja:

val pari = ("laama", "llama")pari: (String, String) = (laama,llama)

Monikon osat voi sijoittaa useaan muuttujaan kerralla:

val (suomeksi, englanniksi) = parisuomeksi: String = laama
englanniksi: String = llama

Parin voi määritellä sulkumerkinnän sijaan myös näin:

val samanlainen = "laama" -> "llama"samanlainen: (String, String) = (laama,llama)

Viimeksi mainittua merkintätapaa käytetään erityisesti hakurakenteiden yhteydessä, kun parit toimivat avain–arvo-pareina; ks. kohta Hakurakenteet (Map).

Lisää merkkijonoista

Merkkijonojen metodeita

Tässä kappaleesta on esimerkkejä eräistä merkkijonojen metodeista (luvut 2.8 ja 4.4). Katso myös yltä johdantokohta Merkkejä ja alta kokoelmien (jollaisia merkkijonotkin ovat) ominaisuuksia yleisemmin esittelevät kohdat Kokoelmien alkeita ja Kokoelmien käsittely korkeamman asteen metodeilla.

Merkkijonon pituuden eli koon voi selvittää kummalla vain seuraavista tavoista:

val jono = "Olavi Eerikinpoika Stålarm"jono: String = Olavi Eerikinpoika Stålarm
jono.lengthres52: Int = 26
jono.sizeres53: Int = 26

Kirjainkokojen muokkausta:

"five hours of Coding can save 15 minutes of Planning".toUpperCaseres54: String = FIVE HOURS OF CODING CAN SAVE 15 MINUTES OF PLANNING
"five hours of Coding can save 15 minutes of Planning".toLowerCaseres55: String = five hours of coding can save 15 minutes of planning
"five hours of Coding can save 15 minutes of Planning".capitalizeres56: String = Five hours of Coding can save 15 minutes of Planning

Osamerkkijono:

"Olavi Eerikinpoika Stålarm".substring(6, 11)res57: String = Eerik
"Olavi Eerikinpoika Stålarm".substring(3)res58: String = vi Eerikinpoika Stålarm

Merkkijonon jakaminen osiin:

"Olavi Eerikinpoika Stålarm".split(" ")res59: Array[String] = Array(Olavi, Eerikinpoika, Stålarm)
"Olavi Eerikinpoika Stålarm".split("la")res60: Array[String] = Array(O, vi Eerikinpoika Stå, rm)

Merkkijonon reunoilla olevan tyhjän poisto:

val teksti = "   tyhjät merkit poistuvat    ympäriltä mutteivät keskeltä  "teksti: String = "   tyhjät merkit poistuvat    ympäriltä mutteivät keskeltä  "
teksti.trimres61: String = tyhjät merkit poistuvat    ympäriltä mutteivät keskeltä

Merkkijonon sisältämien numeromerkkien tulkitseminen luvuksi:

"100".toIntres62: Int = 100
"100".toDoubleres63: Double = 100.0
"100.99".toDoubleres64: Double = 100.99
"sata".toIntjava.lang.NumberFormatException: For input string: "sata"
...
" 100".toIntjava.lang.NumberFormatException: For input string: " 100"
...
" 100".trim.toIntres65: Int = 100

Vertailua Unicode-aakkoston mukaan:

"abc" < "bcd"res66: Boolean = true
"abc" >= "bcd"res67: Boolean = false
"abc".compare("bcd")res68: Int = -1
"bcd".compare("abc")res69: Int = 1
"abc".compare("abc")res70: Int = 0
"abc".compare("ABC")res71: Int = 32
"abc".compareToIgnoreCase("ABC")res72: Int = 0
Palautusarvon etumerkki kertoo vertailun tuloksen.

Arvojen upottaminen merkkijonoon

Lausekkeiden arvoja voi upottaa merkkijonoon (luku 4.4).

val luku = 10luku: Int = 10
val tavallisestiMuodostettuJono = "Muuttujassa on " + luku + ", ja sitä yhtä suurempi luku on " + (luku + 1) + "."tavallisestiMuodostettuJono: String = Muuttujassa on 10, ja sitä yhtä suurempi luku on 11.
val samaUpottamalla = s"Lukumuuttujan arvo on $luku, ja sitä yhtä suurempi luku on ${luku + 1}."samaUpottamalla: String = Muuttujassa on 10, ja sitä yhtä suurempi luku on 11.
Alkuun s-kirjain.
Dollarimerkin perään voi kirjoittaa muuttujan nimen. Muuttujan arvo upotetaan merkkijonoon.
Lauseke rajataan tarvittaessa aaltosulkeilla.

Erikoismerkit merkkijonoissa

Erikoismerkkejä voi kirjoittaa merkkijonoon kenoviivan avulla (luku 4.4):

val rivinvaihto = "\n"rivinvaihto: String =
"
"
println("eka rivi\ntoka rivi")eka rivi
toka rivi
val sarkainEliTabulaattori = "eka\ttoka\tkolmas"sarkainEliTabulaattori: String = eka   toka    kolmas
"tässä lainausmerkki \" ja toinenkin \""res73: String = tässä lainausmerkki " ja toinenkin "
"tässä kenoviiva \\ ja toinenkin \\"res74: String = tässä kenoviiva \ ja toinenkin \

Merkkijonoliteraaliin, joka on rajattu kummastakin päästä kolmella lainausmerkillä yhden sijaan, voi kirjoittaa erikoismerkkejä sellaisenaan:

"""Tässä merkkijonossa on lainausmerkki " ja
kenoviiva \ kahdella eri rivillä."""res75: String =
Tässä merkkijonossa on lainausmerkki " ja
kenoviiva \ kahdella eri rivillä.

toString-metodi

Kaikilla Scala-olioilla on toString-niminen parametriton metodi, joka palauttaa merkkijonokuvauksen oliosta:

100.toStringres76: String = 100
false.toStringres77: String = false

toString-metodi on myös olioilla, jotka ovat sovellusohjelmoijan itse määrittelemää tyyppiä (koska tuo metodi periytyy niille; ks. Periytyminen):

class Kokeilu(val muuttuja: Int)defined class Kokeilu
val kokeiluolio = new Kokeilu(10)kokeiluolio: Kokeilu = Kokeilu@56181
kokeiluolio.toStringres78: String = Kokeilu@56181
kokeiluoliores79: Kokeilu = Kokeilu@56181
Oletusarvoinen toString-metodi tuottaa tämän näköisen merkkijonon (luku 3.1).
REPL käyttää juuri toString-metodia kuvatakseen olioita. Yllä siis kutsuttiin toString-metodia yhteensä kolmeen kertaan.

Oletusarvoisen toString-metodin voi korvata (luku 3.1 ja ks. Periytyminen):

class Testi(val muuttuja: Int) {
  override def toString = "OLIOLLA ON ARVO " + this.muuttuja
}defined class Testi
val testiolio = new Testi(11)testiolio: Testi = OLIOLLA ON ARVO 11

toString-metodi tulee kutsutuksi ilman erillistä käskyä, kun olio määrätään tulostettavaksi tai yhdistetään merkkijonoon:

println(testiolio)OLIOLLA ON ARVO 11
testiolio + "!!!"res80: String = OLIOLLA ON ARVO 11!!!
s"Testiolion toString-palautusarvo upotetaan tähän väliin $testiolio ja täältä jatkuu."res81: String = Testiolion toString-palautusarvo upotetaan tähän väliin OLIOLLA ON ARVO 11 ja täältä jatkuu.

Kokoelmien alkeita

Puskurien peruskäyttöä

Puskurit ovat eräänlaisia alkiokokoelmia (luvut 1.5 ja 4.4). Puskureita kuvaava tyyppi Buffer löytyy pakkauksesta scala.collection.mutable:

import scala.collection.mutable.Bufferimport scala.collection.mutable.Buffer

Puskurien luominen:

Buffer("eka", "toka", "kolmas", "vielä neljäskin")res82: Buffer[String] = ArrayBuffer(eka, toka, kolmas, vielä neljäskin)
val lukuja = Buffer(12, 2, 4, 7, 4, 4, 10, 3)lukuja: Buffer[Int] = ArrayBuffer(12, 2, 4, 7, 4, 4, 10, 3)
Vaikka puskurit ovat olioita, new-sanaa ei käytetä puskurioliota luodessa Buffer-sanan edessä (koska luominen tapahtuu tehdasmetodilla; luku 4.5).

Puskuri voi olla aluksi tyhjä:

val tanneVoiLisataLukuja = Buffer[Double]()tanneVoiLisataLukuja: Buffer[Double] = ArrayBuffer()
Tyyppiparametrilla (luku 1.5) voi kirjata, millaisia alkioita puskuriin varastoidaan. Tämä on erityisen tarpeellista silloin, kun haluttua alkioiden tyyppiä ei voi päätellä, kuten tässä tyhjää puskuria luodessa.

Puskurissa on nolla tai useampia alkioita järjestyksessä, kukin omalla indeksillään. Indeksit alkavat nollasta, eivät ykkösestä. Yksittäisen alkion voi katsoa näin:

lukuja(0)res83: Int = 12
lukuja(3)res84: Int = 7

Nämä ovat itse asiassa lyhennysmerkintöjä, jotka vastaavat näitä puskuriolion apply-metodin kutsuja (luku 4.5):

lukuja.apply(0)res85: Int = 12
lukuja.apply(3)res86: Int = 7

Alkion voi poimia myös lift-metodilla, joka palauttaa tuloksen Option-tyyppisenä eikä kaadu ajonaikaiseen virheeseen indeksin ollessa epäkelpo:

lukuja(10000)java.lang.IndexOutOfBoundsException: 10000
...
lukuja.lift(10000)res87: Option[Int] = None
lukuja.lift(-1)res88: Option[Int] = None
lukuja.lift(3)res89: Option[Int] = Some(7)

Puskurin alkion voi vaihtaa toiseen:

lukuja(3) = 1val neljasAlkioOnNyt = lukuja(3)neljasAlkioOnNyt: Int = 1

Operaattorilla += voi lisätä uuden alkion puskurin loppuun, mikä kasvattaa puskurin kokoa:

lukuja += 11res90: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11)
lukuja += -50res91: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11, -50)

Operaattorilla -= voi poistaa yhden alkion:

lukuja -= 4res92: Buffer[Int] = ArrayBuffer(12, 2, 1, 4, 4, 10, 3, 11, -50)
lukuja -= 4res93: Buffer[Int] = ArrayBuffer(12, 2, 1, 4, 10, 3, 11, -50)

Alkioita voi lisätä ja poistaa myös esimerkiksi näillä metodikutsuilla:

lukuja.append(100)lukuja.prepend(1000)lukujares94: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 10, 3, 11, -50, 100)
lukuja.insert(5, 50000)lukujares95: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 50000, 10, 3, 11, -50, 100)
val poistettuKolmasAlkio = lukuja.remove(3)poistettuKolmasAlkio: Int = 1
lukujares96: Buffer[Int] = ArrayBuffer(1000, 12, 2, 4, 50000, 10, 3, 11, -50, 100)

Kokoelmia voi myös laittaa sisäkkäin siten, että ulomman kokoelman alkioina on viittauksia toisiin kokoelmiin. Tätä esittelee mm. luku 5.6.

Kokoelmatyyppejä: puskurit, vektorit, taulukot

Puskurit (Buffer), vektorit (Vector) ja taulukot (Array) ovat kokoelmia, joissa on alkioita tietyssä järjestyksessä, kukin omalla indeksillään. Tässä niiden eroja (luku 5.6):

Puskurit Vektorit Taulukot
Puskuri on aluksi tietyn kokoinen, mutta koko voi muuttua joustavasti. Vektori on vakiokokoinen. Koko määritetään vektoria luotaessa eikä koskaan muutu. Taulukko on vakiokokoinen kuten vektorikin.
Puskuri tarjoaa mahdollisuuden lisätä alkioita (loppuun, alkuun, keskelle), poistaa niitä ja korvata vanhoja alkioita uusilla. Vektoria ei voi muuttaa lainkaan sen luomisen jälkeen. (Voidaan kuitenkin luoda uusia vektoreita muunnelmiksi vanhasta.) Taulukko tarjoaa ainoastaan mahdollisuuden korvata vanhoja alkioita uusilla.

Valintaan kokoelmatyyppien välillä vaikuttavat mm. ohjelmointiparadigma (luku 7.5) ja laadulliset seikat kuten luettavuus ja tehokkuus.

Vektoreita ja taulukoita käytetään pitkälti samaan tapaan kuin puskureita yllä olevissa esimerkeissä. Kuitenkaan siis vektoreita ei voi muuttaa ja taulukoita vain vakiokoon puitteissa. Sekä vektorit että taulukot ovat Scalassa käytettävissä ilman import-käskyä.

Tässä pari esimerkkiä:

val vektori = Vector(12, 2, 4, 7, 4, 4, 10, 3)vektori: Buffer[Int] = ArrayBuffer(12, 2, 4, 7, 4, 4, 10, 3)
vektori(6)res97: Int = 10
vektori.lift(10000)res98: Option[Int] = None

val taulukko = Array("eka, "toka", "kolmas")taulukko: Array[String] = Array(eka, toka, kolmas)
taulukko(1)res99: String = toka
taulukko.lift(1)res100: Option[String] = Some(toka)
taulukko(2) = "vika"taulukkores101: Array[String] = Array(eka, toka, vika)

Yllä mainittujen lisäksi indekseihin perustuvia kokoelmatyyppejä ovat mm. merkkijonot ja Range-lukuvälit, joista on esimerkkejä alla, sekä listat (List). Indeksittömiä kokoelmia ovat mm. Hakurakenteet (Map), joista on oma osionsa jäljempänä tällä sivulla, sekä joukot (Set). Scalan virallisessa dokumentaatiossa on tiivis yleiskatsaus tarjolla oleviin kokoelmaluokkiin.

Merkkijonot kokoelmina

Merkkijono on kokoelma, jossa on alkioina Char-arvoja:

val jono = "laama"jono: String = laama
jono(3)res102: Char = m
jono.lift(3)res103: Option[Char] = Some(m)

Tavalliset String-tyyppiset merkkijonot ovat muuttumattomia, ja esimerkiksi niiden yhdisteleminen tuottaa uusia merkkijonoja eikä muokkaa alkuperäisiä. (Muuttuvatilaisestikin merkkijonoja on mahdollista kuvata; luku 7.5.)

Lukuvälit: Range

Range-oliot ovat muuttumattomia kokoelmia, jotka kuvaavat lukuja tietyltä väliltä:

val kouluarvosanat = Range(4, 11)kouluarvosanat: Range = Range(4, 5, 6, 7, 8, 9, 10)
kouluarvosanat(0)res104: Int = 4
kouluarvosanat(2)res105: Int = 6
Annettu alkukohta sisältyy väliin mutta loppukohta ei.

Range-olion voi luoda myös käyttämällä Int-olioiden to- tai until-metodia (luku 4.4):

val samaKuinEdella = 4 until 11samaKuinEdella: Range = Range(4, 5, 6, 7, 8, 9, 10)
val samaTamakin = 4 to 10samaTamakin: Range = Range(4, 5, 6, 7, 8, 9, 10)

Osan välille sijoittuvista luvuista voi ohittaa:

val jokaToinen = 1 to 10 by 2jokaToinen: Range = Range(1, 3, 5, 7, 9)
val jokaKolmas = 1 to 10 by 3jokaKolmas: Range = Range(1, 4, 7, 10)

Yleisiä kokoelmien metodeita

Tämä osio täydentää yllä olevaa johdantoa kokoelmiin. Alla on lyhyitä esimerkkejä eräistä yleiskäyttöisistä kokoelmien metodeista. Kaikki tässä kappaleessa esitellyt ovat ensimmäisen asteen metodeita; lisää tehokkaita työkaluja löytyy kohdasta Kokoelmien käsittely korkeamman asteen metodeilla.

Tämän osion esimerkeissä käytetään kokoelmina merkkijonoja ja vektoreita. Kuitenkin kaikki esitellyt metodit on määritelty myös puskureille, taulukoille, listoille ja usealle muulle kokoelmatyypille, osin myös indeksittömille kokoelmille kuten hakurakenteille.

Kokoelman koko: size, length, isEmpty ja nonEmpty

Kokoelman koon tutkiminen (luku 4.4):

Vector(10, 100, 100, -20).sizeres106: Int = 4
Vector().sizeres107: Int = 0
Vector(10, 100, 100, -20).isEmptyres108: Boolean = false
Vector(10, 100, 100, -20).nonEmptyres109: Boolean = true
Vector().isEmptyres110: Boolean = true
Vector().nonEmptyres111: Boolean = false
"laama".isEmptyres112: Boolean = false
"".isEmptyres113: Boolean = true

Alkion etsiminen: contains ja indexOf

Löytyykö alkio kokoelmasta ja miltä indeksiltä (luku 4.4)?

val onkoKokoelmassaAlkioM = "laamamaa".contains('m')onkoKokoelmassaAlkioM: Boolean = true
val onkoKokoelmassaAlkioZ = "laamamaa".contains('z')onkoKokoelmassaAlkioZ: Boolean = false
val ekanAlkionAIndeksi = "laamamaa".indexOf('a')ekanAlkionAIndeksi: Int = 1
val vastaavaVektorille = Vector(10, 100, 100, -20).indexOf(-20)vastaavaVektorille: Int = 3
val negatiivinenKunEiLoydy = "laamamaa".indexOf('z')negatiivinenKunEiLoydy: Int = -1
val etsitaanAlkaenIndeksista3 = "laamamaa".indexOf('a', 3)etsitaanAlkaenIndeksista3: Int = 4
val etsitaanLopustaAlkuun = "laamamaa".lastIndexOf('a')etsitaanLopustaAlkuun: Int = 7

Alkioita alusta, lopusta ja keskeltä: head, tail, take, drop, slice ym.

Alkioiden poimiminen kokoelman alkupäästä (luvut 4.4 ja 5.5):

val ekaAlkio = "kruuna".headekaAlkio: Char = k
val ekaaEiOleJotenEiOnnistu = "".headjava.util.NoSuchElementException: next on empty iterator
...
val ekaKaarittyna = "kruuna".headOptionekaKaarittyna: Option[Char] = Some(k)
val puuttuvaEka = "".headOptionpuuttuvaEka: Option[Char] = None
val ekatKolmeAlkiota = "kruuna".take(3)ekatKolmeAlkiota: String = kru
val liianIsoEiHaittaa = "kruuna".take(1000)liianIsoEiHaittaa: String = kruuna
val kaikkiPaitsiVika = "kruuna".initkaikkiPaitsiVika: String = kruun
val kaikkiPaitsiKaksiLopusta = "kruuna".dropRight(2)kaikkiPaitsiKaksiLopusta: String = kruu
val toimiiEriKokoelmille = Vector(10, 100, 100, -20).dropRight(2)toimiiEriKokoelmille: Vector[Int] = Vector(10, 100)

Mikään äskeisistä metodeista ei muuta alkuperäistä kokoelmaa, vaan ne muodostavat uuden kokoelman, jossa on osa alkuperäisen alkioista. Sama pätee loppupäästä poimiviin käskyihin:

val kaikkiPaitsiEka = "klaava".tailkaikkiPaitsiEka: String = laava
val kaikkiPaitsiEkatKolme = "klaava".drop(3)kaikkiPaitsiEkatKolme: String = ava
val vainVika = "klaava".lastvainVika: Char = a
val vikaKaarittyna = "klaava".lastOptionvikaKaarittyna: Option[Char] = Some(a)
val lopustaKaksi = "klaava".takeRight(2)lopustaKaksi: String = va

Jakaminen kahteen osaan splitAt-metodilla (luku 9.1):

val teksti = "kruuna/klaava"teksti: String = kruuna/klaava
val pariJossaAlkuJaLoppu = teksti.splitAt(6)pariJossaAlkuJaLoppu: (String, String) = (kruuna,/klaava)
val samaMonimutkaisemmin = (teksti.take(6), teksti.drop(6))samaMonimutkaisemmin: (String, String) = (kruuna,/klaava)

Pätkä keskeltä slice-metodilla:

Vector("eka/0", "toka/1", "kolmas/2", "neljäs/3", "viides/4").slice(1, 4)res114: Vector[String] = Vector(toka/1, kolmas/2, neljäs/3)
Alkuindeksin alkio tulee mukaan, loppuindeksin ei.

Alkioiden lisääminen ja kokoelmien yhdistäminen

Uuden kokoelman muodostaminen lisäämällä alkioita:

val lukuja = Vector(10, 20, 100, 10, 50, 20)lukuja: Vector[Int] = Vector(10, 20, 100, 10, 50, 20)
val yksiAlkioLoppuun = lukuja :+ 999999yksiAlkioLoppuun: Vector[Int] = Vector(10, 20, 100, 10, 50, 20, 999999)
val yksiAlkioAlkuun = 999999 +: lukujayksiAlkioAlkuun: Vector[Int] = Vector(999999, 10, 20, 100, 10, 50, 20)
val kokoelmienYhdistelma = lukuja ++ Vector(999, 998, 997)kokoelmienYhdistelma: Vector[Int] = Vector(10, 20, 100, 10, 50, 20, 999, 998, 997)

Tällaiset toiminnot, jotka muodostavat uusia kokoelmia, ovat sallittuja myös tilaltaan muuttumattomille kokoelmille. Olemassa olevan kokoelman muokkaamisesta on esimerkkejä ylempänä kohdassa Puskurien peruskäyttöä.

Alkiot uuteen kokoelmaan: toVector, toArray, jne.

Kokoelmatyyppiä voi vaihtaa kopioimalla olemassa olevan kokoelman sisällön uuteen (luku 5.5):

val vektori = "laama".toVectorvektori: Vector[Char] = Vector(l, a, a, m, a)
val puskuri = vektori.toBufferpuskuri: Buffer[Char] = ArrayBuffer(l, a, a, m, a)
val taulukko = puskuri.toArraytaulukko: Array[Char] = Array(l, a, a, m, a)
val taasVektori = taulukko.to[Vector]taasVektori: Vector[Char] = Vector(l, a, a, m, a)
Monelle kokoelmatyypille on valmis metodi: toVector, toBuffer jne.
Yleiskäyttöiselle metodille to voi kertoa tyyppiparametrilla, millaisen kokoelman haluaa luoda.

Muita metodeita: mkString, indices, zip, reverse, flatten ym.

Kokoelman sisällön muotoileminen merkkijonoksi järjestyy usein kätevimmin mkString-metodilla (luku 4.4):

val vektori = Vector(100, 20, 30)vektori: Vector[Int] = Vector(100, 20, 30)
println(vektori.toString)Vector(100, 20, 30)
println(vektori)Vector(100, 20, 30)
println(vektori.mkString)1002030
println(vektori.mkString("---"))100---20---30

Kokoelman kaikki indeksit Range-tyyppisenä kokoelmana (luku 5.4):

"laama".indicesres115: Range = Range(0, 1, 2, 3, 4)
Vector(100, 20, 30).indicesres116: Range = Range(0, 1, 2)

Kahden kokoelman yhdistäminen pareja sisältäväksi kokoelmaksi (luku 9.1):

val lajit = Vector("laama", "alpakka", "vikunja")lajit: Vector[String] = Vector(laama, alpakka, vikunja)
val korkeudet = Vector(180, 80, 60)korkeudet: Vector[Int] = Vector(180, 80, 60)
val korkeudetJaLajit = korkeudet.zip(lajit)korkeudetJaLajit: Vector[(Int, String)] = Vector((180,laama), (80,alpakka), (60,vikunja))
val kolmePariaKoskaKorkeuksiaVainKolme = korkeudet.zip(Vector("laama", "alpakka", "vikunja", "guanako"))kolmePariaKoskaKorkeuksiaVainKolme: Vector[(Int, String)] = Vector((180,laama), (80,alpakka), (60,vikunja))
val parivektoriKokoelmapariksi = korkeudetJaLajit.unzipparivektoriKokoelmapariksi: (Vector[Int], Vector[String]) = (Vector(180, 80, 60), Vector(laama, alpakka, vikunja))
val lajitJaIndeksit = lajit.zip(lajit.indices)lajitJaIndeksit: Vector[(String, Int)] = Vector((laama,0), (alpakka,1), (vikunja,2))
val sama = elaimet.zipWithIndexsama: Vector[(String, Int)] = Vector((laama,0), (alpakka,1), (vikunja,2))

Käänteisen kokoelman muodostaminen reverse-metodilla (luvut 4.4 ja 5.5):

"laama".reverseres117: String = amaal
Vector(10, 20, 15).reverseres118: Vector[Int] = Vector(15, 20, 10)

Sisäkkäisen kokoelman "litistäminen" flatten-metodilla (luku 5.6):

val kaksiulotteinenVektori = Vector(Vector(1, 2), Vector(100, 200), Vector(2000, 1000))kaksiulotteinenVektori: Vector[Vector[Int]] = Vector(Vector(1, 2), Vector(100, 200), Vector(2000, 1000))
val yksiulotteinen = kaksiulotteinenVektori.flattenyksiulotteinen: Vector[Int] = Vector(1, 2, 100, 200, 2000, 1000)

Scala API -dokumentaatiossa on kuvattu paljon muitakin kokoelmien metodeita, kuten sum, product, grouped, sliding, transpose jne.

Erilaisille kokoelmatyypeille on mainittujen metodien lisäksi yhteistä myös se, että niitä voi käsitellä silmukoilla, mistä on kerrottu heti alla, ja se, että niillä on käteviä korkeamman asteen metodeita, mitä on kuvattu kohdassa Kokoelmien käsittely korkeamman asteen metodeilla alempana.

Toistaminen silmukoilla

for-silmukka

for-silmukalla voi toistaa toimenpiteen kullekin kokoelman alkiolle (luku 5.3):

val puskuri = Buffer(100, 20, 5, 50)puskuri: Buffer[Int] = Buffer(100, 20, 5, 50)
for (alkio <- puskuri) {
  println("Nyt käsiteltävä alkio: " + alkio)
  println("Sitä yhtä suurempi luku: " + (alkio + 1))
}Nyt käsiteltävä alkio: 100
Sitä yhtä suurempi: 101
Nyt käsiteltävä alkio: 20
Sitä yhtä suurempi: 21
Nyt käsiteltävä alkio: 5
Sitä yhtä suurempi: 6
Nyt käsiteltävä alkio: 50
Sitä yhtä suurempi: 51
Silmukan runko suoritetaan kullekin alkiolle vuoron perään.
Sulkeet for-avainsanan perässä ovat pakolliset.
Vasemmalle osoittavan nuolen <- oikealla puolella on lauseke, joka kertoo, mistä arvoja noukitaan vuoron perään käsiteltäviksi.
Nuolen vasemmalle puolelle kirjoitetaan muuttujan nimi. Tämän niminen muuttuja on käytettävissä silmukan rungossa ja sisältää aina parhaillaan käsiteltävän arvon (tässä: vuorossa olevan alkion puskurista).
Aaltosulkeita on käytettävä, jos silmukan runkoon sisältyy useita peräkkäisiä käskyjä. Lisäksi niitä on tapana käyttää (ks. tyyliopas), jos silmukka muuttaa ohjelman tilaa (kuten Ohjelmointi 1 -kurssilla jotakuinkin aina on asian laita).

Silmukan rungossa voi yhdistellä erilaisia käskyjä. Esimerkiksi if-valintakäskyä voi käyttää:

for (alkio <- puskuri) {
  if (alkio > 10) {
    println("Tämä alkio on kymppiä isompi: " + alkio)
  } else {
    println("Tässä kohdassa on pieni alkio.")
  }
}Tämä alkio on kymppiä isompi: 100
Tämä alkio on kymppiä isompi: 20
Tässä kohdassa on pieni alkio.
Tämä alkio on kymppiä isompi: 50

Läpikäytävä kokoelma voi olla muukin, vaikkapa Range-tyyppinen lukuväli tai merkkijono (luku 5.4):

for (luku <- 10 to 15) {
  println(luku)
}10
11
12
13
14
15
for (indeksi <- puskuri.indices) {
  println("Indeksillä " + indeksi + " on luku " + puskuri(indeksi))
}Indeksillä 0 on luku 100
Indeksillä 1 on luku 20
Indeksillä 2 on luku 5
Indeksillä 3 on luku 50
for (merkki <- "testi") {
  println(merkki)
}t
e
s
t
i

Mm. luvut 5.3 ja 5.4 sisältävät runsaasti lisäesimerkkejä for-silmukoiden käytöstä.

Tässä vielä yksi silmukka, joka käy läpi pareja (ks. Parit ja muut monikot yllä tai luku 9.1), joita on muodostettu zipWithIndex-metodilla (ks. Yleisiä kokoelmien metodeita yllä tai luku 9.1):

for ((alkio, indeksi) <- puskuri.zipWithIndex) {
  println("Indeksillä " + indeksi + " on luku " + alkio)
}Indeksillä 0 on luku 100
Indeksillä 1 on luku 20
Indeksillä 2 on luku 5
Indeksillä 3 on luku 50

Monipuolisempaa for-silmukan käyttöä

Scalan for-silmukalla on puolia, joita ei Ohjelmointi 1 -kurssilla varsinaisesti esitellä tai tarvita. Silmukalla voi esimerkiksi tilan muuttamisen sijaan tuottaa uuden kokoelman. Tähän käytetään yield-avainsanaa:

val vektori = Vector(100, 0, 20, 5, 0, 50)vektori: Vector[Int] = Vector(100, 0, 20, 5, 0, 50)
for (luku <- vektori) yield luku + 100res119: Vector[Int] = Vector(200, 100, 120, 105, 100, 150)
for (sana <- Vector("laama", "alpakka", "vikunja")) yield sana.lengthres120: Vector[Int] = Vector(5, 7, 7)

Samassa yhteydessä voi myös suodattaa arvoja:

for (luku <- vektori; if luku != 0) yield 100 / lukures121: Vector[Int] = Vector(1, 5, 20, 2)

for-silmukat ovat toisenlainen tapa kirjoittaa foreach-, map-, flatMap- ja filter-kutsuja, joita esittelee kohta Kokoelmien käsittely korkeamman asteen metodeilla jäljempänä.

Sisäkkäiset silmukat

Silmukan rungossa voi olla toinen silmukka. Tällöin sisempi silmukka suoritetaan kokonaan, kaikkine toistoineen, kullakin ulomman silmukan suorituskerralla.

Esimerkkejä sisäkkäisistä silmukoista löytyy luvuista 5.4, 5.5 ja 6.1. Tässä myös yksi:

val lukuja = Vector(5, 3)lukuja: Vector[Int] = Vector(5, 3)
val merkkeja = "abcd"merkkeja: String = abcd
for (luku <- lukuja) {
  println("Ulomman kierros alkaa.")
  for (merkki <- merkkeja) {
    println(s"luku nyt $luku ja merkki nyt $merkki")
  }
  println("Ulomman kierros päättyy.")
}Ulomman kierros alkaa.
luku nyt 5 ja merkki nyt a
luku nyt 5 ja merkki nyt b
luku nyt 5 ja merkki nyt c
luku nyt 5 ja merkki nyt d
Ulomman kierros päättyy.
Ulomman kierros alkaa.
luku nyt 3 ja merkki nyt a
luku nyt 3 ja merkki nyt b
luku nyt 3 ja merkki nyt c
luku nyt 3 ja merkki nyt d
Ulomman kierros päättyy.

Sisäkkäisyys ja for

Yhteen for-silmukkaan voi yhdistää useita "sisäkkäisiä" läpikäyntejä. Seuraavat kaksi koodia tekevät saman:

for (luku <- lukuja) {
  for (merkki <- merkkeja) {
    println(luku + "," + merkki)
  }
}
for (luku <- lukuja; merkki <- merkkeja) {
  println(luku + "," + merkki)
}

do-silmukka

do-silmukan loppuun kirjoitetaan ehtolauseke, joka määrää, kauanko silmukan runkoa toistetaan. Tässä yksi pikkuesimerkki luvusta 6.1:

var luku = 1luku: Int = 1
do {
  println(luku)
  luku += 4
  println(luku)
} while (luku < 10)1
5
5
9
9
13
Esimerkin ensimmäinen käsky alustaa muuttujan, jota jäljempänä käytetään. Tämä alustus ei ole varsinaisesti osa silmukkaa.
do-silmukan määrittelyssä käytetään alussa sanaa do ja lopussa sanaa while. Välissä on silmukan runko aaltosulkeissa.
while-sanan perään kaarisulkeisiin kirjoitetun ehtolausekkeen tulee olla Boolean-tyyppinen. Se evaluoidaan aina silmukan rungon suorittamisen jälkeen. Jos saadaan false, niin silmukan suoritus päättyy, muuten aloitetaan taas rungon alusta.
do-silmukan runko tulee toistettua yhden tai useampia kertoja. Tässä esimerkissä se toistetaan kolmesti. Ensimmäisen suorituskerran lopussa luku-muuttujan arvo on 5, toisella kerralla 9 ja kolmannella 13, jolloin jatkamisehto ei enää ole voimassa.

while-silmukka

while-silmukka on samantapainen kuin do-silmukka, mutta sen jatkamisehto kirjoitetaan silmukan alkuun ja tarkistetaan jokaisen suorituskierroksen aluksi eikä lopuksi:

var luku = 1luku: Int = 1
while (luku < 10) {
  println(luku)
  luku += 4
  println(luku)
}1
5
5
9
9
13
Määrittelyn alussa on sana while ja sen perässä jatkamisehto sulkeissa. Tämä jatkamisehto tarkistetaan aina ennen kuin runko suoritetaan, ensimmäisen kerran jo ennen kuin runkoa on suoritettu kertaakaan.
Kun luku on aluksi 1, on jatkamisehto luku < 10 heti aluksi voimassa. Tässä tapauksessa silmukka tuottaa täsmälleen saman tulosteen kuin yllä oleva do-silmukkakin.

Toisin kuin do-silmukan, while-silmukan runkoa ei välttämättä suoriteta kertaakaan:

var luku = 20luku: Int = 20
while (luku < 10) {
  println(luku)
  luku += 4
  println(luku)
}
Nyt ehto ei ole aluksi voimassa, eikä runkoa suoriteta kertaakaan. Tämä koodi ei tulosta mitään.

Lisäesimerkkejä luvussa 6.1.

Lisää funktioista

Korkeamman asteen funktiot

Funktion voi välittää parametriksi toiselle. Alla on tiivistelmä yhdestä luvun 7.3 esimerkistä.

Korkeamman asteen funktio kahdesti:

def kahdesti(toiminto: Int => Int, kohde: Int) = toiminto(toiminto(kohde))
Ensimmäiseksi parametriksi annetaan jokin sellainen funktio, joka ottaa parametriksi yhden kokonaisluvun ja joka myös palauttaa kokonaisluvun. Viittaus tähän funktioon tallentuu toiminto-muuttujaan.
kahdesti-funktio toimii siten, että kutsutaan parametriksi saatua funktiota ensin kerran ja sitten saadulle palautusarvolle uudestaan.

Tässä pari tavallista funktiota, jotka sopivat kahdesti-funktion parametriksi:

def seuraava(luku: Int) = luku + 1

def tuplaa(tuplattava: Int) = 2 * tuplattava

Käyttöesimerkkejä:

kahdesti(seuraava, 1000)res122: Int = 1002
kahdesti(tuplaa, 1000)res123: Int = 4000

Nimettömät funktiot: funktioliteraaleja

Funktion voi kirjoittaa koodiin def-merkinnän sijaan literaalina, jolloin syntyy nimetön funktio (luku 7.4).

Käytetään tätä korkeamman asteen funktiota:

def kahdesti(toiminto: Int => Int, kohde: Int) = toiminto(toiminto(kohde))
kahdesti(luku => luku + 1, 1000)res124: Int = 1002
kahdesti(n => 2 * n, 1000)res125: Int = 4000
Funktioliteraali määrittelee nimettömän funktion, joka palauttaa parametriaan yhtä isomman luvun. kahdesti-metodille välitetään parametriksi viittaus tähän nimettömään funktioon.
Funktioliteraalin merkkinä on oikealle osoittava nuoli. Sen vasemmalla puolella mainitaan parametrit (joita on tässä vain yksi) ja oikealla puolella on funktion runko.
Voidaan kirjoittaa (luku: Int) => luku + 1, mutta tuo pidempi merkintä ei ole tässä tapauksessa tarpeen, koska käyttöyhteydestä on automaattisesti pääteltävissä, että parametrin tyyppi on Int.

Tässä toinen korkeamman asteen funktio (luvuista 7.3 ja 7.4):

def onkoJarjestyksessa(eka: String, toka: String, kolmas: String, vertaa: (String, String) => Int) =
  vertaa(eka, toka) <= 0 && vertaa(toka, kolmas) <= 0
Tämä funktio vaatii viimeiseksi parametrikseen funktion, joka tuottaa kahden merkkijonon perusteella kokonaisluvun.
Parametrina saatu funktio määrää kriteerin, jolla muita parametriarvoja vertaillaan keskenään.

Funktiota voi käyttää esimerkiksi näin:

val pituusjarjestyksessa = onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length)pituusjarjestyksessa: Boolean = true
val unicodejarjestyksessa = onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.compare(j2))unicodejarjestyksessa: Boolean = false
Viimeinen parametriarvo on muodostettu kirjoittamalla funktioliteraali, joka määrää vertailutavan.
Sulut ovat pakolliset, kun nimetön funktio ottaa useita parametreja.

Lyhyempiä funktioliteraaleja: nimettömät parametrit

Lyhennettyjä funktioliteraaleja voi muodostaa käyttämällä alaviivaa nimettyjen parametrien sijaan (luku 7.4). Tällöin nuolimerkintää ei tarvita. Nämä kaksi eri koodia vastaavat toisiaan:

kahdesti(luku => luku + 1, 1000)
kahdesti(n => 2 * n, 1000)
kahdesti( _ + 1 , 1000)
kahdesti( 2 * _ , 1000)

Myös nämä koodit tekevät keskenään saman:

onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length )
onkoJarjestyksessa("Java", "Scala", "Haskell", (j1, j2) => j1.compare(j2) )
onkoJarjestyksessa("Java", "Scala", "Haskell", _.length - _.length )
onkoJarjestyksessa("Java", "Scala", "Haskell", _.compare(_) )

Lyhennetyt merkinnät toimivat vain riittävän yksinkertaisissa tapauksissa. Yksi rajoitus on, että nimetöntä alaviivaparametria voi käyttää vain kerran. Pidempi merkintä voi olla tarpeen myös silloin, jos funktioliteraali sisältää toisia funktiokutsuja. Näitä tärkeimpiä rajoituksia on kuvailtu tarkemmin luvussa 7.4.

Kokoelmien käsittely korkeamman asteen metodeilla

Alkiokokoelmilla on paljon yleiskäyttöisiä korkeamman asteen metodeita (luvut 8.1, 9.4 ja 11.1), joille annetaan parametriksi funktio, jota sovelletaan kokoelman alkioihin. Alla on esimerkkejä eräistä. Esimerkeissä käytetään merkkijonoja ja vektoreita, mutta samoja metodeita löytyy muiltakin kokoelmilta.

Toistaminen joka alkiolle: foreach

Metodilla foreach voi toistaa saman käskyn kullekin alkiolle (luku 8.1):

Vector(10, 50, 20).foreach(println)10
50
20
"laama".foreach( kirjain => println(kirjain.toUpper + "!") )L!
A!
A!
M!
A!
Parametriksi voi antaa esimerkiksi nimettömän funktion.

Alkioiden kuvaaminen toisiksi: map, flatMap

Metodi map tuottaa kokoelman, jonka alkiot on muodostettu parametrifunktion osoittamalla tavalla alkuperäisen kokoelman alkioista (luku 8.1):

val sanoja = Vector("laama", "Tyra", "Norma", "ritarit sanoo: ")sanoja: Vector[String] = Vector(laama, Tyra, Norma, "ritarit sanoo: ")
sanoja.map( sana => sana + "nni" )res126: Vector[String] = Vector(laamanni, Tyranni, Normanni, ritarit sanoo: nni)
sanoja.map( sana => sana.length )res127: Vector[Int] = Vector(5, 4, 5, 15)

Sama lyhennetyillä funktioliteraaleilla:

sanoja.map( _ + "nni" )res128: Vector[String] = Vector(laamanni, Tyranni, Normanni, ritarit sanoo: nni)
sanoja.map( _.length )res129: Vector[Int] = Vector(5, 4, 5, 15)

Jos mapille välitetty parametrifunktio palauttaa kokoelman, syntyy sisäkkäinen rakenne:

val lukuja = Vector(100, 200, 150)lukuja: Vector[Int] = Vector(100, 200, 150)
lukuja.map( luku => Vector(luku, luku + 1) )res130: Vector[Vector[Int]] = Vector(Vector(100, 101), Vector(200, 201), Vector(150, 151))

Metodi flatMap tekee saman kuin map ja flatten yhdessä ja tuottaa "litteän" lopputuloksen (luku 8.1):

lukuja.flatMap( luku => Vector(luku, luku + 1) )res131: Vector[Int] = Vector(100, 101, 200, 201, 150, 151)

Arvioimista kriteerin perusteella: exists, forall, filter, takeWhile, ym.

exists-metodilla voi selvittää, toteutuuko annettu kriteeri minkään alkion kohdalla (luku 8.1). Metodi forall vastaavasti selvittää, toteutuuko annettu kriteeri kaikille alkioille. Ja count laskee, monelleko kriteeri toteutuu:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
luvut.exists( _ < 0 )res132: Boolean = true
luvut.exists( _ < -100 )res133: Boolean = false
luvut.forall( _ > 0 )res134: Boolean = false
luvut.forall( _ > -100 )res135: Boolean = true
luvut.count( _ > 0 )res136: Int = 4

Metodi find etsii ensimmäisen alkion, joka täyttää annetun kriteerin (luku 8.1):

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
luvut.find( _ < 5 )res137: Option[Int] = Some(4)
luvut.find( _ == 100 )res138: Option[Int] = None

Metodi filter poimii kaikki kriteerin täyttävät alkiot (luku 8.1). filterNot tekee saman käänteisesti. partition jakaa alkiot kriteerin täyttäviin ja täyttämättömiin:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val vahintaanViitoset = luvut.filter( _ >= 5 )vahintaanViitoset: Vector[Int] = Vector(10, 5, 5)
val alleViitoset = luvut.filterNot( _ >= 5 )alleViitoset: Vector[Int] = Vector(4, -20)
val askeisetParina = luvut.partition( _ >= 5 )askeisetParina: (Vector[Int], Vector[Int]) = (Vector(10, 5, 5),Vector(4, -20))

Metodi takeWhile poimii kokoelman alusta alkioita niin kauan kuin ehto täyttyy (luku 8.1). dropWhile vastaavasti jättää alusta ehdon täyttäviä alkioita pois. span hoitaa nämä molemmat kerralla:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val kunnesPieni = luvut.takeWhile( _ >= 5 )kunnesPieni: Vector[Int] = Vector(10, 5)
val alkaenEkastaPienesta = luvut.dropWhile( _ >= 5 )alkaenEkastaPienesta: Vector[Int] = Vector(4, 5, -20)
val molemmatParina = luvut.span( _ >= 5 )molemmatParina: (Vector[Int], Vector[Int]) = (Vector(10, 5),Vector(4, 5, -20))

Suuruus ja pienuus: maxBy, minBy, sortBy

Metodit maxBy ja minBy etsivät isoimman tai pienimmän alkion parametrifunktiota vertailukriteerinä käyttäen (luku 9.4). sortBy muodostaa järjestetyn kokoelman:

import scala.math.absimport scala.math.abs
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val isoinItseisarvo = luvut.maxBy(abs)isoinItseisarvo: Int = -20
val pieninItseisarvo = luvut.minBy(abs)pieninItseisarvo: Int = 4
val jarjestettyItseisarvonMukaan = luvut.sortBy(abs)jarjestettyItseisarvonMukaan: Vector[Int] = Vector(4, 5, 5, 10, -20)
val sanat = Vector("kaikkein pisin", "lyhin", "keskipitkä", "lyhyehkö")sanat: Vector[String] = Vector(kaikkein pisin, lyhin, keskipitkä, lyhyehkö)
val pisin = sanat.maxBy( _.length )pisin: String = kaikkein pisin
val jarjestettyPituudenMukaan = sanat.sortBy( _.length )jarjestettyPituudenMukaan: Vector[String] = Vector(lyhin, lyhyehkö, keskipitkä, kaikkein pisin)

Äsken mainituille metodeille on myös parametrittomat vastineet max, min ja sorted, jotka käyttävät alkioiden luonnollista järjestystä, olettaen että sellainen on määritelty (luku 9.4):

val numerojarjestys = luvut.sortednumerojarjestys: Vector[Int] = Vector(-20, 4, 5, 5, 10)
val unicodenMukainenJarjestys = sanat.sortedunicodenMukainenJarjestys: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val samaKuinAsken = sanat.sortBy( sana => sana )samaKuinAsken: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)
val samaTamakin = sanat.sortBy(identity)samaTamakin: Vector[String] = Vector(kaikkein pisin, keskipitkä, lyhin, lyhyehkö)

Yleiskäyttöistä alkioiden läpikäyntiä: foldLeft ja reduceLeft

Metodit foldLeft ja reduceLeft sukulaisineen ovat matalamman abstraktiotason työkaluja, joilla voi tarkasti määritellä, miten palautusarvo muodostetaan kokoelman alkioiden perusteella (luku 8.1). Tässä ensin foldLeft:

val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val summa = luvut.foldLeft(0)( (sumSoFar, next) => sumSoFar + next )summa: Int = 4
val samaLyhyemmin = luvut.foldLeft(0)( _ + _ )samaLyhyemmin: Int = 4
Kaksi parametriluetteloa: Ensimmäiseen kirjoitetaan alkuarvo, joka on samalla lopputulos siinä tapauksessa, ettei kokoelmassa olisi alkioita lainkaan, ja...
... toisessa on funktio, jolla yhdistetään välitulos ja seuraava alkio. Tässä esimerkissä kyseessä on yksinkertainen summafunktio.

reduceLeft on samansuuntainen, mutta se käyttää ensimmäistä alkiota lähtöarvona eikä siis tarvitse parametrikseen kuin yhdistämisfunktion:

import scala.math.minimport scala.math.min
val luvut = Vector(10, 5, 4, 5, -20)luvut: Vector[Int] = Vector(10, 5, 4, 5, -20)
val summa = luvut.reduceLeft( _ + _ )summa: Int = 4
val pienin = luvut.reduceLeft(min)pienin: Int = -20

reduceLeftin palautusarvo on samaa tyyppiä kuin käsiteltävän kokoelman alkiot, kun taas foldLeft voi tuottaa muunkintyyppisen tuloksen:

val onkoIsoaLukua = luvut.foldLeft(false)( _ || _ > 10000 )onkoIsoaLukua: Boolean = false

Koska reduceLeft olettaa, että kokoelmassa on ainakin yksi alkio, se tuottaa ajonaikaisen virheen, mikäli näin ei olekaan:

val tyhja = Vector[Int]()tyhja: Vector[Int] = Vector()
val tyhjanSummaFoldilla = tyhja.foldLeft(0)( _ + _ )tyhjanSummaFoldilla: Int = 0
val tyhjanSummaReducella = tyhja.reduceLeft( _ + _ )java.lang.UnsupportedOperationException: empty.reduceLeft
...

reduceLeftOption on kuin reduceLeft, muttei kaadu tyhjän listan tapauksessa, vaan palauttaa Option-tyyppisen tuloksen:

val tyhjanSumma = tyhja.reduceLeftOption( _ + _ )tyhjanSumma: Option[Int] = None

Lisää kokoelmien metodeita Scala API -dokumentaatiossa.

Option kokoelmatyyppinä

Option on kokoelmatyyppi: kussakin Option-oliossa on joko yksi alkio (Some) tai nolla (None). Asiaa on puitu tarkemmin luvussa 8.3. Alla on ainoastaan valikoima esimerkkejä kokoelmien metodeista Option-arvoihin sovellettuina.

Käytetään kokeiluissa seuraavia muuttujia:

val jotain: Option[Int] = Some(100)jotain: Option[Int] = Some(100)
val eiMitaan: Option[Int] = NoneeiMitaan: Option[Int] = None

size:

jotain.sizeres139: Int = 1
eiMitaan.sizeres140: Int = 0

foreach:

jotain.foreach(println)100
eiMitaan.foreach(println) // ei tulosta mitään

contains:

jotain.contains(100)res141: Boolean = true
jotain.contains(50)res142: Boolean = false
eiMitaan.contains(100)res143: Boolean = false

exists:

jotain.exists( _ > 0 )res144: Boolean = true
jotain.exists( _ < 0 )res145: Boolean = false
eiMitaan.exists( _ > 0 )res146: Boolean = false

forall:

jotain.forall( _ > 0 )res147: Boolean = true
jotain.forall( _ < 0 )res148: Boolean = false
eiMitaan.forall( _ > 0 )res149: Boolean = true

filter:

jotain.filter( _ > 0 )res150: Option[Int] = Some(100)
jotain.filter( _ < 0 )res151: Option[Int] = None
eiMitaan.filter( _ > 0 )res152: Option[Int] = None

map:

jotain.map( 2 * scala.math.Pi * _ )res153: Option[Double] = Some(628.3185307179587)
kokeilu2.map( 2 * scala.math.Pi * _ )res154: Option[Double] = None

flatten:

Some(jotain)res155: Some[Option[Int]] = Some(Some(100))
Some(eiMitaan)res156: Some[Option[Int]] = Some(None)
Some(jotain).flattenres157: Option[Int] = Some(100)
Some(eiMitaan).flattenres158: Option[Int] = None

flatMap:

def tuhatPer(luku: Int) = if (luku != 0) Some(1000 / luku) else NonetuhatPer: (luku: Int)Option[Int]
jotain.flatMap(tuhatPer)res159: Option[Int] = Some(10)
Some(0).flatMap(tuhatPer)res160: Option[Int] = None
eiMitaan.flatMap(tuhatPer)res161: Option[Int] = None

Hakurakenteet (Map)

Arvojen hakeminen: get, contains, apply

Hakurakenne on kokoelma, jonka alkioina on avain–arvo-pareja (luku 9.1). Se ei perustu indekseihin vaan arvojen hakemiseen avainten perusteella. Avain–arvo-pareina käytetään tavallisia pareja eli kaksijäsenisiä monikkoja (ks. Parit ja muut monikot). Hakurakenteessa voi esiintyä sama arvo useasti, mutta avainten on oltava keskenään erilaisia.

Hakurakenteen voi luoda näin:

val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Hakurakenteen alkioiksi laitetaan aina avain–arvo-pareja.
Hakurakenteella on kaksi tyyppiparametria: avainten tyyppi ja arvojen tyyppi. Tässä esimerkissä sekä avaimet että arvot ovat merkkijonoja.

contains-metodilla voi tutkia, onko tietty avain käytössä:

suomestaEnglanniksi.contains("tapiiri")res162: Boolean = true
suomestaEnglanniksi.contains("Juhan af Grann")res163: Boolean = false

Arvon hakeminen avaimen perusteella onnistuu get-metodia käyttäen. Se palauttaa arvon Option-kääreessä:

suomestaEnglanniksi.get("kissa")res164: Option[String] = Some(cat)
suomestaEnglanniksi.get("Juhan af Grann")res165: Option[String] = None

Lyhyemminkin (eli apply-metodia kulissien takana käyttäen; luku 4.5) saa haettua, mutta tällöin puuttuva arvo tuottaa ajonaikaisen virheen:

suomestaEnglanniksi("kissa")res166: String = cat
suomestaEnglanniksi("Juhan af Grann")java.util.NoSuchElementException: key not found: Juhan af Grann
...

Hakurakenteen muokkaaminen

Scalan peruskirjastoissa on kaksi eri Map-luokkaa, joista toinen kuvaa muuttuvia hakurakenteita ja toinen muuttumattomia. Muuttumattomat hakurakenteet ovat automaattisesti käytettävissä, ja niitä on käytetty myös tämän sivun esimerkeissä ellei toisin ole mainittu. Nyt kuitenkin kokeillaan muuttuvatilaista hakurakennetta, joka otetaan erikseen käyttöön:

import scala.collection.mutable.Mapimport scala.collection.mutable.Map
val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)

Muuttuvatilaiseen hakurakenteeseen voi lisätä avain–arvo-pareja. Tässä kaksi eri tapaa (luku 9.1):

suomestaEnglanniksi("hiiri") = "mouse"suomestaEnglanniksi += "sika" -> "pig"res167: Map[String, String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, sika -> pig, hiiri -> mouse,
laama -> llama)

Samoja käskyjä voi käyttää myös olemassa olevan parin korvaamiseen: jos lisätty avain on jo hakurakenteessa, uusi pari korvaa vanhan.

Tässä vastaavasti kaksi eri tapaa poistaa pari muuttuvatilaisesta hakurakenteesta:

suomestaEnglanniksi.remove("tapiiri")res168: Option[String] = Some(tapir)
suomestaEnglanniksi -= "laama"res169: Map[String, String] = Map(koira -> puppy, kissa -> cat, sika -> pig, hiiri -> mouse)

Epäonnistuneet haut ja vara-arvot: getOrElse, withDefault ym.

getOrElse-metodille voi antaa parametriksi lausekkeen, joka määrittää "vara-arvon" (luku 9.1):

val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
suomestaEnglanniksi.getOrElse("kissa", "tuntematon hakusana")res170: String = cat
suomestaEnglanniksi.getOrElse("Juhan af Grann", "tuntematon hakusana")res171: String = tuntematon hakusana
Metodin palautusarvo on String eikä Option[String] kuten tavallisen get-metodin tapauksessa.

Jos kyseessä on muuttuvatilainen hakurakenne, voi käyttää myös metodia getOrElseUpdate. Haun epäonnistuessa se lisää hakurakenteeseen jälkimmäisen parametrinsa määräämän arvon, joten haku lopulta onnistuu aina:

import scala.collection.mutable.Mapimport scala.collection.mutable.Map
val suomestaEnglanniksi = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")suomestaEnglanniksi: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
suomestaEnglanniksi.getOrElseUpdate("lude", "bug")res172: String = bug
suomestaEnglanniksires173: Map[String,String] = Map(lude -> bug, koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)

Vaihtoehto äsken mainituille metodeille on määritellä koko hakurakenteelle yleinen vara-arvo, joka palautetaan haun epäonnistuessa. Se onnistuu metodilla withDefaultValue (luku 9.1):

val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefaultValue("ähäkutti")englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat)
englanniksi("kissa")res174: String = cat
englanniksi("Juhan af Grann")res175: String = ähäkutti
withDefaultValue-metodille ilmoitetaan, mitä halutaan käyttää "vara-arvona" silloin, kun haku on huti.
Nyt kun hakurakenteesta haetaan olematonta avainta (ilman get-metodiakin), saadaan tämä vara-arvo.

Äskeisessä esimerkissä vara-arvo oli aina sama. Käyttämällä metodia withDefault voi hakurakenteelle asettaa "varafunktion", joka määrittää palautusarvoja hutihaun tuottaneen avaimen perusteella:

def raportti(haettu: String) = "hait sanaa " + haettu + " muttei löytynyt"raportti: (haettu: String)String
val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefault(raportti)englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat)
englanniksi("kissa")res176: String = cat
englanniksi("Juhan af Grann")res177: String = hait sanaa Juhan af Grann muttei löytynyt

Hakurakenteen muodostaminen kokoelmasta: toMap, groupBy

Kutsumalla toMap-metodia voi hakurakenteen luoda minkä tahansa sellaisen kokoelman perusteella, jonka alkioina on pareja (luku 9.3):

val elaimia = Vector("koira", "kissa", "kala", "saukko", "laama", "porsas")elaimia: Vector[String] = Vector(koira, kissa, kala, saukko, laama, porsas)
val lukumaaria = Vector(2, 12, 35, 5, 7, 5)lukumaaria: Vector[Int] = Vector(2, 12, 35, 5, 7, 5)
val parejaVektorissa = elaimia.zip(lukumaaria)parejaVektorissa: Vector[(String, Int)] = Vector((koira,2), (kissa,12), (kala,35), (saukko,5), (laama,7), (porsas,5))
val hakurakenne = parejaVektorissa.toMaphakurakenne: Map[String,Int] = Map(saukko -> 5, koira -> 2, kala -> 35, porsas -> 5, kissa -> 12, laama -> 7)
hakurakenne("laama")res178: Int = 7
Esimerkissä ensin luodaan pari erillistä kokoelmaa ja yhdistetään ne zip-metodilla. Syntyy pareja sisältävä vektori.
Tällaisen vektorin perusteella toMap voi luoda hakurakenteen.

Metodilla groupBy muodostetaan hakurakenne, johon alkuperäisen kokoelman alkiot on ryhmitelty sen mukaan, mitä parametriksi annettu funktio niiden kohdalla palauttaa:

val lukumaaria = Vector(2, 12, 35, 5, 7, 5)lukumaaria: Vector[Int] = Vector(2, 12, 35, 5, 7, 5)
val ryhmiteltyParillisuudenMukaan = lukumaaria.groupBy( _ % 2 == 0 )ryhmiteltyParillisuudenMukaan: Map[Boolean,Vector[Int]] = Map(false -> Vector(35, 5, 7, 5), true -> Vector(2, 12))
val elaimia = Vector("koira", "kissa", "akvaariokala", "saukko", "laama", "porsas")elaimia: Vector[String] = Vector(koira, kissa, akvaariokala, saukko, laama, porsas)
val ryhmiteltySananPituudenMukaan = elaimia.groupBy( _.length )ryhmiteltySananPituudenMukaan: Map[Int,Vector[String]] = Map(5 -> Vector(koira, kissa, laama), 4 -> Vector(kala),
6 -> Vector(saukko, porsas))

Näillä metodeilla luodut hakurakenteet ovat tilaltaan muuttumattomia.

Lisäesimerkkejä luvussa 9.3.

Muita hakurakenteiden metodeita: keys, values, mapValues ym.

Hakurakenteet ovat alkiokokoelmia, ja niillä on koko joukko yhteisiä metodeita muiden kokoelmatyyppien kanssa (ks. Kokoelmien alkeita, Yleisiä kokoelmien metodeita ja Kokoelmien käsittely korkeamman asteen metodeilla) Indekseihin perustuvia metodeita niillä ei luonnollisestikaan ole, mutta esimerkiksi isEmpty, size ja foreach ja monet muut toimivat kyllä:

val englanniksi = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")englanniksi: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat)
englanniksi.isEmptyres179: Boolean = false
englanniksi.sizeres180: Int = 3
englanniksi.foreach(println)(koira,dog)
(tapiiri,tapir)
(kissa,cat)

Nimenomaan hakurakenteille ominaisia ovat metodit keys ja values (luku 9.1), jotka palauttavat pelkät avaimet tai pelkät arvot sisältävän kokoelman:

englanniksi.keys.foreach(println)koira
tapiiri
kissa
englanniksi.values.foreach(println)dog
tapir
cat

Metodi mapValues (luku 9.1) toimii kuten map-metodi, mutta ei käsittele avain–arvo-pareja vaan vain arvoja:

englanniksi.mapValues( _.length )res181: Map[String,Int] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)

Metodi tuottaa uuden muuttumattoman hakurakenteen, jossa alkuperäisten arvojen tilalla on parametrifunktion palautusarvot (tässä: englanninnosten pituudet).

Lisää hakurakenteidenkin metodeista virallisessa Scala API -dokumentaatiossa.

Ylä- ja alakäsitteitä

Ylä- ja alakäsitteitä voi kuvata piirreluokilla (trait; luku 6.2), joita liitetään tavallisiin luokkiin, tai määrittelemällä luokalle yliluokan (periytyminen; luku 6.4).

Piirreluokat

Piirreluokka määritellään samaan tapaan kuin luokka mutta sanaa trait käyttäen (luku 6.2). Tämä piirreluokka kuvaa abstraktia kuvion käsitettä:

trait Shape {

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double    

}
Kaikilla kuvioilla on isBiggerThan-metodi, jolla voi verrata kuvioiden pinta-aloja keskenään.
Kaikilla kuvioilla on myös area-metodi pinta-alan laskemiseen. Tämä metodi on abstrakti: sillä ei ole runkoa eikä sitä voi sellaisenaan kutsua. Pinta-alan laskentatapa määritellään erikseen alikäsitteille eli niissä luokissa, joihin piirre Shape liitetään (ks. alta).
Vertailumetodille voi antaa parametriksi viittauksen mihin tahansa Shape-tyyppiseen olioon. Kaikilla tällaisilla olioilla on jonkinlainen area-metodi, joten tuota metodia voi käyttää osana vertailumetodin toteutusta.
Tavallisilla luokilla on usein konstruktoriparametreja. Piirreluokalla ei koskaan ole (ainakaan Scalan nykyversiossa).

Piirreluokan liittäminen luokkaan

Seuraaviin kahteen luokkaan on liitetty Shape-piirre (luku 6.2). Ne edustavat kuviokäsitteen alakäsitteitä:

class Circle(val radius: Double) extends Shape {
  def area = scala.math.Pi * this.radius * this.radius
}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
  def area = this.sideLength * this.anotherSideLength
}
Piirre liitetään avainsanalla extends. Tästä seuraa, että kaikki Circle-tyyppiset oliot ovat paitsi ympyröitä myös kuvioita. Niillä on mm. piirreluokassa Shape määritelty isBiggerThan-metodi.
Luokissa voidaan tarjota toteutukset piirreluokan abstrakteille metodeille. Esimerkiksi tässä määritellään, että ympyrä on sellainen kuvio, jonka pinta-ala lasketaan pii * r2, ja suorakaide on sellainen kuvio, jonka pinta-ala lasketaan sivujen kertolaskulla.

Luokkaan voi liittää useita piirreluokkia. Ensimmäinen yläkäsite mainitaan extends-sanan perässä ja loput with-sanoin eroteltuina:

class X extends A with B with C with D with Etc

Staattiset ja dynaamiset tyypit

Luvussa 6.2 erotetaan toisistaan staattinen ja dynaaminen tyyppi:

var kuvio: Shape = new Circle(1)kuvio: o1.shapes.Shape = o1.shapes.Circle@1a1a02e
kuvio = new Rectangle(10, 5)kuvio: o1.shapes.Shape = o1.shapes.Rectangle@7b519d
Muuttujan kuvio staattinen tyyppi on Shape. Sillä voi viitata mihin tahansa Shape-tyyppiseen, esimerkiksi ympyrään tai suorakaiteeseen. Staattinen tyyppi käy ilmi pelkästä ohjelmakoodista.
Muuttujaan kuvio on tässä esimerkissä ensin sijoitettu arvo, jonka dynaaminen tyyppi on Circle. Se korvataan arvolla, jonka dynaaminen tyyppi on Rectangle. Dynaamisen tyypin on oltava yhteensopiva muuttujan staattisen tyypin kanssa.

Kaikille Scala-olioille yhteisen isInstanceOf-metodin avulla voi tutkia arvon dynaamista tyyppiä. Tässä todetaan, että kuvio-muuttujassa on parhaillaan viittaus olioon, joka on sekä Rectangle että Shape-tyyppinen:

kuvio.isInstanceOf[Rectangle]res182: Boolean = true
kuvio.isInstanceOf[Shape]res183: Boolean = true

Yllä kuviomuuttujan tyyppi oli erikseen määritelty Shapeksi. Tässä ei, minkä vuoksi sijoitus epäonnistuu:

var kokeilu = new Circle(1)kokeilu: o1.shapes.Circle = o1.shapes.Circle@1c4207e
kokeilu = new Rectangle(10, 5)<console>:11: error: type mismatch;
 found   : o1.shapes.Rectangle
 required: o1.shapes.Circle
       kokeilu = new Rectangle(10, 5)
                 ^
Muuttujan staattiseksi tyypiksi tulee alkuarvon perusteella päätellyksi Circle, jolloin siihen voi sijoittaa vain Circle-tyyppisiä arvoja eikä muita kuvioita.

Staattinen tyyppi rajoittaa arvojen käyttöä (6.2):

var testi: Shape = new Circle(10)testi: o1.shapes.Shape = o1.shapes.Circle@9c8b50
testi.radius<console>:12: error: value radius is not a member of o1.shapes.Shape
             testi.radius
                   ^
Lausekkeen test staattinen tyyppi on Shape. Mielivaltaiselle Shape-oliolle ei ole määritelty radius-muuttujaa, vaikka ympyröille onkin.

Tällä rajoituksella on syynsä:

trait Shape {

  def isBiggerThan(another: Shape) = this.area > another.area

  def area: Double

}
isBiggerThan-metodissa ei voisi ryhtyä vertailemaan kuvioita lauseketta this.radius > another.radius käyttäen, koska sädettä ei ole kaikilla mahdollisilla olioilla, joihin this ja radius voivat viitata. Jos moinen kutsu sallittaisiin, ei tämä yleiskäyttöiseksi tarkoitettu metodi toimisikaan esimerkiksi suorakaiteille.

Periytyminen

Luokka voi periä toisen tavallisen (eli ei-piirre-)luokan. Tässä käytetään luvun 6.4 tapaan Rectangle-luokkaa, jonka perii luokka Square:

class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
  def area = this.sideLength * this.anotherSideLength
}
class Square(size: Double) extends Rectangle(size, size)
Käytetään jälleen extends-sanaa. Aliluokka Square perii nyt yliluokan Rectangle, ja Square-oliot ovat nyt myös Rectangle-tyyppisiä (ja Shape-tyyppisiä, koska Rectangle-luokkaan on liitetty Shape-piirre).
Luokalla Square on yksi konstruktoriparametri, joka kertoo kunkin sivun mitan.
Kun aliluokasta luodaan ilmentymä, tehdään myös yliluokassa määritellyt alustustoimenpiteet. Esimerkiksi tässä määritellään, että kun Square-oliota luodaan, niin tehdään samat alustustoimenpiteet kuin Rectangle-oliolle, kuitenkin siten, että molemmiksi suorakaiteiden konstruktoriparametreiksi (eli molemmiksi sivunpituuksiksi) laitetaan neliöolion saaman konstruktoriparametrin arvo.

Konkreettisessa luokassa kaikilla metodeilla on toteutus. Voidaan myös määritellä abstrakti luokka, jollaisessa saa olla abstrakteja, toteutuksettomia metodeita kuten piirreluokassakin. Tässä esimerkki luvusta 6.4:

abstract class Tapahtuma(val alvLisatty: Boolean) {

  def kokonaishinta: Double

  def verotonHinta = if (this.alvLisatty) this.kokonaishinta / 1.24 else this.kokonaishinta

}
Sana abstract tekee luokasta abstraktin. Tästä luokasta ei voi luoda suoraan ilmentymiä.
kokonaishinta-metodi on abstrakti. Konkreettisten aliluokkien on tarjottava toteutus tälle metodille, jotta kaikilla Tapahtuma-tyyppisillä olioilla on tämä metodi toteutettuna.

Tässä vertailutaulukko samasta luvusta 6.4:

  Piirreluokka Abstrakti yliluokka Konkreettinen yliluokka
Voiko sisältää abstrakteja metodeita? Voi. Voi. Ei voi.
Voiko luoda suoraan ilmentymiä newllä? Ei voi. Ei voi. Voi.
Voiko olla konstruktoriparametreja? Ei voi. Voi. Voi.
Voiko käyttää useita yläkäsitteinä (with-sanojen perässä)? Voi. Ei voi. Ei voi.

Näitä tekniikoita voi myös yhdistellä keskenään. Luokka voi esimerkiksi periä yhden yliluokan ja siihen voi lisäksi liittyä piirreluokkia. Tai piirreluokka voi periä luokan.

Myös yksittäisoliot voivat periä luokkia ja niihin voi liittää piirteitä.

Metodin korvaaminen: override

Alakäsitteessä voi korvata yläkäsitteelle määritellyn metodin käyttäen override-sanaa (luku 6.4). Eräs yleinen korvattava on toString-metodi. Tässä toisenlainen esimerkki:

class Yli {
  def eka() = {
    println("Yliluokan eka")
  }
  def toka() = {
    println("Yliluokan toka")
  }
}
class Ali extends Yli {
  override def eka() = {
    println("Aliluokan eka")
  }
  override def toka() = {
    println("Aliluokan toka")
    super.toka()
  }
}
val kokeilu = new Alikokeilu: Ali = Ali@1bd9da5
kokeilu.eka()Aliluokan eka
kokeilu.toka()Aliluokan toka
Yliluokan toka
Tässä on korvattu molemmat yliluokan metodit.
Ali-tyyppisen olion eka-metodi toimii yliluokan toteutuksesta riippumattomasti. Korvaava toteutus ratkaisee.
Osana aliluokan toka-metoditoteutusta kutsutaan yliluokan versiota metodista, joten...
... Ali-tyyppisen olion metodi tuottaa ensin aliluokassa määritellyn tulosteen ja tekee sitten sen, mitä korvattu Yli-luokan metodikin tekee.

super-sanaa voi käyttää yläkäsitteen määrittelyyn viittaamiseen muutenkin kuin korvatussa metodissa, mutta tuo on suhteellisen yleinen käyttötapaus.

Scalan luokkahierarkia

Kaikki Scala-oliot ovat kattotyyppiä Any. Sillä on välittömät aliluokat AnyVal ja AnyRef, joista jälkimmäinen tunnetaan myös nimellä Object (luku 6.4):

  • AnyVal-luokasta periytyvät tutut tietotyypit Int, Double, Boolean, Char, Unit, ja muutama muu. Sille harvemmin laaditaan itse uusia aliluokkia ja moinen pitää erikseen ilmoittaa.
  • AnyRef puolestaan on yliluokka kaikille muille (ei-piirre-)luokille ja yksittäisolioille. Esimerkiksi luokat String ja Array periytyvät tästä luokasta. Myös itse laaditut luokat periytyvät automaattisesti AnyRefistä ellei erikseen toisin määritellä.

Näitä luokkahierarkian yläpään tyyppejä voi käyttää esimerkiksi kokoelmien tyyppiparametreina, kuten alla:

val sekalaisiaAny = Vector(123, "laama", true, Array(123, 456), new Square(10))sekalaisiaAny: Vector[Any] = Vector(123, laama, true, Array(123, 456), o1.shapes.Square@114c3c7)
val sekalaisiaAnyVal = Vector(123, true)sekalaisiaAnyVal: Vector[AnyVal] = Vector(123, true)
val sekalaisiaAnyRefEliObject = Vector("laama", Array(123, 456), new Square(10))sekalaisiaAnyRefEliObject: Vector[Object] = Vector(laama, Array(123, 456), o1.shapes.Square@667113)

Tällöin pitää kuitenkin muistaa rajoitukset kohdasta Staattiset ja dynaamiset tyypit. Jokaisella Any-tyyppisellä oliolla on yleismetodit toString ja isInstanceOf muttei esimerkiksi area-metodia:

val jokuOlio = sekalaisiaAny(4)jokuOlio: Any = o1.shapes.Square@4bc399
jokuOlio.isInstanceOf[Square]res184: Boolean = true
jokuOlio.area<console>:12: error: value area is not a member of Any
            jokuOlio.area
                     ^

Tiedostonkäsittelyä

Tässä esimerkki tekstitiedoston lukemisesta (luku 9.3). Ohjelma tulostaa tiedoston teksti.txt kunkin rivin ja sen eteen rivinumeron:

import scala.io.Source

object TulostaNumeroituna extends App {

  val tiedosto = Source.fromFile("teksti.txt")

  try {

    var rivinumero = 1
    for (rivi <- tiedosto.getLines) {
      println(rivinumero + ": " + rivi)
      rivinumero += 1
    }

  } finally {
    tiedosto.close()
  }

}
Metodi fromFile ottaa parametriksi tiedoston nimen ja palauttaa Source-tyyppisen olion, joka osaa käydä läpi tiedoston sisällön.
Silmukalla käydään tässä läpi kukin niistä riveistä, jotka getLinesia kutsumalla saadaan. (On myös muita tapoja käydä läpi tiedoston sisältöä kuin rivi kerrallaan; ks. luku 9.3.)
tryfinally-rakenne huolehtii siitä, että finally-lohkoon sijoitettu tiedostoyhteyden sulkeva käsky tulee suoritetuksi, vaikka datan lukeminen jostain syystä epäonnistuisikin.

Ja tässä esimerkki tekstitiedoston kirjoittamisesta:

import java.io.PrintWriter
import scala.util.Random

object Kirjoittamisesimerkki extends App {

  val numerogeneraattori = new Random
  val tiedostonNimi = "satunnaisia.txt"

  val tiedosto = new PrintWriter(tiedostonNimi)
  try {
    for (n <- 1 to 10000) {
      tiedosto.println(numerogeneraattori.nextInt(100))
    }
    println("Luotiin tiedosto " + tiedostonNimi + ", jossa on näennäissatunnaislukuja.")
    println("Jos tiedosto oli ennestään olemassa, sen vanha sisältö korvautui uusilla luvuilla.")
  } finally {
    tiedosto.close()
  }

}
PrintWriter-oliolle annetaan konstruktoriparametriksi kirjoitettavan tiedoston nimi. Jos tämän niminen tiedosto jo oli, niin vanhat tiedot häviävät ja korvautuvat ohjelman kirjoittamalla uudella datalla.
println-metodilla voi kirjoittaa tiedostoon yhden rivin tekstiä.
Yhteyden sulkeminen on tiedostoa kirjoittaessa erityisen tärkeää, koska vasta yhteyttä suljettaessa vahvistuu viimeisten merkkien tallentaminen levylle.

Graafiset käyttöliittymät

Graafisten käyttöliittymien rakentamiseen sopivia perustyökaluja on esitelty kootusti luvussa 11.2. Tässä lyhyt esimerkki sieltä:

../_images/gui61.png
import scala.swing._
import scala.swing.event._

object Tapahtumakokeilu extends SimpleSwingApplication {
  val ekaNappi = new Button("Tästä sopii painaa")
  val tokaNappi = new Button("Tästä myös")
  val kehote = new Label("Paina jompaakumpaa napeista.")

  val kaikkiJutut = new BoxPanel(Orientation.Vertical)
  kaikkiJutut.contents += kehote
  kaikkiJutut.contents += ekaNappi
  kaikkiJutut.contents += tokaNappi
  val nappulaikkuna = new MainFrame
  nappulaikkuna.contents = kaikkiJutut
  nappulaikkuna.title = "Kokeiluikkuna"
  this.listenTo(ekaNappi)
  this.listenTo(tokaNappi)
  this.reactions += {
    case painallus: ButtonClicked =>
      val lahdenappula = painallus.source
      val nappulanTeksti = lahdenappula.text
      Dialog.showMessage(kaikkiJutut, "Painoit nappia, jossa lukee: " + nappulanTeksti, "Viesti")
  }
}
Sovellusta kuvaa yksittäisolio, joka perii Swing-käyttöliittymien laatimiseen sopivan luokan.
Luodaan käyttöliittymäelementtejä kuvaavat oliot.
Asemoidaan elementit paneeliin allekkain.
Sijoitetaan paneeli ikkunan sisällöksi ja alustetaan ikkunan ominaisuudet muutenkin.
Asetetaan olio (tässä: sovellusolio itse) tapahtumankuuntelijaksi nappuloille.
Määritellään, miten havaittuihin tapahtumiin reagoidaan.
Kun tapahtuma havaitaan, suoritetaan apuikkunan näyttävä koodi. Koodissa voi käyttää muuttujaa painallus, johon on tallentunut tapahtumaa kuvaava ButtonClicked-tyyppinen olio.

Varatut sanat

Scala-kielen varatut sanat, eli sanat, joita ei voi käyttää esimerkiksi muuttujien niminä, ovat:

abstract case     catch    class    def       do      else    extends  false  final
finally  for      forSome  if       implicit  import  lazy    match    new    null
object   override package  private  protected return  sealed  super    then   this
throw    trait    try      true     type      val     var     while    with   yield
_        :        =        =>       <<:       <%      >:      #        @      ⇒     ←

Palaute

Palautusta lähetetään...

Palautus on vastaanotettu.