Einführung in Java Generics
Warum Java Generics existieren — Typsicherheit, Code-Wiederverwendung und die Vermeidung von Casts in Collections und APIs.
Generics sind das Feature, das einer Klasse, einem Interface oder einer Methode erlaubt, mit einem unbestimmten Typ zu arbeiten, wobei der Compiler diesen Typ an der Verwendungsstelle festlegt. Eine List<String> ist eine Liste von Strings, der Compiler weiß das, und jeder Versuch, ein Date hineinzufügen, wird abgelehnt, bevor das Programm jemals läuft. Bevor Generics in Java 5 eingeführt wurden, war dieselbe Liste eine List von Object, und jedes Lesen daraus erforderte einen manuell geschriebenen Cast, der zur Laufzeit gelingen konnte oder nicht. Generics verwandelten dieses Laufzeitrisiko in eine Compile-Zeit-Prüfung, und fast jede moderne Java-API ist durch sie geprägt.
Das Problem, das Generics lösen
Um zu verstehen, warum Generics existieren, stellen Sie sich Java ohne sie vor. Ein Container, der beliebige Dinge enthält, muss seinen Inhalt als Object deklarieren:
// Pre-Java-5 style — what the standard library actually looked like.
List names = new ArrayList();
names.add("Ada");
names.add("Linus");
String first = (String) names.get(0); // cast required, never checked by the compilerZwei Probleme. Erstens ist der Cast Lärm — jedes Lesen aus dem Container benötigt einen. Zweitens, und schlimmer, hindert nichts jemanden daran, ein Date in dieselbe Liste einzufügen:
names.add(new java.util.Date()); // compiler is fine with this
String oops = (String) names.get(2); // ClassCastException at runtimeDer Fehler tritt beim Lesen auf, weit entfernt vom Schreiben. Der Cast lügt — er sagt „das ist ein String", und die JVM stellt es erst fest, wenn es zu spät ist, um Ihnen einen nützlichen Stack Frame nahe dem eigentlichen Fehler zu liefern.
Generics beheben beides:
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");
names.add(new Date()); // ❌ compile error — won't even build
String first = names.get(0); // no cast — the compiler already knows it's a StringDie spitzen Klammern <String> sind der Typparameter. Sie teilen dem Compiler mit „diese Liste enthält Strings", und von diesem Moment an wird jedes add und get gegen dieses Versprechen geprüft.
Drei Dinge, die man kostenlos bekommt
Generics bieten drei konkrete Vorteile, und deshalb ist jede Collection, jeder Stream und jedes Optional im modernen JDK generisch:
- Stärkere Compile-Zeit-Prüfungen. Die falsche Typzuweisung von oben wird zur Build-Zeit erkannt, nicht in der Produktion. Eine Klasse von
ClassCastExceptionhört einfach auf aufzutreten. - Keine Casts mehr. Das Lesen aus einer
Map<String, User>liefert Ihnen einUser, keinObject, das Sie casten müssen. Weniger syntaktisches Rauschen, weniger zu lesen, weniger zu pflegen. - Code-Wiederverwendung ohne Copy-Paste. Eine
List<E>Klasse funktioniert für jeden Elementtyp. Vor Generics akzeptierte die Standardbibliothek entweder überallObjectoder lieferte eineStringList,IntList,DateListund so weiter. Jetzt schreiben Sie eine Klasse und lassen den Aufrufer sie parametrisieren.
Der letzte Punkt ist der größere architektonische Gewinn. Generics sind die Art, wie man einen Container, einen Algorithmus oder eine Callback-Form einmal schreibt und auf jeden Typ anwendet, den der Aufrufer übergeben könnte.
Eine erste generische Klasse
Die Konvention ist, einen Typparameter mit einem einzelnen Großbuchstaben zu benennen — T für einen generischen „Typ", E für „Element" einer Collection, K/V für „Schlüssel" und „Wert" einer Map, R für „Rückgabe". Hier ist die einfachstmögliche generische Klasse — ein Paar zweier Dinge desselben Typs:
public class Pair<T> {
private final T first;
private final T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T first() { return first; }
public T second() { return second; }
}Das <T> nach dem Klassennamen führt den Typparameter ein. Von dort aus ist T innerhalb der Klasse überall verwendbar, wo ein normaler Typ stehen könnte. Der Aufrufer wählt T, wenn er das Objekt erstellt:
Pair<String> names = new Pair<>("Ada", "Grace");
Pair<Integer> scores = new Pair<>(100, 87);
String n1 = names.first(); // already a String, no cast
int s1 = scores.first(); // auto-unboxed from IntegerDas leere <> auf der rechten Seite (der Diamantoperator, Java 7+) weist den Compiler an, den Typ aus der linksseitigen Deklaration abzuleiten — Sie müssen das Typargument fast nie wiederholen.
Was parametrisiert wird und was nicht
Ein Typparameter kann eingesetzt werden für:
- Den Typ eines Feldes (
private T value;) - Einen Parameter- oder Rückgabetyp einer Methode (
public T get() { ... },void put(T value)) - Den Elementtyp eines Arrays dieses Typs (
T[] items— mit einigen Einschränkungen)
Ein Typparameter kann nicht eingesetzt werden für:
- Einen primitiven Typ (
Pair<int>ist illegal — verwenden SiePair<Integer>und lassen Sie Autoboxing die Arbeit machen) - Ein statisches Feld oder den Typparameter einer statischen Methode (der Parameter gehört zu einer Instanz, nicht zur Klasse selbst)
- Das Ziel von
new T()oderinstanceof T— Java löscht Generics zur Laufzeit, sodass das Programm keinTzum Konstruieren oder Testen hat
Die vollständige Liste der „Dinge, die man nicht tun kann" bekommt ihr eigenes Kapitel am Ende dieses Teils — Java Generics Restrictions — sobald wir genug Mechanismen behandelt haben, um die Regeln verständlich zu machen.
Ein ausgearbeitetes Beispiel: Typsicherheit vs. Raw Types, nebeneinander
Das folgende Programm erstellt denselben Container zweimal — einmal als rohe List (die Form vor Generics) und einmal als List<String>. Beide kompilieren; nur die parametrisierte ist sicher.
Die rohe Version stürzt mitten in der Iteration ab, weil die Schleife einem Cast vertraute, dem sie nicht hätte vertrauen sollen. Die generische Version machte denselben Fehler undarstellbar — das fehlerhafte add(42) kompiliert erst gar nicht. Diese Verlagerung von der Laufzeit zur Compile-Zeit ist der einzige Grund, warum Generics existieren.
Was dieser Teil des Buches behandelt
Die verbleibenden Kapitel in diesem Teil nehmen Generics Stück für Stück auseinander:
- Generische Klassen — der Typparameter auf Klassenebene, den Sie gerade gesehen haben, in mehr Tiefe.
- Generische Methoden — Methoden, die ihren eigenen Typparameter einführen, unabhängig von der Klasse.
- Generische Interfaces — Gestaltung von API-Verträgen, die über einen Typ parametrisiert werden.
- Begrenzte Typparameter — „T muss
Numbererweitern" zu sagen, damit Sie Methoden aufTaufrufen können. - Wildcards —
? extends T,? super Tund die PECS-Regel, die entscheidet, wann welche verwendet wird. - Type Erasure — wie die JVM Generics intern implementiert und warum einige Dinge, die Sie erwarten könnten, nicht funktionieren.
- Einschränkungen — der Katalog der Dinge, die die Sprache verbietet, mit den Gründen dahinter.
Lesen Sie sie der Reihe nach — jedes Kapitel setzt die vorherigen voraus.
Was als nächstes kommt
Beginnen Sie mit der häufigsten Form — einer Klasse, deren Felder und Methoden über einen Typ parametrisiert sind, den der Aufrufer wählt. Weiter zu Java Generic Classes.