A.5 Objektorientierung 

Klassen sind die wichtigsten Grundbausteine in der objektorientierten Programmierung. Mit Ihnen werden Daten und Verhalten modelliert. Letzteres steckt in den Methoden und deren wechselseitigen Aufrufen. Die Daten stecken (zur Laufzeit) in Instanzvariablen der Objekte. Vereinfacht ausgedrückt sind Methoden Funktionen im Kontext einer Klasse.
A.5.1 Einfache Klassen 

Listing A.35 zeigt eine vollständige Klasse in Kotlin. Auf das Schlüsselwort class folgt der Name der Klasse, etwaige Typparameter, das Schlüsselwort constructor und eine Liste von Argumenten. Der sogenannte primäre Konstruktor gehört zum Klassenkopf. Seine Signatur gibt die bevorzugte Instanziierung an. Er kann keinen Code enthalten. Ist dies nötig, werden init-Blöcke verwendet. Dazu komme ich etwas später. Das Schlüsselwort constructor kann entfallen, wenn im Konstruktor keine Annotationen und Zugriffsmodifizierer angegeben werden.
class Person constructor(var name: String, var age: Int) {
}
Listing A.35 Eine vollständige Klasse in Kotlin
Auffällig ist, dass weder Eigenschaften noch Methoden definiert wurden. Aber was tut Person dann? Listing A.36 erzeugt ein Objekt des Typs Person und weist es der Variablen p zu. Danach werden die beiden Eigenschaften name und age mit neuen Werten überschrieben. Der Aufruf von println() führt dann zur Ausgabe von »Max Mustermann ist 42 Jahre alt.«. Ist Ihnen aufgefallen, dass im Gegensatz zu Java und C# kein new für die Instanziierung verwendet wird? Tatsächlich kennt Kotlin dieses Schlüsselwort überhaupt nicht. Auch Swift kommt ohne aus.
fun main() {
val p = Person("Max", 123)
p.name = "Max Mustermann"
p.age = 42
println("${p.name} ist ${p.age} Jahre alt.")
}
Listing A.36 Verwendung der Klasse »Person«
Parameter des primären Konstruktors werden zu Eigenschaften, die gelesen und geschrieben (var) oder nur gelesen (val) werden können. Zugriffe auf Eigenschaften lassen sich mit Sichtbarkeitsmodifikatoren steuern. Wird var bzw. val weggelassen, kann die Variable nur in init-Blöcken verwendet werden.
Datenklassen
Sie können die Klasse Person mit dem Schlüsselwort data vor class zu einer Datenklasse machen, eine der wichtigsten Neuerungen in Kotlin gegenüber Java. Häufig braucht man Klassen, die ausschließlich Datenstrukturen repräsentieren oder als Transferobjekte fungieren. Diese speichern Werte und ermöglichen den Zugriff darauf, enthalten aber keine Fachlogik. Da Java keine echten Eigenschaften mit Getter und Setter auf Sprachebene kennt, müssen Zugriffsmethoden explizit ausprogrammiert werden. Das entfällt in Kotlin. Zusätzlich bezieht der Compiler bei Datenklassen in den Methoden equals(), hashCode() und toString() automatisch alle Eigenschaften mit ein.
Beispielsweise führt println(Person("Max", 123).toString()) bei der Klassendefinition in Listing A.36 zu etwas wie »Person@5a07e868«. Haben Sie mit data die Klasse Person aber zu einer Datenklasse gemacht, erscheint stattdessen »Person(name=Max, age=123)«. Außerdem enthalten Datenklassen eine Funktion zum Kopieren. Mit copy() können gezielt Eigenschaften geändert werden.
A.5.2 Sekundäre Konstruktoren und init-Blöcke 

Sekundäre Konstruktoren ermöglichen die Instanziierung der Klasse mit anderen Parametern als über den primären Konstruktor. Falls die Klasse einen primären Konstruktor definiert, muss jeder sekundäre Konstruktor diesen aufrufen, entweder direkt oder über einen anderen sekundären Konstruktor. Hierfür wird das Schlüsselwort this verwendet. Listing A.37 zeigt, wie Sie einen sekundären Konstruktor definieren.
fun main() {
val p = Person()
println("${p.age}")
}
class Person(var name: String, var age: Int) {
init {
println("name: $name, age: $age")
}
constructor(): this("???",-1) {
println("Instanziierung über parameterlosen Konstruktor")
}
}
Listing A.37 Verwendung eines sekundären Konstruktors
Auf das Schlüsselwort constructor folgt die Parameterliste. In meinem Beispiel ist sie leer. Ihr folgt ein Doppelpunkt und, sofern nötig, der Aufruf eines anderen Konstruktors – hier der primäre Konstruktor mit den Argumenten "???" und -1. Da der primäre Konstruktor keinen Code enthalten kann, packen Sie Initialisierungsaufgaben in einen oder mehrere init-Blöcke. Während der Instanziierung werden diese in der Reihenfolge, in der sie im Quelltext stehen, abgearbeitet. Eigenschaften, die vor einem init-Block definiert wurden, können darin verwendet werden. Wie das aussehen kann, zeigt Listing A.38.
class InitBlockDemo(name: String) {
val a = name.also(::println)
init {
println("Erster Block: $a")
}
val b = "${a.toUpperCase()}".also(::println)
init {
println("Zweiter Block: $b")
}
constructor() : this("Hallo") {
println("Sekundärer Konstruktor")
}
}
fun main() {
InitBlockDemo()
}
Listing A.38 Gestufte Initialisierung mit init-Blöcken
Die Bildschirmausgaben sind in Abbildung A.4 zu sehen. init-Blöcke werden vor Konstruktor-Code ausgeführt. Der Rumpf meines parameterlosen sekundären Konstruktors wird zuletzt abgearbeitet.
Abbildung A.4 Bildschirmausgaben von Listing A.38
A.5.3 Gleichheit und Identität 

Sofern keine explizite Elternklasse angegeben wird, leiten Klassen in Kotlin von Any ab. Wie Object in Java enthält die Wurzel des Typsystems die grundlegenden Methoden equals(), hashCode() und toString(). Die ersten beiden werden unter anderem verwendet, um Objekte auf Gleichheit und Identität zu prüfen. toString() liefert eine Darstellung des Objekts als Zeichenkette. Für Any ist dies eine Referenz in der Form »java.lang.Object@5a07e868« (sofern die Erzeugung in einer Java Virtual Machine stattfindet).
Kotlin unterscheidet zwischen struktureller und referenzieller Gleichheit. Die Unterschiede demonstriert Listing A.39. === prüft, ob zwei Referenzen auf dasselbe Objekt verweisen. Da b als neuer String erzeugt wird, ist das nicht der Fall, auch wenn er den gleichen Inhalt wie a hat. == hingegen vergleicht die Zeichen. Diese sind gleich, da b aus einem ByteArray von a erzeugt wurde.
val a = "Hallo Kotlin"
val b = String(a.toByteArray())
// Referenzielle Gleichheit
println("a === b: ${a === b}")
// Strukturelle Gleichheit
println("a == b: ${a == b}")
Listing A.39 Strukturelle und referenzielle Gleichheit
== entspricht equals, Sie könnten also auch ${a.equals(b)} schreiben. Wenn Sie bereits in Java programmiert haben, vergegenwärtigen Sie sich bitte, dass == in Java Objektreferenzen vergleicht und deshalb dem === von Kotlin entspricht. Nur bei primitiven Datentypen verhält sich == in Java so wie in Kotlin.
A.5.4 Vererbung 

Soll eine Klasse von einer bestimmten Elternklasse erben, wird diese im Klassenkopf nach der Parameterliste des primären Konstruktors in der Form : <Name der Elternklasse> angegeben. Hat die Basisklasse einen primären Konstruktor, muss dieser aufgerufen werden. Das ist in Listing A.40 zu sehen. Im Klassenkopf von B wird mit : A(24) der Konstruktor der Elternklasse A aufgerufen. Wie Java kennt Kotlin, anders als C++, keine Mehrfachvererbung.
fun main() {
A(42).hallo()
B().hallo()
}
open class A(a: Int) {
init {
print("A").also { println(" mit a=$a")}
}
open fun hallo() = println("Hallo")
}
class B : A(24) {
init {
println("B")
}
override fun hallo() = println("Kotlin")
}
Listing A.40 Einfaches Beispiel von Vererbung
Abbildung A.5 zeigt, in welcher Reihenfolge die init-Blöcke der beiden Klassen abgearbeitet werden. »A mit ...« erscheint zweimal. Die erste Ausgabe ergibt sich aus der Zeile A(42) in der Funktion main(). Die zweite ist die Folge des Konstruktoraufrufs A(24) im Klassenkopf von B.
Abbildung A.5 Die Bildschirmausgabe von Listing A.40
Ist Ihnen das Schlüsselwort open im Klassenkopf von A aufgefallen? Kotlin-Klassen sind final, können also standardmäßig nicht abgeleitet werden. Ist Vererbung gewünscht, müssen Sie open verwenden. Gleiches gilt für Methoden. Nur die Klasse zu öffnen, reicht nicht. Gleichzeitig müssen abgeleitete Klassen bei Methoden, die sie überschreiben, das Schlüsselwort override verwenden. Sie können Methoden der Elternklasse wie in Java mit super. aufrufen.
Abstrakte Klassen und Methoden müssen mit dem Schlüsselwort abstract versehen werden. Im Klassenkopf kann open entfallen. Die ableitende Klasse muss überschriebene Methoden aber mit override kennzeichnen (Listing A.41).
fun main(args: Array<String>) {
B().hallo("Kotlin")
}
abstract class A {
abstract fun hallo(s: String)
}
class B : A() {
override fun hallo(s: String) = println("Hallo, $s")
}
Listing A.41 Abstrakte Klassen und Methoden
Darüber hinaus gibt es noch eine interessante Verwendung von abstract. Anders als beispielsweise in Java können Sie abstrakte Eigenschaften definieren. Wie, sehen Sie in Listing A.42. Die abstrakte Klasse A ist mit T typisiert. Diesen Typ verwendet die abstrakte Eigenschaft value. var bedeutet, dass Werte gesetzt und gelesen werden können.
fun main() {
println(B().value)
println(C().value)
}
abstract class A<T> {
abstract var value: T
}
class B : A<String>() {
override var value = "Hallo"
}
class C : A<Int>() {
override var value = 42
}
Listing A.42 Abstrakte Eigenschaften
Die Klasse B leitet von A<String> ab, value nimmt deshalb Zeichenketten auf. C typisiert A mit Int, deshalb enthält value in C ganze Zahlen.
Interfaces werden mit dem Schlüsselwort interface definiert. Klassen, die es implementieren möchten, verwenden wie bei der Ableitung den Doppelpunkt (Listing A.43). Interfaces können neben den klassischen Methodendeklarationen auch vollständige Implementierungen anbieten. Auch die Definition von Eigenschaften ist möglich.
fun main() {
val greeter = ConsoleGreeter()
greeter.sayHello(greeter.defaultName)
}
interface Greeter {
val defaultName: String
get() = "world"
fun sayHello(name: String)
fun console(s: String) {
println(s)
}
}
class ConsoleGreeter: Greeter {
override val defaultName = "Kotlin"
override fun sayHello(name: String) = console("Hallo, $name")
}
Listing A.43 Interfaces in Kotlin
Allerdings können Sie Eigenschaften nicht direkt initialisieren (val defaultName = "world"). Das führt zur Fehlermeldung »Property initializers are not allowed in interfaces«. Sie müssen wie gezeigt den Wert in einem Getter setzen. Sollen die implementierenden Klassen den Wert vorgeben, muss die Eigenschaft abstract sein.
In Java ist es – anders als beispielsweise in C# – mit anonymen inneren Klassen einfach, ein Interface zu implementieren, ohne extra eine Klasse definieren zu müssen. Das geht auch in Kotlin. Die Vorgehensweise zeigt Listing A.44.
val greeter = object: Greeter {
override val defaultName = "Kotlin"
override fun sayHello(name: String) = console("Hallo, $name")
}
greeter.sayHello(greeter.defaultName)
Listing A.44 Direkte Implementierung eines Interfaces