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

13 Pakkauksista ja importista

Muutettu viimeksi 22.4.2010 / Sivu luotu 19.4.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Javassa pakkaus on luokkakokoelma tiedostoina jossakin hakemistossa, ja Javan ilmauksella import tuodaan käännösyksikköön yksittäisiä luokkia tai annetaan lupa löytää kaikki jonkin pakkauksen luokat.

Scalassa molemmat ovat paljon monipuolisempia ja vahvempia. Niiden avulla mm. voidaan toteuttaa "moduleita klassiseen tyyliin", ym., ym.

Pakkaus

Kuten Javassa myös Scalassa voidaan yhden ohjelmatiedoston määrittelyt asettaa pakkaukseen kirjoittamalla tiedoston alkuun package-määre:
  package bobsrockets.navigation
  class Navigator
Tosin Javan käännösyksikkö voi sisältää vain luokkia. Scalassa monenlaista muutakin sälää.

Mutta Scalassa pakkauksia voidaan rakennella myös toisin - sisäkkäin määritellen - tässäkin asiassa Scala on "ei-litteä":

  package bobsrockets {
    package navigation {

      // In package bobsrockets.navigation
      class Navigator

      package tests {

        // In package bobsrockets.navigation.tests
        class NavigatorSuite
      }
    }
  }
Jos bobsrockets ei sisällä kuin pakkauksen navigation, voi kirjoittaa lyhyesti:
  package bobsrockets.navigation {

    // In package bobsrockets.navigation
    class Navigator

    package tests {

      // In package bobsrockets.navigation.tests
      class NavigatorSuite
    }
  }
Luvun ensimmäinen esimerkki olikin oikeastaan vain scalamainen lyhennysmerkintä seuraavalle:
  package bobsrockets.navigation {class Navigator}
Vaikka Javan pakkaukset jäsentyvätkin alipakkauksiksi hakemistojen puumaiseen hierarkiaan, ne eivät ole nimiavaruuksina hierarkioita. Mutta Scalassapa muiden sisäkkäisten rakenteiden tapaan ovat:
  package bobsrockets {
    package navigation {
      class Navigator
    }
    package launch {
      class Booster {
        // No need to say bobsrockets.navigation.Navigator
        val nav = new navigation.Navigator
      }
    }
  }
Muiden lohkorakenteiden tapaan myös sisäkkäisissä pakkauksissa tunnuksille voidaan antaa uusia merkityksiä sisempänä ja samalla peittää ulomman tason nimiä näkymästä:
  // In file launch.scala
  package launch {
    class Booster3
  }

  // In file bobsrockets.scala
  package bobsrockets {
    package navigation {
      package launch {
        class Booster1
      }
      class MissionControl {
        val booster1 = new launch.Booster1
        val booster2 = new bobsrockets.launch.Booster2
        val booster3 = new _root_.launch.Booster3
      }
    }
    package launch {
      class Booster2
    }
  }
Tässä vain tuo _root_.launch.Booster3 kaivannee lisävalaistusta: kyseessä on kaikkien ohjelmoijan kannalta uloimman tason pakkausten yhteinen ylipakkaus.

import

Scalan import-ilmaus on paljon Javan vastaavaa monipuolisempi ja joustavampi.

Javan tapaan sillä voidaan tietenkin tuoda pakkauksen luokka "käännösyksikköön". Olkoot Bobin herkut paketoidut:

  package bobsdelights

  abstract class Fruit(
    val name: String,
    val color: String
  )

  object Fruits {
    object Apple extends Fruit("apple", "red")
    object Orange extends Fruit("orange", "orange")
    object Pear extends Fruit("pear", "yellowish")
    val menu = List(Apple, Orange, Pear)
  }
Kun tämän kääntää scalac-komennolla, käännöshakemistoon syntyy alihakemisto bobsdelights, joka sisältää luokkatiedostot:
	Fruit.class	     Fruits.class   Fruits$Orange$.class
	Fruits$Apple$.class  Fruits$.class  Fruits$Pear$.class
Ja nyt käyttöön voi Javan tapaan saada pakkauksen nimetyn luokan:
  import bobsdelights.Fruit
tai pakkauksen kaikki jäsenet:
  import bobsdelights._
ja samaan tapaan myös vaikkapa pakkauksen sisältämän olion alioliot ja kentän(!!):
  import bobsdelights.Fruits._
Kokeillaan malliksi tuota viimeistä
import bobsdelights.Fruits._
object Hedelma extends Application {
 println(menu(0).name)   // apple
 println(menu(2).color)  // yellowish
}
(Objektin kenttien tuonti vastaa Javan staattisten kenttien tuomista.)

Toki tuonti toimii myös "suoritettavaan tekstitiedostoon", ei pelkästään yllä nähdyllä tavalla sovellukseen.

Vaan eipä tässä vielä kaikki! Ensinnäkin import-ilmauksia saa kirjoitella muuallekin kuin "käännösyksikön" alkuun. Toiseksi niillä voidaan tuoda näkyviin paljon muutakin kuin luokkia. Esimerkki:

  def showFruit(fruit: Fruit) {
    import fruit._
    println(name +"s are "+ color)
  }
Tässä siis tuodaan parametrin luokan jäsenet! Ja tulostuslauseessa käytetään pelkkiä kenttänimiä sen sijaan, että kirjoitettaisiin fruit.name ja fruit.color!

Scalassa - Javasta poiketen - voidaan tuoda myös itse pakkauksia, ei vain pakkausten luokkia:

  import java.util.regex

  class AStarB {
    // Accesses java.util.regex.Pattern
    val pat = regex.Pattern.compile("a*b")
  }
Tuotujen rakenteiden kenttiä voidaan myös kätkeä tai nimetä uudelleen:
  // Tuodaan vain nimetyt jäsenet:
  import Fruits.{Apple, Orange}

  // Tuodaan nimetyt ja nimetään uudelleen 
  import Fruits.{Apple => McIntosh, Orange}

  // Nimetään sql-nimi uudelleen, jotta Java-nimi jää käyttöön
  import java.sql.{Date => SDate}

  // Lyhenne Javan sql-pakkaukselle
  import java.{sql => S}

  // Pidempi ilmaus lyhyemmälle import Fruits._
  import Fruits.{_}

  // Kaikki tuodaan, mutta yhdelle uusi nimi
  import Fruits.{Apple => McIntosh, _}

  // Tuodaan kaikki paitsi päärynä
  import Fruits.{Pear => _, _}
Tuosta viimeisestä melkein järkevä esimerkki:
  import Notebooks._
  import Fruits.{Apple => _, _}
Tuodaan kaikki Notebooksista ja Fruitsistakin kaikki, paitsi Apple. Näin hedelmistä Apple jääkin tarkoittamaan Notebooksin Applea. Jos Noteboksissa sattui olemaan Orange, se ei ole suoraan käytössä, koska Fruitsin Orange voittaa...

Oletus-import ohjelmatiedostoihin

Oletusarvoisesti Scala-ohjelmiin tuodaan seuraavat pakkaukset:
  import java.lang._ // everything in the java.lang package
  import scala._     // everything in the scala package
  import Predef._    // everything in the Predef object

Ensikisikin siis kaikki Javan oletuskalusto on käytettävissä. Pakkauksessa scala on Scalan oma peruskalusto Anystä UninitializedFieldErroriin. Predef-oliosta saadaan tyyppejä ArrayIndexOutOfBoundsExceptionista unitiin, "staattista" kalustoa, mm. Map- ja Set-tehtaat ja "luokkametodeita" joka lähtöön, mm. assert, int2double, println, readInt, yms. Katso Scala-API.

Huom: Tällaisissa import-jonoissa myöhemmin luetellun pakkauksen tunnukset peittävät aiemmin mainituista löytyvät kaimansa. Jos vaikka jostain syystä haluaa välttämättä käyttää Javan StringBuilderia, siihen pitää viitata koko nimellä java.lang.StringBuilder, koska myös Scalan omassa oletuspakkauksessa on saman niminen luokka.

Saannin sääntelyä

Tunnusten näkyvyyttä voidaan rajoittaa määrein private ja protected. Scalassa ei ole public-määrettä -- julkisuus on luokkien ja olioden sisältämien tunnusten oletusnäkyvyys.

Näkyvyysalueita, joihin näkyvyys voidaan rajoittaa, on paljon enemmän kuin Javassa. Toisaalta onhan Scalassa toki rakenteita ja rakenteiden sisäkkäisyyksia Javaa enemmän.

Luokan sisältämä luokka on (Javasta poiketen) oma näkyvyysalueensa:

  class Outer {
    class Inner {
      private def f() { println("f") }
      class InnerMost {
        f() // OK
      }
    }
    (new Inner).f() // error: f is not accessible
  }
Tässäkin asiassa Java on litteä: Luokasta pääsee käsiksi privaatteihin sisäluokkiin! Miksi ihmessä? Arvelen asian johtuvan siitä, että Javaan "liimattiin päälle" sisäluokat aika myöhään.

Määre protected rajoittaa näkyvyyden oman luokan lisäksi vain aliluokkiin:

  package p {
    class Super {
      protected def f() { println("f") }
    }
    class Sub extends Super {
      f()  // OK
    }
    class Other {
      (new Super).f()  // error: f is not accessible
    }
  }
Scalan protected on juuri sellainen kuin sen toivoin Javassakin olevan ja petyin. Javassahan määre päästää myös luokan oman pakkauksen muut luokat käpälöimään protected-jäseniä.

Näkyvyysmääreitä (access modifiers) voidaan tarkentaa tarkentein (qualifiers) tyyliin private[X] ja protected[X], missä X on luokan, olion tai pakkauksen nimi. Tarkennetulla määrellä säädellään tunnuksen näkyvyyttä nimenomaan ja erityisesti määrittelykohtaa (luokka, olio, pakkaus) ympäröivän rakenteen (luokka, olio, pakkaus) sisällä; "mille tasolle ulospäin näytetään":

 package bobsrockets {
   package navigation {
     private[bobsrockets] class Navigator { 
       protected[navigation] def useStarChart() {}
       class LegOfJourney {
         private[Navigator] val distance = 100
       }
       private[this] var speed = 200
     }
   }
   package launch {
     import navigation._
     object Vehicle { 
       private[launch] val guide = new Navigator
     }
   }
 }
Myös siis this-viite olioon kelpaa! Tähän olioprivaattiuteen törmäsimme jo aiemminkin. Siis vaikkapa seuraava on kielletty Navigator-luokan sisällä:
  val other = new Navigator
  other.speed

Näkyvyys olio- ja luokkakumppanien kesken muistuttaa "hämmästyttävän" paljon Javan staattisen ja ei-staattisen kaluston tapausta: Kumppanukset jakavat saman näkyvyysalueen:
  class Rocket {
    import Rocket.fuel
    private def canGoHomeAgain = fuel > 20
  }

  object Rocket {
    private def fuel = 10
    def chooseStrategy(rocket: Rocket) {
      if (rocket.canGoHomeAgain)
        goHome()
      else
        pickAStar()
    }
    def goHome() {}
    def pickAStar() {}
  }
Ja kuten Javan static-puolella, viittaus oliokumppanista luokkakumppaniin edellyttää luokan ilmentymää, johon luokkakumppanin metodia sovelletaan.

Yksi ero Javaan verraten: Javassa aliluokasta pääsee käsiksi yliluokan staattisiin jäseniin. Scalassa oliokumppanilla ei voi olla olemassa "alioliokumppaneita", koska olioilla ei yleensäkään ole "aliolioita".