Einführung in Generics in Kotlin
Generics (Verallgemeinerungen) sind eine der Schlüsselfunktionen der Sprache Kotlin, die es ermöglicht, flexiblen, typsicheren und wiederverwendbaren Code zu schreiben. Wenn Sie jemals List<String> oder Map<Int, User> geschrieben haben, haben Sie Generics bereits verwendet, ohne darüber nachzudenken. Aber was steckt hinter diesen spitzen Klammern und wie holt man das Maximum daraus? In diesem Artikel werden wir untersuchen, wozu Generics in Kotlin benötigt werden, wie man sie in der Praxis anwendet und welche fortgeschrittenen Funktionen (variance, reified, where) Ihnen helfen, noch saubereren und sichereren Code zu schreiben.
Dieser Artikel richtet sich an Entwickler, die bereits mit den Grundlagen von Kotlin vertraut sind und ihr Wissen vertiefen möchten. Wir behandeln alles: von der grundlegenden Syntax bis zu realen Beispielen aus dem Production-Code.
Wozu braucht man Generics? Das Problem ohne sie
Stellen Sie sich vor, Sie müssen eine Funktion schreiben, die eine Liste beliebiger Elemente entgegennimmt und das erste Element zurückgibt. Ohne Generics würden Sie so schreiben:
fun getFirst(list: List<Any>): Any { return list[0]}Wenn Sie nun List<String> übergeben, erhalten Sie Any als Ausgabe. Um damit wie mit einem String zu arbeiten, müssen Sie eine explizite Typumwandlung durchführen:
val first = getFirst(listOf("Kotlin", "Java")) as Stringfirst.length // Gefährlich! Kann ClassCastException auslösenDas ist umständlich und unsicher. Der Fehler zeigt sich erst zur Laufzeit. Generics lösen dieses Problem, indem sie die Typinformation zur Kompilierzeit bewahren.
Hauptvorteile von Generics:
- Typsicherheit — der Compiler prüft Typen zur Kompilierzeit, nicht zur Laufzeit.
- Vermeidung expliziter Umwandlungen — der Code wird sauberer und lesbarer.
- Wiederverwendbarkeit — eine Klasse oder Funktion arbeitet mit verschiedenen Typen.
Grundlegende Syntax: Klassen, Funktionen und Einschränkungen
Generische Klassen
Der einfachste Weg, loszulegen, ist die Deklaration einer Klasse mit einem oder mehreren Typparametern. Die Typparameter werden in spitzen Klammern <T> angegeben (der Buchstabe kann beliebig sein, traditionell verwendet man T, E, K, V):
class Box<T>(val value: T)
fun main() { val stringBox = Box("Kotlin") // Typ wird automatisch abgeleitet val intBox = Box(42) // Box<Int>
println(stringBox.value.length) // Funktioniert ohne Umwandlung! println(intBox.value + 1) // 43}Kotlin kann den Typ ableiten, daher muss man Box<String> nicht schreiben — Box("Kotlin") reicht aus.
Generische Funktionen
Funktionen können ebenfalls generisch sein. Der Typparameter wird vor dem Funktionsnamen angegeben:
fun <T> getMiddleElement(list: List<T>): T? { if (list.isEmpty()) return null return list[list.size / 2]}
fun main() { val numbers = listOf(1, 2, 3, 4, 5) val middle = getMiddleElement(numbers) // Typ Int? wird automatisch abgeleitet println(middle) // 3}Einschränkungen (where / upper bound)
Manchmal muss man einschränken, welche Typen in einem Generic verwendet werden können. Um beispielsweise die Methode .compareTo() aufzurufen, muss der Typ das Interface Comparable implementieren:
fun <T : Comparable<T>> maxOfTwo(a: T, b: T): T { return if (a >= b) a else b}
fun main() { println(maxOfTwo(10, 20)) // 20 println(maxOfTwo("A", "B")) // B // maxOfTwo(10, "A") // Compiler-Fehler: Typen stimmen nicht überein}Wenn mehrere Einschränkungen benötigt werden, verwenden Sie das Schlüsselwort where:
interface Printable { fun print()}
fun <T> process(item: T) where T : Comparable<T>, T : Printable