Wildcards und gebundene Parameter
Wildcards
- Wildcards
<?>
zeigen an, dass jede beliebige Ausprägung eines generischen Typs möglich ist <?>
ist die Kurzform von<? extends Object>
- Bound Wildcards
<? extends T>
bzw.<? super T>
stellen sicher, dass nicht jede beliebige Ausprägung des generischen Typs, sondern nur bestimmte möglich sind - Wildcards können nur bei der Deklaration von Parametern und Variablen auftauchen
- Bei der Objekterzeugung und der Deklaration von generischen Typen können sie nicht verwendet werden
Anders als man also erwarten würde, führt die Tatsache, dass Generics von Subtypen selbst nicht wieder Subtypen sind, dazu, dass man für die Aussage „egal welcher Typ-Parameter für den generischen Typ gesetzt wurde, ich nehme alles“ nicht GenerischerTyp<Object>
nehmen kann, sondern man GenerischerTyp<?>
verwenden muss. Hintergrund ist, dass GenerischerTyp<Object>
eben nicht wie Object
selbst die Superklasse aller Ausprägungen des generischen Typs GenerischerTyp
ist.
Eine polymorphe Referenz, die in der Lage ist auf Generics mit unterschiedlicher Ausprägung des Typ-Parameters zu zeigen, kann man durch Verwendung des Wildcards <?>
definieren. Da es sich um die Deklaration einer Referenz handelt, kann das Wildcard nicht bei der Objekterzeugung verwendet werden. Damit entspricht Generic<?>
der universellen Referenz Object
.
Um den vollen Umfang der Polymorphie nutzen zu können, muss man in der Lage sein, den Wildcard einzuschränken. Dies geschieht über gebundene Wildcards (bound wildcards). Hier kann man einen Wildcard bezüglich der Vererbungshierarchie einschränken, d. h. der referenzierte generische Typ muss bezüglich seines Typ-Parameters Kriterien erfüllen:
? super X
der Typ-Parameter muss vom TypX
oder einer Superklasse von X sein? extends X
der Typ-Parameter muss vom TypX
oder einer Subklasse von X sein
?
ist nur eine Kurzform für ? extends Object
.
Besonders das super
ist verwirrend, da man diese Konstruktion aus der normalen Polymorphie nicht kennt. Hier kann eine Referenz immer nur auf den Typ oder einen Supertyp zeigen, nicht aber umgekehrt. Warum man diese Besonderheit bei Generics benötigt, wird in den nächsten Abschnitten noch erläutert.
Beispiel: Wildcard
public class StackPrinter {
public void printStack(SimpleStack<?> stack) {
for (int i = stack.getSize(); i >= 0; i--) {
System.out.printf("%d %s%n", i, stack.pop());
}
}
}
var stack = new SimpleStack<String>(10);
stack.push("!");
stack.push("World");
stack.push("Hello");
StackPrinter printer = new StackPrinter();
printer.printStack(stack);
SimpleStack<?> wildcard1 = new SimpleStack<String>(10);
SimpleStack<?> wildcard2 = new SimpleStack<?>(10); // FEHLER!!
Das Beispiel zeigt die Verwendung eines ungebundenen Wildcards. Die Methode printStack
soll in der Lage sein, den Inhalt eines beliebigen Stacks auszugeben. Deswegen muss sie Stacks mit ganz unterschiedlichen Typ-Parametern akzeptieren, z. B. SimpleStack<String>
oder SimpleStack<Form>
. Dies kann man erreichen, indem man den Typ-Parameter als SimpleStack<?>
deklariert, was eine Kurzform von SimpleStack<? extends Object>
ist. Somit kann man alle Stacks übergeben, deren Typ-Parameter Object
oder eine Subklasse davon ist. Da Object
die Wurzel aller Vererbungshierarchien ist, kann man jeden SimpleStack
übergeben.
Der Versuch ein Objekt von SimpleStack<?>
zu erzeugen, schlägt fehl, weil das Wildcard nur bei Referenzvariablen, nicht aber bei der Objekterzeugung auftreten darf.
Beispiel: Bound Wildcard
public class Berechner {
public double berechneFlaeche(SimpleStack<? extends Form> formen) {
double summe = 0;
for (int i = formen.getSize(); i >= 0; i--) {
summe += formen.pop().flaeche();
}
return summe;
}
}
Will man nur bestimmte Typ-Parameter zulassen, muss man den Wildcard einschränken. Dies passiert im vorliegenden Beispiel: Die Methode berechneFlaeche
soll nur Stacks akzeptieren, die mindestens eine Form
enthalten, weil nur Form
eine Methode flaeche()
hat. Aus diesem Grund wird das Wildcard auf <? extends Form>
eingeschränkt. Jetzt kann die Methode nur mit Stacks aufgerufen werden, die den Typ-Parameter Form
oder eine Subklasse davon haben, also im Beispiel Form
, Kreis
und Rechteck
.
Da der Compiler jetzt weiß, dass immer mindestens ein Form
-Objekt aus der pop
-Methode kommt, kann man die flaeche()
-Methode ohne Cast aufrufen.
Es ist typisch, dass man beim Herausholen von Objekten aus einem generischen Typ extends verwendet.
public class SimpleStack<T> {
...
public void popAll(Collection<? super T> collection) {
while (pos > 0) {
collection.add(pop());
}
}
}
var s = new SimpleStack<Form>(10);
s.push(new Rechteck(0.4, 0.4));
s.push(new Rechteck(0.7, 0.3));
s.push(new Kreis(0.4));
s.push(new Kreis(0.7));
List<Object> l1 = new ArrayList<>();
s.popAll(l1);
In diesem Beispiel sollen alle Objekte eines Stacks in eine andere generische Klasse umkopiert werden. Aufgrund der Polymorphie muss man jetzt beim Wildcard super T
verwenden, da es andernfalls vorkommen könnte, dass die falsche Collection
übergeben würde.
Man kann dies wie folgt erklären: Wenn der SimpleStack
den Typ-Parameter B
hat, dann können darin Objekt vom Typ B
oder einer Subklasse von B
gespeichert werden (z. B. C
, D
, E
, …). Will man diese Objekte umkopieren, dann muss die aufnehmende Klasse alle Objekte, die potenziell in SimpleStack
enthalten sind, aufnehmen können. Ihr Typ-Parameter kann also nur B
oder A
oder Object
sein, also muss er dem Typ-Parameter von SimpleStack
entsprechen oder eine Superklasse davon sein.
Es ist typisch, das man beim Speichern von Objekten in einem generischen Typ super verwendet.
Wildcard mit extends
Die Grafik zeigt noch einmal, welche Klasse welche Objekte enthalten kann, bzw. welche Referenzen auf welche Typen zeigen können. So kann eine Referenz vom Typ Generic<? extends B>
auf die generischen Klassen Generic<B>
und Generic<C>
zeigen. Generic<B>
kann dann Objekte vom Typ B
und C
enthalten.
Beispiel: Wildcard mit extends
A a = new A();
B b = new B();
C c = new C();
Generic<A> ga = new Generic<>();
Generic<B> gb = new Generic<>();
Generic<C> gc = new Generic<>();
ga.add(a); ga.add(b); ga.add(c);
gb.add(b); gb.add(c);
gc.add(c);
Generic<? extends A> gea = ga;
gea = gb; gea = gc;
Generic<? extends B> geb = gb;
geb = gc;
Generic<? extends C> gec = gc;
Wildcard mit super
Die Grafik zeigt noch einmal, welche Klasse welche Objekte enthalten kann bzw. welche Referenzen auf welche Typen zeigen können. So kann eine Referenz vom Typ Generic<? super B>
auf die generische Klassen Generic<B>
und Generic<A>
zeigen. Generic<B>
kann dann Objekte vom Typ B
und C
enthalten.
Beispiel: Wildcard mit super
A a = new A();
B b = new B();
C c = new C();
Generic<A> ga = new Generic<>();
Generic<B> gb = new Generic<>();
Generic<C> gc = new Generic<>();
ga.add(a); ga.add(b); ga.add(c);
gb.add(b); gb.add(c);
gc.add(c);
Generic<? super A> gsa = ga;
Generic<? super B> gsb = gb;
gsb = ga;
Generic<? super C> gsc = gc;
gsc = gb; gsc = ga;
Beispiel: super und extends
public void adder(Generic<? super B> g) {
g.add(new C());
g.add(new B());
g.add(new A()); // FEHLER!! könnte ein Generic<B> sein
B b = g.get(); // FEHLER!! könnte ein Generic<A> sein
}
public void getter(Generic<? extends B> g) {
A a = g.get();
B b = g.get();
C c = g.get(); // FEHLER!! g könnte ein Generic<B> sein
g.add(new B()); // FEHLER!! g könnte ein Generic<C> sein
}
public void doIt() {
adder(new Generic<C>()); // FEHLER!! höchstens Generic<B> erlaubt
adder(new Generic<B>());
adder(new Generic<A>());
getter(new Generic<C>());
getter(new Generic<B>());
getter(new Generic<A>()); // FEHLER!! mindestens Generic<B> benötigt
}
Man sieht an dem Beispiel deutlich, dass man beim Hinzufügen von Objekten zu einem generischen Typen als Wildcard ? super T
nehmen muss, wobei T
der minimale Typ ist, den man hinzufügen kann – Subtypen von T
kann man dann ebenfalls hinzufügen. Umgekehrt muss man beim Lesen von Daten aus dem Generic ? extends T
nehmen, wobei T
der maximale Typ ist, den man als Rückgabetyp erwartet.
Gebundene Parameter
Typ-Parameter können ebenfalls eingeschränkt werden
- Analog zu Wildcards können Typ-Parameter auf bestimmte (Unter-)Klassen beschränkt werden
<T extends K>
- Schränkt T auf K und Subklassen von K ein
Beispiel: Gebundene Parameter
public class FormenStack<T extends Form> {
...
public double flaeche() {
double summe = 0;
for (int i = getSize(); i >= 0; i--) {
summe += pop().flaeche();
}
return summe;
}
}
Das Beispiel zeigt einen gebundenen Typ-Parameter T extends Form
. Bei der Klasse FormenStack
kann man den Typ-Parameter T
nur auf Form
oder eine Subklasse davon (Kreis
, Rechteck
) setzen. Hierdurch ist jetzt garantiert, dass alle Objekte, die in dem Stack gespeichert werden, eine flaeche
-Methode haben, die dann in der gleichnamigen Methode des FormenStack
benutzt wird.
var s1 = new FormenStack<Kreis>(10);
s1.push(new Kreis(0.4));
s1.push(new Kreis(0.7));
var s2 = new FormenStack<Rechteck>(10);
s2.push(new Rechteck(0.4, 0.4));
s2.push(new Rechteck(0.7, 0.3));
var s3 = new FormenStack<Form>(10);
s3.push(new Rechteck(0.4, 0.4));
s3.push(new Rechteck(0.7, 0.3));
s3.push(new Kreis(0.4));
s3.push(new Kreis(0.7));
System.out.printf("Gesamtflaeche: %g%n", s1.flaeche());
System.out.printf("Gesamtflaeche: %g%n", s2.flaeche());
System.out.printf("Gesamtflaeche: %g%n", s3.flaeche());