Advanced   Java   Services Kovarianz, Kontravarianz und Invarianz


Kleine Wiederholung

Für das bessere Verständnis dieser Begriffe wiederholen wir kurz die Zuweisungskompatibilität von primitiven Datentypen und Referenztypen mit einigen typischen Beispielen.


Zuweisungskompatibilität von primitiven Datentypen

Bekannt ist folgendes (siehe auch Implizite Datentypanpassung).

short a = 17;
int b = a;  // implizit kompatibel = kein cast notwendig

int c = 71;
short d = (short)c;  // explizit kompatibel = cast notwendig

boolean boo = true;
//int x = boo;  // nicht kompatibel

Eine Zuweisung an einen breiteren Datentyp ist ohne cast möglich, für die umgekehrte Richtung ist ein expliziter cast notwendig. Der Datentyp boolean ist nicht kompatibel zu anderen primitiven Datentypen.


Zuweisungskompatibilität von Referenzen

Für Referenzen gelten etwas andere Regeln. Hier entsteht Kompatibilität durch Vererbung.

String st = "Hallo";
Object ob = st;  // (upcast) implizit kompatibel = kein cast notwendig

CharSequence  cs = st;  // implizit kompatibel = kein cast notwendig
st = (String)cs ;  // (downcast) explizit kompatibel = cast notwendig

//StringBuilder sb = st;  // nicht kompatibel

Eine Zuweisung an einen Elterntyp (es gibt nur einen) ist ohne cast möglich, für die umgekehrte Richtung ist ein expliziter cast notwendig. Referenztypen, die nicht in direkter Vererbung stehen sind nicht kompatibel.


Quasikompatibilität durch Autoboxing

Zwischen primitiven und nichtprimitiven Datentypen gibt es keine Kompatibilität.

String s = "hi";
//int y = s; // nicht kompatibel
int y = 17;
//s = y; // nicht kompatibel

Durch Autoboxing bzw. Autounboxing entsteht jedoch etwas, was wir hier Quasikompatibilität nennen wollen.

// autoboxing
int e = 17;
Integer in = e;  // shortcut für:  in = new Integer(e);

// auto-unboxing
int f = in;  // shortcut für:  f = in.intValue();

Kovarianz, Kontravarianz und Invarianz

Wir haben also Grunddatentypen wie int, long , etc. (primitive Datentypen) und String, StringBuilder etc. (nichtprimitive Datentypen) und bei diesen die Zuweisungskompatibilität als Verwandtschaftsbeziehung.

Von diesen Datentypen leiten sich weitere Datentypen ab und sind somit abhängigge Datentypen. Dies sind zum einen Arrays als echte Datentypen, also z.Bsp. int[], long[], String[] usw. und zum anderen generische Datentypen wie List<T>, HashSet<T>, MyClass<T> usw.

Die Frage nach der Varianz ist nun die Frage, inwiefern sich evtl. Zuweisungskompatibiltäten der Grunddatentypen auf die abgeleiteten Datentypen übertragen. Man unterscheidet dabei drei Fälle.

VarianztypBedeutung
KovarianzDie Verwandtschaft der Grunddatentypen überträgt sich in der gleichen Richtung auf die abgeleiteten Typen
Kontravarianz    Die Verwandtschaft der Grunddatentypen überträgt sich in der umgekehrten Richtung auf die abgeleiteten Typen
InvarianzDie Verwandtschaft der Grunddatentypen überträgt sich in keiner Richtung auf die abgeleiteten Typen

Wie übertragen sich diese Kompatibilitäten auf Arrays und generische Typen? Diese Frage führt uns zu den verschiedenen Varianzbegriffen. Wir beginnen mit primitiven Arrays.


Invarianz bei Arrays primitiver Datentypen

Ein typisches Beispiel zeigt das Verhalten.

int[] iArr = new int[17];
//double[] dArr = iArr;   nicht kompatibel
double[] dArr = new double[17];
//int[] arr = dArr;  // nicht kompatibel

Die Verwandtschaft zwischen int und double überträgt sich also nicht auf Arrays. Dieses Verhalten heißt invariant.


Kovarianz und Kontravarianz bei Arrays von Referenzen

Arrays von Referenzen verhalten sich anders. Hier sind casts und upcasts möglich.

String[] sArr = { "foo", "bar"};
Object[] oArr = sArr;  // impliziter upcast = implizit kompatibel =  (implizit) kovariant

String[] stArr = (String[])obArr;  // expliziter downcast = explizit kompatibel = (explizit) kontravariant

Die "natürliche" Verwandtschaft von Referenzen entsteht durch Vererbung. Überträgt sich diese Verwandtschaft auf Arrays, so spricht man von Kovarianz, gibt es dagegen eine Verwandtschaft in umgekehrtewr Richtung, so nennt man das kontravariant. Arrays von Referenzen sind also implizit kovariant und explizit kontravariant, wenn die zugrunde liegenden Typen verwandt sind.


Probleme durch Kontravarianz bei Arrays von Referenzen

Bei einem expliziten downcast prüft der Compiler nicht, ob dieser zulässig ist. Dies geschieht erst zur Laufzeit durch die JVM. Diese Prüfungen sind ein relativ hoher Aufwand und kosten Performance. Eine Typverletzung hier führt dann zu einer ClassCastException. Hierzu das folgende kleine Beispiel:

Object[] obArr = { "foo", new StringBuilder("bar") };
String[] stArr = (String[])obArr;  // expliziter downcast

// JVM führt den downcast in einer Schleife durch
//for (int i=0; i < obArr.length; i++)
//{
//   stArr[i] = (String)obArr[i];
//}
// führt zu ClassCastException

Varianz bei generischen Typen

Hier unterschieden wir mehrere Fälle.


Generische Typen mit verschiedenem Grundtyp und gleichen Parametern sind kovariant

Wenn der Typparameter gleich bleibt überträgt sich die Verwandtschaft der Grundtypen.

List l = new ArrayList();
// und
List<String> sList = new ArrayList<>();  //

Dies gilt auch für Parameter mit ?-Platzhalter.


Generische Typen mit gleichem Grundtyp und verschiedenen Parametern sind invariant

Hierzu reicht ein Beispiel:

// ohne ? Platzhalter
List<Object> oList = new ArrayList<>();
List<String> sList = new ArrayList<>();
//oList = sList;  // nicht kompatibel
//sList = oList;  // nicht kompatibel

Generischen Typen mit <? extends X> sind implizit kovariant

Zur Erinnerung: <? extends X> macht die Collection readonly!

List<? extends Object> obList ;  // ? = shortcut für ? extends Object, ? extends bedeutet readonly
List<String> stList = new ArrayList<>();
obList = stList ;  // impliziter upcast möglich = implizit kompatibel = implizit kovariant
//stList = obList;  // expliziter downcast nicht möglich = NICHT explizit kompatibel = nicht kontravariant
List<? extends Number> stList2 = new ArrayList<>();
obList = stList2;

Hier überträgt sich also die Verwandtschaft durch Vererbung, es liegt Kovarianz vor.


Generischen Typen mit <? super X>
List<? super Integer> supIntList = new ArrayList<>();
List<Object> objList;
//objList = supList; // umpliziter upcast nicht möglich = NICHT implizit kompatibel = nicht kovariant
objList = (List<Object>)supIntList; // expliziter downcast = explizit kompatibel = explizit kontravariant

Hier ergibt sich eine Verwandtschaft umgekehrt zur Verwandtschaft durch Vererbung, es liegt also Kontravarianz vor.

Zur Erinnerung: Im Falle <? super X> besteht Schreibzugriff nur mit Basistyp X.

List<? super Integer> supIntList = new ArrayList<>();
int i = 17 ;
supIntList.add(i);   // autoboxing !
// aber
Double d = 71;
Number num = d;
// supList.add(num);  compiletime error