Java Generic Classes
Erfahre, wie du Java-Generic-Classes mit Typparametern, mehreren Parametern, dem Diamantoperator und einem Stack-Beispiel schreibst.
Eine generische Klasse ist eine Klasse, deren Deklaration einen oder mehrere Typparameter enthält – Platzhalter, die der Aufrufer beim Erstellen einer Instanz ausfüllt. Derselbe Klassenrumpf beschreibt dann eine ganze Familie von Typen: Box<String>, Box<Integer> und Box<User> sind zur Kompilierzeit unterschiedliche Typen, die eine gemeinsame Quelle haben. Das ist die häufigste Form, in der Generics auftreten, und genau so ist jede Collection in java.util, jedes Optional, jedes Future und jedes CompletableFuture geschrieben.
Die Syntax
Die Typparameterliste steht zwischen dem Klassennamen und dem Rumpf, in spitzen Klammern:
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}Lies die Deklaration als „ein Box, parametrisiert über irgendeinen Typ T." Innerhalb der Klasse verhält sich T wie jeder andere Typ – du kannst Felder vom Typ T, Methoden, die T zurückgeben, und Parameter vom Typ T deklarieren. Der Compiler behandelt es als echten, unbekannten Typ, bis der Aufrufer einen konkreten wählt.
An der Aufrufstelle gibst du den tatsächlichen Typ an:
Box<String> greeting = new Box<>("hello");
Box<Integer> answer = new Box<>(42);
String s = greeting.get(); // already a String — no cast
int i = answer.get(); // auto-unboxed from IntegerDas <> auf der rechten Seite ist der Diamantoperator – der Compiler leitet das Typargument aus der Deklaration auf der linken Seite ab. Du könntest new Box<String>("hello") explizit schreiben, aber das ist fast nie nötig.
Mehrere Typparameter
Eine Klasse kann mehr als einen Typparameter deklarieren. Das klassische Beispiel ist ein Schlüssel-Wert-Paar:
public class Entry<K, V> {
private final K key;
private final V value;
public Entry(K key, V value) {
this.key = key;
this.value = value;
}
public K key() { return key; }
public V value() { return value; }
}
Entry<String, Integer> score = new Entry<>("Ada", 100);
String name = score.key();
int n = score.value();Die Konvention sind einbuchstabige Namen – K für Key, V für Value, E für Element, R für Return, T für „generischer Typ." Wenn mehr Klarheit nötig ist (selten), sind längere Namen erlaubt: Map<KeyType, ValueType> ist gültig, aber unüblich.
Den Typparameter einschränken
Standardmäßig steht ein Typparameter für „irgendeinen Typ", sodass du innerhalb der Klasse nur Methoden aufrufen kannst, die jedes Objekt hat (equals, toString, hashCode). Wenn deine Klasse etwas mit den Werten tun muss – sie vergleichen, addieren oder eine Eigenschaft lesen –, schränkst du T mit einer oberen Schranke mittels extends ein:
// T can be any type that is (or extends) Number, so .doubleValue() is callable.
public class NumberBox<T extends Number> {
private final T value;
public NumberBox(T value) { this.value = value; }
public double asDouble() { return value.doubleValue(); }
}
NumberBox<Integer> n = new NumberBox<>(42); // fine — Integer is a Number
// NumberBox<String> bad = ...; // ❌ String is not a Numberextends bedeutet hier „ist ein Subtyp von" und funktioniert sowohl für Klassen als auch für Interfaces. Du kannst sogar mehrere Schranken gleichzeitig fordern – <T extends Number & Comparable<T>> – wobei die Klassengebundene (falls vorhanden) zuerst steht. Die Schranke ist auch das, was den Typ nutzbar macht: Ohne extends Number würde value.doubleValue() nicht kompilieren.
Generische Konstruktoren
Der Typparameter wird durch die Instanz festgelegt, sodass jeder Konstruktor einer generischen Klasse bereits Zugriff auf T hat:
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 Pair(T both) { this(both, both); }
}Konstruktoren selbst können auch über zusätzliche Typparameter generisch sein, die unabhängig von denen der Klasse sind – das ist jedoch selten genug, um im nächsten Kapitel über generische Methoden behandelt zu werden.
Generische Klassen können voneinander erben
Eine Unterklasse kann auf drei Arten von einer generischen Klasse erben. Jede hat eine andere Bedeutung:
// 1. Lock the parent's type parameter — concrete subclass for one element type.
public class StringList extends ArrayList<String> { ... }
// 2. Pass the type parameter through — the subclass is still generic.
public class MyList<E> extends ArrayList<E> { ... }
// 3. Add new type parameters of your own.
public class TaggedList<E, Tag> extends ArrayList<E> { ... }Die mittlere Form ist die häufigste – du propagierst den Parameter der Elternklasse an deine eigenen Aufrufer. Die erste Form verwendest du, wenn die Unterklasse spezialisiert ist: ein Baum aus String-Knoten.
Felder und der Typparameter
Jede Box<...>-Instanz trägt ihr eigenes T. Der Bytecode nicht – zur Laufzeit sieht die JVM nur Box (das ist Type Erasure, das später in diesem Teil behandelt wird). Die Konsequenz ist, dass der Typparameter zur Instanz gehört, nicht zum Klassenobjekt:
Box<String> a = new Box<>("hi");
Box<Integer> b = new Box<>(5);
a.getClass() == b.getClass(); // true — both are class BoxDas ist eine nützliche Tatsache: Box<String> und Box<Integer> sind unterschiedliche Typen für den Compiler, aber dieselbe Klasse zur Laufzeit. Darauf kommen wir in Java Type Erasure zurück.
Statische Member sehen den Typparameter nicht
Statische Felder und statische Methoden gehören zur Klasse, nicht zu einer einzelnen Instanz – daher können sie das T der Instanz nicht sehen. Das ist unzulässig:
public class Box<T> {
private static T defaultValue; // ❌ won't compile — no T at the static level
public static T empty() { ... } // ❌ same problem
}Eine statische Methode, die einen Typparameter benötigt, muss ihren eigenen deklarieren, unabhängig von dem der Klasse. Das ist das Thema des nächsten Kapitels.
Eigene Klassen entwerfen: ein kleiner typisierter Stack
Eine vollständige, funktionierende Klasse, die alles zusammenbringt – ein generischer Stack mit push, pop, peek und size. Er ist über E (Element) parametrisiert, intern durch ein Object[] gestützt (wegen der Einschränkungen bei generischen Arrays), und der unchecked Cast bei pop ist die Art von gut eingegrenzter Umgehung, die man in echtem Code findet.
Die @SuppressWarnings("unchecked")-Annotationen befinden sich an den zwei Lesestellen, die von Object zurück nach E casten müssen. Diese Casts sind sicher – push speichert nur Werte vom Typ E –, aber der Compiler kann das nicht sehen, weil Erasure E aus dem Bytecode entfernt hat. Die Warnung lokal, im kleinstmöglichen Scope, zu unterdrücken, ist der richtige Ansatz.
Was kommt als Nächstes
Du hast den Parameter auf Klassenebene kennengelernt. Manchmal braucht man eine einzelne Methode, die generisch ist, mit einem eigenen Typparameter unabhängig von der Klasse – nützlich für Utility-Methoden, statische Hilfsfunktionen und jede Operation, deren Typbeziehung nur innerhalb dieser einen Methode lebt. Weiter zu Java Generic Methods.