Sichtbarkeitsmodifizierer

Wer aus der Java-Programmierung kommt, dem dürfte beim Studium von Scala Quelltexten auffallen, das sehr wenig Sichtbarkeitsmodifizerer wie public, private oder protected verwendet werden. Wird in Scala kein Sichtbarkeitsmodifizierer angegeben, so handelt es sich um die Sichtbarkeitsstufe public (öffentlich), welche für alle Klassen sichtbar ist. Das Weglassen des Sichtbarkeitsmodifizierers in Java würde zur Default-Sichtbarkeit führen, die so in Scala nicht definiert ist. Die Sichtbarkeitsstufe public ist in Scala Standard. Ein Sichtbarkeitsmodifizierer public wird in Scala nicht angegeben. Eine Angabe würde sogar zu einem Compiler-Fehler führen.

Für Elemente, die mit der Sichtbarkeitsstufe private gekennzeichnet sind, gilt, dass diese Elemente nur innerhalb Ihrer eigenen Klasse sichtbar sind.

Die Sichtbarkeitsstufe protected definiert eine Sichtbarkeit der Elemente innerhalb der eigenen Klasse und in allen von Ihr abgeleiteten Klassen. Im Unterschied zu Java sind mit protected gekennzeichnete Elemente (ohne weitere Definition) nicht innerhalb Ihres eigenen Packages für andere Klassen sichtbar.

Bei den Sichtbarkeitsmodifizierern private und protected kann innerhalb folgenden, rechteckigen Klammern, die Sichtbarkeit feiner eingestellt bzw. ausgeweitet werden. So ist es z.B. möglich die Sichtbarkeit auf eine fest definierte andere Klasse (oder ein anderes Package) auszuweiten (private[X] def ...). Auch das Einschränken der Sichtbarkeit auf das eigene Objekt ist mit private[this] ... möglich, sodass andere Objekte der gleichen Klasse nicht auf das jeweilige Element zugreifen können.

Insgesamt lässt sich feststellen, dass die Sichtbarkeit in Scala wesentlich feiner eingestellt werden kann, als dies bei Java der Fall ist.

class ScalaVisibility {
  // Sichtbarkeitsstufe public: von überall aufrufbar
  def myPublicFunction : Int = 1
  
  // Sichtbarkeitsstufe protected: Nur für die eigene und abgeleitete
  // Klassen sichtbar
  protected def myProtectedFunction : Int = 2
  
  // Sichtbarkeitsstufe private: nur innerhalb der eigenen Klasse
  // sichtbar
  private def myPrivateFunction : Int = 3
}
          
class myTestClass{
  val visio = new ScalaVisibility
  
  println(visio.myPublicFunction)         // OK
  println(visio.myProtectedFunction)      // Fehler
  println(visio.myPrivateFunction)        // Fehler
}
          
class myTestClass2 extends ScalaVisibility{
  val visio = new ScalaVisibility
  
  println(visio.myPublicFunction)        // OK     
  println(visio.myProtectedFunction)     // Fehler
  println(visio.myPrivateFunction)       // Fehler
  println(myProtectedFunction)           // OK
  println(myPrivateFunction)             // Fehler
}
itmapa.de - X2H V 0.18



Packages

Selbst in kleineren Projekten steigt die Anzahl der verwendeten Klassen, Traits, Singleton Objekte ... schnell auf ein Maß an, dass die Übersichtlichkeit darunter leidet. Um nun den Überblick zu behalten, bietet es sich an, Klassen in Packages zu organisieren. Die Package-Struktur entspricht dabei der Struktur aktueller Dateisysteme, wo eine Baumstruktur aufgebaut wird und die Elemente in den Unterschiedlichen Ebenen abgelegt werden. Alle Elemente, die sich in einer Ebene befinden, haben dabei eine zusammengehörige Aufgabe.

Auch wenn in Scala weit aus flexibler mit Packages gearbeitet werden kann, bietet es sich für den Anfang an, die Vorgehensweise von Java zu übernehmen und jede Klasse (Trait ...) am Anfang des Quelltextes einem Package zuzuordnen.

package de.scalatutorial.berechnung.helfer
         
class Rechenknecht {
  // Inhalt der Klasse
}
itmapa.de - X2H V 0.18

Die Definition eines Packages beginnt mit dem Schlüsselwort package gefolgt vom Package-Namen. Die Namen der einzelnen Baumebenen werden durch einen Punkt voneinander getrennt. Im obigen Beispiel definieren wir ein Package in der vierten Ebene. Vorausgesetzt unser Quelltext Root Verzeichnis entspricht C:\projekte\scalatutorial\src entspricht die obige Package Definition dem Verzeichnis C:\projekte\scalatutorial\de\scalatutorial\berechnunug\helfer .

Gibt man keine Package-Definition im Quelltext an, so befinden sich die enthaltenen Elemente im sogenannten Default-Package. Auch wenn die meisten Beispiele hier auf scalatutorial.de ohne Package Angabe gezeigt werden (dies dient der Kürze und Verständlichkeit), wird von dessen Verwendung in realen Projekten abgeraten.

Eine Konvention besagt, dass Package Namen so gewählt werden sollen, dass Sie der umgekehrten qualifizierten Schreibweise einer zugehörigen Web-Präsenz entsprechen. Dem entspricht, dass die Beispiele auf scalatutorial.de als Basis Package de.scalatutorial haben sollten. Hintergrund dieser Konvention ist, dass damit sichergestellt werden kann, dass die Verwendung von Klassen (Traits ...) aus verschiedenen Quellen, nicht zu Namenskonflikten führen kann. Der vollständig qualifizierende Namen einer Klasse besteht zum Beispiel aus Package Namen und Klassen Namen. Der voll qualifizierende Name der Klasse aus obigem Beispiel ist somit de.scalatutorial.berechnung.helfer.Rechenknecht . Auch wenn diese Konvention nicht für alle Projekte verwenden kann, sollte man Sie jedoch in Projekten anwenden, dessen Quellen man für andere Projekte (anderer Personen, Teams) zur Verfügung stellen möchte.



Elemente (Klassen ...) importieren

Um Klassen (Objekte ...) verwenden zu können, müssen diese entweder mit Ihrem voll qualifizierenden Namen angegeben werden oder zuvor in den Sichtbarkeitsbereich für die entsprechenden Quelltextteil geholt werden.

Eine Klasse kommt in Scala durch einen der folgenden zwei Mechanismen in den Sichtbarkeitsbereich, so dass wir diese ohne voll qualifizierenden Namen verwenden können.

  • Expliziter Import
  • Automatischer Import

Expliziter Import

Die meisten Elemente einer Scala-Blibliothek wie Klassen oder Traits können wir nicht direkt, ohne Angabe des Packages, in unserem Quelltext verwenden. Beispielsweise können wir keine Instanz der Klasse scala.collection.mutable.ListBuffer direkt in der REPL (Scala Interpreter) erzeugen.

scala> val listBuffer = new ListBuffer[String]
<console>:7: error: not found: type ListBuffer
       val listBuffer = new ListBuffer[String]
itmapa.de - X2H V 0.18

Um dies zu tun, können wir den voll qualifizierten Namen, d.h. Package-Name plus Klassenname, verwenden.

scala> val listBuffer = new scala.collection.mutable.ListBuffer[String]
listBuffer: scala.collection.mutable.ListBuffer[String] = ListBuffer()
itmapa.de - X2H V 0.18

Jedes Mal den voll qualifizierten Namen anzugeben ist nicht nur aufwendig, sondern führt auch zu schwerer lesbaren Quelltext. Möchten wir nun die Klasse ListBuffer, ohne Angabe des Packages, verwenden, müssen wir diese zunächst in den Sichtbarkeitsbereich unseres aktuellen Kontextes (hier REPL) holen. Helfen tut uns hierbei das Schlüsselwort import, mit dem wir Klassen in den Sichtbarkeitsbereich holen können. Um nun die Klasse ListBuffer in den Sichtbarkeitsbereich zu holen, verwenden wir import, gefolgt vom Namen der zu importierenden Klasse. Da noch keine (hier helfenden) import-Anweisungen ausgeführt worden sind, müssen wir den voll qualifizierten Namen der Klasse angeben. Nachdem wir dies getan haben, können wir von der Klasse ListBuffer Instanzen (Objekte), ohne Angabe des Packages, erzeugen.

scala> import scala.collection.mutable.ListBuffer
import scala.collection.mutable.ListBuffer
          
scala> val listBuffer = new ListBuffer[String]
listBuffer: scala.collection.mutable.ListBuffer[String] = ListBuffer()
         
itmapa.de - X2H V 0.18

Möchten wir mit mehreren Elementen (z.B. Klassen) eines Packages arbeiten, können wir alle Elemente eines Paketes mit dem Namen des Packages, gefolgt vom Unterstrich, in den aktuellen Sichtbarkeitsbereich holen.

scala> import scala.collection.mutable._
import scala.collection.mutable._
          
scala> val aLinkedList = new LinkedList[String]
aLinkedList: scala.collection.mutable.LinkedList[String] = LinkedList()
itmapa.de - X2H V 0.18

Möchten wir mehrere Elemente eines Packages, aber nicht alle, importieren, können wir das in Scala mit einem einzigen import-Statement tun. Dazu geben wir wie gewohnt zunächst das import-Statement gefolgt vom Packagenamen und einem abschließenden Punkt an. Danach geben wir die zu importierenden Elemente des Packages in geschweiften Klammern durch ein Komma getrennt an. Im nachfolgenden Beispiel importieren wir die Klassen ListBuffer, HashMap und HashSet des Packages scala.collection.mutable. Alle anderen Elemente (Klassen) werden durch dieses import-Statement nicht importiert.

scala> import scala.collection.mutable.{ListBuffer, HashMap, HashSet}
import scala.collection.mutable.{ListBuffer, HashMap, HashSet}
itmapa.de - X2H V 0.18

Automatischer Import

Einige Elemente (Klassen ...) sind so elementar, dass sie nicht explizit importiert werden müssen. Elemente, die in folgenden Packages (Singleton Objekt) definiert sind werden automatisch importiert und stehen somit ohne explizitem Import immer zur Verfügung.

  • Package: scala
  • Singleton Object: Scala.Prefed
  • Package: java.lang


Sichtbarkeit ausweiten bzw. einschränken

Mithilfe der Schlüsselwörter protected und private können wir die Sichtbarkeit von Klassen/Objekten und Methoden/Funktionen (auch Konstruktoren) feiner einstellen als oben einleitend erklärt. Um dies zu erreichen, geben wir in eckigen Klammern an, wie wir die Sichtbarkeit verfeinern wollen.

Verfeinerung von private mittels Paketangabe

Geben wir in den rechteckigen Klammern hinter private einen Paketnamen an, so bedeutet dies, dass wir die Sichtbarkeit auf das angegebene Paket und all dessen Unterpakete erweitern. Zu beachten ist hier, dass das Element sich im angegebenen Paket oder in einem dessen Unterpakete der entsprechenden Klasse befindet. Pakete, die nicht im Paketpfad der aktuellen Klasse liegen, können nicht zur Verfeinerung herangezogen werden. Weiter zu beachten ist, dass wir nur den Namen des "tiefsten" Paketes angeben und nicht den voll qualifizierenden Namen des Paketes. Sehen wir uns dazu Beispiele an:

private[apackage] def aMethod = ...
          
class AClass private[apackage] = ...
itmapa.de - X2H V 0.20

Auf die Methode aMethod können wir aus der eigenen Klasse (bzw. Companion Object) zugreifen, sowie aus allen Klassen (Objekten) die sich im Paket apackage und deren Unterpakete befinden. Den Primärkonstruktor der Klasse AClass können wir aus der eigenen Klasse (bzw. Companion Object) und aus allen Elementen im und unterhalb des Pakets apackage aufrufen.

Verfeinerung von private mittels Klassenangabe

Eine Klasse in rechteckigen Klammern bedeutet, dass wir die Sichtbarkeit des Elements auf die entsprechende Klasse verfeinern. Zu beachten ist hier, dass die zu erweiternde Klasse hier innerhalb der angegebenen Klasse definiert ist. Klassen, welche die aktuelle Klasse nicht umgeben, sind bei private zur Verfeinerung der Sichtbarkeit nicht erlaubt.

Sehen wir uns nun ein Beispiel an, wo wir die Konstruktion eines Objektes durch Einschränkung des Konstruktors verfeinern.

class B {
  val myC = new C()
  val myD = new myC.D()
    
  class C {
    val myD = new D()
    val myE = new myD.E()       // Fehler
    val myF = new myD.F()       // OK
    class D {
      val myInnerE = new E()    // OK
      class E private[D](){}
      class F() {}
    }
  }
}
itmapa.de - X2H V 0.20

Im Beispiel ist es möglich, ein Objekt der KIasse F außerhalb der Klasse D zu erzeugen. Die Einschränkung der Klasse E führt dazu, dass das Objekt nicht außerhalb der Klasse D erzeugt werden kann (es gibt nur den Primärkonstruktor und keine Factories).

Im nachfolgenden Beispiel können die E-Objekte zwar außerhalb von D erzeugt werden jedoch ist der Zugriff auf die Methode foo versperrt.

class B {
  val myC = new C()
  val myD = new myC.D()
    
  class C {
    val myD = new D()
    val myE = new myD.E()       // OK
    myE.foo()                   // Fehler
    val myF = new myD.F()       // OK
    myF.foo()                   // OK
    class D {
      val myInnerE = new E()    // OK
      class E() { 
        private[D] def foo(): Unit = println("I do something")
      }
      class F() {
        def foo(): Unit = println("I do something")
      }
    }
  }
}
itmapa.de - X2H V 0.20

Vollsperrung mit private[this]

Geben wir in den rechteckigen Klammern das Schlüsselwort this an, so bedeutet dies, dass das Element nur aus dem eigenem Objekt aus aufgerufen werden kann. Der Zugriff ist also auf die eigene Referenz begrenzt. Ein Zugriff aus Referenzen der gleichen Klasse bzw. aus dem Companion Objekt ist nicht möglich.

object Vollsperrung {
  def main(args: Array[String]): Unit = {
    val test1 = new TestClass()
    val test2 = new TestClass() 
  }
}
          
class TestClass {
  private def foo1(): Unit = println("Foo 1")
  
  private[this] def foo2(): Unit = println("Foo 2")
  
  def testIt1(that: TestClass): Unit = that.foo1()    // OK  
  def testIt2(that: TestClass): Unit = that.foo2()    // Fehler
}
itmapa.de - X2H V 0.20

Die Verfeinerung mit [this] kann auch bei Konstruktoren angegeben werden. Geben wir diese Verfeinerung bei einem Primärkonstruktor an, so müssen wir nicht private Hilfskonstruktoren anbieten, da ansonsten eine Objekterzeugung unmöglich wird.



Zusammenfassung

Die nachfolgende Tabelle fasst die wichtigsten Regeln zur Sichtbarkeit zusammen.

Modifizierer Bedeutung
ohne (kein Modifizierer) Standardsichtbarkeit öffentlich
private Nur sichtbar für Elemente dieser Klasse
protected Sichtbar in der aktuellen Klasse und allen abgeleiteten Klassen
private[package] Sichtbar in der aktuellen Klasse und allen abgeleiteten Klassen im selben Package oder Unterpackage
private[class] Sichtbar in der aktuellen Klasse und allen abgeleiteten Klassen umgeben von der angegebenen Klasse
private[this] Nur Sichtbar innerhalb der selben Referenz