T1 T2 A(T1) A(T2) A(T1) - Beuth Hochschule für Technik Berlin

Werbung
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.
Zugehörige Unterlagen
Herunterladen