Generics in Kotlin: wozu sie dienen und wie man sie verwendet
Bei der Entwicklung mit Kotlin stehen Sie oft vor der Notwendigkeit, Code zu schreiben, der mit verschiedenen Datentypen arbeiten kann. Anstatt separate Funktionen oder Klassen für Int, String, Double und so weiter zu erstellen, können Sie den Mechanismus der Generics (Generika) verwenden. Dieser Artikel erklärt, was Generics in Kotlin sind, wozu sie dienen und wie man sie in der Praxis anwendet.
Was sind Generics und wozu dienen sie?
Generics (Generika) sind ein Werkzeug, das es ermöglicht, Klassen, Schnittstellen und Funktionen zu schreiben, die mit beliebigen Datentypen arbeiten. Stellen Sie sich vor, Sie erstellen eine Box, in die Sie alles Mögliche legen können: ein Buch, ein Telefon oder Essen. Anstatt für jeden Gegenstand eine separate Box zu machen, machen Sie eine universelle.
Die wichtigsten Vorteile der Verwendung von Generics:
- Typsicherheit (Type Safety) – der Compiler überprüft die Typen zur Kompilierzeit, was Laufzeitfehler verhindert.
- Wiederverwendbarkeit von Code – einmal geschriebener Code funktioniert mit beliebigen Typen.
- Lesbarkeit – der Code wird verständlicher, da explizit angegeben wird, mit welchen Typen er arbeitet.
Ohne Generics müssten Sie Any verwenden und ständig Typumwandlungen durchführen, was fehleranfällig ist. Mit Generics überwacht der Compiler selbst die Korrektheit der Typen.
Grundlagen von Generics in Kotlin: Typparameter
In Kotlin werden Generics über Typparameter realisiert. Sie werden in spitzen Klammern <> angegeben. Am häufigsten werden die Buchstaben T (Type), E (Element), K (Key), V (Value) verwendet, aber es können beliebige Namen verwendet werden.
Generische Funktionen
Beginnen wir mit einem einfachen Beispiel – einer Funktion, die das ihr übergebene Argument zurückgibt:
fun <T> identity(value: T): T { return value}
fun main() { val number: Int = identity(42) val text: String = identity("Hallo") println(number) // 42 println(text) // Hallo}Hier deklariert <T> vor dem Funktionsnamen den Typparameter. Der Typ T wird automatisch aus dem Argument abgeleitet, daher kann man ihn nicht explizit angeben.
Generische Klassen
Klassen können ebenfalls generisch sein. Erstellen wir einen einfachen Container:
class Box<T>(private val item: T) { fun getItem(): T { return item }}
fun main() { val stringBox = Box("Buch") val intBox = Box(123)
println(stringBox.getItem()) // Buch println(intBox.getItem()) // 123}Die Klasse Box<T> kann ein Objekt beliebigen Typs speichern. Bei der Erstellung einer Instanz geben wir den konkreten Typ an, und der Compiler garantiert, dass getItem() genau diesen Typ zurückgibt.
Typbeschränkungen (Type Bounds)
Manchmal muss man die Typen einschränken, die in Generics verwendet werden können. Zum Beispiel, damit eine Funktion nur mit Zahlen arbeitet. Dafür werden Beschränkungen (bounds) mit dem Schlüsselwort : verwendet.
Obere Schranke (Upper Bound)
fun <T : Number> sum(a: T, b: T): Double { return a.toDouble() + b.toDouble()}
fun main() { println(sum(10, 20)) // 30.0 println(sum(3.5, 2.5)) // 6.0 // println(sum("A", "B")) // Kompilierfehler}Hier bedeutet T : Number, dass der Typ T ein Nachkomme der Klasse Number sein muss. Int, Double, Float sind geeignet, String jedoch nicht.
Mehrfache Beschränkungen
Wenn mehrere Beschränkungen benötigt werden, verwenden Sie where:
interface Printable { fun print()}
fun <T>