Klassen und Objekte

Scala ist eine objektorientierte Programmiersprache
Aufbau einer Klasse
Konstruktoren
Singleton Objekte
Companion object/class
Case Classes
Vererbung
Innere Klassen
Package Objects
Links

Scala ist eine objektorientierte Programmiersprache

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-Objekte" bezeichnet.

Aufbau einer Klasse

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.

Beispiel
package test

import de.test.Test

class TestClass{
  // Klassenquelltext
}
          

Konstruktoren

Primärkonstruktor (engl. primary constructor)

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
}
timpt.de - X2H V 0.9

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.

Singleton Objekte

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 
  }
}          
          

Companion object/class

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

Case classes bieten dem Programmierer einige Annehmlichkeiten, die implizit vom Compiler vorgenommen werden:

  • Sie besitzen eine Factory-Methode mit dem Namen der Klasse. Es ist daher nicht notwendig Cases Classes mit dem Schlüsselwort new zu erzeugen.

  • Alle Parameter, die dem Konstruktor übergeben werden, sind implizit eine val - Variable der Klasse.

  • Case Classes erhalten eine Implementierung der Methoden toString, hashCode und equals.

  • Case Classes können beim Pattern Matching eingesetzt werden.

  • Case Classes können ohne das Schlüsselwort new instanziiert werden.

  • Zum einfachen kopieren von Objekten besitzen Case Classes eine 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
}
timpt.de - X2H V 0.5

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)
timpt.de - X2H V 0.10

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.

Vererbung

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, 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:

  • Subklasse
  • abgeleitete Klasse
  • Unterklasse
  • Kindklasse

Dem entgegengesetzt werden Klassen, die Ihre Eigenschaften vererben wie folgt bezeichnet:

  • Vaterklasse
  • Superklasse
  • Basisklasse
  • Oberklasse
  • Elternklasse

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
}
timpt.de - X2H V 0.11

Innere Klassen

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)
  }
}
timpt.de - X2H V 0.11

Der 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
  }
}
timpt.de - X2H V 0.11

Der Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:

 
Mustermann, Michael
Maier, Hans
          

Package Objects

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:

  • Typen
  • Feldern
  • Methoden / Funktionen
  • Impliziten Konvertierungen

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")
  }
}
timpt.de - X2H V 0.11

Im nachfolgenden Beispiel wird die einfache Anwendung des Package Objects gezeigt.

package mypackage

object MainClass {

  def main(args: Array[String]) : Unit = {    
    printMyPackageObject()
  }
}
timpt.de - X2H V 0.11

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._          
          

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