Java Generic Wildcards
Obere, untere und ungebundene Wildcards in Java Generics sowie die PECS-Regel verständlich erklärt.
Ein Wildcard ist das ?-Token, das in generischen Typen anstelle eines konkreten Typarguments erscheint — List<?>, List<? extends Number>, List<? super Integer>. Es ist die Antwort auf ein Problem, auf das man fast sofort stößt, wenn man generischen Code schreibt: List<Integer> ist kein Subtyp von List<Number>, obwohl Integer ein Subtyp von Number ist. Wildcards beschreiben "eine Liste von irgendeinem Number", ohne sich auf einen bestimmten Elementtyp festzulegen — und sie sind mit Abstand das verwirrендste Element von Javas Typsystem.
Der nicht-intuitive Ausgangspunkt
Hier ist die Tatsache, die Wildcards notwendig macht:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = ints; // ❌ does not compileObwohl Integer extends Number gilt, erweitert List<Integer> nicht List<Number>. Generische Typen sind invariant — List<Sub> und List<Super> sind unabhängig voneinander, egal was Sub und Super sind.
Der Grund dafür ist nachvollziehbar, wenn auch überraschend. Wenn List<Integer> eine List<Number> wäre, könnte man Folgendes tun:
List<Number> nums = ints; // pretend this is legal
nums.add(3.14); // legal — 3.14 is a Number
int x = ints.get(3); // KABOOM at runtime — it's a DoubleDer Cast in der letzten Zeile würde explodieren. Um das zu verhindern, verweigert der Compiler bereits den ersten Schritt: List<Integer> ist keine List<Number>. Punkt.
Wildcards sind der sichere Weg, diese Flexibilität zurückzugewinnen.
Der ungebundene Wildcard: List<?>
Der einfachste Wildcard ist das allein stehende ? — "eine Liste eines unbekannten Typs":
public static void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}
printAll(List.of(1, 2, 3)); // List<Integer> — OK
printAll(List.of("a", "b")); // List<String> — OK
printAll(new ArrayList<>()); // List<Object> — OKIm Methodenrumpf kann man mit Elementen einer List<?> nur das tun, was mit Object möglich ist — denn der Compiler weiß nicht, was ? tatsächlich ist. Man kann nichts hinzufügen zu einer List<?> (mit der einzigen Ausnahme von null):
public static void corrupt(List<?> list) {
list.add("hello"); // ❌ does not compile — ? is unknown
list.add(null); // ✓ — null is a value of every reference type
}List<?> schreibt man, wenn man ausdrücken möchte: "Ich akzeptiere jede Liste und lese sie nur als Object."
Oberer begrenzter Wildcard: ? extends T
Wenn man Elemente als einen bestimmten Typ lesen möchte — zum Beispiel alle als Number behandeln — verwendet man einen oberen begrenzten Wildcard:
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue(); // legal — every element IS-A Number
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> — OK, Integer extends Number
sum(List.of(1.5, 2.5)); // List<Double> — OK
sum(List.of(1L, 2L, 3L)); // List<Long> — OKList<? extends Number> bedeutet: "Eine Liste eines bestimmten Typs, der Number ist oder ein Subtyp von Number." Man kann daraus lesen und die Elemente als Number behandeln. Man kann nichts hinzufügen — wieder mit der null-Ausnahme — weil der Compiler nicht weiß, welcher Number-Subtyp in der Liste tatsächlich steckt. Einen Integer zu einer List<? extends Number> hinzuzufügen, die heimlich eine List<Double> ist, würde sie beschädigen; anstatt zu versuchen, den Subtyp zu ermitteln, verweigert der Compiler schlicht jedes add.
Unterer begrenzter Wildcard: ? super T
Das Spiegelbild. ? super T bedeutet: "Der Elementtyp der Liste ist T oder ein Supertyp von T":
public static void addOneTwoThree(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Integer> ints = new ArrayList<>(); addOneTwoThree(ints); // ✓
List<Number> nums = new ArrayList<>(); addOneTwoThree(nums); // ✓ — Number is a supertype of Integer
List<Object> objs = new ArrayList<>(); addOneTwoThree(objs); // ✓ — Object is tooHier kann man gefahrlos jeden Integer (oder Subtyp) hinzufügen — der Elementtyp der Liste ist garantiert Integer oder ein Vorfahre davon, also passt ein Integer hinein. Was man nicht kann, ist einen bestimmten Typ herauszulesen — das Einzige, was man über ein Element sagen kann, ist, dass es ein Object ist, weil die eigentliche Liste eine List<Object> sein könnte.
Die PECS-Regel
Es gibt eine Eselsbrücke, die sich jeder Java-Entwickler früher oder später merkt:
PECS — Producer Extends, Consumer Super.
Das ist die Faustregel dafür, wann welcher Wildcard zu verwenden ist:
- Wenn der Parameter Werte produziert (man liest daraus):
? extends Tverwenden. - Wenn der Parameter Werte konsumiert (man schreibt hinein):
? super Tverwenden.
Die klassische Signatur, die dies zeigt, ist Collections.copy:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}src wird gelesen (es produziert Ts) — ? extends T. dest wird beschrieben (es konsumiert Ts) — ? super T. Das ist der eigentliche Grund für die Asymmetrie: Dieselbe Signatur funktioniert, ob src eine List<Integer> und dest eine List<Number> ist oder umgekehrt, solange T ein gemeinsamer Punkt zwischen ihnen ist.
Wenn man aus diesem Kapitel nur eine Sache behält, dann PECS.
Wann man keinen Wildcard verwenden sollte
Wenn ein Parameter im selben Methode sowohl gelesen als auch beschrieben wird, funktioniert weder ? extends T noch ? super T — keiner erlaubt beides. In diesem Fall verwendet man einfach einen normalen Typparameter:
public static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i); // read
list.set(i, list.get(j)); // write
list.set(j, tmp); // write
}Ein Wildcard ist das richtige Werkzeug, wenn eine Seite der Beziehung "ich lese nur" oder "ich schreibe nur" ist. Ein Typparameter ist das richtige Werkzeug, wenn man auf beiden Seiten über einen bestimmten Elementtyp sprechen möchte.
Wildcards vs. begrenzte Typparameter
Vergleich:
public static <T extends Number> double sumNamed(List<T> list) { ... }
public static double sumWildcard(List<? extends Number> list) { ... }Funktional akzeptieren beide dieselbe Menge von Argumenten. Der Unterschied liegt darin, was der Rumpf ausdrücken kann:
- Die benannte Form (
<T extends Number>) gibt einem den NamenT— nützlich, wenn manTzurückgeben, eine weitereList<T>als zweiten Parameter entgegennehmen oderT tmp = list.get(0)schreiben möchte, um den genauen Elementtyp beizubehalten. - Die Wildcard-Form (
? extends Number) gibt keinen Namen — man kann Elemente nur alsNumberreferenzieren. Im API ist das kompakter (kein Name taucht in der Signatur auf), aber im Rumpf weniger ausdrucksstark.
Faustregel: Wenn man die Elemente nur als Number benötigt, ist der Wildcard die kleinere, sauberere Wahl. Wenn der Rumpf über ein bestimmtes T sprechen muss, sollte man es benennen.
Ein ausgearbeitetes Beispiel: PECS in der Praxis
Das Programm kopiert Elemente von einer Liste in eine andere und berechnet die laufende Summe — beide Operationen nach dem PECS-Prinzip parametrisiert. Man beachte die Aufrufstellen: copyOf(intList, numberList) mischt Elementtypen, weil die Wildcards einem Number-Ziel erlauben, Integer-Werte aufzunehmen.
sum akzeptiert sowohl eine List<Integer> als auch eine List<Double>, weil der Wildcard "irgenein Number-Subtyp" bedeutet. fillWithSquares fügt Integer-Werte in eine List<Number> ein, weil der Wildcard "jede Liste, die Integer oder einen seiner Vorfahren halten kann" bedeutet. copyTo verwendet beides — die Quelle ist ein Produzent, das Ziel ist ein Konsument, und T ist der gemeinsame Elementtyp, den der Compiler aus der Übereinstimmung beider Seiten ableitet.
Wie geht es weiter
Man hat nun die vier Arten gesehen, wie Generics im Quellcode vorkommen — Klassen, Methoden, Interfaces und Wildcards. Jetzt schauen wir eine Ebene tiefer, wie die JVM das alles tatsächlich implementiert. Die Antwort — Type Erasure — erklärt einige überraschende Einschränkungen (kein new T(), kein instanceof T, keine generischen Arrays) und ist das eine Einsicht, die Javas Generics-Geschichte vollständig verständlich macht. Weiter zu Java Type Erasure.