Jede Klasse hat genau einen Primärkonstruktor, der zur Konstruktion des Objektes aufgerufen werden muss. Die Parameter des Primärkonstruktors werden direkt nach dem Klassennamen in runden Klammern angegeben. Diese Parameter werden auch als Klassenparameter bezeichnet. Auf die Klassenparameter kann innerhalb der gesamten Klasse zugegriffen werden.
class MySimpleClass(paramA: Int, paramB: Double) { // Inhalt des Primaerkonstruktors val myParam = paramA * paramB }
Der Rumpf einer Klasse stellt den Inhalt des Primärkonstruktors dar. Dieser wird dementsprechend bei der Konstruktion einer Klasse aufgerufen.
Neben dem Primärkonstruktor stehen in Scala Hilfskonstruktoren (eng. auxiliary constructor) als Einstiegspunkt zur Objekterzeugung in Scala zur Verfügung.
Ein Hilfskonstruktor (engl. auxiliary constructor) muss im ersten Schritt immer einen weiteren Konstruktor aufrufen. Dies kann ein weiterer Hilfskonstruktor oder der Primärkonstruktor sein. Zu beachten ist hier, dass wenn ein weiterer Hilfskonstruktor aufgerufen werden soll, dass dieser im Quelltext vor dem aufrufenden Konstruktor definiert werden muss. Diese Vorgehensweise führt auch dazu, dass der Primärkonstruktor immer aufgerufen werden muss.
Die Definition eines Hilfskonstruktors entspricht der Definition einer Methode. Anstelle eines frei wählbaren Methodennamens muss an dessen Stelle das Schlüsselwort
this
verwendet werden. Das nachfolgende Beispiel zeigt die Verwendung eines Hilfskonstruktors.
object Test{ def main(args: Array[String]){ val v1 = new MyAuxiliaryConstructor(3,2) val v2 = new MyAuxiliaryConstructor(4) } } class MyAuxiliaryConstructor(value1: Int, value2: Int) { println(value1+" "+value2) def this(value1: Int) = { this(value1,1) } }
Im vorangegangenen Beispiel wird in der
main
-Methode zunächst ein Objekt
v1
vom Typ
MyAuxiliaryConstructor
über den Primärkonstruktor erzeugt. Im Anschluss wird ein Objekt
v2
über den Hilfskonstruktor der Klasse erzeugt. Die erste Anweisung im Hilfskonstruktor ist der Aufruf des Primärkonstruktor der Klasse
MyAuxiliaryConstructor
.
Der Ablauf des Beispieles führt zu folgender Ausgabe auf der Systemausgabe:
3 2 4 1
Wie bereits erwähnt, muss ein Scala Hilfskonstruktor nicht unbedingt den Primärkonstruktor aufrufen, sondern kann einen anderen Hilfskonstruktor aufrufen. Am Ende einer solchen Konstruktorenverkettung muss allerdings der Primärkonstruktor aufgerufen werden.
Das nachfolgende Beispiel
ConstructorChain
zeigt beispielhaft eine Konstruktorenverkettung.
class ConstructorChain(value1: Int, value2: String, value3: Double) { println(value1+" "+value2+" "+value3) def this (value1: String, value2: Double) = this(1,value1,value2) def this (value: String) = this(value,2.) def this () = this("Test") } object Starter { def main(args: Array[String]) { new ConstructorChain() new ConstructorChain("Hallo") new ConstructorChain("Scala rockt",3.) new ConstructorChain(4,"Dies ist ein Test",7) } }
Der Ablauf dieses Programmes führt zu folgender Ausgabe:
1 Test 2.0 1 Hallo 2.0 1 Scala rockt 3.0 4 Dies ist ein Test 7.0
Hat eine zu erweiternde Klasse zumindest einen nicht-leeren Konstruktor, muss die erweiternde Klasse einen Konstruktor der Basisklasse aufrufen. Dies geschieht, indem man nach dem
extends
Schlüsselwort und dem Namen der zu erweiternden Klasse die Parameter in runden Klammern angibt. Das nachfolgende Beispiel zeigt diese Praxis, wo die Klasse B den Primärkonstruktor der Basisklasse A aufruft.
class A (value1: Int, value2: Double){ def foo = value1 * value2 } class B extends A(3,4.5){ println(foo) }
Die erweiternde Klasse muss nicht den Primärkonstruktor der Basisklasse aufrufen, sondern kann auch jeden definierten Hilfskonstruktor aufrufen. Das nachfolgende Beispiel zeigt diese Praxis.
class C (value1: Int, value2: Double){ def this(value3: Int) = this(value3,4.6) def foo = value1 * value2 } class D extends C(5){ println(foo) }
Auf Konstruktor Argumente kann innerhalb der gesamten Klasse zugegriffen werden. Werden im Konstruktor nur Name und Typ des Arguments angegeben, kann auf die Argumente von außerhalb der Klasse nicht zugegriffen werden. Weiterhin ist eine Veränderung der Argumente auch innerhalb der Klasse nicht möglich.
Das nachfolgende Beispiel zeigt eine Klasse, wo auf die Konstruktor Argumente von außen nicht zugegriffen werden kann.
class ConstructorArguments(a: Int, b: Double){ def foo: Double = a * b }
Der Scala Compiler gibt uns Möglichkeiten, für die Konstruktor Argumente automatisch "Getter" und "Setter" Methoden zu erzeugen. Setzen wir vor einem Konstruktor Argument das Schlüsselwort
val
erzeugt der Compiler, für das Konstruktor Argument, automatisch eine "Getter"-Methode mit dem Namen des Konstruktor Arguments. Wird vor dem Konstruktor Argument das Schlüsselwort
var
angegeben, wird das Argument veränderbar und erhält eine Standard "Setter"-Methode.
Sehen wir uns nun folgende einfache Scala Klasse an:
class ConstructorTest(valueA: Int, valueB: Double, valueC: String)
Disassemblieren wir die Klasse mit
javap
sehen wir, das wir eine einfache Klasse mit einem Konstruktor erhalten. Die Klasse enthält weder
Setter
noch
Getter
- Methoden.
Compiled from "ConstructorTest.scala" public class ConstructorTest extends java.lang.Object implements scala.ScalaObject{ public ConstructorTest(int, double, java.lang.String); }
Setzen wir nun vor der Variablen
valueA
das Schlüsselwort
val
und vor der Variablen
valueB
das Schlüsselwort
var
an:
class ConstructorTest(val valueA: Int, var valueB: Double, valueC: String)
Kompilieren wir die Klasse erneut und disassemblieren Sie mit
javap
sehen wir, dass die Variable
valueA
eine
Getter
-Methode erhalten hat. Die Variable
valueB
hat sowohl eine
Getter
als auch eine
Setter
- Methode erhalten. Die Variable
valueC
hat keine
Getter
und keine
Setter
-Methode.
Compiled from "ConstructorTest.scala" public class ConstructorTest extends java.lang.Object implements scala.ScalaObject{ public int valueA(); public double valueB(); public void valueB_$eq(double); public ConstructorTest(int, double, java.lang.String); }
Das nachfolgende Beispiel zeigt den fehlerhaften Versuch, auf einfache Klassenparameter als Feld zuzugreifen die nicht mit
val
oder
var
gekennzeichnet sind.
object MainClass { def main(args: Array[String]) { val aClass = new AClass(1,"123") println(aClass.value1) // Fehler println(aClass.value2) // Fehler aClass.value2 = "abc" // Fehler } } class AClass(value1: Int, value2: String)
Eine Möglichkeit das Problem zu lösen wäre die Bereitstellung von entsprechenden Settern und Gettern. Da Variable und Methoden (Funktionen) sich den gleichen Namensraum teilen, müssten wir zusätzliche Bezeichner einführen. Innerhalb der Klasse hätten wir dann zwei Namen für das gleiche Element.
Durch voranstellen von
val
bzw.
var
erreichen wir, dass die entsprechenden Zugriffsmethoden erzeugt werden.
Im nachfolgenden Beispiel verwenden wir diese Vorgehensweise im Konstruktor von
AClass
, sodass der Compiler das Programm fehlerfrei übersetzt.
object MainClass { def main(args: Array[String]) { val aClass = new AClass(1,"123") println(aClass.value1) // OK println(aClass.value2) // OK aClass.value2 = "abc" // OK } } class AClass(val value1: Int, var value2: String)
Wie bei Funktionen können wir in Konstruktoren benannte Argumente verwenden. Auch eine Mischung aus benannten Argumenten und Argumenten, die über Ihre Reihenfolge beim Aufruf definiert werden, ist möglich. Wurde jedoch einmal ein Argument über den Namen bestimmt, können die weiteren Argumente nur noch über Ihren Namen belegt werden.
object MainClass{ def main(args: Array[String]) { new A(1, 2, v4=3, v3=4) } } class A(v1: Int, v2: Int, v3: Int, v4: Int) { println(v1+" "+v2+" "+v3+" "+v4) }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
1 2 4 3
Wie bei Funktionen können wir in Konstruktoren Standardargumente definieren. Wird ein Argument bei einem Konstruktorenaufruf nicht übergeben, so wird der Wert auf den Standard gesetzt, welcher in der Argumentenliste definiert wird. Standardargumente eignen sich hervorragend Hilfskonstruktoren zu vermeiden deren Hauptaufgabe ist Standardwerte an den Primärkonstruktor übergeben werden. Im nachfolgenden Beispiel werden die Konstruktorargumente
value1
und
value2
als Standardargumente definiert.
object MainObject { def main(args: Array[String]) { new StandardArgument(1,2,3) new StandardArgument(1,2) new StandardArgument(1) } } class StandardArgument(value1: Int, value2: Int = 4, value3: Int = 5) { println(value1+" "+value2+" "+value3) }
Der Ablauf des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
1 2 3 1 2 5 1 4 5
Sofern in der Konstruktorenargumentliste Argumente mit und ohne Standardwerte verwendet werden, ist darauf zu achten, dass die Argumente mit Standardwert möglichst am Ende der Liste definiert werden. Ist dies nicht der Fall, kann (außer beim Einsatz benannter Argumente) nicht entschieden werden, welche Elemente beim Aufruf gemeint sind und die Standardwerte verlieren ihre Hilfskraft.
Eine Vielzahl von Hilfskonstruktoren dienen der Konstruktion der Objekte unter Verwendung von Standardwerten. Wie in den vorherigen Abschnitten gesehen, können wir Standardwerte definieren und auch Argumente benannt übergeben. Diese Werkzeuge können uns helfen, die Anzahl der Hilfskonstruktoren merklich zu verringern.
Sehen wir uns zunächst ein Beispiel mit Hilfskonstruktoren an.
class Constructors { new A(-1,-2,-3) new A(-1,-2) new A(-1) new A() } class A(v1: Int, v2: Int, v3: Int) { println("v1: "+v1+" ,v2: "+v2+" ,v3: "+v3) def this(v1: Int, v2: Int) = this(v1,v2,3) def this(v1: Int) = this(v1,2) def this() = this(1) }
Durch einfache Verwendung von Standardargumenten können in unserem Beispiel alle Hilfskonstruktoren überflüssig gemacht werden.
class Constructors { new A(-1,-2,-3) new A(-1,-2) new A(-1) new A() } class A(v1: Int = 1, v2: Int = 2, v3: Int = 3) { println("v1: "+v1+" ,v2: "+v2+" ,v3: "+v3) }
Nicht immer möchten wir die Konstruktoren einer Klasse für alle Klassen eines Systems sichtbar haben. Möchten wir z.B. den Konstruktor nur aus der eigenen Klasse / aus dem zugehörigen Companion Objekt oder nur innerhalb der Vererbungshirarchie aufrufen, helfen die Sichtbarkeitsmodifizierer
private
und
protected
weiter. Mit diesen Sichtbarkeitsmodifizierern können wir auch die Sichtbarkeit auf Packetebene (engl. Package) definieren.
Diese Sichtbarkeitsmodifizierer sind nicht auf Konstruktoren beschränkt, sondern Sie finden auch Verwendung im Zusammenhang mit Funktionen/Methoden oder Feldern (Konstruktoren sind ja auch nur spezielle Methoden/Funktionen).
Stellen wir uns nun die Frage, wo wir die Sichtbarkeitsmodifizierer im Quelltext platzieren sollen, um die Sichtbarkeit der Konstruktoren einzuschränken. Bei Hilfskonstruktoren ist es einfach. Wir stellen den Sichtbarkeitsmodifizierer (wie bei "normalen" Methoden / Funktionen) vor dem einleitenden
def
des Hilfskonstruktors.
private def this(a: Int) = // Konstruktoren Inhalt
Nun stellt sich die Frage, an welcher Stelle wir den Sichtbarkeitsmodifizierer bei Primärkonstruktoren platzieren, die bekanntlich kein einleitendes
def
haben. Die Antwort ist, wir platzieren ihn direkt vor der Parameterliste des Primärkonstruktors. Ist keine Parameterliste (was in etwa einer leeren Parameterliste entspricht) vorhanden, plazieren wir den Modifizierer direkt an der Stelle (oder vor der Stelle) der "imaginären" Parameterliste.
class A private(a: Int) { } class B protected { }
Die Bedeutug der Sichtbarkeitsmodifizierer ist gleich der Bedeutung der Sichtbarkeitsmodifizierer für Funktionen und Methoden.
Nachfolgend noch ein vollständiges Beispiel zu Sichtbarkeit und Konstruktoren.
object PrivatePrimaryConstructor { def main(args: Array[String]): Unit = { new A(42,"-1") // Error new A(4711) // OK } } object A { def createAnA(): A = { new A(42,"42") // OK } } class A private(aValue: Int, aString: String) { println(aValue+" "+aString) def this(a: Int) = this(a,"Nummer 5 lebt") // OK }