Elvis-Operator in Kotlin
Der Elvis-Operator liefert lesbare Fallback-Werte für nullable Ausdrücke. Du lernst, wann Defaults helfen und wann sie Fehler verdecken.
Der Elvis-Operator ?: ist ein kleines Kotlin-Werkzeug mit großer Wirkung im Alltag: Er gibt dir einen gut lesbaren Fallback, wenn ein nullable Ausdruck keinen Wert enthält. Gerade in Android-Projekten arbeitest du ständig mit Daten, die fehlen können: API-Felder, Intent-Extras, gespeicherte Einstellungen, Datenbankwerte oder UI-State. Mit ?: formulierst du klar, welcher Standardwert verwendet werden soll, statt überall verschachtelte if-Abfragen zu schreiben.
Was ist das?
Der Elvis-Operator ist ein Operator für nullbare Werte in Kotlin. Links steht ein Ausdruck, der einen echten Wert oder null liefern kann. Rechts steht der Ersatzwert, der nur dann verwendet wird, wenn links null ist. Das Muster lautet:
val displayName = user.name ?: "Unbekannter Nutzer"
Wenn user.name einen Wert enthält, landet dieser Wert in displayName. Wenn user.name null ist, wird "Unbekannter Nutzer" verwendet. Der Operator heißt Elvis-Operator, weil ?: mit etwas Fantasie wie ein Gesicht mit Frisur aussieht. Für deine Praxis ist der Name weniger wichtig als das Denkmodell: Nimm den vorhandenen Wert, sonst nimm den definierten Fallback.
Das Problem dahinter ist Nullability. Kotlin unterscheidet bewusst zwischen Typen, die null sein dürfen, und Typen, die nicht null sein dürfen. Ein String ist nicht nullable. Ein String? darf fehlen. Dadurch zwingt dich der Compiler, über fehlende Werte nachzudenken. Genau an dieser Stelle hilft ?:, denn du kannst aus einem String? kontrolliert einen String machen.
Im Android-Kontext ist das sehr relevant. Android-Apps bekommen Daten aus vielen Quellen, die nicht immer vollständig sind. Ein Backend kann ein Profilbild weglassen. Ein Deep Link kann einen Parameter nicht enthalten. Ein Bundle kann nach einem Prozess-Neustart anders aussehen als erwartet. Ein Compose-Screen kann einen Ladezustand anzeigen, bevor die echten Inhalte verfügbar sind. Der Elvis-Operator ist dann kein Trick, sondern eine klare Aussage im Code: Für diesen Fall akzeptieren wir einen Default.
Wichtig ist die Grenze: ?: ist für Fallbacks gedacht, nicht für das Verstecken von Fehlern. Wenn ein Wert fachlich zwingend vorhanden sein muss, ist ein stiller Standardwert oft gefährlich. Dann solltest du eher validieren, einen Fehlerzustand modellieren oder den Fehler sichtbar machen. Ein fehlender Anzeigename kann mit "Unbekannt" ersetzt werden. Eine fehlende Zahlungs-ID in einem Checkout-Prozess sollte nicht durch "0" ersetzt werden.
Wie funktioniert es?
Der Elvis-Operator wertet zuerst die linke Seite aus. Ist das Ergebnis nicht null, wird genau dieses Ergebnis zurückgegeben. Ist das Ergebnis null, wird die rechte Seite ausgewertet und zurückgegeben. Das ist ein wichtiger Punkt: Die rechte Seite wird nur gebraucht, wenn links wirklich null ist. Dadurch kannst du rechts auch einen Funktionsaufruf, ein return oder einen throw verwenden.
val userId = intent.getStringExtra("user_id") ?: return
In diesem Beispiel beendet die Funktion ihre Ausführung, wenn kein user_id-Extra vorhanden ist. Das funktioniert, weil return in Kotlin als Ausdruck an dieser Stelle erlaubt ist. Ebenso kannst du eine klare Ausnahme werfen, wenn ein Wert für den weiteren Ablauf zwingend ist:
val token = session.token ?: error("Session ohne Token")
Für Anfänger ist das mentale Modell so: ?: ist eine kompakte if-null-else-Entscheidung. Der Ausdruck links ist der bevorzugte Wert. Der Ausdruck rechts ist der Plan B. Du solltest beim Lesen einer Zeile immer fragen: Was ist der normale Wert, und was ist der Fallback?
Der Operator arbeitet häufig zusammen mit dem Safe-Call-Operator ?.. Mit ?. greifst du nur dann auf eine Eigenschaft oder Funktion zu, wenn der Ausdruck davor nicht null ist. Mit ?: legst du danach fest, was bei null passieren soll.
val city = user.address?.city ?: "Kein Ort angegeben"
Hier kann address fehlen, und auch city kann fehlen. Die linke Seite liefert dann insgesamt null, wenn ein Teil der Kette nicht vorhanden ist. Der Elvis-Operator macht daraus einen nicht-nullbaren String für die UI.
In Kotlin ist auch der Typ wichtig. Wenn links String? ist und rechts ein String, kann das Gesamtergebnis ein String sein. Das ist praktisch, weil nach dem Elvis-Operator oft kein weiteres Null-Handling nötig ist. Der Compiler erkennt, dass du einen Fallback bereitgestellt hast.
val title: String = article.title ?: "Ohne Titel"
Ohne ?: könntest du article.title nicht direkt einer Variablen vom Typ String zuweisen, wenn title nullable ist. Mit ?: sagst du dem Compiler: Hier kommt garantiert ein String heraus.
Der rechte Ausdruck sollte aber typologisch passen. Wenn links ein Int? ist, sollte rechts ein Int stehen. Wenn links ein komplexes Objekt ist, sollte rechts ein Ersatzobjekt desselben erwarteten Typs stehen. Kotlin kann gemeinsame Obertypen ableiten, aber dadurch wird Code nicht automatisch besser. Wenn dein Elvis-Ausdruck plötzlich den Typ Any bekommt, ist das meist ein Zeichen dafür, dass die beiden Seiten nicht sauber zusammenpassen.
Im Alltag findest du ?: oft in ViewModels, Mappern und UI-Code. In einem ViewModel wandelst du Rohdaten aus Repository oder Netzwerk in einen UI-State um. Dort ist ein Fallback oft sinnvoll, weil die UI konkrete Werte anzeigen muss. In Jetpack Compose kannst du nullable State-Werte ebenfalls sauber abfangen, bevor du sie an Composables weitergibst.
Text(text = state.userName ?: "Gast")
Das ist besser lesbar als eine längere if-Abfrage, solange der Fallback wirklich die gewünschte UI-Entscheidung ist. Bei Architekturfragen solltest du überlegen, in welcher Schicht der Default entstehen soll. Ein UI-Text wie "Gast" gehört meist eher in die UI- oder Presentation-Schicht. Ein fachlicher Default, etwa eine Standard-Sortierung, kann in der Domain- oder Repository-Schicht liegen. Der Elvis-Operator löst nicht automatisch die Architekturfrage, er macht nur deine Entscheidung kompakt sichtbar.
Auch bei List, Boolean und Zahlen wird ?: häufig genutzt. Beispiele sind items ?: emptyList(), enabled ?: false oder retryCount ?: 0. Diese Muster sind gut, wenn ein fehlender Wert tatsächlich wie eine leere Liste, ein deaktivierter Zustand oder eine Null-Wiederholung behandelt werden soll. Du solltest sie nicht gedankenlos einsetzen, weil gerade false und 0 fachlich stark wirken können. Ein fehlendes Feld ist nicht immer dasselbe wie eine bestätigte Null.
In der Praxis
Stell dir vor, du baust einen Profil-Screen in einer Android-App. Das Backend liefert ein UserDto, bei dem manche Felder nullable sind. Für die UI willst du daraus ein stabiles Modell machen, damit dein Compose-Code nicht überall mit String? arbeiten muss.
data class UserDto(
val id: String?,
val name: String?,
val bio: String?,
val avatarUrl: String?
)
data class ProfileUiState(
val id: String,
val displayName: String,
val bioText: String,
val avatarUrl: String?
)
fun UserDto.toUiState(): ProfileUiState {
val requiredId = id ?: error("UserDto ohne id")
return ProfileUiState(
id = requiredId,
displayName = name ?: "Gast",
bioText = bio ?: "Noch keine Beschreibung vorhanden",
avatarUrl = avatarUrl
)
}
Hier siehst du drei unterschiedliche Entscheidungen. id ist fachlich erforderlich. Wenn sie fehlt, erzeugst du keinen stillen Ersatzwert, sondern brichst mit einer klaren Fehlermeldung ab. name darf fehlen, deshalb bekommt die UI "Gast". bio darf ebenfalls fehlen, deshalb zeigst du einen neutralen Text. avatarUrl bleibt nullable, weil ein fehlendes Bild vielleicht direkt in der UI durch einen Avatar-Platzhalter behandelt wird.
In Compose kann der Screen anschließend ohne ständige Nullprüfung arbeiten:
@Composable
fun ProfileHeader(state: ProfileUiState) {
Column {
Text(text = state.displayName)
Text(text = state.bioText)
if (state.avatarUrl == null) {
DefaultAvatar()
} else {
RemoteAvatar(url = state.avatarUrl)
}
}
}
Der Elvis-Operator sitzt hier im Mapping, nicht überall verteilt im UI-Code. Das macht den Screen leichter lesbar. Gleichzeitig bleiben wichtige Unterschiede erhalten: Eine fehlende ID ist ein Fehler, ein fehlender Name ist ein darstellbarer Zustand, eine fehlende Avatar-URL wird bewusst als nullable weitergereicht.
Eine praktische Entscheidungsregel lautet: Nutze ?:, wenn der Fallback fachlich korrekt, lokal verständlich und für den Leser überraschungsarm ist. Nutze keinen stillen Fallback, wenn der fehlende Wert auf kaputte Daten, eine falsche Navigation oder einen Programmierfehler hinweist.
Typische gute Fälle sind UI-Labels, optionale Beschreibungen, leere Listen in Anzeigezuständen und Default-Konfigurationen. Kritische Fälle sind IDs, Tokens, Preise, Berechtigungen, Navigation-Argumente und Daten, die für eine Aktion verbindlich sind. Bei solchen Werten ist ?: return, ?: error(...) oder ein eigener Fehlerzustand oft klarer als ein scheinbar harmloser Ersatzwert.
Eine häufige Stolperfalle ist der zu breite Default. Beispiel:
val priceText = product.price ?: "0,00 €"
Das sieht bequem aus, kann aber fachlich falsch sein. Wenn price fehlt, bedeutet das nicht automatisch, dass das Produkt kostenlos ist. Besser wäre vielleicht:
val priceText = product.price ?: "Preis nicht verfügbar"
Oder du modellierst im UI-State einen eigenen Zustand, damit der Screen nicht so tut, als wäre ein echter Preis vorhanden. Der Elvis-Operator ist nur so gut wie die fachliche Entscheidung rechts davon.
Eine zweite Stolperfalle ist die Kette aus vielen Safe Calls mit einem Default am Ende:
val label = order.customer?.address?.city?.uppercase() ?: "Unbekannt"
Das ist kompakt, aber beim Debuggen kann unklar sein, welcher Teil fehlt: customer, address oder city. Für einfache Anzeigen ist das okay. Wenn der Unterschied wichtig ist, solltest du Zwischenschritte benennen oder den Zustand genauer modellieren.
val customer = order.customer ?: return MissingCustomer
val address = customer.address ?: return MissingAddress
val city = address.city ?: return MissingCity
Dieser Code ist länger, aber fachlich genauer. Als Junior-Dev solltest du lernen, dass kurze Syntax nicht automatisch besserer Code ist. Lesbarkeit entsteht, wenn der Code die richtige Information sichtbar macht.
In Tests kannst du dein Verständnis sehr gut prüfen. Schreibe für eine Mapping-Funktion mindestens zwei Tests: einen mit vollständigen Daten und einen mit fehlenden optionalen Daten. Wenn ein Wert erforderlich ist, teste auch den Fehlerfall. So merkst du, ob dein Fallback nur bei echten null-Fällen greift und ob du nicht aus Versehen wichtige Fehler verdeckst.
@Test
fun mapsMissingNameToGuest() {
val dto = UserDto(
id = "42",
name = null,
bio = "Android-Entwickler",
avatarUrl = null
)
val state = dto.toUiState()
assertEquals("Gast", state.displayName)
}
@Test
fun missingIdFailsFast() {
val dto = UserDto(
id = null,
name = "Mina",
bio = null,
avatarUrl = null
)
assertFailsWith<IllegalStateException> {
dto.toUiState()
}
}
Beim Debuggen lohnt es sich, Breakpoints vor den Elvis-Ausdruck zu setzen und die linke Seite bewusst zu inspizieren. Frage dich: Ist null hier ein erwarteter Zustand oder ein Hinweis auf einen Fehler? Genau diese Frage trennt sauberes Null-Handling von zufälligem Default-Code.
In Code-Reviews solltest du bei jedem ?: kurz prüfen, ob der rechte Wert wirklich sinnvoll ist. Ein Review-Kommentar muss nicht groß sein. Schon die Frage „Ist 0 hier ein echter Default oder nur ein Platzhalter?“ kann einen Fehler verhindern. Besonders bei Android-Apps mit Releases im Play Store sind solche Details relevant, weil fehlerhafte Defaults oft nicht sofort crashen, sondern falsches Verhalten ausliefern.
Fazit
Der Elvis-Operator ?: hilft dir, nullable Ausdrücke in Kotlin klar und kompakt mit Fallbacks zu behandeln. Für Android ist das besonders nützlich, weil viele Datenquellen unvollständig sein können und UI-Code stabile Werte braucht. Setze Defaults dort ein, wo ein fehlender Wert fachlich akzeptiert ist, und mache zwingende Werte durch return, error(...), Tests oder genaue Zustände sichtbar. Nimm dir als Übung eine Mapper-Funktion aus deinem Projekt, markiere jeden Elvis-Operator und entscheide bewusst, ob der Fallback ein echter Default ist oder ob ein Fehler besser sichtbar werden sollte.