Einleitung: Warum Generics nicht beängstigend, sondern praktisch sind
\n\nStellen Sie sich vor, Sie schreiben eine Klasse „Box“. Sie können alles hineinlegen: ein Buch, einen Apfel, ein Telefon. Aber wenn Sie sie für Object erstellen, müssen Sie bei jedem Herausnehmen as? Book verwenden und hoffen, dass Sie keinen ClassCastException erhalten. Kommt Ihnen das bekannt vor?
Hier kommen Generics ins Spiel – ein leistungsstarker Typisierungsmechanismus in Kotlin, der es Ihnen ermöglicht, flexiblen Code zu schreiben, ohne die Typsicherheit zu opfern. In diesem Artikel behandeln wir Generics in Kotlin vom Einfachen zum Komplexen: grundlegende Klassen, Funktionen, Variance (in/out), reifizierte Typen und Star-Projektionen. Sie erfahren, wie Sie Ihren Code sauberer machen und den Compiler zu Ihrem wichtigsten Verbündeten machen.
\n\nDieser Artikel wurde für das Bildungsportal pythonlib.ru erstellt – Ihrem Wegweiser in die Welt der modernen Programmiersprachen.
\n\n1. Grundlagen: Was sind Generics in Kotlin?
\n\nGenerics (generische Typen) ermöglichen es, Klassen, Schnittstellen und Funktionen mit einem Typ zu parametrisieren, der bei der Verwendung angegeben wird. Dies vermeidet Code-Duplizierung und gibt Ihnen Kontrolle über die Typen zur Kompilierzeit.
\n\nSehen Sie sich dieses einfache Beispiel an:
\n\nclass Box<T>(val item: T)\n\nfun main() {\n val bookBox = Box<String>("Kotlin in Action")\n val intBox = Box<Int>(42)\n // Der Compiler kennt den Typ!\n val title: String = bookBox.item\n}\n\nWichtige Punkte:
\n- \n
<T>– der Typparameter (kann jeder Buchstabe sein, aber üblich sind T, R, E, K, V). \n - Der Typ wird bei der Objekterstellung festgelegt – keine Casts zur Laufzeit. \n
- Generics funktionieren nur zur Kompilierzeit (Type Erasure), aber Kotlin fügt „Reifikation“ hinzu (dazu später mehr). \n
Generische Funktionen
\nDie Typisierung in Kotlin funktioniert auch für Funktionen. Zum Beispiel eine Funktion, die das erste Element einer Liste zurückgibt:
\n\nfun <T> firstItem(list: List<T>): T? = list.firstOrNull()\n\nfun main() {\n val numbers = listOf(1, 2, 3)\n val first: Int? = firstItem(numbers) // T = Int\n println(first) // 1\n}\n\nBeachten Sie: Kotlin kann den Typ aus dem Kontext ableiten – firstItem<Int>(numbers) zu schreiben ist nicht notwendig.
2. Variance: in und out – wie man die Typsicherheit nicht bricht
\n\nDas schwierigste Thema für Anfänger ist Variance (Varianz). Sie beantwortet die Frage: Kann List<String> ein Subtyp von List<Any> sein?
In Java wird dies mit Wildcards gelöst (? extends / ? super). In Kotlin mit out und in.
out (Kovarianz) – nur lesen
\nDie Markierung out bedeutet, dass der generische Typ nur „herausgegeben“ (von Methoden zurückgegeben) werden kann. Beispiel: List<out T>.
interface Source<out T> {\n fun next(): T // OK: T als Ausgabe\n // fun add(item: T) // Fehler! T als Eingabe ist verboten\n}\n\nfun demo(source: Source<String>) {\n val anySource: Source<Any> = source // out erlaubt dies\n}\n\nWarum ist das sicher? Wenn wir nur String aus der Quelle lesen können, kann sie als Quelle für Any betrachtet werden.
in (Kontravarianz) – nur schreiben
\nin ist das Gegenteil: Der Typ kann nur als Eingabe verwendet werden. Klassiker: Comparable.
interface Comparable<in T> {\n operator fun compareTo(other: T): Int\n}\n\nfun demo(comp: Comparable<Number>) {\n val intComp: Comparable<Int> = comp // in erlaubt dies\n}\n\nWenn Comparable<Number> beliebige Zahlen vergleichen kann, dann kann es definitiv auch Int vergleichen.
in out (Invarianz) – Standard
\nWenn weder in noch out angegeben ist, ist der Typ inv