Über Kovarianz und Kontravarianz generischer Typen in Java

von Hubert Schmid vom 2013-02-03

Die Begriffe Kovarianz und Kontravarianz werden in mehreren Zusammenhängen verwendet. Bezogen auf generische Typen beschreiben sie üblicherweise die Beziehungen zwischen unterschiedlich parametrisierten Klassen. Ist beispielsweise Foobar<T> ein generischer Typ und Derived eine Spezialisierung von Base, so sieht die Definition vereinfacht wie folgt aus:

Mit einem konkreten Beispiel ist das einfacher zu erklären. Als generischen Typ verwende ich die minimale Schnittstelle einer Stack-Datenstruktur – und für die Parametrisierung die Typen Object und String.

interface Stack<T> { boolean isEmpty(); void push(T element); T pop(); }

Ist dieser Typ kovariant? Oder anders formuliert: Kann man einen Stack<String> anstelle eines Stack<Object> verwenden, so wie man einen String anstelle eines Object verwenden kann? Das folgende Code-Fragment zeigt, warum diese Ersetzung nicht möglich ist.

Stack<String> strings = createStack(String.class); Stack<Object> objects = strings; // assume assignment is allowed objects.push(new Object()); String element = strings.pop();

Nehmen wir für einen Augenblick an, die Zuweisung in der zweiten Zeile wäre in Java erlaubt. In diesem Fall verweisen die beiden Variablen strings und objects auf das gleiche Objekt, und das Object aus der dritten Zeile wird zu einem String in der vierten Zeile. Das ist offensichtlich Unsinn und zeigt, dass die Ersetzung nicht möglich ist.

Mit der gleichen Argumentation kann man sich auch davon überzeugen, dass Stack<T> genauso wenig kontravariant ist. Dafür muss man lediglich die Typen der ersten beiden Zeilen umkehren – wie im folgenden Code-Fragment zu sehen.

Stack<Object> objects = createStack(Object.class); Stack<String> strings = objects; // assume assignment is allowed objects.push(new Object()); String element = strings.pop();

Der generische Typ Stack<T> ist also weder kovariant nocht kontravariant. Das Gleiche gilt auch für die meisten anderen, generischen Typen der Standardbibliothek. Es gibt aber auch Ausnahmen: So ist beispielsweise Iterator<T> kovariant und Comparator<T> kontravariant.

Nochmals zurück zum Stack<T>: Es gibt eine alternative Schnittstelle dieser Datenstruktur, die interessanterweise kovariant ist. Dafür muss man lediglich die push-Methode so abändern, dass sie ein neues Objekt mit dem hinzugefügten Element zurück liefert anstatt die eigene Datenstruktur zu ändern.

interface Stack<T> { boolean isEmpty(); T pop(); Stack<T> push(T element); }

Keine Frage: Das sieht ein wenig gewöhnungsbedürftig aus. Doch einige Programmiersprachen setzen dieses Vorgehen konsequent ein und können dadurch auf relativ vielen kovarianten und kontravarianten Typen aufbauen. In Java wäre das allerdings nicht sinnvoll gewesen. Denn die meisten Klassen sind älter als die Unterstützung für generische Typen.

Dafür gibt es einen anderen, interessanten Mechanismus in Java: Die sogenannten Wildcards können zusammen mit Referenzen verwendet werden. Dadurch erhält man über die Referenzen eine Sicht auf die Objekte, die entweder den kovarianten oder kontravarianten Teil der Objekte widerspiegelt.

Stack<? extends String> strings = createStack(String.class); Stack<? extends Object> objects = strings; Object element = objects.pop(); Stack<? super Object> objects = createStack(Object.class); Stack<? super String> strings = objects; strings.push("a string");

Diese beiden Code-Fragmente sind sehr ähnlich zu den weiter oben gezeigten Beispielen. Und im Gegensatz zu Letzteren lassen sie sich mit Java 7 und der ersten Version der Stack-Schnittstelle fehlerfrei übersetzen. Das zeigt, dass Kovarianz und Kontravarianz in Java also sehr wohl existiert und funktioniert. Doch zu den Wildcard-Mechanismen schreibe ich ein anderes Mal mehr.