Kovarianz und Kontravarianz in Java Abstract In diesem Artikel werden die Java-Regeln für Kovarianz und Kontravarianz zusammengefasst. Hierzu wird Typkompatibilität definiert und die Fälle untersucht, wo abhängige Typen kompatibel sind oder nicht. Auch die Kompatibilität von Methoden wird erörtert. Definition Kovarianz und Kontravarianz ist die von Typen abhängige Kompatibilität. Ein von T abhängiges Element A(T) ist dann kovariant („mit-verschieden“), wenn aus der Kompatibilität von T1 zu T2 die Kompatibilität von A(T1) zu A(T2) folgt. Wenn aus der Kompatibilität von T1 zu T2 die Kompatibilität von A(T2) zu A(T1) folgt, dann ist der Typ A(T) kontravariant („gegenverschieden“). Wenn aus der Kompatibilität von T1 zu T2 keine Kompatibilität zwischen A(T1) und A(T2) folgt, dann ist A(T) invariant. T1 A(T1) T2 A(T2) A(T1) A(T2) Kovarianz und Kontravarianz In Java gibt es zwei Sprachelemente, die von Typen abhängig sind: Methoden und (abhängige) Typen. Es gibt zweierlei abhängige Typen: Reihungstypen (arrays) und generische (parametrisierte) Typen. Methoden sind von den Typen ihrer Parameter abhängig. Ein Reihungstyp T[] ist vom Typ seiner Elemente T abhängig. Ein generischer Typ G<T> ist von seinem Typparameter T abhängig. Kompatibilität unterschiedlicher Typen Der Typ T1 ist zu T2 implizit bzw. explizit kompatibel, wenn die Zuweisung einer Variable vom Typ T1 in eine Variable vom Typ T2 ohne bzw. mit Kennzeichnung möglich ist. Die häufigste Möglichkeit, explizite Kompatibilität zu kennzeichnen ist die Typkonvertierung (casting): variableVomTypT2 = variableVomTypT1; // implizit kompatibel variableVomTypT2 = (T2)variableVomTypT1; // explizit kompatibel Beispielsweise ist int implizit kompatibel zu long und explizit kompatibel zu short: int i = 5; long l = i; // implizit kompatibel short s = (short)i; // explizit kompatibel Implizite und explizite Kompatibilität besteht nicht nur bei Zuweisungen, sondern auch bei Parameterübergabe. Neben Eingabeparameter gehört dazu auch die Übergabe eines Funktionsergebnisses (als Ausgabeparameter). boolean ist mit keinem anderen Typ kompatibel; zwischen einem primitiven und einem Referenztyp besteht ebenfalls keine Kompatibilität. Eine Untertyp ist implizit kompatibel zum Obertyp, explizit in die andere Richtung. Referenztypen sind also nur innerhalb eines Hierarchiezweiges (aufwärts implizit und abwärts explizit) untereinander kompatibel: referenzVomUntertyp = referenzVomObertyp; // implizit kompatibel referenzVomObertyp = (Obertyp)referenzVomUntertyp; // explizit kompatibel Der Java-Compiler erlaubt typischerweise implizite Kompatibilität für eine Zuweisung dort, wo zur Laufzeit keine „Verluste“ durch die Unterschiede zwischen den Typen zu befürchten sind1. Beispielsweise ist int implizit kompatibel zu long, weil eine long-Variable jeden int- Wert aufnehmen kann. Im Gegensatz dazu kann eine short-Variable nicht jeden int-Wert aufnehmen – deswegen wird hier nur explizite Kompatibilität erlaubt. char int byte long float double short Implizite Kompatibilität von arithmetischen Typen in Java2 Ähnlich gelingt die Zuweisung einer Referenz vom Untertyp in eine Referenz vom Obertyp immer. Die Zuweisung in die andere Richtung kann eine Ausnahme ClassCastException auslösen3. Kovarianz und Kontravarianz von Reihungen Die Kovarianz von Reihungstypen bedeutet, dass wenn T zu U kompatibel ist, dann ist T[] zu U[] auch kompatibel; Kontravarianz bedeutet, dass U[] zu T[]kompatibel ist. Reihungen aus primitiven Typen sind in Java invariant: longReihung = intReihung; // Typfehler shortReihung = (short[])intReihung; // Typfehler Reihungen aus Referenztypen sind jedoch in Java implizit kovariant und explizit kontravariant: Obertyp[] oberreihung; Untertyp[] unterreihung; … oberreihung = unterreihung; // implizit kovariant unterreihung = (Untertyp[])oberreihung; // explizit kontravariant Hierdurch ist jedoch das sog. Kovarianzproblem entstanden. Dies bedeutet, dass eine Zuweisung von Reihungskomponenten zur Laufzeit die Ausnahme ArrayStoreException auslösen kann, wenn eine Obertyp-Reihungsreferenz eine Untertyp-Reihung referiert, ihrer Komponente jedoch ein Obertyp-Objekt zugewiesen werden soll: oberreihung[1] = new Obertyp(); // throws ArrayStoreException T2 T2[] T1 T1[] Kovarianz für Reihungen Das eigentliche Problem ist aber gar nicht die Ausnahme (die durch Programmdisziplin vermeidbar ist), sondern dass die Virtuelle Maschine zur Laufzeit jede Zuweisung auf ein Reihungselement in Bezug auf das Kovarianzproblem prüfen muss. Dies ist eine beträchtliche Effizienzverminderung gegenüber Sprachen ohne Kovarianz (wo eine kompatible Zuweisung für Reihungsreferenzen verboten ist) oder wo Kovarianz ausgeschaltet werden kann (wie z.B. in Scala). 1 Diese Regel gilt nicht für Genauigkeitsverluste, beispielsweise bei einer Zuweisung von int nach float. 2 Mit rotem Pfeil wurde der potentielle Genauigkeitsverlust dargestellt. 3 s. Typkompatibilität in Java unter http://public.beuthhochschule.de/~solymosi/veroeff/typkompatibilitaet/Typkompatibilitaet.html Ein einfaches Beispiel ist, wenn die Reihungsreferenz vom Typ Object[], das Reihungsobjekt und die Elemente jedoch von unterschiedlichen Klassen sind: Object[] reihung; // Reihungsreferenz reihung = new String[3]; // Reihungsobjekt; kovariante Zuweisung reihung[0] = new Integer(5); // throws ArrayStoreException Wegen der Kovarianz kann der Compiler die Korrektheit der Zuweisungen auf die Reihungselemente nicht überprüfen – die JVM tut dies und erbringt dabei einen beträchtlichen Zusatzaufwand. Dieser kann jedoch vom Compiler optimiert werden, wenn von der Typkompatibilität zwischen Reihungstypen kein Gebrauch gemacht wird. Das Kovarianzproblem für Reihungen Unspezifische Kovarianz für parametrisierte Typen Generische (parametrisierte) Typen sind in Java implizit invariant, d.h. unterschiedliche Instanziierungen eines generischen Typs sind untereinander nicht kompatibel. Auch Typkonvertierung (casting) ermöglicht hier keine Kompatibilität: Generisch<Obertyp> oberGenerisch; Generisch<Untertyp> unterGenerisch; unterGenerisch = (Generisch<Untertyp>)oberGenerisch; // Typfehler oberGenerisch = (Generisch<Obertyp>)unterGenerisch; // Typfehler Der Typfehler wird gemeldet, obwohl unterGenerisch.getClass() == oberGenerisch.getClass() – die Methode getClass() ermittelt nämlich den rohen (unparametrisierten) Typ. Dies ist der Grund, warum ein aktueller Typparameter nicht zu Signatur einer Methode gehört. Die beiden Methodenvereinbarungen void methode(Generisch<Obertyp>); void methode(Generisch<Untertyp>); dürfen daher nicht in derselben Schnittstellendefinition vorkommen. Obwohl generische Typen in Java implizit invariant sind, gibt es Variablen, die kovariant benutzt werden können. Sie werden mit Hilfe des Jokers (wildcard) ? vereinbart, der als aktueller Typparameter eingesetzt werden darf. Generisch<?> ist der abstrakter Obertyp aller Instanziierungen des generischen Typs, d.h. zu Generisch<?> sind alle Instanziierungen von Generisch kompatibel: Generisch<?> jokerReferenz; jokerReferenz = new Generisch<String>(); // implizit kompatibel jokerReferenz = new Generisch<Integer>(); Da der Joker-Typ abstrakt ist, kann er nur für Referenzen und nicht für Objekte benutzt werden: new Generisch<?>() ergibt keinen Sinn und wird vom Compiler abgelehnt. Ein Beispiel für die Verwendung des Jokers ist der Parameter einer Methode, die eine Sammlung (Collection oder Reihung) unabhängig vom Elementtyp manipuliert. Eine solche Methode für Reihungen zu schreiben ist – wegen der Kovarianz – einfach: static void swap(Object[] reihung, int i, int j) { … // vertauscht die Elemente i und j } Der Aufruf kann für eine beliebige Reihung infolge der Kompatibilität zu Object erfolgen: Integer[] reihung = {1, 2, 3}; swap(reihung, 0, 2); // Integer[] ist kompatibel zu Object[] Die generische Version4 dieser Methode ist typsicherer5: static <T> void swap(T[] reihung, int i, int j) { … } Eine ähnliche Lösung für ArrayList funktioniert wegen Inkompatibilität für generische Typen nicht. Der Joker bewirkt jedoch Kovarianz: static void swap(List<?> liste, int i, int j) { … } // ähnlich Der Aufruf ist nun mit einem beliebigen Elementtyp möglich: List<Integer> liste = …; swap(liste, 0, 2); // List<Integer> ist kompatibel zu List<?> Eine solche Kompatibilität nennen wir unspezifische Kovarianz, weil hier nicht spezifiziert wird, welcher (Ober-) Typ die Kovarianz ermöglicht. Solche Kompatibilität kann sogar auf zwei Ebenen gleichzeitig bestehen: auf der Ebene der generischen Typen (ArrayList zu List) und auf der Ebene der Kovarianz (Integer zu ?): ArrayList<Integer> arrayList = … ; // ArrayList<T> implements List<T> swap(liste, 0, 2); // ArrayList<Integer> ist kompatibel zu List<?> Explizite Kovarianz für parametrisierte Typen Diese „leichte Kovarianz“ kann man verallgemeinern. Implizite Kovarianz würde bestehen, wenn Generisch<Untertyp> zu Generisch<Obertyp> kompatibel wäre. Die Einschränkung des Jokers mit extends bewirkt dasselbe Effekt explizit: Generisch<Untertyp> ist kompatibel zu Generisch<? extends Obertyp>. Von diesem eingeschränkten Joker-Typ kann nun eine Referenz vereinbart werden: Generisch<? extends Obertyp> kovarianteReferenz; In diese Referenz können beliebige Instanzen von Generisch eingehängt werden, deren aktueller Typparameter ein Untertyp von Obertyp ist: kovarianteReferenz = new Generisch<Obertyp>(); kovarianteReferenz = new Generisch<Untertyp>(); Die Einschränkung des Jokers ist dann sinnvoll, wenn nicht beliebige aktuelle Typparameter zugelassen werden sollen, weil von ihnen bestimmte Eigenschaften erwartet werden – beispielsweise, wenn die Elemente der Parametersammlung manipuliert werden sollen: static void inkrement(Number[] reihung) { … } // für jedes Element + 1 static void inkrement(Collection<? extends Number> sammlung) { … } // ähnlich Der Aufruf der Methoden ist infolge der Kovarianz mit einem beliebigen Number-Parameter möglich: inkrement(reihung); // Integer[] ist kompatibel zu Number[] inkrement(liste); // ArrayList<Integer> ist kompatibel zu Collection<? extends Number> 4 Die beiden können aber nicht in derselben Klasse vereinbart werden, weil sie keine unterscheidbare Signatur haben. 5 Der Aufruf erfolgt nicht duch Kompatibilität sondern durch generische Instanziierung. In der letzten Programmzeile wird Kompatibilität wieder gleichzeitig auf zwei Ebenen genutzt: Der parametrisierte ArrayList<T> ist ein Untertyp von Collection<T> und Integer ist ein Untertyp von Number. Der eingeschränkte Joker ermöglicht also explizite Kovarianz unter parametrisierten Typen. Kovarianter Typparameter als Parametertyp Diese Kovarianz wirkt über den Typparametern, nicht aber über den Parametertypen der Methoden in der generischen Klasse. Angenommen, in der Klasse Generisch benutzen wir den Typparameter T als Typ der (Ein- und Ausgabe-) Parametern von Methoden: class Generisch<T> { private T t; void schreiben(T t) { this.t = t; } // T ist Eingabeparametertyp T lesen() { return t; } } // T ist Ausgabeparametertyp Dann kann die Methode schreiben mit jokerReferenz nicht direkt (nur nach Typkonvertierung) aufgerufen werden: jokerReferenz.schreiben(new Object()); // Typfehler ((Generisch<Object>)jokerReferenz).schreiben(new Object()); // OK Der Grund ist, dass zum Joker kein Typ (auch nicht Object) kompatibel ist - ? ist eigentlich gar kein Typ. Der Joker selbst ist jedoch zu Object (und zu keinem anderen Typ) kompatibel, so kann das Typparameter-Ergebnis einer Funktion in eine Object-Referenz übernommen werden: Object o = jokerReferenz.lesen(); Für eingeschränkte Joker-Typen gelten dieselben Regeln: Eingabeparameter können nicht (nur nach casting) übergeben werden, Ausgabeparameter sind vom Typ der Schranke: kovarianteReferenz.schreiben(new Obertyp()); // Typfehler kovarianteReferenz.schreiben(new Untertyp()); // Typfehler ((Generisch<Obertyp>)kovarianteReferenz).schreiben(new Obertyp()); // OK ((Generisch<Obertyp>)kovarianteReferenz).schreiben(new Untertyp()); // OK ((Generisch<Untertyp>)kovarianteReferenz).schreiben(new Untertyp()); //OK Object objekt = kovarianteReferenz.lesen(); // OK Obertyp ober = kovarianteReferenz.lesen(); // OK Untertyp unter1 = kovarianteReferenz.lesen(); // Typfehler Untertyp unter2 = ((Generisch<Untertyp>)kovarianteReferenz).lesen(); //OK Untertyp unter3 = (Untertyp)kovarianteReferenz.lesen(); // typunsicherer Man kann dies so interpretieren, dass der uneingeschränkte Joker-Typ durch Object eingeschränkt ist: Generisch<?> ist gleichwertig mit Generisch<? extends Object>. So gesehen ist die unspezifische Kovarianz eine explizite Kovarianz über die Kompatibilität zu Object. Kontravarianz für parametrisierte Typen Kontravarianz bedeutet die Kompatibilität in die andere Richtung, nämlich abwärts. Reihungen sind explizit kontravariant; syntaktisch wird dies durch Typkonvertierung (casting) ausgedrückt: unterreihung = (Untertyp[])oberreihung; // explizit kompatibel (kontravariant) Zwischen unterschiedlichen Instanziierungen eines generischen Typs wird die Typkonvertierung vom Compiler abgelehnt. Aber auch generische Typen sind explizit kontravariant. Syntaktisch wird dies ausgedrückt, indem man den Joker von unten mit Hilfe von super einschränkt: Generisch<? super Untertyp> kontravarianteReferenz; In diese Variable können nun Instanziierungen von Generisch mit einem beliebigen Obertyp (z.B. Object) von Untertyp gehängt werden: kontravarianteReferenz = new Generisch<Untertyp>(); // normal kontravarianteReferenz = new Generisch<Obertyp>(); // kontravariant kontravarianteReferenz = new Generisch<Object>(); // auch möglich Hier findet also die Zuweisung von der Obertyp-Instanziierung (bzw. von noch weiter oben, von der Object-Instanziierung) nach unten, nämlich zur Untertyp-Instanziierung kontravarianteReferenz statt – dies heißt Kontravarianz. Ein Beispiel für die Anwendung der Kontravarianz ist die Methode java.util.Collections.sort mit Comparator. Ihre Signatur ist public static <T> void sort(List<T> list, Comparator<? super T> c) Diese Methode sortiert eine beliebige Liste; die fürs Sortieren notwenige ElementVergleichsmethode wird hier nicht im Elementtyp (etwa mit List<T extends Comparable<T>>, wie in der überladenen Version von sort) vereinbart, sondern in einem extra Comparator-Objekt. Der Vorteil hiervon ist, dass die Objekte nach verschiedenen Kriterien (z.B. einmal nach Namen, einmal nach Kundennummern) sortiert werden können. In den meisten Fällen würde hier Comparator<T> reichen, dessen Methode int compare(T o1, T o2) zwei beliebige Elemente im List<T>-Objekt miteinander vergleichen kann: class DateComparator implements Comparator<java.util.Date> { public int compare(Date d1, Date d2) { return … } // vergleicht die zwei Date-Objekte } List<java.util.Date> liste = … ; // Liste aus Date-Objekten sort(liste, new DateComparator()); // sortiert die Liste Die Methode Collection.sort() deckt aber auch zusätzliche Fälle ab. Wegen des kontravarianten Typparameters von Comparable kann mit ihr auch eine Liste vom Typ List<java.sql.Date> sortiert werden, zumal java.util.Date ein Obertyp von java.sql.Date ist: List<java.sql.Date> sqlListe = … ; sort(sqlListe, new DateComparator()); Ohne Kontravarianz (d.h. ohne <? super T> , nur mit <T> oder dem unspezifischen, typunsicheren <?> in der sort-Signatur) würde die letzte Zeile vom Compiler als Typfehler abgelehnt werden – man müsste dafür extra eine nichtssagende Klasse class SqlDateComparator extends DateComparator implements Comparator<java.sql.Date> {} anfertigen, um sort(sqlListe, new SqlDateComparator()); aufrufen zu können. Nicht nur Collections.sort wurde mit einem kontravarianten Parameter versehen: Viele andere Methoden in Collections, wie addAll, binarySearch, copy, fill usw. können ähnlich flexibel aufgerufen werden. Andere Methoden wie max und min haben kontravariante Ergebnistypen: public static <T extends Object & Comparable<? super T>> T max( Collection<? extends T> coll) Hier ist es ersichtlich, wie einem Typparameter mehrere Bedingungen mit Hilfe von & auferlegt werden können. Das überflüssig wirkende extends Object bewirkt hier, dass max im Bytecode (wo es keine Typparameter mehr gibt) ein Ergebnis vom Typ Object und nicht Comparable zurückgibt. Ihre überladene Comparator-Versionen public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) haben je einen kovarianten und einen kontravarianten Parameter: Während die Elemente der Collection Untertypen eines (nicht explizit angegebenen) Typs sein müssen, muss Comparator für einen Obertyp desselben instanziiert worden sein. Eine hohe Intelligenz wird vom InferenzAlgorithmus6 des Compilers verlangt, um diesen Zwischentyp aus einem Aufruf wie Collection<EinTyp> sammlung = … ; Comparator<EinAndererTyp> vergleicher = … ; max(sammlung, vergleicher); ermitteln zu können. Noch interessanter ist die Signatur der Methode java.util.Collections.sort mit Comparable; sie verwendet auch sowohl extends wie auch super, aber diesmal verschachtelt: public static <T extends Comparable<? super T>> void sort(List<T> list) Hier sprechen wir jedoch nicht von Ko- und Kontravarianz, weil es hier nicht um Kompatibilität von Referenzen geht, sondern um die Einschränkung der Instanziierung. Diese Methode sortiert also ein List-Objekt, dessen Elemente von einer Klasse sind, die Comparable implementiert. Diese generische Schnittstelle enthält die einzige Objektmethode int compareTo(T o) die ihr Zielobjekt (vom Typ des Typparameters T) mit dem Parameterobjekt (ebenfalls vom Typ T) vergleicht. Ohne <? super T> (also nur mit <T>) in der sort-Signatur würde das Sortieren in den meisten Fällen funktionieren: sort(liste); // java.util.Date implements Comparable<java.util.Date> sort(sqlListe); // java.sql.Date implements Comparable<java.sql.Date> Die Einschränkung des Typparameters von unten erlaubt aber zusätzliche Flexibilität: Comparable muss nicht unbedingt in der Elementklasse implementiert werden; es reicht, wenn sie für eine Oberklasse implementiert ist. Beispielsweise class Ober implements Comparable<Ober> { public int compareTo(Ober ober) { … } } class Unter extends Ober {} // ohne Überschreiben von compareTo() List<Ober> oberliste = …; sort(oberliste); List<Unter> unterliste = …; sort(unterliste); Die letzte Zeile wird vom Compiler mit static <T extends Comparable<? super T>> void sort(List<T> list) { … } akzeptiert und mit static <T extends Comparable<T>> void sort(List<T> list) { … } abgelehnt. Der Grund für die Ablehnung ist, dass der Typ Unter (den der Compiler aus dem Typ 7 List<Unter> des aktuellen Parameters unterliste ermittelt ) nicht als aktueller Typparameter für T extends Comparable<T> geeignet ist: Unter implementiert Comparable<Unter> nämlich nicht, nur Comparable<Ober>; die beiden sind aber (mangels impliziter Kovarianz) nicht kompatibel, auch wenn Unter kompatibel zu Ober ist. Im anderen Fall (mit <? super T>) wird jedoch nicht erwartet, dass Comparable<Ober> von Unter implementiert wird; es reicht, wenn Ober das tut. Es reicht, weil die Methode compareTo auch für Unter-Objekte aufgerufen werden kann: sie wird von Ober geerbt. Dies wird mit <? super T> ausgedrückt, was also Kontravarianz bewirkt. Die letzte Programmzeile könnte nur dann akzeptiert werden, wenn (kompliziertererweise) class Unter extends Ober {} implements Comparable<Unter> { … } vereinbart worden wäre. 6 leitet geeignete unbekannte Typen aus bekannten Typen ab 7 durch Inferenz Kontravarianter Typparameter als Parametertyp Allerdings, die obere oder untere Schranke bezieht sich nur auf den Typparameter der Instanziierungen, die in eine ko- oder kovariante Referenz eingehängt werden. Im Falle von Generisch<? extends Obertyp> kovarianteReferenz; und Generisch<? super Untertyp> kontravarianteReferenz; können also aus verschiedenen GenerischInstanziierungen Objekte gebildet und eingehängt werden. Auf den Parameter- und Ergebnistyp der Methoden (also für Eingabe- und Ausgabeparametertypen) aus einem generischen Typ gelten andere Regeln. Beispielsweise kann als Parameter der Methode schreiben ein beliebiges Objekt übergeben werden, das zum Untertyp kompatibel ist: kontravarianteReferenz.schreiben(new Untertyp()); // OK kontravarianteReferenz.schreiben(new UnterUntertyp()); // auch OK kontravarianteReferenz.schreiben(new Obertyp()); // Typfehler ((Generisch<? super Obertyp>)kontravarianteReferenz).schreiben( new Obertyp()); // OK Durch die Kontravarianz wird also die Parameterübergabe an schreiben()möglich – im Gegensatz zum kovarianten (auch uneingeschränkten) Joker-Typ. Beim Ergebnistyp verändert sich die Situation durch Einschränkung nicht: lesen() liefert nach wie vor ein Ergebnis vom Typ ?, das nur zu Object kompatibel ist: Object o = kontravarianteReferenz.lesen(); Untertyp ut = kontravarianteReferenz.lesen();// Typfehler Die letzte Zeile ist fehlerhaft, obwohl wir kontravarianteReferenz vom Typ Generisch<? super Untertyp> vereinbart haben. Zu einem anderen Typ ist der Ergebnistyp nur nach expliziter Typkonvertierung der Referenz kompatibel: OberObertyp oo = ((Generisch<OberObertyp>)kontravarianteReferenz). lesen(); oo = (OberObertyp)kontravarianteReferenz.lesen(); // typunsicherere Alternative Erzeugung von Objekten Einerseits können von Joker-Typen (weil sie abstrakt sind) keine Objekte gebildet werden; andererseits können Reihungsobjekte nur von uneingeschränkten Joker-Typen (wie von allen abstrakten Typen), jedoch von keinen anderen generischen Instanziierungen gebildet werden: Generisch<Object>[] generischeReihung; // OK generischeReihung = new Generisch<Object>[20]; // Fehler Generisch<?>[] jokerReihung = new Generisch<?>[20]; // OK generischeReihung = (Generisch<Object>[])jokerReihung; generischeReihung[0] = new Generisch<Object>(); generischeReihung[0] = new Generisch<String>(); // Typfehler jokerReihung[0] = new Generisch<String>(); // OK Wegen der Kovarianz für Reihungen ist hier der Joker-Reihungstyp der Obertyp der Reihungstypen aller Instanziierungen, deswegen ist die abwärtskompatible Zuweisung möglich. Vom Typparameter innerhalb einer generischen Klasse können auch keine Objekte erzeugt werden. Beispielsweise im Konstruktor einer eigenen ArrayList-Implementierung muss das Reihungsobjekt vom Typ Object[] erzeugt und zum Typparameter-Reihungstyp konvertiert werden: class MyArrayList<E> implements List<E> { private E[] inhalt; public MyArrayList(int größe) { inhalt = new E[größe]; // Fehler inhalt = (E[])new Object[größe]; // workaround } … } Mehrere Typparameter Ein generischer Typ kann mehrere Typparameter haben. Sie verändern das Verhalten bzgl. Ko- und Kontravarianz nicht; diese können auch zusammen vorkommen: class G<T1, T2> {} G<? extends Obertyp, ? super Untertyp> referenz; referenz = new G<Obertyp, Untertyp>(); // ohne Varianz referenz = new G<Untertyp, Obertyp>(); // Ko- und Kontravarianz Ein häufig verwendetes Beispiel für mehrere Typparameter ist die generische Schnittstelle java.util.Map mit zwei Typparametern für Schlüssel K (key) und Werte V (value). Ihre Implementierung HashMap hat einen Konstruktor, der ein beliebiges Map-Objekt in eine Assoziationstabelle umwandelt: public HashMap(Map<? extends K,? extends V> t) Die Typparameter des Parameterobjekts t müssen dabei auch nicht auf die genauen Typparameter der Klasse K und V entsprechen, sondern können kovariant angepasst werden: Map<Kundennummer, Kunde> kunden; … kontakte = new HashMap<Id, Person>(kunden); // kovariant wobei Id ein Obertyp von Kundennummer und Person ein Obertyp von Kunde ist. Abhängigkeit von Methoden Eine Methode ist vom Typ seiner Parameter abhängig. Neben den Eingabeparametern zählen wir auch ihren Ergebnistyp (return type) als Ausgabeparameter hinzu. Die Signatur einer Methode bestimmen jedoch nur die Eingabeparametertypen. Methoden mit unterschiedlichen Namen oder mit einer unterschiedlichen Anzahl von Parametern sind zueinander nicht kompatibel. Die Frage nach Kompatibilität stellt sich also nur bei Methoden mit demselben Namen und gleicher Anzahl von Parametern8. Hierbei kann ein Methodenaufruf (im Rumpf einer Klasse) zu Methodendefinitionen (in Klassen) und zu Methodenvereinbarungen (in abstrakten Klassen und Schnittstellen) kompatibel oder inkompatibel sein. Bei einer Methodendefinition wird die Frage nach Kompatibilität zu anderen Definitionen oder Vereinbarungen, bei einer Methodenvereinbarungen zu anderen Vereinbarungen gestellt, um zu entscheiden, ob es sich hier um Überschreiben oder Überladen handelt. kovariant invariant Abhängigkeiten von Methoden bezüglich Signatur 8 wobei der letzte Parameter auch eine variable Anzahl ermöglicht. Bei der Kompatibilität zwischen Vereinbarungen und Definitionen gibt es in Java keine Varianz in Bezug auf die Signatur: Wenn eine Methode eine andere überschreibt, müssen die Signaturen gleich sein. Es gilt jedoch Kovarianz in Bezug auf den Ergebnistyp: interface Ober { void prozedur(Obertyp parameter); Obertyp funktion(); } interface Unter extends Ober { void prozedur(Untertyp parameter); // überladen @Override Untertyp funktion(); // überschrieben } Nach den strengen Java-Regeln für Signaturen wird hier prozedur überschrieben, nicht überladen: Eine Annotation @Override für prozedur würde eine Fehlermeldung des Compilers auslösen, weil der Parametertyp von prozedur in den beiden Schnittstellen nicht gleich ist. In anderen Sprachen (wie Ada oder Scala) wird Überschreiben auch mit ko- oder kontravarianten Methodenparametern ermöglicht. Im Gegensatz dazu gehört der Ergebnistyp einer Funktion nicht zu Signatur; somit kann er beim Überschreiben (wie oben) erweitert (aber nicht anders verändert) werden: Methodenvereinbarungen und -definitionen bzgl. Ergebnistypen sind in Java kovariant. Ob nun ein Aufruf zu einer Vereinbarung oder Definition kompatibel ist, wird aufgrund der Signatur entschieden. Hier gilt Kovarianz: Aus der Typkompatibilität der Parameter nach oben folgt die Kompatibilität des Aufrufs. Bezüglich Zielobjekts eines Aufrufs sprechen wir jedoch nicht von Varianz sondern von Polymorphie: oberReferenz.prozedur(untertypParameter); // Aufruf ist kovariant bezüglich Signatur unterReferenz.prozedur(obertypParameter); // polymorph Bezüglich Funktionsergebnisse beim Aufruf sprechen wir nicht von Varianz sondern einfach von Aufwärtskompatibilität: Obertyp ergebnis = unterReferenz.funktion(); // aufwärtskompatibel Untertyp ergebnis = oberReferenz.funktion(); // Typfehler: nicht abwärtskompatibel Manchmal wird diese Aufwärtskompatibilität jedoch als Kovarianz bezeichnet. Der Zugriffschutz und die Ausnahmespezifikation gehört – wie das Funktionsergebnis – nicht zur Signatur. Diese können beim Überschreiben erweitert werden. Auch final kann hinzugefügt werden, das weiteres Überschreiben unterbindet: class Oberklasse { void methode() throws OberException { … } } class Unterklasse extends Oberklasse { @Override public final void methode() throws UnterException { … } } Zusammenfassung Kovarianz und Kontravarianz ist die Kompatibilität typabhängiger Sprachelemente. In Java sind Reihungstypen (arrays) implizit kovariant und explizit (durch Typkonvertierung, casting) kontravariant. Parametrisierte (generische) Typen sind implizit invariant. Auch durch Typkonvertierung wird keine Ko- und Kontravarianz ermöglicht. Ko- und Kontravarianz kann für parametrisierte Typen jedoch durch Einschränkung des Jokers vereinbart werden: Kovarianz durch Einschränkung von oben (<? extends Schranke>), Kontravarianz durch Einschränkung von unten (<? super Schranke>). Joker-Typen (Instanziierungen eines parametrisierten Typs mit dem Joker) sind abstrakt, daher können mit ihnen nur Referenzen vereinbart werden. Methodenaufrufe zu Definitionen und Vereinbarungen sind kovariant bezüglich Signatur. Methodendefinitionen und -vereinbarungen untereinander sind in Java invariant.