Scala ist eine vollständig objektorientierte Programmiersprache. In Scala wird, wie auch in anderen objektorientierten Sprachen, der Bauplan für Objekte mit Hilfe von Klassen definiert. Im Gegensatz zu Java gibt es in Scala keine primitiven Datentypen. In Scala gibt es entsprechend vollwertige Objekte, die deren Aufgaben übernehmen.
Eingeleitet wird eine Klasse im Quelltext mit einem der beiden Schlüsselwörtern
class
oder object
. Klassen, die mit dem Schlüsselwort
class
eingeleitet werden, sind Klassen von denen beliebig viele
Instanzen erzeugt werden können. Statische Methoden bzw. Variablen gibt
es in Scala nicht. Dort schafft die Einleitung einer Klasse mit dem Schlüsselwort
object
Abhilfe. Von Klassen, die mit dem Schlüsselwort object
eingeleitet werden, existiert höchstens ein einziges Objekt (keins, wenn es
noch nicht verwendet wurde) und wird beim erstmaligen Gebrauch initialisiert.
In Scala werden diese Objekte als "Singleton-Object" bezeichnet.
Der Aufbau des Quelltextes einer Klasse entspricht dem Aufbau in Java.
Sofern die Klasse sich nicht im "default-Package" befindet, wird
das Package angegeben, indem sich die Klasse befindet. Anschließend
werden die importierten Klassen angegeben. Danach folgt die Einleitung der
Klasse mit dem Schlüsselwort class
oder object
gefolgt vom Namen der Klasse. Abschließend wird im Rumpf von
geschweiften Klammern der Quelltext der Klasse definiert.
Innerhalb des Rumpfes werden die Variablen und Methode der Klasse definiert.
package test import de.test.Test class TestClass{ // Klassenquelltext }
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.
object TestObject { // Inhalt des Singleton Objektes }
Der Rumpf einer Klasse stellt den Inhalt des Primärkonstruktors dar. Dieser wird dementsprechend bei der Konstruktion einer Klasse aufgerufen.
Dem Thema Konstruktoren ist in diesem Tutorial ein eigenes Kapitel gewidmet.
Scala kennt keine statischen Methoden und Variablen. Abhilfe schaffen
hier sogenannte Singleton Objekte. Die Definition eines
Singleton Objektes erfolgt ähnlich der Definition einer Klasse.
Hauptunterscheidungsmerkmal ist, dass eine Singleton Objekt Definition
mit dem Schlüsselwort object
statt dem Schlüsselwort
class
eingeleitet wird.
object TestObject { // Inhalt des Singleton Objektes }
Da Singleton Objekte nicht mit new
angelegt werden, können diese auch nicht mit Parameter
über einen Konstruktor initialisiert werden. Die Definition
von Funktionen/Methoden und Variablen innerhalb eines Singleton
Objektes ist identisch der Definition innerhalb von Klassen.
object ScalaObject { val b = 5 def test(a: Double) : Double = { 3.* + b } }
Scala Klassen kennen keine statischen Elemente. Abhilfe schafft hier eine Zusammenfassung von Scala Singleton Objekten und Klassen. Definiert man ein Singleton Objekt und eine Klasse mit gleichem Namen, können diese gegenseitig auf spezielle weise aufeinander zugreifen. Beispielsweise können diese gegenseitig auf ihre privaten Elemente zugreifen. Voraussetzung ist jedoch, dass das Singleton Objekt und die Klasse in der gleichen Quelltextdatei definiert werden.
Bei einer derartigen Kombination bezeichnet man das Singleton Objekt auch als companion object (Begleitobjekt) und die Klasse als companion class (Begleitklasse). Singleton Objekte, die keine companion class Klasse haben, werden als standalone object (Einzelobjekt) bezeichnet.
Die Initialisierung eines Singleton Objektes findet bei der ersten Verwendung desselben statt.
Das nachfolgende Beispiel zeigt ein companion object
FussballSpiel
mit der zugehörigen companion class
FussballSpiel
.
object FussballSpiel { val dauer = 90 def restSpielZeit(spielZeit: Int): Int= { dauer - spielZeit } } class FussballSpiel{ var toreHeim = 0 var toreGast = 0 var spielZeit = 0 def torDifferenz(): Int={ Math.abs(toreHeim-toreGast); } def restSpielZeit(): Int= { FussballSpiel.restSpielZeit(spielZeit) } }
Case classes bieten dem Programmierer einige Annehmlichkeiten, die implizit vom Compiler vorgenommen werden:
new
zu erzeugen.
val
- Variable der Klasse.
toString
, hashCode
und equals
.
new
instanziiert werden.
copy
Methode.
Eine case class wird durch voranstellen des Schlüsselwortes case
vor dem einleitenden Schlüsselwortes class
definiert.
Das nachfolgende Beispiel zeigt die Definition einer case class Person
:
case class Person (firstName : String, lastName : String, age : Int){ def isAdult : Boolean = if (age >= 18) true else false }
Ein Unterschied zwischen "normalen" und case
Klassen lässt
sich auch am Ergebnis des Kompiliervorgangs betrachten. Kompilieren wir hierzu
folgenden Scala Quelltext (der Name der zu kompilierenden Datei kann frei gewählt werden):
class A1(v1: Int, v2: Double) case class A2(v1: Int, v2: Double)
Mit Ausnahme des case
Schlüsselwortes sind die Klassen
A1
und A2
identisch. Sieht man sich die erzeugten
.class
Dateien an, sieht man, das zur Klasse A2
zusätzlich eine .class
Datei mit dem Namen
A2$.class
erzeugt wurde. Diese Datei enthält
das zur case class
automatisch generierte Begleitobjekt
(engl. companion object). Der Unterschied zwischen A1
und A2
kann durch die Verwendung des Java Disassembler javap
sichtbar gemacht
werden. Die Verwendung von javap
ist möglich, da Scala
Klassen zu gewöhnlichen Klassen für die JVM kompiliert werden,
die wie gewöhnliche Java Klasses disassembliert werden können.
Die Ausführung von javap A1
zeigt folgenden Inhalt für A1
:
Compiled from "ScalaTest.scala" public class A1 extends java.lang.Object implements scala.ScalaObject{ public A1(int, double); }
Die Ausführung von javap A2
und javap A2$
zeigt das Ergebnis der Kompilierung der Klasse A2
:
Compiled from "ScalaTest.scala" public class A2 extends java.lang.Object implements scala.ScalaObject,scala.Product,java.io.Serializable{ public static final scala.Function1 tupled(); public static final scala.Function1 curry(); public static final scala.Function1 curried(); public scala.collection.Iterator productIterator(); public scala.collection.Iterator productElements(); public double copy$default$2(); public int copy$default$1(); public int v1(); public double v2(); public A2 copy(int, double); public int hashCode(); public java.lang.String toString(); public boolean equals(java.lang.Object); public java.lang.String productPrefix(); public int productArity(); public java.lang.Object productElement(int); public boolean canEqual(java.lang.Object); public A2(int, double); } Compiled from "ScalaTest.scala" public final class A2$ extends scala.runtime.AbstractFunction2 implements scala.ScalaObject{ public static final A2$ MODULE$; public static {}; public scala.Option unapply(A2); public A2 apply(int, double); public java.lang.Object apply(java.lang.Object, java.lang.Object); }
Das Disassemblieren mit javap
zeigt, dass bei case
Klassen eine Fülle von Methoden/Funktionen vom Scala Compiler automatisch
generiert werden.
Neben Ihren funktionalen Eigenschaften ist Scala eine objektorientierte Programmiersprache. Und wie in allen objektorientierten Programmiersprachen spielt die Vererbung eine wesentliche Rolle. Vererbt eine Klasse Ihre Eigeneschaften (Variable und Funktionen/Methoden), so spricht man auch vom Ableiten von einer Klasse. Der Vorgang des Ableitens wird auch als Spezialisierung und der Vorgang der Bildung einer Superklasse wird als Generalisierung bezeichnet.
Klassen, die von anderen Klassen Eigenschaften erben, werden wie folgt bezeichnet:
Dem entgegengesetzt werden Klassen, die Ihre Eigenschaften vererben wie folgt bezeichnet:
Die unterschiedlichen Begriffe deuten nicht auf unterschiedliche Bedeutungen hin, sondern sind synonym zu verstehen.
Um in Scala von einer Klasse abzuleiten, wird das Schlüsselwort extends
verwendet. Zunächst erfolgt die Einleitung der Klasse mit dem Schlüsselwort class
,
gefolgt vom Namen der Klasse und ggf. einer Parameterliste. Bevor nun die geschweifte Klammer den
Inhalt der Klasse einleitet, wird das Schlüsselwort extends
gefolgt
vom Namen der abzuleitenden Klasse angegeben. Das nachfolgende Beispiel zeigt die Ableitung einer
Klasse A
durch eine Klasse namens B
.
class B (arg: Int) extends A { // Inhalt der Klasse }
Bei der Vererbung werden alle Variablen und Funktionen/Methoden, die nicht als private
gekennzeichnet sind an die Subklasse vererbt. Die Subklasse erbt demnach die Eigenschaften der Vaterklasse.
Die Subklasse muss sich jedoch nicht mit den Eigenschaften der Vaterklasse abfinden. Die Subklasse
kann die Eigenschaften ändern, indem Sie die entsprechenden Variablen überschattet
bzw. Funktionen/Methoden überschreibt.
Beim Überschreiben einer Methode definieren wir in der Subklasse eine Methode mit der gleichen
Signatur (Name und Argumentenliste) wie in der Vaterklasse. Immer wenn nun auf einem Objekt der Kindklasse
die Methode aufgerufen wird, wird nicht mehr die Methode der Vaterklasse verwendet. Diese Eigenschaft
der Vaterklasse wurde verändert. In Scala müssen wir (im Gegensatz zu anderen Sprachen)
an einer überschreibenden Funktion/Methode zusätzlich das Schlüsselwort
override
angeben, um den Compiler unsere Absicht mitzuteilen.
Im nachfolgenden Beispiel überschreiben wir in der Klasse SubClass
die Methode
foo2()
der Vaterklasse BaseClass
.
object Overload { def main(args: Array[String]): Unit = { val baseClass = new BaseClass baseClass.foo1() baseClass.foo2() val subClass = new SubClass subClass.foo1() subClass.foo2() subClass.foo3() } } class BaseClass { val rrr = 2 def foo1(): Unit = println("BaseClass: foo1") def foo2(): Unit = println("BaseClass: foo2") } class SubClass extends BaseClass { override def foo2(): Unit = println("SubClass: foo2") def foo3(): Unit = println("SubClass: foo3") }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
BaseClass: foo1 BaseClass: foo2 BaseClass: foo1 SubClass: foo2 SubClass: foo3
Die Angabe des Schlüsselwortes override
mag zunächst als Overhead
zu anderen Sprachen gesehen werden, bietet uns jedoch folgende Wertvolle Vorteile. Zunächst
stellen wir sicher, dass wir auch eine Funktion/Methode einer Basisklasse ändern und nicht
versehentlich eine falsche Signatur verwenden. Anderseits schützt uns diese Vorschrift
vor ungewolltem Überschreiben, indem wir versehentlich eine Funktion/Methode einer
Vaterklasse überschreiben. Das Schlüsselwort override
gibt uns dem
eine erhöhte Sicherheit während des Programmierens.
Das Überschatten von Variablen in einer Basisklasse erfolgt analog zum Überschreiben
einer Funktion/Methode. In der Subklasse definieren wir eine Variable mit dem Namen einer
Variable in der Vaterklasse und kennzeichnen diese mit override
als
überschattend (auch wenn override
hier anders übersetzt werden kann).
final
In manchen Fällen möchten wir vermeiden, dass Variable, Funktionen/Methoden oder
ganze Klassen abgeleitet werden können. In diesen Fällen hilft uns das Schlüsselwort
final
weiter. Geben wir dieses Schlüsselwort bei einer Variablen, Funktion/Methode
oder Klasse an, ist es nicht möglich dieses von diesem Element abzuleiten. Versuchen wir es
doch quittiert das der Compiler mit einer Fehlermeldung und der Quelltext wird nicht compliliert.
Versuchen wir das nachfolgende Beispiel zu complilieren, erhalten wir vom Compiler für die mit
// Fehler
gekennzeichneten Quelltextzeilen eine Fehlermeldung, dass eine Ableitung an
dieser Stelle nicht erlaubt ist.
class A { val aValue : Double = 0.0 final val bValue : Double = 42.0 def aMethod(a: Double) : Double = a * 42.0 final def bMethod(b: Double) : Double = 43.0 * b } final class B class C extends A{ override val aValue : Double = 1.0 override val bValue : Double = 32.0 // Fehler override def aMethod(a: Double) : Double = 32.0 + a override def bMethod(b: Double) : Double = 44.0 + b // Fehler } class D extends B // Fehler
Abstrakte Klassen sind Traits sehr ähnlich. In abstrakten Klassen können wir Methoden und Variable definieren, die wir nicht ausprogrammieren müssen. Klassen, die von abstrakten Klassen abgeleitet sind, müssen diese Methoden und Variable definieren (implementieren) um nicht selbst als abstrakt zu gelten. Neben den nicht ausprogrammierten Elementen können abstrakte Klassen, wie auch Traits, konkrete, ausprogrammierte Methoden und Variable enthalten. Da abstrakte Klassen nicht vollständig implementiert sind, können von abstrakten Klassen keine Objekte direkt erzeugt werden, sondern es muss eine abgeleitete Klasse gewählt werden, welche die abstrakten Elemente implementiert.
Der wesentliche Unterschied zu Traits besteht darin,
dass abstrakte Klassen mit Konstruktoren versehen und damit unterschiedlich initialisiert werden können (bzw. Sie haben immer den
Default-Konstruktor). Der Nachteil abstrakter Klassen gegenüber Traits
besteht darin, dass abstrakte Klassen abgeleitet werden müssen, also mit Hilfe des Schlüsselwortes
extends
eingebunden werden. Ein einmischen mit with
ist bei abstrakten Klassen
nicht möglich. Da Scala keine Mehrfachvererbung unterstützt, kann eine Klasse, welche von
einer abstrakten Klasse erbt, von keiner weiteren Klasse erben.
Um eine abstrakte Klasse zu definieren müssen wir der Klasseneinleitung class
das
Schlüsselwort abstract
voranstellen.
Nchfolgend ein Beispiel einer abstrakten Klasse MyAbstractClass
und deren implementierung
in MyAbstractImplementation
.
abstract class MyAbstractClass { val myAbstractValue: Double val myConcreteValue: Double = 3.0 def myAbstractMethod(d: Double): Double def myConcreteMethod(d: Double) = 2.0 * d } trait T {} class MyAbstractImplementation extends MyAbstractClass() with T { val myAbstractValue = 3.0 def myAbstractMethod(d: Double) = 3.0 }
Beim genaueren Hinsehen stellen wir fest, dass wir quasi die abstrakten Elemente überschreiben
aber, dass in diesem Falle das Schlüsselwort override
nicht angegeben werden
muss. Eine Angabe des Schlüsselwortes override
ist hier optional, kann
also, sofern gewünscht angegeben werden.
Nachdem wir eine Implementierung definiert haben können wir ein Objekt der abstrakten Klasse mithilfe der Implementierung definieren.
object Main{ def main(args: Array[String]): Unit = { val t: MyAbstractClass = new MyAbstractImplementation } }
Innerhalb einer Klasse können weitere Klassen definiert werden. Derartige Klassen können jedoch
nur im jeweiligen Geltungsbereich der Definition verwendet werden. Wird zum Beispiel innerhalb einer Methode
eine Klasse benötigt, die an keiner anderen Stelle benötigt wird, definieren wir die Klasse
einfach innerhalb dieser Methode. Im nachfolgenden Beispiel definieren wir eine Klasse Person
innerhalb der Methode startWorking
der Klasse Outer
. Anschließend
definieren wir eine Instanz dieser Klasse und geben deren String
Repräsentation auf
der Systemausgabe aus. Bei der Verwendung println(person)
wird deren toString()
Methode aufgerufen, welche wir in Person
überschrieben haben.
object MyMain { def main(args: Array[String]) { new Outer().startWorking } } class Outer{ def startWorking() { class Person(firstName: String, lastName: String) { override def toString() = lastName+", "+firstName } val person = new Person("Hans","Maier") println(person) } }
Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
Maier, Hans
Innerhalb der Methode können wir die Klasse Person
erst nach der Position
der Definition verwenden.
Benötigen wir eine Klasse innerhalb einer Klasse (und in keiner anderen), können
wir die Klasse auch außerhalb einer Methode im Klassenrumpf der Äußeren definieren.
Im nachfolgenden Beispiel wurde die Klasse Person
außerhalb der Methode
startWorking
im Klassenrumpf definiert. Da der Klassenrunpf dem Inhalt des Primärkonstruktors
entspricht, ist die Klasse Person
Bestandteil des Konstruktors und steht allen Methoden
der Klasse Outer
zur Verfügung.
object MyMain { def main(args: Array[String]) { new Outer().startWorking } } class Outer{ println(new Person("Michael","Mustermann")) def startWorking() { val person = new Person("Hans","Maier") println(person) } class Person(firstName: String, lastName: String) { override def toString() = lastName+", "+firstName } }
Der Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
Mustermann, Michael Maier, Hans
Mit Scala 2.8 hielten die sogenannten Package Objects Einzug in die Sprache. Für jedes Package können wir ein Package Object definieren, das dann im gesamten Package "sichtbar" ist. Package Objects eignen sich insbesondere zur Definition von:
die im gesamten Package, ohne gesonderten import verwendet werden können.
Die Definition eines Packages Objects beginnt mit package object
gefolgt vom Namen des Packages. Im Anschluss erfolgt wie bei anderen Objekten (Klassen, Traits)
die Definition des Objekt Inhaltes. Gespeichert wird der Quelltext hierarchisch im entsprechenden
Verzeichnis unter den Namen package.scala
Das nachfolgende Beispiel zeigt eine einfache Definition eines Package Objects.
package object mypackage { def printMyPackageObject() { println("Hey, I'm a Package Object") } }
Im nachfolgenden Beispiel wird die einfache Anwendung des Package Objects gezeigt.
package mypackage object MainClass { def main(args: Array[String]) : Unit = { printMyPackageObject() } }
Die Ausführung des Programmes führt zur erwarteten Ausgabe auf der Systemausgabe:
Hey, I'm a Package Object
Möchten wir das Package Object aus anderen Packages zugreifen, besteht eine Möglichkeit darin, den voll qualifizierten Namen des Packages gefolgt vom gewünschten Methodennamen anzugeben.
mypackage.printMyPackageObject()
Eine weitere Möglichkeit besteht darin, dass Package Object mit Hilfe einer import
-Anweisung
in den Sichtbarkeitsbereich zu holen. Dazu geben wir nach dem import
-Statement den
Package Namen gefolgt von Punkt und Unterstrich an.
import mypackage._
Zunächst wollen wir den Unterschied zwischen "gleich" und "identisch" herausarbeiten. Zwei Objektreferenzen (Objekte) sind identisch, wenn Ihre Referenzen auf die gleiche Speicheradresse zeigen. Sind die Objektreferenzen ungleich, sind die entsprechenden Objekte nicht identisch. Die Objekte sind auch dann nicht identisch, wenn die Objekte ansonsten identische (gleiche) Attribute enthalten.
Objekte sind in der Regel gleich, wenn Sie gleiche (identische) Attribute haben. Es ist jedoch nicht erforderlich, dass alle Attribute übereinstimmen. Welche Attribute herangezogen werden hängt von der jeweiligen Implementierung ab.
Nachfolgend eine Grafik, welche den Unterschied zwischen Gleichheit und Identität verdeutlichen soll.
Die Rechtecke in der Grafik sollen Speicheradressen im Computer darstellen, auf welche die Objekte obj1
,
obj2
und obj3
zeigen. Die Länge der einzelnen Objekte legen wir der einfachheitshalber
mit 5 Elementen fest. Im Bezug zu Gleichheit und Identität lässt sich Folgendes festhalten:
obj1
und obj3
sind identisch, da Sie auf die gleiche Speicheradresse zeigen. Des weiteren
sind die Objekte auch gleich (kann auch nicht anders sein, da zwingend alle Attribute übereinstimmen).
obj1
und obj2
sind nicht identisch, da Sie auf unterschiedliche Speicheradressen zeigen.
Sind jedoch gleich, da der Inhalt übereinstimmt.
obj2
und obj3
sind nicht identisch, da Sie auf unterschiedliche Speicheradressen zeigen.
Sind jedoch gleich, da der Inhalt übereinstimmt.
Nachfolgend wollen wir uns die Scala Methoden ansehen, mit denen wir Objekte auf Gleichheit und Identität hin untersuchen können.
==
Mit ==
vergleichen wir Objekte auf Gleichheit. Für Java Entwickler sei hier darauf hingewiesen, dass
in Java mit ==
auf Identität geprüft wird. Hier lauert also ein Fallstrick für Fehler im Programm.
!=
!=
ist das Gegenstück zu ==
und liefert true
, wenn die Objekte ungleich sind.
eq
Mit eq
prüfen wir, ob zwei Objekte identisch sind.
ne
ne
liefert true
, wenn zwei Objekte nicht identisch sind.
Für eq
und ne
gibt es in Scala jedoch eine Einschränkung.
Die Methoden sind nur für Klassen definiert, die von scala.AnyRef
(java.lang.Object
) abgeleitet sind. Bei allen anderen Klassen
(z.B. Wertetypen) stehen die Methoden nicht zur Verfügung.
case class AClass(aValue: Int) class EqÖrNe { val aClass1 = new AClass(42) val aClass2 = new AClass(42) val aInt1 = 3 val aInt2 = 3 val classEq = aClass1 eq aClass2 // OK val intEq = aInt1 eq aInt2 // Fehler (compiliert nicht) }
null
Problematik
Wer aus der Java - Programmierung kommt und sich mit der Prüfung auf Gleichheit auseinandergesetzt hat kennt folgende
Problematik: Was passiert, wenn die zu vergleichenden Elemente null sein können. Rufen wir die Methode equals
auf eine Variable mit dem Wert null
auf, erhalten wir eine java.lang.NullPointerException
.
Nachfolgend ein Java - Beispiel, an dem dieses Problem nachvollzogen werden kann.
public class JEQDemo {
public static void main(String[] args) {
String s1 = null;
String s2 = "Hello";
System.out.println(s1.equals(s2)); // NullPointerException
}
}
In Java haben wir also die Problematik, dass wir immer sicherstellen müssen, dass Referenzen
nicht null
sein dürfen, wenn wir Referenzen auf Gleichheit prüfen. Dieser
Umstand kann zu nicht unerheblichen zusätzlichen Programmieraufwand führen (und die
Prüfung wird unübersichtlicher).
Wie sieht die Sache nun in Scala aus? Ganz einfach. In Scala wird die null
Prüfung
automatisch vorgenommen. Jeder Aufruf von ==
(entspricht equals
in Java)
auf eine null
-Referenz führt zum Ergebnis false
, sofern das übergebene
Objekt nicht null
ist.
Nachfolgend ein Beispiel in Scala, wo ==
auf eine null
Referenz aufgerufen wird.
object SEQDemo {
def main(args: Array[String]): Unit = {
val s1 = null
val s2 = "Hello world"
println(s1 == s2) // kein Problem
}
}
Übergeben wir einer null
-Referenz eine null
-Referenz zur Prüfung
auf Gleichheit, erhalten wir als Ergebnis true
. Da es vom Typ Null
nur eine
Instanzvariable null
gibt, sind diese nicht nur gleich, sondern auch
identisch 1, also auch gleich.
Heiko Seeberger
Scala 2.8: Package Scopes und Package Objects Fortsetzung, Teil 2
http://it-republik.de/jaxenter/artikel/Scala-2.8-Package-Scopes-und-Package-Objects-2816.html
codeviolation.de
Unterschiede zwischen Überladen, Überschreiben, Überschatten in der OOP
http://www.codeviolation.de/objektorientierte-programmierung/unterschiede-zwischen-ueberladen-ueberschreiben-ueberschatten-in-der-oop/
scala.Null