Android Coden
Android 7 min lesen

Vererbung in Kotlin: open, override und Hierarchien

Vererbung ordnet Klassen in Kotlin bewusst streng. Du lernst, wann Hierarchien helfen und wann Komposition klarer bleibt.

Vererbung ist ein Werkzeug, um gemeinsames Verhalten in einer Basisklasse zu bündeln und spezialisierte Klassen darauf aufzubauen. In Kotlin ist dieses Werkzeug absichtlich streng: Klassen und Funktionen sind nicht automatisch erweiterbar. Du musst mit open freigeben, was überschrieben werden darf, und mit override klar markieren, wo du Verhalten ersetzt. Genau diese Klarheit ist im Android-Alltag wichtig, weil App-Code lange lebt, getestet werden muss und oft von mehreren Personen weiterentwickelt wird.

Was ist das?

Inheritance, auf Deutsch Vererbung, beschreibt eine Beziehung zwischen Klassen: Eine Klasse übernimmt Eigenschaften und Funktionen einer anderen Klasse. Die übergeordnete Klasse wird oft Basisklasse, Superklasse oder Parent genannt. Die abgeleitete Klasse ist die Subklasse oder Child-Klasse. Daraus entsteht eine hierarchy, also eine Klassenhierarchie.

Das mentale Modell ist einfach: Die Basisklasse beschreibt, was alle Varianten gemeinsam haben. Die Subklasse beschreibt, was eine konkrete Variante zusätzlich kann oder anders macht. Wenn du zum Beispiel verschiedene Arten von Repositorys, Validatoren oder UI-Zuständen modellierst, kann eine gemeinsame Oberklasse helfen, einen stabilen Vertrag zu formulieren.

Kotlin unterscheidet sich hier bewusst von manchen anderen objektorientierten Sprachen. Eine Klasse ist standardmäßig final. Das heißt: Du kannst nicht einfach von ihr erben. Erst wenn du sie mit open markierst, erlaubst du Vererbung. Dasselbe gilt für Funktionen und Properties. Wenn eine Subklasse Verhalten austauschen soll, muss das Mitglied in der Basisklasse open sein und in der Subklasse mit override überschrieben werden.

Für Android-Entwicklung ist das relevant, weil du ständig mit Framework-Klassen, Jetpack-Bausteinen und eigenen Architekturschichten arbeitest. Eine Activity, ein ViewModel oder ein Test-Double kann Teil einer Hierarchie sein. Gleichzeitig setzen moderne Android-Apps stark auf Komposition: Du baust Verhalten aus kleineren Objekten zusammen, statt viele Ebenen von Vererbung anzulegen. Gerade mit Jetpack Compose ist das deutlich sichtbar. UI wird eher über Funktionen, State und Parameter zusammengesetzt, nicht über tiefe View-Hierarchien aus eigenen Subklassen.

Vererbung löst also ein echtes Problem: Du kannst gemeinsamen Code und gemeinsame Regeln zentral ausdrücken. Sie erzeugt aber auch Kopplung. Eine Subklasse hängt an Details der Basisklasse. Änderst du die Basisklasse, kann Verhalten in mehreren Subklassen brechen. Deshalb solltest du Vererbung nicht als Standardlösung sehen, sondern als bewusstes Modellierungswerkzeug.

Wie funktioniert es?

In Kotlin beginnt Vererbung mit einer open Klasse. Ohne open ist eine Klasse geschlossen. Das schützt dich vor unbeabsichtigten Erweiterungen. Wenn du eine Klasse freigibst, sagst du damit: Diese Klasse ist als Basis gedacht, und ihr Verhalten darf von anderen Klassen erweitert werden.

Eine einfache Basisklasse kann so aussehen:

open class UserMessageFormatter {
    open fun format(name: String): String {
        return "Hallo, $name"
    }
}

Die Subklasse nutzt einen Doppelpunkt, um die Basisklasse anzugeben. Wenn sie eine Funktion überschreibt, muss sie override schreiben:

class FormalUserMessageFormatter : UserMessageFormatter() {
    override fun format(name: String): String {
        return "Guten Tag, $name"
    }
}

override ist mehr als Syntax. Es ist ein Signal an dich, an andere Entwicklerinnen und Entwickler und an den Compiler: Diese Funktion gehört zu einem bestehenden Vertrag. Wenn sich die Signatur in der Basisklasse ändert, erkennt der Compiler, dass die Überschreibung nicht mehr passt. Das ist ein wichtiger Sicherheitsmechanismus.

Auch Properties können open sein und überschrieben werden. Konstruktoren spielen ebenfalls eine Rolle. Eine Subklasse muss die Basisklasse korrekt initialisieren. Wenn die Basisklasse Parameter erwartet, gibst du sie beim Erben weiter:

open class ScreenState(val title: String)

class LoadingState : ScreenState("Laden")
class ErrorState(message: String) : ScreenState(message)

Trotzdem solltest du bei Android-Code genau prüfen, ob eine offene Klassenhierarchie wirklich nötig ist. Häufig ist ein Interface oder eine Komposition klarer. Ein Interface beschreibt, was ein Objekt kann, ohne festzulegen, von welcher Klasse es abstammt. Komposition bedeutet, dass eine Klasse ein anderes Objekt nutzt, statt von ihm zu erben.

Ein typisches Beispiel ist ein ViewModel. Du könntest eine große BaseViewModel-Klasse bauen, in der Logging, Fehlerbehandlung, Coroutine-Start, Navigation und Ladezustände liegen. Am Anfang wirkt das praktisch. Nach einigen Features wird die Klasse aber oft schwer verständlich. Jede Subklasse erbt Verhalten, das sie vielleicht nicht braucht. Tests werden unübersichtlich, weil du nicht sofort siehst, welches Verhalten aus der Subklasse kommt und welches aus der Basisklasse.

Eine bessere Regel lautet: Nutze Vererbung, wenn eine echte Ist-ein-Beziehung besteht und die gemeinsame Basisklasse einen stabilen, kleinen Vertrag bietet. Nutze Komposition, wenn du Verhalten nur wiederverwenden möchtest. Ein LoginViewModel ist nicht wirklich ein NetworkErrorHandler. Es kann aber einen NetworkErrorMapper verwenden.

In Kotlin gibt es außerdem wichtige Alternativen zur klassischen Vererbung. data class eignet sich für reine Daten. sealed class oder sealed interface eignet sich für begrenzte Hierarchien, etwa UI-Zustände. Das ist besonders nützlich, wenn alle Varianten bekannt sind und der Compiler dich bei vollständigen when-Ausdrücken unterstützen soll. Für dieses Thema bleibt der Kern aber: open öffnet Erweiterbarkeit, override ersetzt Verhalten, und die Hierarchie sollte fachlich begründet sein.

In der Praxis

Stell dir vor, du baust eine kleine Profilansicht. Du möchtest Nutzernamen unterschiedlich anzeigen: normal in der App, anonymisiert in einem Test oder in einem Demo-Modus. Mit Vererbung könntest du eine Basisklasse definieren:

open class DisplayNameFormatter {
    open fun format(firstName: String, lastName: String): String {
        return "$firstName $lastName"
    }
}

class ShortDisplayNameFormatter : DisplayNameFormatter() {
    override fun format(firstName: String, lastName: String): String {
        return "$firstName ${lastName.first()}."
    }
}

class AnonymousDisplayNameFormatter : DisplayNameFormatter() {
    override fun format(firstName: String, lastName: String): String {
        return "Anonymer Nutzer"
    }
}

Das ist verständlich, solange die Hierarchie klein bleibt. Du hast einen gemeinsamen Vertrag: Aus Vor- und Nachname wird ein Anzeigename. Die Varianten ändern nur diese eine Entscheidung. In einem ViewModel könntest du den Formatter verwenden:

class ProfileViewModel(
    private val formatter: DisplayNameFormatter
) : ViewModel() {

    fun displayName(user: User): String {
        return formatter.format(user.firstName, user.lastName)
    }
}

data class User(
    val firstName: String,
    val lastName: String
)

Achte auf den Unterschied: Das ProfileViewModel erbt von ViewModel, weil es fachlich und technisch ein ViewModel ist. Den Formatter erbt es nicht. Es bekommt ihn als Abhängigkeit. Das ist Komposition. Dadurch kannst du im Test leicht eine passende Variante einsetzen:

@Test
fun displayName_usesFormatter() {
    val viewModel = ProfileViewModel(AnonymousDisplayNameFormatter())

    val result = viewModel.displayName(User("Mira", "Schulz"))

    assertEquals("Anonymer Nutzer", result)
}

Diese Form ist im Android-Alltag gut wartbar. Du nutzt Vererbung dort, wo das Framework sie vorgibt oder wo eine kleine Hierarchie den Code klarer macht. Du nutzt Komposition dort, wo du austauschbares Verhalten brauchst. Das passt auch gut zu Tests und Continuous Integration: Kleine, klar injizierte Abhängigkeiten lassen sich zuverlässig testen, ohne dass du eine umfangreiche Basisklasse vorbereiten musst.

Eine typische Stolperfalle ist die überladene Basisklasse. Sie beginnt mit zwei nützlichen Funktionen und endet als Sammelstelle für alles, was mehrere Klassen irgendwann brauchen könnten. Dann entstehen versteckte Abhängigkeiten. Eine Subklasse überschreibt eine Methode, ruft vielleicht super nicht auf, und plötzlich fehlt ein Log-Eintrag, ein Ladezustand wird nicht zurückgesetzt oder ein Fehler wird nicht gemeldet. Solche Fehler sind schwer zu finden, weil sie nicht direkt in der Klasse stehen, an der du gerade arbeitest.

Eine zweite Stolperfalle ist ein unklarer Vertrag beim Überschreiben. Wenn eine open Funktion bestimmte Vorbedingungen hat, müssen Subklassen diese respektieren. Beispiel: Wenn format() nie einen leeren String zurückgeben soll, darf eine Subklasse diese Regel nicht brechen. Schreibe solche Regeln entweder in den Namen, in Tests oder mit klaren Checks im Code fest. Noch besser: Halte überschreibbare Funktionen klein und eindeutig.

Eine praktische Entscheidungsregel hilft dir im Code-Review: Frage bei jeder neuen Basisklasse, ob die Subklasse wirklich eine Spezialform der Basisklasse ist. Wenn die Antwort eher lautet „sie braucht nur ein paar Hilfsfunktionen“, ist Vererbung wahrscheinlich zu stark. Dann sind ein Interface, eine separate Helper-Klasse, eine Extension Function oder eine injizierte Abhängigkeit oft sauberer.

Beim Debuggen solltest du dir angewöhnen, die tatsächliche Laufzeitklasse zu prüfen. Wenn eine Variable den Typ DisplayNameFormatter hat, kann dahinter trotzdem ShortDisplayNameFormatter oder AnonymousDisplayNameFormatter stehen. Setze einen Breakpoint in die Basisklasse und in die überschreibende Methode. So siehst du, welche Implementierung wirklich ausgeführt wird. Dieses Verständnis ist zentral, bevor du mit komplexeren Architekturen, Mocks oder UI-State-Modellen arbeitest.

Fazit

Vererbung in Kotlin ist bewusst explizit: Mit open öffnest du eine Klasse oder Funktion, mit override ersetzt du Verhalten in einer Subklasse, und aus mehreren solchen Beziehungen entsteht eine Hierarchie. In Android-Projekten solltest du dieses Werkzeug sparsam und klar einsetzen. Es eignet sich für stabile Ist-ein-Beziehungen und kleine, gut benannte Verträge. Für reine Wiederverwendung ist Komposition meistens verständlicher, testbarer und robuster. Prüfe dein Verständnis aktiv: Baue eine kleine Formatter-Hierarchie, debugge die aufgerufenen Methoden, schreibe zwei Tests für unterschiedliche Subklassen und achte im Code-Review darauf, ob eine neue Basisklasse wirklich nötig ist.

Quellen (5)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.