Java Bounded Type Parameters
Java-Generics mit begrenzten Typparametern einschränken — einfache und mehrfache Bounds mit extends.
Ein begrenzter Typparameter ist ein Typparameter mit einer extends-Klausel, die einschränkt, welche Typen ihn füllen können. Ohne eine Grenze wird T als Object behandelt — der Compiler hat keine Möglichkeit zu wissen, ob T eine compareTo-, intValue- oder eine andere Methode besitzt. Mit einer Grenze machst du dem Compiler ein Versprechen darüber, was T ist, und im Gegenzug erlaubt dir der Compiler, die Methoden aufzurufen, die dieses Versprechen impliziert. Fast jede interessante generische Methode hat mindestens eine Grenze irgendwo in ihrer Signatur.
Die grundlegende Form: <T extends Bound>
Die Syntax ist extends zwischen dem Parameter und seiner Grenze — dasselbe Schlüsselwort, egal ob die Grenze eine Klasse oder ein Interface ist:
public static <T extends Number> double sum(List<T> values) {
double total = 0;
for (T n : values) total += n.doubleValue(); // legal — T is-a Number
return total;
}Lies <T extends Number> als „jedes T, das ein Number oder eine Unterklasse davon ist." Im Methodenrumpf kannst du nun jeden T-Wert als Number behandeln — doubleValue(), intValue() und alles aus der öffentlichen API von Number aufrufen.
Verwende es genauso wie jede andere generische Methode:
sum(List.of(1, 2, 3)); // T = Integer — fine
sum(List.of(1.5, 2.5)); // T = Double — fine
sum(List.of(1L, 2L, 3L)); // T = Long — fine
sum(List.of("a", "b")); // ❌ String is not a NumberOhne die Grenze könnte sum nicht einmal versuchen, doubleValue() aufzurufen — T würde zu Object gelöscht, und Object hat diese Methode nicht. Die Grenze ist das, was den Rumpf legal macht.
extends funktioniert auch für Interfaces
In Java-Generics ist extends überladen — es bedeutet „ist ein Subtyp von", egal ob die Grenze eine Klasse oder ein Interface ist. Es gibt kein separates implements-Schlüsselwort in einer Typparameterliste:
public static <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T candidate : list) {
if (candidate.compareTo(best) > 0) best = candidate;
}
return best;
}
max(List.of(3, 1, 4, 1, 5, 9)); // T = Integer
max(List.of("Ada", "Grace", "Linus")); // T = StringComparable<T> ist ein Interface, aber die Syntax ist weiterhin extends. Lies <T extends Comparable<T>> als „jedes T, das mit sich selbst vergleichbar ist" — die Einschränkung ist das, was dir erlaubt, candidate.compareTo(best) in der Schleife aufzurufen.
Der kleine Kniff hier ist das <T> innerhalb der Grenze — das ist die selbstreferenzielle Form, die wir in generischen Interfaces kennengelernt haben. Sie erzwingt, dass compareTo ein T akzeptiert, nicht nur ein beliebiges Comparable. Ohne sie könnte man Integer mit String vergleichen, und das Typsystem würde es nicht bemerken.
Mehrfache Grenzen
Ein Typparameter kann mehr als eine Grenze haben, verbunden durch &:
public static <T extends Number & Comparable<T>> T maxNumber(List<T> list) {
T best = list.get(0);
for (T n : list) {
if (n.compareTo(best) > 0) best = n;
}
return best;
}Jetzt muss T sowohl ein Number als auch ein Comparable<T> sein. Im Rumpf kannst du jede Methode aus beiden aufrufen. Integer, Long, Double, BigDecimal erfüllen beide Bedingungen — du kannst jeden davon übergeben.
Einige Regeln:
- Die Klassengrenze (falls vorhanden) muss zuerst kommen.
<T extends Number & Comparable<T>>ist legal;<T extends Comparable<T> & Number>ist es nicht. - Höchstens eine Klassengrenze. Java hat einfache Vererbung für Klassen, also ergibt sich das aus den bestehenden Sprachregeln.
- Beliebig viele Interface-Grenzen. Stapele so viele, wie du wirklich brauchst; in der Praxis sind zwei das übliche Maximum, bevor das Design überdacht werden muss.
Die Klasse-zuerst-Regel spiegelt den Bytecode wider: Type Erasure ersetzt T durch seine linkste Grenze, daher ist der linkste Slot besonders. Darauf kommen wir in Type Erasure zurück.
Grenzen bei Typparametern auf Klassenebene
Grenzen sind nicht einzigartig für Methoden. Eine generische Klasse oder ein Interface kann seine Typparameter in der Deklaration begrenzen:
public class SortedBag<T extends Comparable<T>> {
private final List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
Collections.sort(items);
}
public T smallest() { return items.get(0); }
}
SortedBag<Integer> bag = new SortedBag<>(); // OK — Integer is Comparable<Integer>
// SortedBag<Object> bag2 = new SortedBag<>(); // ❌ Object is not Comparable<Object>Dies ist der richtige Ort für eine Grenze, die für die gesamte Klasse gilt. Jede Methode, jedes Feld, jede Standardoperation hat Zugriff auf die Einschränkung.
Untere Grenzen gehören zu Wildcards, nicht zu Typparametern
Eine häufige Verwechslung: Typparameter können obere Grenzen (extends) haben, aber keine unteren Grenzen (super). Dies ist legal:
public static <T extends Number> void foo(T x) { ... }Dies hingegen nicht:
public static <T super Integer> void bar(T x) { ... } // ❌ no such syntaxUntere Grenzen gibt es in Javas Generics — aber sie leben nur auf Wildcards, dem ?-Token, nicht auf benannten Typparametern. Wir werden die vollständige ? extends / ? super Geschichte im Kapitel Wildcards behandeln; fürs Erste reicht es zu wissen, dass super auf ? funktioniert und nicht auf T.
Wann man eine Grenze hinzufügt
Der Standard sollte „keine Grenze" sein — je lockerer die Einschränkung, desto mehr Typen akzeptiert deine Methode. Füge eine hinzu, wenn und nur wenn der Rumpf eine bestimmte Methode auf T aufrufen muss:
- „Ich muss
T-Werte vergleichen" →<T extends Comparable<T>>. - „Ich muss
T-Werte als Zahlen addieren" →<T extends Number>. - „Ich muss Felder von
Twie einUserlesen" →<T extends User>. - „Ich brauche
Tals auto-closeable, um es in try-with-resources zu verwenden" →<T extends AutoCloseable>.
Wenn du keine Methode auf T aufrufst, brauchst du keine Grenze — und eine trotzdem hinzuzufügen schränkt deine API ohne Grund ein.
Ein ausgearbeitetes Beispiel: ein begrenztes between und ein sortiertes top
Zwei Methoden, jede mit einem anderen Stil der Grenze. between verwendet Comparable<T>, um einen Wert einzuklemmen; topN verwendet Number & Comparable<T>, um sowohl zu vergleichen als auch eine numerische Summe zu melden.
between akzeptiert jedes Comparable — einschließlich Character, das Comparable<Character> ist und nichts mit Zahlen zu tun hat. topN ist enger — es braucht sowohl Ordnung (zum Sortieren) als auch numerische Werte (zum Summieren), daher stapelt es die beiden Grenzen mit &. Jede Methode fragt genau nach der Fähigkeit, die sie verwendet — nicht mehr.
Was kommt als Nächstes
Eine Grenze bei einem Typparameter fixiert einen einzelnen Typ an der Aufrufstelle — List<Integer> bedeutet „diese Liste, diese Methode, dieser Typ." Manchmal möchte man weniger spezifisch sein: „eine Liste von irgendeinem Number — könnte Integer sein, könnte Double sein, egal." Dafür sind Wildcards gedacht, und sie lösen eine der größten Verwirrungsquellen in Javas Typsystem. Weiter zu Java Generic Wildcards.