Warum es Generics gibt
Ein generischer Typ erlaubt dir, Code einmal zu schreiben und für viele Typen wiederzuverwenden, ohne auf Typsicherheit zu verzichten. Statt eine separate IntBox, StringBox und UserBox zu schreiben, schreibst du eine einzige Box<T>, bei der T ein Platzhalter ist, den der Aufrufer ausfüllt.
Du hast Generics bereits jedes Mal verwendet, wenn du ArrayList<String> oder HashMap<String, Integer> geschrieben hast. Der <...>-Teil ist ein Typargument. Diese Seite zeigt, wie du eigene schreibst.
Die Alternative - alles als Object zu speichern - wirft die Typinformation weg und erzwingt hässliche, fehleranfällige Casts:
Der Cast in der letzten Zeile wirft zur Laufzeit eine ClassCastException - genau die Art von Fehler, die Generics unmöglich machen sollen.
Eine generische Klasse
Deklariere einen Typparameter in spitzen Klammern nach dem Klassennamen. Per Konvention ist es ein einzelner Großbuchstabe: T für „type" (Typ), E für „element", K/V für Schlüssel/Wert.
Innerhalb von Box wird jedes T zu dem, was der Aufrufer angegeben hat. Box<String> ist eine Box, die nur String aufnimmt und zurückgibt. Der Compiler lehnt name.set(99) ab, bevor das Programm überhaupt läuft.
Die leeren <> auf der rechten Seite (der Diamond-Operator) lassen den Compiler das Typargument von der linken Seite ableiten, sodass du <String> nicht zweimal wiederholen musst.
Generische Methoden
Eine einzelne Methode kann ihren eigenen Typparameter haben, unabhängig von der Klasse. Setze den Parameter <T> vor den Rückgabetyp:
Du übergibst T nie explizit - der Compiler leitet es aus dem Argument ab. Generische Methoden sind der Grund, warum Werkzeuge wie Collections.sort oder List.of über jeden Elementtyp hinweg typsicher bleiben.
Beschränkte Typparameter
Manchmal ergibt ein generischer Typ nur für manche Typen Sinn. extends schränkt den Parameter ein, sodass du die Methoden der Schranke aufrufen kannst. Hier bedeutet T extends Number, dass T ein Number oder eine beliebige Unterklasse (Integer, Double, ...) ist, sodass doubleValue() verfügbar ist:
Beachte, dass extends hier „ist ein Untertyp von" bedeutet und sowohl für Klassen als auch für Interfaces funktioniert - <T extends Comparable<T>> ist äußerst verbreitet, wenn du Elemente vergleichen musst.
Wildcards: ? extends und ? super
Ein subtiler Fallstrick: List<Integer> ist keine List<Number>, obwohl Integer ein Number ist. Generics sind invariant. Wildcards lockern dies, wenn du nur lesen oder nur schreiben musst.
Verwende ? extends T für einen Producer, aus dem du liest, und ? super T für einen Consumer, in den du schreibst (die „PECS"-Regel - Producer Extends, Consumer Super):
Eine ? extends Number-Liste lässt dich Elemente als Number lesen, aber nichts hinzufügen (der Compiler kann den genauen Elementtyp nicht kennen). Eine ? super Integer-Liste lässt dich Integer hinzufügen, aber Lesezugriffe kommen als Object zurück. Wähle die Wildcard, die zum Datenfluss passt.
Type Erasure und ihre Grenzen
Generics sind ein Feature der Kompilierzeit. Nach dem Kompilieren wird der Typparameter gelöscht - zur Laufzeit sind Box<String> und Box<Integer> beide nur eine Box. Das hält Generics abwärtskompatibel mit altem Code, bringt aber echte Einschränkungen mit sich.
// Keines davon kompiliert - der Typparameter existiert zur Laufzeit nicht:
T value = new T(); // ein Typparameter kann nicht instanziiert werden
T[] array = new T[10]; // ein generisches Array kann nicht erstellt werden
if (list instanceof List<String>) { } // das Typargument kann nicht geprüft werden
Da der Typ zur Laufzeit verschwunden ist, kannst du nicht per Reflection fragen „was war T?", und du kannst keine Methoden überladen, die sich nur durch ihr generisches Argument unterscheiden (foo(List<String>) und foo(List<Integer>) werden auf dieselbe Signatur reduziert). Wenn du den Typ wirklich zur Laufzeit brauchst, übergib ein Class<T>-Token als Konstruktor- oder Methodenparameter.
Als Nächstes: Lambda-Ausdrücke
Du hast gesehen, dass eine generische Methode einen Typ als Parameter entgegennimmt. Der nächste Schritt ist, Verhalten als Parameter zu behandeln. Lambda-Ausdrücke erlauben dir, ein Stück Code - eine Funktion - an eine Methode zu übergeben, und genau so wirst du die generischen Collections sortieren, filtern und transformieren, die du gerade typsicher zu schreiben gelernt hast.
Häufig gestellte Fragen
Was sind Generics in Java?
Mit Generics kannst du eine Klasse oder Methode schreiben, die mit einem Typ arbeitet, den du erst später angibst, anstatt sie auf einen konkreten Typ festzulegen. Du deklarierst einen Typparameter in spitzen Klammern - class Box<T> - und der Aufrufer setzt den tatsächlichen Typ ein - Box<String>. Der Compiler erzwingt diesen Typ dann überall, sodass du Inkompatibilitäten zur Kompilierzeit erkennst und manuelle Casts vermeidest.
Warum Generics statt Object verwenden?
Die Verwendung von Object verwirft jede Typinformation: Der Compiler kann dich nicht daran hindern, das Falsche hineinzulegen, und du musst jeden herausgenommenen Wert casten (mit dem Risiko einer ClassCastException zur Laufzeit). Generics verlagern diese Prüfung auf die Kompilierzeit. Eine List<String> akzeptiert schlicht kein Integer, und get() liefert bereits ein String zurück - kein Cast, keine böse Überraschung zur Laufzeit.
Was ist Type Erasure bei Java-Generics?
Type Erasure bedeutet, dass die generische Typinformation nur zur Kompilierzeit existiert. Nach dem Kompilieren sind List<String> und List<Integer> zur Laufzeit beide nur eine List - der Typparameter wird gelöscht. Deshalb kannst du weder new T[10] schreiben, noch list instanceof List<String> aufrufen oder einen Typparameter per Reflection auslesen. Generics geben dir Sicherheit zur Kompilierzeit, keine Typdaten zur Laufzeit.