1. Generische Funktionen und Klassen

[THEORIE]

Eine Funktion, die eine Liste sortiert, muss nicht wissen, ob die Liste Int, Double oder String enthält — sie muss nur wissen, dass sich zwei Elemente vergleichen lassen. Ohne Generics bräuchte man für jeden Typ eine eigene Funktion mit identischer Logik:

fun sortiereGanzzahlen(werte: List<Int>): List<Int> { /* ... */ }
fun sortiereKommazahlen(werte: List<Double>): List<Double> { /* ... */ }
fun sortiereText(werte: List<String>): List<String> { /* ... */ }

Das ist reine Kopierarbeit. Generics lösen das, indem sie einen Typ durch einen Type Parameter ersetzen — einen Platzhalter wie T, der erst beim Aufruf bzw. beim Erzeugen einer Instanz durch einen konkreten Typ ersetzt wird.

Generische Funktionen

Der Typparameter steht in spitzen Klammern direkt vor der Parameterliste:

fun <T> erstesElement(liste: List<T>): T? = liste.firstOrNull()

fun main() {
    val referenznummern = listOf("A-100", "A-101")
    val mengen = listOf(5, 12, 3)

    println(erstesElement(referenznummern)) // A-100
    println(erstesElement(mengen))          // 5
}

erstesElement funktioniert für jede Liste, unabhängig vom Elementtyp. Beim Aufruf muss der konkrete Typ nicht angegeben werden — Kotlin leitet ihn aus dem übergebenen Argument ab. Explizit ginge es auch: erstesElement<Int>(mengen), ist hier aber unnötig.

Generische Klassen

Genauso lassen sich Klassen generisch machen. Ein Lagerfach, das irgendeine Ware aufnehmen kann, muss nicht wissen, ob dort ein Buch oder ein Ersatzteil liegt:

class Lagerfach<T>(val inhalt: T) {
    fun ausgeben(): T = inhalt
}

fun main() {
    val fachFuerBuecher = Lagerfach("Kotlin Grundlagen") // T wird zu String
    val fachFuerZahlen = Lagerfach<Int>(42)               // T explizit als Int

    println(fachFuerBuecher.ausgeben())
    println(fachFuerZahlen.ausgeben())
}

Auch hier reicht Typinferenz meistens aus (Lagerfach("Kotlin Grundlagen")). Die explizite Form Lagerfach<Int>(42) ist nur nötig, wenn sich der Typ nicht eindeutig aus den Argumenten ergibt, oder wenn sie der Lesbarkeit dient.

Ein Type Parameter kann dabei mehr als eine Verwendung im Rumpf haben — als Konstruktor-Parameter, als Rückgabetyp einer Methode, oder beides gleichzeitig, wie in Lagerfach. Wie man diesen Platzhalter einschränkt, damit er nur Typen mit bestimmten Fähigkeiten akzeptiert, ist Thema der nächsten Lektion.


Weiterlesen: Generics: in, out, where – Generic functions and classes

[VORHERSAGE]

Lies den Code genau durch, bevor du ihn ausführst.

vorhersage.kt
class Lagerfach<T>(val inhalt: T)

fun main() {
    val fach1 = Lagerfach(42)
    val fach2 = Lagerfach("Netzwerkkabel")
    println(fach1.inhalt)
    println(fach2.inhalt)
}

Was gibt dieses Programm aus? Muss man den Typ `T` beim Aufruf von `Lagerfach(...)` irgendwo explizit angeben, damit das kompiliert?

[AUFGABE]

Schreib eine generische Funktion letztesElement, die das letzte Element einer Liste beliebigen Typs zurückgibt — oder null, wenn die Liste leer ist. Die Funktion soll ohne Einschränkung an T für jede Liste funktionieren, egal ob List<String>, List<Int> oder eine Liste eigener Klassen.

aufgabe.kt
fun main() { val referenznummern = listOf("A-100", "A-101", "A-102") val mengen = listOf(5, 12, 3) val leer = emptyList<String>() // TODO: Schreib die generische Funktion letztesElement<T>(liste: List<T>): T? // und ruf sie hier mit allen drei Listen auf. }

Bevor du einen Hint bekommst: Was hast du schon probiert, und woran hängt es genau?

[VERSTÄNDNIS-CHECK]

1. Wofür steht der Typparameter T in `class Lagerfach<T>(val inhalt: T)`?

2. Ohne Generics müsste man für `Lagerfach<Int>`, `Lagerfach<String>` und `Lagerfach<Bestellung>` jeweils eine eigene Klasse schreiben. Was ist der Hauptgrund, das mit Generics zu vermeiden?

3. Muss man bei `val box = Lagerfach(42)` den Typ `T` explizit als `Lagerfach<Int>(42)` schreiben?