Die „praktische Erklärung“ (ohne die Verwendung von Worten wie „Kovarianz“ ) wird manchmal auch als „PECS“ bezeichnet: What is PECS (Producer Extends Consumer Super)?
Ganz allgemein:
- Wenn das Ding, das einen Generic-Parameter hat, etwas produziert (also „liefert“), dann kann man es mit
<? extends T>
auflockern: Wenn es eine Number
liefern soll, dann kann es auch etwas sein, was etwas spezifischeres liefert - z.B. einen Integer
, weil der ja eine Number
ist
- Wenn das Ding, das einen Generic-Parameter hat, etwas konsumiert (also „empfängt“), dann kann man es mit
<? super T>
auflockern: Wenn es eine Number
bekommen soll, dann kann es auch etwas sein, was etwas allgemeineres bekommen kann - z.B. ein Object
, weil eine Number
ja ein Object
ist
Ich hatte irgendwann mal ein paar Beispiele dazu erstellt - die genauen Beispiele waren spezifischer, aber ich hab’ den „Kern“ nochmal hier zusammengefasst:
package bytewelt;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class PECS
{
public static void main(String[] args)
{
Supplier<Object> objectSupplier = null;
Supplier<Number> numberSupplier = null;
Supplier<Double> doubleSupplier = null;
Consumer<Object> objectConsumer = null;
Consumer<Number> numberConsumer = null;
Consumer<Double> doubleConsumer = null;
// Without PECS:
passItOnWithoutPecs(objectSupplier, objectConsumer); // OK :-)
//passItOnWithoutPecs(objectSupplier, numberConsumer); // Not OK :-(
//passItOnWithoutPecs(objectSupplier, doubleConsumer); // Not OK :-(
//passItOnWithoutPecs(numberSupplier, objectConsumer); // Not OK :-(
passItOnWithoutPecs(numberSupplier, numberConsumer); // OK :-)
//passItOnWithoutPecs(numberSupplier, doubleConsumer); // Not OK :-(
//passItOnWithoutPecs(doubleSupplier, objectConsumer); // Not OK :-(
//passItOnWithoutPecs(doubleSupplier, numberConsumer); // Not OK :-(
passItOnWithoutPecs(doubleSupplier, doubleConsumer); // OK :-)
// With PECS:
passItOnWithPecs(objectSupplier, objectConsumer); // OK :-)
//passItOnWithPecs(objectSupplier, numberConsumer); // Not OK :-(
//passItOnWithPecs(objectSupplier, doubleConsumer); // Not OK :-(
passItOnWithPecs(numberSupplier, objectConsumer); // OK :-)
passItOnWithPecs(numberSupplier, numberConsumer); // OK :-)
//passItOnWithPecs(numberSupplier, doubleConsumer); // Not OK :-(
passItOnWithPecs(doubleSupplier, objectConsumer); // OK :-)
passItOnWithPecs(doubleSupplier, numberConsumer); // OK :-)
passItOnWithPecs(doubleSupplier, doubleConsumer); // OK :-)
}
private static <T> void passItOnWithoutPecs(
Supplier<T> supplier, Consumer<T> consumer)
{
consumer.accept(supplier.get());
}
private static <T> void passItOnWithPecs(
Supplier<? extends T> supplier, Consumer<? super T> consumer)
{
consumer.accept(supplier.get());
}
}
(Dass hier der Typ als <T>
bei der Methode steht, „verzerrt“ das ganze ein bißchen. In der Realität geht es oft um eine komplette Klasse, die ein <T>
hat, und wo man PECS dann bei einzelnen Methoden anwendet, die ggf. nur einen Producer oder nur einen Consumer bekommen. Aber wenn man es so kombiniert, ist es IMHO recht übersichtlich, und geeignet, um den Punkt zu verdeutlichen: )
Ohne PECS muss der Typ von Producer und Consumer genau zueinander passen. Mit PECS wird das erreicht, was man intuitiv erwarten würde: Jemand, der eine Number
liefert, kann sie auch an jemanden weitergeben, der ein Object
erwartet.
(Da das Ding in Java nicht „Producer“ heißt, sondern „Supplier“, wäre „Supplier extends, Consumer super“ eigentlich passender. Das Akronym wäre dann aber „SECS“. *albern kichert*)
Das spezifische Beispiel, um das es damals ging, war etwas, was mit einer „Distanzmatrix“ zu tun hatte. Das grobe Muster wa da sowas:
class Computer<T> {
DistanceFunction<T> distanceFunction; // + setter
DistanceMatrix compute(List<T> elements) {
....
distances[i][j] = distanceFunction.apply(ei, ej);
...
}
}
Was man dann leicht verallgemeinern konnte: Mit einem
DistanceFunction<? super T> distanceFunction; // super hier
DistanceMatrix compute(List<? extends T> elements) { // extends hier
konnten damit dann viel mehr Kombinationen abgefrühstückt werden
Aber nebenbei: Ich habe den spezifischen Fehler, um den es hier ursprünglich ging, nicht im Detail nachvollzogen. Was ich aber auch (schmerzhaft) erfahren habe, ist das irgendwelche „Selbstreferentiellen Typen“ (also Baumstukturen, wie etwa ein Szenegraph) nur schwer schön zu generifizieren sind. Irgendwo fährt man sich immer fest, und in einem Fall habe ich dann schlicht aufgegeben, und mich mit einigen unchecked casts abgefunden, um den Code zumindest ansatzweise les- und nachvollziehbar zu halten…