Fachkonzept - Objekte als Werte
Objekte sind Werte
In den bisherigen Projekten waren die Daten, mit denen wir gearbeitet haben,
meistens einfache Werte: Zahlen vom Typ Int, Wahrheitswerte vom
Typ Boolean oder Zeichenketten vom Typ String.
Solche Werte haben wir als Attribute gespeichert, als Parameter an Methoden
übergeben und als Rückgabewerte aus Methoden zurückbekommen.
Tatsächlich gibt es in Kotlin aber gar keinen grundsätzlichen Unterschied
zwischen einer Zahl wie 5 und einem selbst geschriebenen Objekt.
Auch 5 ist ein Objekt – und ebenso ist ein Objekt einer eigenen
Klasse ein Wert.
Überall dort, wo bisher ein einfacher Wert stehen durfte, darf deshalb auch ein
beliebiges Objekt stehen.
Wir betrachten das an einem Beispiel rund um Bücher. Ein einzelnes Buch sieht so aus:
class Buch(val titel: String, val autor: String) {
fun druckeInfo() {
println("„$titel“ von $autor")
}
}
In Kotlin sind alle Werte Objekte. Deshalb kann man Objekte überall dort verwenden, wo man auch einfache Werte verwendet: als Attribut, als Parameter und als Rückgabewert einer Methode.
Objekte als Rückgabewerte
Eine Methode kann nicht nur eine Zahl oder eine Zeichenkette zurückgeben,
sondern ein beliebiges Objekt.
Ein Verlag kann zum Beispiel ein neues Buch herstellen und dieses
als Objekt zurückgeben. Im Rückgabetyp steht dann einfach der Name der Klasse:
class Verlag(val name: String) {
fun veroeffentliche(titel: String, autor: String): Buch {
return Buch(titel, autor)
}
}
Das zurückgegebene Objekt kann anschließend genauso weiterverwendet werden wie jeder andere Wert. Insbesondere kann man an ihm sofort wieder eine Methode aufrufen:
class Buch(val titel: String, val autor: String) {
fun druckeInfo() {
println("„$titel“ von $autor")
}
}
class Verlag(val name: String) {
fun veroeffentliche(titel: String, autor: String): Buch {
return Buch(titel, autor)
}
}
fun main() {
val verlag = Verlag("Beispiel-Verlag")
val buch = verlag.veroeffentliche("Emil und die Detektive", "Erich Kästner")
buch.druckeInfo()
// oder direkt am Rückgabewert weiterarbeiten:
verlag.veroeffentliche("Pünktchen und Anton", "Erich Kästner").druckeInfo()
}
Objekte als Parameter
Genauso können Objekte als Parameter an Methoden übergeben werden.
Eine Vitrine stellt jeweils ein Buch aus. Mit der Methode
stelleAus übergibt man ihr ein ganzes Buch-Objekt:
class Vitrine {
var ausgestelltesBuch: Buch? = null
fun stelleAus(buch: Buch) {
ausgestelltesBuch = buch
}
}
Beim Aufruf übergibt man ein konkretes Buch-Objekt. Dieses kann z.B. zuvor vom Verlag erzeugt worden sein:
val vitrine = Vitrine()
vitrine.stelleAus(buch) // ein Objekt wird als Parameter übergeben
Wenn es kein Objekt gibt
Beim Arbeiten mit Objekten taucht eine neue Situation auf, die es bei den bisherigen Aufgaben so noch nicht gab: Manchmal gibt es gar kein Objekt. In einer frisch aufgestellten Vitrine ist zum Beispiel noch nichts ausgestellt. Was soll dann in der Vitrine stehen, wenn man nach dem ausgestellten Buch fragt?
Damit man solche Fälle sauber behandeln kann, unterscheidet Kotlin streng
zwischen Typen, die immer ein Objekt enthalten, und Typen, die
auch nichts enthalten dürfen. Dieses „nichts“ heißt in Kotlin
null.
Ein Datentyp mit angehängtem Fragezeichen wie Buch? heißt
nullbarer Typ (englisch nullable type).
Man kann ihn als „Buch oder nichts“ lesen.
Ein Wert dieses Typs ist also entweder ein echtes Buch-Objekt oder
null.
Ein Datentyp ohne Fragezeichen wie Buch enthält dagegen
garantiert immer ein echtes Objekt und niemals null.
Deshalb hat das Attribut ausgestelltesBuch den Typ
Buch? und beginnt mit dem Wert null:
Solange nichts ausgestellt wurde, gibt es kein Buch.
Eine Methode, die das ausgestellte Buch zurückgibt, hat dann ebenfalls den
Rückgabetyp Buch?:
fun aktuellesBuch(): Buch? {
return ausgestelltesBuch // Buch oder null
}
Das Problem mit null
An einem Objekt, das gar nicht da ist, kann man keine Methode aufrufen.
Der Versuch, an null die Methode druckeInfo()
aufzurufen, ergibt keinen Sinn – schließlich gibt es kein Buch, dessen
Informationen man drucken könnte.
Kotlin verhindert deshalb von vornherein, dass man an einem Wert vom Typ
Buch? einfach so eine Methode aufruft:
class Buch(val titel: String, val autor: String) {
fun druckeInfo() {
println("„$titel“ von $autor")
}
}
class Vitrine {
var ausgestelltesBuch: Buch? = null
fun aktuellesBuch(): Buch? {
return ausgestelltesBuch
}
}
fun main() {
val vitrine = Vitrine()
val b: Buch? = vitrine.aktuellesBuch()
b.druckeInfo() // Fehler! b könnte null sein
}
Schon der Compiler weist diesen Code zurück, bevor das Programm überhaupt
läuft. Das ist ein großer Vorteil: Viele Programmierfehler, die in anderen
Sprachen erst zur Laufzeit zu Abstürzen führen, fallen in Kotlin bereits beim
Schreiben des Programms auf.
Bevor man an einem nullbaren Wert eine Methode aufrufen darf, muss man also
erst den Fall null behandeln.
Umgang mit nullbaren Werten
Es gibt mehrere Möglichkeiten, mit einem Wert vom Typ Buch?
umzugehen. Alle haben gemeinsam, dass sie den Fall null ausdrücklich
berücksichtigen.
Abfrage mit if
Die anschaulichste Möglichkeit ist, mit einer if-Abfrage zu prüfen,
ob der Wert ungleich null ist.
Innerhalb des if-Blocks weiß Kotlin dann, dass es sich um ein
echtes Buch handelt, und der Methodenaufruf ist erlaubt:
val b: Buch? = vitrine.aktuellesBuch()
if (b != null) {
b.druckeInfo() // hier ist b garantiert ein echtes Buch
} else {
println("Die Vitrine ist leer")
}
Sicherer Aufruf mit ?.
Oft möchte man eine Methode nur dann aufrufen, wenn das Objekt vorhanden ist,
und sonst gar nichts tun. Dafür gibt es den sicheren
Aufruf (englisch safe call) mit ?..
Ist der Wert null, wird der Aufruf einfach übersprungen und es
passiert nichts.
val b: Buch? = vitrine.aktuellesBuch()
b?.druckeInfo() // druckt nur, wenn b nicht null ist
Ersatzwert mit dem Elvis-Operator ?:
Manchmal möchte man für den Fall null einen Ersatzwert oder eine
Ausweichaktion angeben. Das leistet der Elvis-Operator
?:. Steht links davon null, wird der Ausdruck rechts
davon verwendet.
class Buch(val titel: String, val autor: String) {
fun druckeInfo() {
println("„$titel“ von $autor")
}
}
class Vitrine {
var ausgestelltesBuch: Buch? = null
fun stelleAus(buch: Buch) {
ausgestelltesBuch = buch
}
fun aktuellesBuch(): Buch? {
return ausgestelltesBuch
}
}
fun main() {
val vitrine = Vitrine()
// vitrine.stelleAus(Buch("Emil und die Detektive", "Erich Kästner"))
vitrine.aktuellesBuch()?.druckeInfo() ?: println("Die Vitrine ist leer")
}
Hier werden die Informationen des ausgestellten Buchs gedruckt, falls eines
vorhanden ist – andernfalls erscheint die Meldung „Die Vitrine ist leer“.
Der Name „Elvis-Operator“ ist übrigens ein kleiner Scherz: Dreht man
?: um 90 Grad, erkennt man die Augen und die Haartolle von
Elvis Presley.
Ein nullbarer Wert (Typ mit ?) muss behandelt werden, bevor man
eine Methode an ihm aufruft. Dafür gibt es drei häufige Wege:
die Abfrage mit if (... != null),
den sicheren Aufruf ?.
und den Elvis-Operator ?: für einen Ersatzwert.