W3docs

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 compiler

Zwei 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 runtime

Der 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 String

Die 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 ClassCastException hört einfach auf aufzutreten.
  • Keine Casts mehr. Das Lesen aus einer Map<String, User> liefert Ihnen ein User, kein Object, 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 überall Object oder lieferte eine StringList, IntList, DateList und 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 Integer

Das 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 Sie Pair<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() oder instanceof T — Java löscht Generics zur Laufzeit, sodass das Programm kein T zum 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.

java— editable, runs on the server

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 Number erweitern" zu sagen, damit Sie Methoden auf T aufrufen können.
  • Wildcards? extends T, ? super T und 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.

Practice

Übung
Eine Methode deklariert `public static List getNames() { ... }` (kein Typparameter für die Liste). Der Aufrufer schreibt `String first = getNames().get(0);`. Warum warnt der Compiler — und was ist die Gefahr, wenn Sie die Warnung ignorieren?
Eine Methode deklariert `public static List getNames() { ... }` (kein Typparameter für die Liste). Der Aufrufer schreibt `String first = getNames().get(0);`. Warum warnt der Compiler — und was ist die Gefahr, wenn Sie die Warnung ignorieren?
Was this page helpful?