Teil 11: Vererbung

Wir verfügen bereits über die Grundkenntnisse in Scalas Objektorientierung, gehen wir also noch ein wenig tiefer hinein in den Kaninchenbau.

Schauen wir uns noch einmal eine der einfachsten Klassen an:

class Person(val name: String, val age: Int)

Der Compiler generiert uns für die beiden Attribute name und age Getter und Setter. Das ist praktisch, was machen wir aber wenn wir in den Gettern oder Settern noch gerne etwas anderes erledigen wollen? Vielleicht müssen die an den Setter übergebenen Werte erst auf Gültigkeit geprüft werden? Oder aber wir wollen alle Zugriffe auf ein Attribut mitloggen.

Für diese Anwendungsfälle dürfen wir dem Compiler nicht die vollständige Codegenerierung überlassen, wir müssen selbst Hand anlegen. Sobald wir das val vor dem Attributnamen weglassen generiert uns der Compiler keine Access-Methoden mehr – wir können sie nun von Hand erstellen.

class Person(nameOfBirth: String, val age: Int = 0) {
  def name: String = {
    println("someone wants to know information of: "+nameOfBirth)
    nameOfBirth
  }
}

Da sowohl die Namen der Methoden und der Variablen vom Compiler innerhalb des gleichen Sichtbarkeitsbereichs verwaltet werden, können wir nicht die gleichen Namen vergeben. Wir müssen einen der beiden Namen ändern, in diesem Fall hat es den Konstruktorparameter getroffen. Das Ändern der öffentlichen Felder sollte man wenn möglich vermeiden, da es u.U. noch anderen Code gibt, der auf die Member unserer Klasse zugreift. Würden wir die Schnittstelle zu unserer Klasse ändern, müsste auch aller Code geändert werden, der auf die Schnittstelle zugreift. Hier kommt auch das als „Uniform Access Principle“ genannte Prinzip zum tragen, das besagt, dass die öffentlichen Member einer Schnittstelle mit möglichst der gleichen Notation angesprochen werden können. Für unsere Klasse heißt das, dass es egal ist ob unser Feld im Konstruktor oder innerhalb des Klassenkörpers deklariert wurde. Wir können immer mit dem Identifier „name“ auf das Feld zugreifen wobei der Initialisierungswert der Variable nach außen hin unsichtbar bleibt. Hier findet sich ein ganz brauchbarer Artikel zu diesem Thema.

scala> val p = new Person("max")
p: Person = Person@41cc5b64

scala> p.name
someone wants to know information of: max
res4: String = max

scala> p.nameOfBirth
:10: error: value nameOfBirth is not a member of Person
              p.nameOfBirth
                ^

Getter sind sehr intuitiv zu erstellen, bei Settern sieht das etwas anders aus:

class Person(val name: String, private var initAge: Int) {
  def age = initAge
  def age_=(age: Int) {
    if (age >= 0)
      initAge = age
  }
}

Das Problem hier ist, dass wir nicht einfach so das Attribut als veränderlich kennzeichnen können, da wir keine Methode gleichen Namens erstellen können mit der wir auf die Variable zugreifen können. Wir benötigen also eine interne Variable, die den Zustand speichert und auch einen Getter, wenn wir wollen, dass man auf die Variable Zugriff erhält. Die Syntax für den Methodenkopf eines Setters lautet:

def <def_name>_=(<param>)

Nach dem Namen der Methode folgt ein Unterstrich und ein Gleichheitszeichen bevor dann der Parameter notiert werden kann. Sieht komisch aus? Ja, aber ist es auch komisch in der Handhabung?

scala> val p = new Person("markus", 45)
p: Person = Person@5d17bf94

scala> p.age_=(46)

scala> p.age
res10: Int = 46

scala> p.age = 47
p.age: Int = 47

scala> p.age
res11: Int = 47

Der erste Methodenaufruf sieht so aus wie wir es erwartet haben. Beim zweiten hingegen begegnen wir wieder ein wenig syntaktischem Zucker. Der Compiler erlaubt uns den Unterstrich und die Klammern wegzulassen. Der Grund ist wieder der Uniform Access Modifier: Von außen soll nicht erkenntlich sein ob wir auf eine Methode oder auf eine Variable zugreifen.

Anmerkung:
Die Schreibweise

def <def_name>_<operator>(<param>)

erlaubt uns Alphabets- und Sonderzeichen zusammen in einem Identifier zu benutzen.
Wir können also Namen wie hello_+-* oder isset_? erstellen. Besonders letztere Schreibweise findet man des Öfteren, da es nochmal verdeutlichen kann, dass ein Boolean zurückgeben wird. Im Gegensatz zum Setter erlaubt uns der Compiler aber nicht den Unterstrich beim Aufruf des Identifiers wegzulassen. Von diesem Syntaxzucker kann man ausschließlich beim Setter Gebrauch machen. Die Kombination der Zeichen ist auch nur erlaubt wenn die Sonderzeichen zum Schluss kommen. Namen wie +_hello oder !_x_! sind ungültig.

Abstrakte Member

Manchmal haben wir Objekte, die im Grunde zusammengehören, da sie fast gleich funktionieren und sich nur in wenigen Punkten unterscheiden. Damit wir jetzt nicht bei allen solch zusammengehörenden Objekten fast den gleichen Code schreiben müssen besteht die Möglichkeit, das Verhalten, das für alle Objekte identisch ist, nur in einem Objekt zu definieren und es dann an andere Objekte zu vererben. Das Objekt, das Verhalten an ein anderes weitergibt wird Ober- oder Vaterobjekt genannt – bei der Klasse in der das Verhalten definiert wurde spricht man neben Oberklasse auch von Superklasse.

abstract class Person {
  def sayHello() { println("hello") }
  def doWork()
}
class Manager extends Person {
  def doWork() { println("rake in money") }
}
class Programmer extends Person {
  def doWork() { println("write code") }
}

Unsere Superklasse wäre hier Person, die zwei Methoden besitzt, eine mit einem Verhalten und eine ohne eins. Mit dem Schlüsselwort extends erstellen wir eine Vererbungshierarchie und weisen den Klassen Manager und Programmer das Verhalten von Person zu. Ins Auge dürfte uns gleich noch das abstract fallen. Was bedeutet es? Deklarieren wir eine Klasse als abstrakt heißt das, dass wir nicht wollen, dass irgendjemand davon irgendwann mal ein Objekt erstellt. Falls es dennoch mal jemand versuchen sollte bekommt er eine Fehlermeldung:

scala> new Person
:9: error: class Person is abstract; cannot be instantiated
              new Person
              ^

Dies schützt uns davor, dass ein Objekt erstellt wird, dessen Verhaltensweisen noch gar nicht festgelegt wurden, was bei Person mit der Methode doWork genau der Fall ist. Die Methode ist ebenfalls abstrakt, wir müssen dies aber nicht extra angeben. Es reicht einfach den Methodenkörper wegzulassen – der Compiler erkennt dann selbstständig, dass die Methode noch eine Implementierung benötigt. Wenn eine Klasse abstrakte Member besitzt, dann muss sie selbst als abstrakt gekennzeichnet werden. Tun wir das nicht, dann dürfen wir uns mal wieder die Klagen des Compilers anhören:

scala> class NotAbstract {
     |   def missingImplementation
     | }
:7: error: class NotAbstract needs to be abstract, since method missingImplementation is not defined
       class NotAbstract {
             ^

Die Methodendeklaration sieht auf den ersten Blick etwas merkwürdig aus, das liegt aber einzig allein daran, das der Rückgabetyp nicht angegeben wurde. Wir könnten auch def missingImplementation: Unit schreiben um deutlicher zu machen, dass der Rückgabewert der Methode nicht von Belang ist. Neben abstrakten Methoden kann eine Klasse auch abstrakte Variablen aufnehmen:

class Abstract {
  val someInt: Int
}
class Concrete extends Abstract {
  val someInt = 10
}

In der erbenden Klasse müssen wir die Variable einfach nochmal deklarieren und auch mit einem Wert initialisieren.

Aber kommen wir zu unserem Personen-Beispiel zurück. In den beiden Unterklassen definieren wir die Körper unserer abstrakten Klasse. Je nach dem welche Klasse wir nun instanziieren erhalten wir unterschiedliche Ausgaben:

scala> val m = new Manager
m: Manager = Manager@138524a1

scala> m.doWork()
rake in money

scala> val p = new Programmer
p: Programmer = Programmer@14d0fd23

scala> p.doWork()
write code

Nun hatte unsere Person aber einen Konstruktor, fügen wir diesen also gleich wieder hinzu:

scala> abstract class Person(val name: String, val age: Int)
defined class Person

scala> class Manager extends Person
<console>:8: error: not enough arguments for constructor Person: (name: String, age: Int)Person.
Unspecified value parameters name, age.
       class Manager extends Person
                     ^

scala> class Programmer extends Person
<console>:8: error: not enough arguments for constructor Person: (name: String, age: Int)Person.
Unspecified value parameters name, age.
       class Programmer extends Person
                        ^

In Scala werden Konstruktoren nicht mitvererbt. Das bedeutet, dass unsere beiden Unterklassen nur mit einem Konstruktor ohne Parameter erzeugt werden können. Da die Oberklasse aber zur Instanziierung zwei Parameter erwartet, die in den Unterklassen nirgendwo angegeben werden, erhalten wir die betreffenden Fehlermeldungen. Bleibt uns also nichts anderes übrig als auch den Unterklassen einen passenden Konstruktor zu verpassen:

scala> class Manager(val name: String, val age: Int) extends Person
<console>:8: error: not enough arguments for constructor Person: (name: String, age: Int)Person.
Unspecified value parameters name, age.
       class Manager(val name: String, val age: Int) extends Person
                    ^

Hm, die Fehlermeldung ist aber immer noch die Gleiche. Um zu erklären woher sie kommet hilft es vielleicht wenn wir uns folgendes Beispiel anschauen:

scala> class X { println("x") }
defined class X

scala> class Y extends X { println("y") }
defined class Y

scala> new X
x
res8: X = X@1f5e8c9d

scala> new Y
x
y
res9: Y = Y@42674cc4

Wir haben zwei Objekte: X und Y. Y erbt zwar von X, das heißt aber nicht, dass die Unterklasse Y die Oberklasse X komplett ersetzt. Wenn wir ein Y erzeugen wollen, dann müssen wir auch ein X erzeugen. Wir können das deutlich an der Ausgabe erkennen. Bei der Instanziierung von Y wird als erstes die Oberklasse X erzeugt, erst dann kommt Y an die Reihe. Dies ändert sich nicht wenn wir eine Klasse als abstrakt kennzeichnen. Der Compiler unterbindet uns dann zwar die Möglichkeit, dass wir die Oberklasse direkt erzeugen können – das heißt aber nicht, dass sie gar nicht erzeugt wird. Stattdessen wird sie genau dann erzeugt wenn eine der Unterklassen instanziiert wird.

Bezogen auf unser vorheriges Beispiel heißt das, dass wir als ersten eine Person erzeugen müssen bevor wir uns dem Manager oder dem Programmer zuwenden können. Die Initialisierung einer Oberklasse durch eines seiner Kinder ist in Scala denkbar einfach. Wie bei einer ganz normalen Erzeugung eines Objekts reicht es, die Initialisierungswerte in runden Klammern hinter die Oberklasse zu schreiben:

scala> class Manager(val name: String, val age: Int) extends Person(name, age)
<console>:8: error: overriding value name in class Person of type String;
 value name needs `override' modifier
       class Manager(val name: String, val age: Int) extends Person(name, age)
                         ^
<console>:8: error: overriding value age in class Person of type Int;
 value age needs `override' modifier
       class Manager(val name: String, val age: Int) extends Person(name, age)
                                           ^

Das sieht doch schon einmal ganz gut aus: Wir erhalten eine andere Fehlermeldung. Das bedeutet, dass wir der Sache also ein wenig näher kommen. Der Compiler möchte, dass wir unsere beiden Attribute mit einem override kennzeichnen. Tun wir dies hört er auf zu meckern:

scala> class Manager(override val name: String, override val age: Int) extends Person(name, age)
defined class Manager

Aber warum will er plötzlich ein override haben? Zuvor konnten wir abstrakte Methoden ja auch überschreiben, ohne dass er sich beschwert hat. Die Antwort darauf ist, dass das override immer dann gebraucht wird wenn wir bestehendes Verhalten tatsächlich auch überschreiben und nicht nur neu definieren. Bei dem Beispiel mit der Methode hatte diese keinen Körper, sie war also abstrakt und musste erst noch definiert werden. Bei dem Personen-Beispiel erzeugt uns der Compiler aber schon Getter und Setter. Die Felder wurden also schon in der Oberklasse definiert und wir wollen sie in der Unterklasse noch einmal definieren? Das geht zu recht nicht. Wir wollen sie eigentlich ja auch nicht neu definieren, sondern überschreiben. Deshalb fordert der Compiler auch das override, das ihm signalisiert, dass er das Verhalten der Oberklasse mit Verhalten aus der Unterklasse austauschen soll. Genau genommen wollen wir das ja aber eigentlich auch nicht. Wir wollen ja nur unsere Unterklassen instanziieren können ohne irgendetwas zu überschreiben.

Anmerkung:
Eine abstrakte Methode, die erst in einer Unterklasse definiert wird kann dort mit override gekennzeichnet werden, sie muss es aber nicht. Ob man es hinschreibt oder nicht bleibt einem selbst überlassen. Ich empfehle aber es zu unterlassen, da override das Überschreiben eines Members kennzeichnet und ein abstrakter Member wird ja eigentlich nicht überschrieben sondern erst einmal definiert.

Was machen wir jetzt also dagegen? Die Antwort ist ziemlich einfach, vielleicht könnt ihr sie euch schon denken:

scala> class Manager(name: String, age: Int) extends Person(name, age)
defined class Manager

scala> class Programmer(name: String, age: Int) extends Person(name, age)
defined class Programmer

Durch das Weglassen des val vor dem Attributnamen signalisieren wir dem Compiler, dass er keine neuen Felder erzeugen soll, sondern einfach nur einen Konstruktor mit Parameter. Aber das hatten wir schon alles.

scala> val m = new Manager("heinrich", 55)
m: Manager = Manager@75305547

scala> m.name
res10: String = heinrich

scala> m.age
res11: Int = 55

Wir können nun einen Manager erstellen und erben dabei die Getter aus der Oberklasse. Besonders toll beim Arbeiten mit Oberklassen ist, dass wir nicht wissen müssen mit welcher Kind-Klasse sie instanziiert wurde. Es wird uns erlaubt nur auf Schnittstellen-Basis zu arbeiten und konkretes Verhalten einzufordern obwohl der konkrete Typ nicht bekannt ist.

abstract class Person(val name: String, val age: Int) {
  def work()
}
class Manager(name: String, age: Int) extends Person(name, age) {
  def work() { manage() }
  def manage() { println(name+" manages his company") }
}
class Programmer(name: String, age: Int) extends Person(name, age) {
  def work() { program() }
  def program() { println(name+" programs software") }
}

Bei diesem Beispiel besitzen unsere Unterklassen je eine spezielle Methode, die die Oberklasse nicht kennt. Diese Methoden werden über die gemeinsame Schnittstelle, die Methode work, aufgerufen:

scala> val xs = List(new Manager("peter", 35), new Programmer("hugo", 42), new Manager("susie", 41))
xs: List[Person] = List(Manager@45637b37, Programmer@62e7b78, Manager@55ac0673)

scala> for (x <- xs) x.work()
peter manages his company
hugo programs software
susie manages his company

scala> xs(0).manage()
<console>:12: error: value manage is not a member of Person
              xs(0).manage()
                    ^

Wir haben mehrere Personen in einer List[Person] (deren gemeinsamer Obertyp Person vom Compiler korrekt erkannt wurde) und lassen diese alle arbeiten. Das funktioniert auch wie erwartet. Je nach dem ob wir einen Manager oder einen Programmierer erstellt haben wird die entsprechende Methode aufgerufen. Versuchen wir aber auf eine der konkreten Methoden zuzugreifen erhalten wir eine Fehlermeldung, da der Klasse Person diese ja nicht bekannt sind.

Innerhalb einer abstrakten Klasse besteht auch noch die Möglichkeit, dass wir einen bereits definierten Member nachträglich auf abstrakt setzen um dessen Überschreibung in den Unterklassen zu erzwingen:

scala> abstract class Printable {
     |   override def toString: String
     | }
defined class Printable

scala> class Test extends Printable
<console>:8: error: class Test needs to be abstract, since there is a deferred declaration of method toString in class Printable of type ()String which is not implemented in a subclass
       class Test extends Printable
             ^

Mit Hilfe der abstrakten Klasse Printable wollen wir erreichen, dass eine Klasse auf jeden Fall eine spezifische Stringrepräsentation besitzt. Durch erweitern der Klasse gehen wir sicher, dass die Methode toString auch tatsächlich überschrieben und nicht vergessen wird.

Zugriff auf die Superklassen

Wenn wir uns noch einmal das Beispiel von Setterdefinitionen anschauen stellen wir fest, dass wir unterschiedliche Namen für unsere Variablen benötigen:

class Person(val name: String, private var initAge: Int) {
  def age = initAge
  def age_=(age: Int) {
    if (age >= 0)
      initAge = age
  }
}

Wir haben hier im Konstruktor eine Variable namens initAge und im Setter eine namens age. Das ist ein wenig unhandlich. Schöner wäre es doch wenn wir beiden Variablen den gleichen Namen geben könnten. In obigem Beispiel ist das leider nicht möglich, da der Getter schon den Namen für sich beansprucht. Wir können aber die Parameternamen ändern:

class Counter {
  private var c = 0
  def count = c
  def count_=(c: Int) {
    this.c = c
  }
}

Wenn wir nun innerhalb des Setters c = c schreiben beschwert sich der Compiler weil wir den Wert der Variable sich selbst zuweisen wollen:

scala> var c = 0
c: Int = 0

scala> def change(c: Int) { c = c }
<console>:8: error: reassignment to val
       def change(c: Int) { c = c }
                              ^

Da alle Parameter in Scala als val erstellt werden, bekommen wir einen Zuweisungsfehler. Der Compiler kann ja nicht wissen, dass wir den Wert der äußeren Variable zuweisen wollen. Innerhalb einer Klasse besteht die Möglichkeit mit der this-Referenz auf den Scope der Klasse zuzugreifen und die äußere Variable also direkt anzusprechen, wie wir es beim Counter-Beispiel sehen können. Da this aber nicht auf den nächstäußeren Scope, sondern auf den der momentanen Klasse zeigt können wir damit nicht eine Variable gleichen Namens ansprechen, die sich auf einer äußeren Ebene befindet:

def x {
  var a = 0
  def y {
    var a = 3
    def z() {
      a = 5
    }
    z()
    println(a)
  }
  y
  println(a)
}

scala> x
5
0

Die erste Definition von a können wir in der innersten Methode z nicht ansprechen, da sie von dem a in Methode y verdeckt wird.

Manchmal wollen wir in einer Unterklasse eine Methode überschreiben aber gleichzeitig auch auf die Methode der Oberklasse zugreifen. Dafür gibt es dann die super-Referenz:

class Foo {
  def x() {
    println("foo")
  }
  def y(i: Int) = i*2
}

class Bar extends Foo {
  override def x() {
    super.x
    println("bar")
  }
  override def y(i: Int) = super.y(i)+10
}

Beim Aufruf der Methoden erhalten wir das erwartete Ergebnis:

scala> bar.x
foo
bar

scala> bar.y(5)
res6: Int = 20

Finale Member

Es ist nicht immer erwünscht, dass Teile unseres Codes durch Unterklassen erweitert oder sogar überschrieben werden. Nehmen wir an, eine Klasse besitzt eine Methode, die einen Parameter auf Gültigkeit überprüft. Wir wollen nicht, dass diese Überprüfung irgendjemand durch Überschreiben der Methode umgeht. Um dies nun also zu verhindern gibt es das Schlüsselwort final.

scala> class Tester {
     |   final def isValid(i: Int) = i < 5 
     | }
defined class Tester

scala> class Cracker extends Tester {
     |   override def isValid(i: Int) = true
     | }
<console>:9: error: overriding method isValid in class Tester of type (i: Int)Boolean;
 method isValid cannot override final member
         override def isValid(i: Int) = true
                      ^

Deklarieren wir eine Klasse als final können wir sogar verhindern, dass die Klasse erweitert wird:

scala> final class Tester
defined class Tester

scala> class Cracker extends Tester
<console>:8: error: illegal inheritance from final class Tester
       class Cracker extends Tester
                             ^
Advertisements

1 comment so far

  1. ruediger moeller on

    Tolles tutorial, danke !! Und noch !!!!


Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: