Scala Schritt für Schritt: Teil 1

Scala Tutorial: Objects, Vererbung mit Traits, Apply und Co.

Johannes Dienst

© Shutterstock.com / Aquir

Als Multiparadigmensprache besitzt Scala neben Objektorientierung auch ausgeprägte Merkmale einer funktionalen Sprache. Dadurch wird sie für viele Einsatzgebiete interessant. In diesem Tutorial lernen Sie Schritt für Schritt die Programmierung mit Scala und lernen so ihre Vorteile kennen – viel Spaß!

Scala bietet neben einer schlanken Syntax eine effektive Typinferenz, indem der Compiler dem Entwickler viel Boilerplate-Code erspart. Außerdem bietet sie mit Pattern Matching eine Möglichkeit, eine Switch-Anweisung für Objekte zu implementieren. Sie unterstützt explizit die Entwicklung von verteilten Anwendungen z. B. durch Futures. Abgerundet wird das Paket durch eine nahtlose Java-Integration.

Erste Schritte in Scala

Die Installation von Scala auf einem Windows-System ist einfach. Über die Homepage den Download-Bereich aufsuchen, den Installer herunterladen und ausführen. Auch für alle anderen gängigen Betriebssysteme gibt es entsprechende Pakete. Ebenfalls wird auf dieser Seite erläutert, wie die Installation mit dem Scala-spezifischen Buildtool sbt funktioniert oder welche IDE für die Scala-Entwicklung heruntergeladen werden kann. Hat man die Installation abgeschlossen, begeben wir uns direkt in den Scala-Read-Eval-Print-Loop (kurz REPL). Dieser ist nützlich, um die Sprache kennenzulernen oder schnell etwas Neues auszuprobieren.

In Windows kann der REPL über die Eingabeaufforderung gestartet werden. Einfach scala eingeben und schon geht es los.
Unter Linux ist es ratsam, sich das entsprechende scala-*.tgz herunterzuladen und an einem Ort der Wahl zu entpacken. Im entpackten Ordner unter bin findet sich die ausführbare Datei scala, die den REPL startet. Mit der Scala-IDE steht eine ausgereifte Version von meiner Lieblingsentwicklungsgebung Eclipse zur Verfügung. Dort kann der REPL unter Window → Show View → Other → Scala Interpreter gestartet werden. Der REPL sollte in etwa die folgende Begrüßung auf der Konsole ausgeben (in Eclipse entfällt das):

$ ./scala
Welcome to Scala 2.12.1 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0_66).
Type in expressions for evaluation. Or try :help.

scala> 

Das folgende Listing 1 zeigt die ersten Experimente mit Scala, die gerne nebenbei nachvollzogen werden dürfen. Der Artikel wartet solange, bis Sie Scala installiert haben.

Listing 1 – Basistypen

scala> val aNumber = 42
aNumber: Int = 42

scala> var aNumber2: Double = 2.2
aNumber2: Double = 2.2

scala> aSum = aNumber + aNumber2
aSum: Double = 44.2

scala> aSum = 77
<console> error: reassignment to val
      aSum = 77

Gibt man im REPL eine Anweisung ein, gibt er sofort Rückmeldung, was im Hintergrund passiert. So wird in der ersten Zeile ein Value aNumber mit dem Schlüsselwort val und Wert 42 angelegt. Als Ausgabe wird gemeldet, dass der Typ von aNumber Int ist und den Wert 42 besitzt. In der nächsten Anweisung wird ein Double angelegt. Diesmal aber als Variable mit dem Schlüsselwort var. Einfache Rechenanweisungen sind ebenfalls möglich, was die dritte Anweisung zeigt. Eine Neuzuweisung eines Wertes zu einem Value ist nicht möglich, bei einer Variable geht das schon. Da Scala von erfahrenen Scala-Entwicklern sehr funktional genutzt wird, sollte (fast) immer das Schlüsselwort val gewählt werden, wo immer es möglich ist. Außerdem fällt auf, dass es anscheinend keine primitiven Datentypen gibt. Tatsächlich sind Int und Double Objekte, auf denen Methoden aufgerufen werden können. Das ist eine grundsätzliche Designentscheidung der Scala-Erfinder. Es gibt einfach keine primitiven Datentypen, was eine ganze Reihe von Problemen aus dem Programmiermodell eliminiert. Z. B. muss der Entwickler sich keine Gedanken über Autoboxing machen oder explizite Typkonversionen von primitiven Datentypen auf Objekte vornehmen. Die wichtigsten Teile der Typenhierarchie von Scala sind in Abbildung 1 dargestellt.

unified_types

Abbildung 1: Überblick über die Typenhierarchie von Scala

Stellt sich noch die Frage, woher der Compiler weiß, von welchem Typ aNumber und aSum sind? Es fehlt nämlich jeweils die Angabe, welcher Typ in dem Value steckt. Scala wirbt damit, dass das Typsystem statisch ist und für den Entwickler arbeitet. So können Typinformationen entweder explizit mit angegeben werden, wie bei aNumber2 das Double, oder aber der Compiler ermittelt es per Inferenz selbst. Das erspart Schreibarbeit und macht den Code lesbarer. In der Praxis wird die automatische Typinferenz eigentlich immer fehlerfrei durchgeführt.

Bleiben wir noch kurz im REPL für ein obligatorisches Hello-World-Beispiel. Listing 2 enthält ein Scala-Object mit einer main-Methode die den allseits beliebten Ausspruch auf der Konsole ausgibt. Wo genau der Unterschied zwischen einem Object und einer Klasse ist, dazu kommen wir im weiteren Verlauf dieses Tutorials.

Listing 2 – Hello World

scala> object HelloWorld {
     |    def main(args: Array[String]) {
     |        println("Hello, World!")
     |    }
     | }
defined object HelloWorld

scala> HelloWorld.main(Array())
Hello, world!

Ein REPL ist für die professionelle Entwicklung natürlich nicht geeignet. Deswegen verwende ich persönlich die Scala-IDE. Nach dem Download muss sie nur entpackt werden und ist sofort Einsatzbereit. Zu beachten ist, dass nicht immer die neueste Scala-Version mitgeliefert wird. Das lässt sich aber leicht in den Einstellungen unter dem Punkt Scala → Installations ändern. Mit dieser kurzen Vorbereitung kann auch schon ein Projekt erstellt werden.

Ein Beispielprojekt: Die Bibliothek

Um nicht weiter nur kleine Beispiele zu zeigen, soll im Laufe der Serie eine Applikation implementiert werden. Der Name ist „Die Bibliothek“ und soll für die Verwaltung von Büchern von Nutzen sein. Die Struktur eines Scala-Projekts gleicht der eines Java-Projekts. Die Aufteilung in Packages ist deshalb sinnvoll. Als Erstes soll ein Basisobjekt Book entworfen werden. Ein minimaler Anfangspunkt stellt die Klasse Book in Listing 3 dar. Unser Book wird im Package de.bookstore.main abgelegt. Es fallen zuerst die Parameter title, author und isbn10 direkt im Klassenkopf auf. Tatsächlich handelt es sich dabei um Konstruktorparameter, mit denen eine Instanz von Book erstellt werden kann. Das geht dann z. B. so: val book = new Book(„Clean Code“, „Uncle Bob“, 3826655486L).

Die Parameter landen auch als Klassenvariablen in der Klasse Book. Das alles wird vom Compiler implizit erledigt. Es entfällt also die manuelle Pflege, was den Code lesbar und übersichtlich hält. Wer genauer hinsieht, erkennt auch das val bei title, den fehlenden Modifier von author und das var bei isbn10.
Diese Angaben regeln, ob auf eine Klassenvariable nur lesend, gar nicht oder lesend und schreibend zugegriffen werden kann. So kann auf title nur lesend zugegriffen werden:

println(book.title) // Prints: Clean Code
book.title = "Another One" // Error: reassignment to val

Die Variable author ist weder modifizierbar noch kann auf sie zugegriffen werden:

println(book.author) // Error: not a member
book.author = "Robert" // Error: not a member

Der mit var vorangestellte Parameter isbn10 kann ausgegeben und sogar geändert werden:

println(book.isbn10) // Prints: 3826655486
book.isbn10 = 1234567890

Sollte ein anderer Konstruktor gebraucht werden, so hilft einem def this(…) weiter. Hiermit können Konstruktoren mit anderer Signatur implementiert werden. Zu beachten ist, dass zusätzliche Konstruktoren immer den Standardkonstruktor als erstes aufrufen müssen. Eine korrekte Implementierung eines weiteren Konstruktors ist in Listing 3 zu sehen.

Listing 3 – Book

package de.bookstore.main

class Book (val title: String, author: String, var isbn10: Long) {
    def this(title: String) {
        this(title, "", -1)
    }
}

Ein wichtiges Merkmal der ISBN-Nummer ist die Prüfziffer, welche die letzte Ziffer in der Zahlenfolge darstellt. Für diese soll ein eigener Getter mit der Methode getDigit implementiert werden (Listing 4). Methoden werden mit dem Schlüsselwort def eingeleitet, das nur innerhalb von Objects, Klassen und Traits verwendet werden kann. Falls es Parameter für diese Methode gibt, werden sie in Klammern mit angegeben. Listing 4 zeigt getDigit aber ohne jegliche Klammern, da diese in diesem Fall nicht nötig sind. Für den Compiler gibt es jedoch einen Unterschied zwischen getDigit und getDigit(). Ersteres kann sowohl mit als auch ohne Klammern aufgerufen werden, während letzteres nur mit Klammern aufgerufen werden kann. Als Konvention hat sich etabliert, dass Methoden mit Seiteneffekten immer mit Klammern geschrieben werden und Akzessoren grundsätzlich ohne. Eine weitere Konvention beim Methodenaufruf sollte beachtet werden. So ist der Aufruf isbn10.toString() takeRight(1) ebenfalls korrekt. Er kann aber zu Kompilierungsfehlern führen und sollte deswegen nicht verwendet werden.

Listing 4 – Methode: Prüfziffer ausgeben

def getDigit: String = {
    isbn10.toString().takeRight(1)
}

Das Überschreiben von Methoden in abgeleiteten Klassen ist in objektorientierten Programmiersprachen üblich. Scala benutzt hierfür das Schlüsselwort override, das der Methodendefinition vorangestellt wird (Listing 5). Wer genauer hinsieht, erkennt auch, dass der Methodenrumpf ohne umschließende Klammern auskommt. Das ist möglich und vor allem bei Einzeilern sehr gut lesbar. Ist der Rumpf länger, so sollte wie in Listing 4 mit umschließenden Klammern gearbeitet werden. Anders als bei der Parameterliste einer Methode besteht hier aber kein semantischer Unterschied für den Compiler.

Listing 5 – Überschreiben der toString()

override def toString: String =
    title + ", " + author + ", " + this.isbn10

Objects

Scala erlaubt explizit keine statischen Methoden in Klassen. Das hat den Vorteil eines vereinfachten Programmiermodells, da nicht mehr auf Objekten einer bestimmten Klasse statische Methoden aufgerufen werden können, was im Grunde dem Sinn von statischen Methoden widerspricht. Außerdem hilft es Entwicklern schneller im Code zu navigieren. Die Deklaration ist einfach. So wird in Listing 6 ein Object Book erstellt – die Scala-Dokumentation spricht von einem Singleton-Object –, indem das Schlüsselwort object benutzt wird. Ansonsten sieht das Ganze wie bei einer Klassendefinition aus, nur ohne Parameter in der Signatur. Da das Object Book direkt mit der Klasse Book zusammenhängt, trägt es den gleichen Namen. Man spricht dann in Scala auch von einem Companion Object, das üblicherweise in der gleichen Datei angelegt wird.

Die Methoden in einem Object sind von Natur aus statisch, deswegen entfällt eine zusätzliche Kennzeichnung mit einem Schlüsselwort. Ansonsten sieht es aus wie eine Klasse.
Unser Book Object enthält im Moment genau eine Methode validateISBN10(…), das eine Überprüfung einer ISBN10 durchführt und zurückgibt, ob sie gültig ist. Wer sich für den Algorithmus interessiert, der kann sich hier schlau machen. Viel interessanter sind dann schon die Zeilen 9 und 10 des Listings. Hier deutet sich bereits an, dass Scala volle Unterstützung für das funktionale Programmierparadigma bereitstellt. Eine Kernidee einiger funktionaler Sprachen ist, dass alles eine Liste ist, was viele Operationen generalisiert. So liefert in Zeile 9 der Aufruf von toString() keinen String zurück sondern einen Wrapper vom Typ StringOps, der alle Operationen bereitstellt, die auch auf Sequenzen möglich sind. Auf diesen kann ohne Probleme ein map angewendet werden, das die Funktion toChar auf jedem Element der Liste ausführt und eine neue Liste zurück liefert. Im Value digits landet am Ende ein String.

In Zeile 10 wird eine for-Schleife mit einem Index von 0 bis 8 durchlaufen. Bei der Schreibweise der for-Schleife handelt es sich tatsächlich um eine sogenannte for-Comprehension. Diese hat den Vorteil, dass bei Bedarf in der Schleife selbst eine Filterung der Objekte stattfinden kann. Will man tatsächlich nur auf jedem Element eine gewisse Operation durchführen, so sollte man eher foreach benutzen. Hier findet sich dazu eine schöne Erklärung.

Listing 6 – Companion Object

package de.bookstore.main

object Book {

    def validateISBN10(isbn: Long): Boolean = {
        var s: Int = 0
        var t: Int = 0

        val digits = isbn.toString().map(_.toChar)
        for (i &amp;lt;- 0 to 8) {
            t += digits(i);
            s += t;
        }

        (s % 11) == digits(9).asDigit
    }
}

Vererbung mit Traits

Scala geht bei der Vererbung einen interessanten Weg über sogenannte Traits. Wie der Name schon andeutet, sind das eher Eigenschaften, die einer Klasse hinzugefügt werden. Das bringt einige Vorteile im Vergleich zu klassischer Vererbung mit Interfaces/Klassen. Wer sich eingehend mit der Problematik von klassischer Vererbung beschäftigen will, dem empfehle ich diesen anschaulichen Blogpost.

Zurück zu unserer Bibliothek. Zum Austausch mit anderen Bibliotheken und zu Backup-Zwecken soll es möglich sein, Bücher im CSV-Format zu exportieren. Um es nicht unnötig kompliziert zu machen ist diese Funktionalität in einen Trait eingebettet (Listing 7). Traits können abstrakte und implementierte Methoden enthalten. Die Methode isEncoded() ist implementiert und kann bei Bedarf später überschrieben werden. Im Gegensatz dazu ist exportCSV() abstrakt und muss in jeder Klasse, die von diesem Trait ableitet, implementiert werden. Die abstrakte Methode kommt dabei ohne ein zusätzliches Schlüsselwort wie abstract aus. Das Fehlen des Methodenrumpfes ist genug, damit der Compiler weiß, dass es sich um eine abstrakte Methode handelt. Zusätzlich kann ein Trait auch bestimmte Klassenvariablen fordern, wie die Variable encode, die gesetzt werden können, oder wie in diesem Fall abstrakt bleiben.

Listing 7 – Export Trait

package de.bookstore.main

trait Export {
    def isEncoded() = println(encode)
    def encode: Boolean;
    def exportCSV(): String;
}

Die Klasse Book erweitert in Listing 8 den Trait Export. Dafür wird das Schlüsselwort extends verwendet. Soll eine Klasse von mehreren Traits erben, dann wird das Schlüsselwort with für jeden weiteren verwendet. Das sieht dann allgemein so aus:

class A extends B with C with D

Book muss nur encode definieren und die Methode exportCSV implementieren, damit sie den Vertrag erfüllt. Die Implementierung von isEncoded() wird nicht überschrieben, sondern übernommen.

Listing 8 – Trait einer Klasse hinzufügen

package de.bookstore.main

class Book (val title: String, author: String, var isbn10: Long)
    extends Export {

    . . .

    def encode = false
    def exportCSV: String = {
        title + ";" + author + ";" + this.isbn10
    }
}

Vererbung über Klassen

Traits sind für die meisten Anwendungsfälle die richtige Wahl. In manchen Fällen ist es jedoch erforderlich, dass eine echte Is-A-Beziehung zwischen Typen hergestellt wird. Da in den letzten Jahren die Nutzung von EBooks stark zugenommen hat, soll diese Spezialform eines Buchs modelliert werden. Dazu wird in Listing 9 die Klasse Book von der Klasse EBook erweitert. Die Syntax und Funktionsweise bedarf einer genaueren Erläuterung. Um die Klasse Book als Oberklasse zu definieren, muss sie mit extends an die Klassensignatur angehängt werden. Dabei sind alle Konstruktorparameter mit anzugeben, allerdings ohne Modifier. In den Konstruktorparametern für EBook sind die Parameter der Oberklasse inklusive ihrer Typinformationen anzugeben. Soll die Unterklasse zusätzliche Parameter entgegennehmen, werden diese am Ende angehängt. Modifier können nur bedingt geändert werden. So ist bei einem Objekt vom Typ E-Book der Zugriff auf author möglich, indem es als val definiert wird. Die Umdefinition von val auf var oder anders herum wird vom Compiler als Problem deklariert. Erzeugt man nun ein neues Objekt vom Typ EBook, dann kann author tatsächlich ausgegeben werden:

val ebook = new EBook(title="Learning Scala", "John");
    println(ebook.author) // Prints John

Beim Erzeugen des obigen EBook-Objekts fällt auf, dass gar keine isbn10 und keine Liste an möglichen Formaten formats angegeben wurde. Bei EBooks kommt es häufig vor, dass diese nicht offiziell veröffentlicht sind und deswegen keine ISBN-Nummer besitzen. Diesem Umstand wird mit einem sogenannten Defaultparameter Rechnung getragen. Der Parameter isbn10 wird bei Nichtangabe auf den Wert -1 gesetzt. Ebenfalls sind die möglichen Formate nicht genau festgelegt. Deswegen werden sie bei Nichtangabe als leere Liste angelegt.

println(ebook.isbn10) // Prints -1
println(ebook.formats) // Prints List()

Eine weitere Besonderheit ist bei der Definition von weiteren Konstruktoren zu beachten. Es wird vom Compiler gefordert, dass als Erstes immer der Konstruktor der Oberklasse aufgerufen wird. Alles andere kompiliert nicht.

Listing 9 – EBook erweitert Book

package de.bookstore.main

class Ebook(
    title: String,
    val author: String,
    isbn10: Long = -1
    val formats: List[String] = List[String]())
    extends Book(title, author, isbn10) {
  
        def this(title: String) = {
            this(title, "John")
        }
}

Das Konzept eines Interfaces gibt es in Scala nicht. Stattdessen können neben Traits auch abstrakte Klassen verwendet werden. Da noch keine Klasse zur Modellierung der Bibliothek vorhanden ist, wird in Listing 10 eine abstrakte Klasse Library erstellt. Im Grunde genommen verhält sich eine abstrakte Klasse in Scala wie ein Trait. Es gibt jedoch zwei Unterschiede:

• Konstruktorparameter sind möglich: In diesem Fall ein Objekt vom Typ Database
• Eine ableitende Klasse kann nur von einer abstrakten Klasse erben

Listing 10 – Abstrakte Klasse

package de.bookstore.main

abstract class Library(db: Database) {

    def persist = { 
        db.update()
        db.save()
    }

    def add(book: Book)
}

Methoden vs. Funktionen

Scala ist eine vollwertig funktionale Programmiersprache. Bis jetzt hat man davon wenig gesehen. Tatsächlich handelt sich bei allen bis jetzt vorgestellten Methoden nicht um Funktionen im Sinne von echten Objekten. Methoden sind immer innerhalb eines Objects, Klasse oder Traits zu finden und stellen ein eigenes Konzept dar. Funktionen sind in Scala dagegen echte Objekte, die vom Typ von einer der FunctionN-Klassen sind. Zur weiteren Erläuterung lohnt es sich einmal den REPL zu konsultieren. Methoden definiert man mit dem Schlüsselwort def. Anschließend können Sie aufgerufen werden und geben ein Ergebnis zurück. So wie die Methode m1 im folgenden Beispiel:

scala> def m1(x: Int) = x+1 

        m1: (x: Int)Int
 

    scala> m1(2)
 
        res3: Int = 3 

Man beachte den Typ (x: Int)Int von m1. Im Grunde genommen ist das ein Verweis auf Code, der eine Variable x vom Typ Int entgegennimmt und ein Int zurückliefert.
Funktionen dagegen können einem val oder var zugewiesen werden. Damit sind sie echte Objekte wie die unten stehende Funktion f1 zeigt. Lediglich der Aufruf von f1 ist von der Schreibweise her gleich:

scala> val f1 = (x: Int) => x+1
 
        f1: Int =&amp;gt; Int = <function1>
 

    scala> f1(1)
 
        res2: Int = 2

Der Typ von f1 ist Int => Int, wobei => anzeigt, dass es sich um eine Funktion handelt.

Gehen wir noch einen Schritt weiter und schreiben eine Funktion composedFunction, die eine Funktion als Parameter von Typ Int => Int verlangt. Die Vermutung liegt nahe, dass f1 hier akzeptiert wird und es ist auch wirklich so:

scala> val composedFunction = (y: Int => Int) => y(1)
    composedFunction: (Int => Int) => Int = <function1>

    scala> composedFunction(f1)
        res5: Int = 2 

Interessanterweise wird aber auch m1 akzeptiert. Selbst nach gründlicher Recherche konnte ich nicht herausfinden, warum es sich so verhält und dieser Aufruf funktioniert:

scala&amp;gt; composedFunction(m1)
 
        res6: Int = 2

Die einzige Erklärung ist, dass der Typ der Methode im Grunde genommen passt und der Compiler an dieser Stelle implizit eine Funktion aus der Methode macht. Vielleicht liegt es auch am REPL, der hier im Hintergrund eingreift. Das gleiche Verhalten kann nämlich durch einen Kniff erreicht werden. Schreibt man

scala> val f2 = m1 _
 
        f2: Int => Int = <function1>

erzeugt Scala eine partiell ausgewertete Funktion aus m1. Semantisch steht hier, dass aus m1 eine neue Funktion erzeugt werden soll und der erste Parameter nicht belegt sein soll. Fehlt der Unterstrich, dann gibt der REPL einen Fehler aus, da eine Methode nicht einem val/var zugeordnet werden kann:

scala> val f3 = m1
 
        <console>:11: error: missing arguments for method m1;
 
        follow this method with `_' if you want to treat it as
    a partially applied function
 
               val f3 = m1

Die Zuweisung eines Funktionsobjekts an ein val ist wie erwartet korrekt ausführbar:

scala> val f4 = f1
 
        f4: Int => Int = <function1>

Apply

In Scala gibt es eine spezielle Funktion namens apply. Sie wird zum Beispiel aufgerufen wenn man über den Index auf ein Listenelement zugreift.

scala> val aList = List.range(1, 5)
    aList: List[Int] = List(1, 2, 3, 4, 5)
scala> aList(4)
    res0: Int = 5

Das Gleiche Ergebnis erreicht man indem man schreibt:

scala> aList.apply(4)
    res1: Int=5

Es handelt sich bei der Schreibweise ohne .apply also um eine Abkürzung oder wie man so schön sagt, um syntaktischen Zucker, den der Compiler im Hintergrund expandiert. Die Idee dahinter ist, dass alles wie ein Methodenaufruf aussehen soll. Innerhalb des Scala-API wird das oft benutzt, um über das Companion Object mit einer Factory-Methode neue Objekte zu erzeugen, ohne das Schlüsselwort new zu verwenden:

scala> BigInt(314159265)
    res3: scala.math.BigInt = 314159265

scala> Array(27, 42, 7)
    res4: Array[Int] = Array(27, 42, 7)

Fazit

Im ersten Teil dieses Tutorials wurden die grundlegendsten Sprachelemente von Scala vorgestellt und an einem Beispielprojekt angewendet. Es wurde ausgiebig über den Umgang mit dem REPL gesprochen und anhand erster Beispiele die Typenhierarchie erläutert, die vollständig auf Objekten aufbaut und keine primitiven Basistypen besitzt.
Außerdem wurde der Unterschied zwischen den Schlüsselwörtern var und einem val erklärt. So sollten vor allem Values verwendet werden, um die funktionalen Aspekte von Scala vollständig auszunutzen. Da Scala nicht nur eine vollständig funktionale Sprache ist, sondern dem Multiparadigmen-Prinzip folgt, wurde anschließend an einem Beispielprojekt ihr objektorientiertes Potential vorgestellt. Klassen in Scala besitzen einige Vorzüge, die Boilerplate-Code wie Getter und Setter vermeiden.

Bereits in der Signatur werden die Konstruktorparameter zusammen mit ihren Modifiern angegeben. Aufgrund der Schlüsselwörter var bzw. val erstellt der Compiler Getter/Setter, nur Getter oder bei Fehlen eines Schlüsselworts eine private Klassenvariable. Kommt ein Parameter ohne Modifier daher, so ist er privat. Die Definition von Methoden, die nur innerhalb einer Klasse, Objects oder Traits liegen können, wird mit dem Schlüsselwort def eingeleitet. Sollen Methoden überschrieben werden, so wird das Schlüsselwort override der Signatur vorangestellt. Zusätzliche Konstruktoren haben die Signatur def this(…) und müssen als erstes Statement den Standardkonstruktor aufrufen.

Klassen erlauben keine statischen Methoden, dafür gibt es sogenannte Objects. Diese werden wie Klassen definiert, die Signatur setzt aber das Schlüsselwort object statt class voraus. Alle Methoden innerhalb eines Objects sind automatisch statisch. Für Vererbung stehen zwei Mechanismen zur Verfügung. Der erste basiert auf Traits, die man sich wie eine Art Interfaces vorstellen kann. Traits können sowohl abstrakte Methoden, als auch Defaultimplementierungen enthalten. Ebenfalls sind abstrakte und nicht abstrakte Klassenvariablen erlaubt.

Interessant ist, dass der Compiler selbstständig erkennt, wann eine Methode keinen Methodenrumpf besitzt und diese dann als abstrakt definiert. Es entfällt eine weitere Kennzeichnung mit einem Schlüsselwort wie abstract. Traits können sinnbildlich als Eigenschaften verstanden werden, von denen eine Klasse durchaus auch mehrere implementieren kann. Dies erlaubt eine sinnvolle Mehrfachvererbung ohne den Aufbau einer komplizierten Klassenhierarchie.

Als zweite Möglichkeit bietet sich die Vererbung von Klassen an sich und die Erweiterung von abstrakten Klassen an. Abstrakte Klassen werden mit dem Schlüsselwort abstract eingeleitet. Sie können abstrakte Methoden als auch implementierte Methoden enthalten. Der Unterschied zu Traits ist, dass eine abstrakte Klasse Konstruktorparameter besitzen kann und eine ableitende Klasse nicht mehrere abstrakte Klassen beerben kann.

Als Nächstes wurde auf die Unterschiede von Methoden zu Funktionen eingegangen. So sind Scala-Funktionen Objekte vom Typ einer der FunctionN-Klassen, während Methoden nur innerhalb von Objects, Klassen und Traits vorkommen können.

Als Abschluss wurde noch die apply-Methode vorgestellt, die in der Scala-API weitreichende Anwendung in Form von Factory-Methoden findet. Tatsächlich handelt es sich dabei um syntaktischen Zucker. Das API-eigene Object Array implementiert zum Beispiel die apply-Methode. Will man ein neues Array erstellen, schreibt man einfach Array(1, 2, 3). Im Hintergrund wird dabei die Methode apply(…) aufgerufen.

Das Projekt ist auf Github verfügbar. Mit git checkout tags/part-one erhält man den abschließenden Stand dieses Tutorials.

 

Verwandte Themen:

Geschrieben von
Johannes Dienst
Johannes Dienst
Johannes Dienst ist Clean Coder aus Leidenschaft bei der MULTA MEDIO Informationssysteme AG. Seine Tätigkeitsschwerpunkte sind die Wartung und Gestaltung von serverseitigen Java, Python und JavaScript-Applikationen. Twitter: @JohannesDienst
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.