Gründe für Invarianz
Wie im letzten Beispiel des letzten Kapitels festgestellt, scheint folgende Zuweisung nicht erlaubt zu sein:
In Bezug auf den Typparameter verhält sich die Zuweisungskompatibilität bei den Generics in Java also invariant. Hierfür gibt es jedoch auch einen guten Grund. Einmal angenommen, die obige Zuweisung wäre doch erlaubt. Dann wäre folgender Code ebenfalls erlaubt:
In Zeile 3 kann nun ohne Probleme irgendein Zahlenobjekt in numList eingefügt werden, da numList selbst ja über den Typ
Number
parametrisiert zu sein scheint und die
add()
-Methode daher jeden Parameter akzeptiert, der zuweisungskompatibel zu
Number
ist. Da numList ja aber nur auf das
Vector<Integer>
-Objekt referenziert, wäre in diesem so ein
Double
-Objekt angekommen, obwohl dies natürlich nicht erwünscht war.
Der Grund für die Invarianz liegt also hier in der Veränderlichkeit der Containerobjekte.
Wildcards
Zunächst einmal kann das Zuweisungsproblem durch Benutzung der sogenannten Wildcards umgangen werden. Hierbei gibt man explizit an, dass man nicht genau weiß, welcher Typ als Typparameter verwendet wurde:
Die logische Folge ist, dass nun keine Objekte mehr über die neue Referenz eingefügt werden können. Da für den Compiler nicht ersichtlich ist, mit welchem Typ
list
parametrisiert wurde, kann er für die
add()
-Methode nicht den Typ des nötigen Parameters bestimmen. Das einzige, was er zulassen kann, ist der Wert
null
, da dieser quasi Subtyp aller Klassen ist: Egal, mit welchem Typ das Objekt, auf das
list
referenziert, parametrisiert wurde,
null
kann immer eingefügt werden.
Ähnlich verhält es sich beim Auslesen von Objekten per
get()
-Methode:
Der Compiler kann nichts über die konkreten Objekte, die von
get()
geliefert werden, garantieren, außer dass sie zuweisungskompatibel zu
Object
sind (dies gilt für jedes konkrete Objekt).
Kovarianz und Kontravarianz
Die durch die Verwendung von Wildcards gewonnene Flexibilität ist natürlich nicht sehr befriedigend. Daher kann man die Wildcards im Klassenbaum nach unten (Kovarianz) oder nach oben (Kontravarianz) einschränken. Dies geschieht mit den Schlüsselwörtern
extends
bzw.
super
:
In Zeile 3 wird die Variable
list
definiert und für den Typparameter festgelegt, dass dieser
Number
oder aber eine Unterklasse von
Number
sein muss. Diese Bedingung erfüllt das Objekt, das von
intList
referenziert wird, so dass die Zuweisung hier erlaubt ist.
Nun ist natürlich über den aktuellen Typparameter wieder nicht der konkrete Typ bekannt, sondern nur, dass er zuweisungskompatibel zu
Number
ist. Daher liefert die
get()
-Methode nun auch nur eine
Number
-Referenz (Zeile 6).
Hinzufügen kann man nun wieder nichts außer
null
. Dies mag überraschen, ist aber auch leicht einsichtig: Im "schlimmsten" Fall ist der aktuelle Parameter eben nicht
Number
, sondern ein speziellerer Typ, z. B.
Integer
oder
Double
. Da dies alles nicht zugesichert werden kann, kann wieder nur der Wert eingefügt werden, der zuweisungskompatibel zu allen Typen ist.
Das Schlüsselwort
super
hat umgekehrte Auswirkungen:
In Zeile 1 wird dieses Mal eine Liste erzeugt, die mit
Number
parametrisiert ist und somit neben
Integer
-Objekten auch
Double
-,
Long
- und andere Objekte akzeptiert. Beispielhaft wird in Zeile 2 ein
Integer
-Objekt eingefügt.
In Zeile 3 wird nun eine Variable definiert, die auf eine Liste referenziert, die mit dem Typ
Integer
oder einer Oberklasse davon (also bis einschließlich
Object
) parametrisiert wurde. Diese Bedingung erfüllt das Objekt, das von
numList
referenziert wird, so dass die Zuweisung erlaubt ist.
Nun kann beim Auslesen von Objekten über deren konkreten Typ natürlich nichts zugesichert werden, außer dass sie zuweisungskompatibel zu
Object
sind (dies wäre ja der "schlimmste" Parameter, den man annehmen könnte). Daher liefert die
get()
-Methode auch nur eine
Object
-Referenz (Zeile 7).
Beim Hinzufügen von Elementen verhält es sich umgekehrt: Es werden nur noch
Integer
-Referenzen akzeptiert (theoretisch auch Unterklassen von
Integer
, also alles, was zuweisungskompatibel zum Typ
Integer
ist), da dies wiederum der speziellste Typ ist, der hätte verwendet werden können. Ließe man allgemeinere Typen zu, bestünde die Möglichkeit, dass ja doch
Integer
als Typparameter verwendet wurde, und somit das Typsystem umgangen wäre.
Allgemein mag die Einschränkung eines Parameters nach oben mit dem Schlüsselwort
super
verwirren. Anwendungsbeispiele sind hier viel seltener als beim Schlüsselwort
extends
. Im Ausblick im letzten Kapitel wird dies noch einmal kurz angeschnitten.
Mit diesen Erweiterungen ist es nun möglich, das Problem vom Ende des letzten Kapitels zu lösen. Die Bibliotheksfunktion müßte jetzt wie folgt aussehen:
Die kovariante bzw. kontravariante Einschränkung kann man auch direkt für den Typparameter einer Klasse vornehmen. Somit schränkt man bereits den Nutzungsbereich dieser Klasse von vornherein ein. Es ist z. B. denkbar, um das Summationsbeispiel logisch zu erweitern, eine Klasse zu schreiben, die nur für Zahlentypen zugelassen ist und die sich "selbst" summieren kann. Parametrisieren könnte man diese Klasse dann mit
Number
, um alle Zahlentypen gleichzeitig zuzulassen, oder auch mit einem spezielleren Zahlentyp. Für den Typparameter heißt dies, dass er immer mindestens
Number
oder eine Unterklasse sein muss. Die Klasse könnte wie folgt aussehen:
In Zeile 1 wird für den Typparameter E festgelegt, dass er mindestens
Number
oder eine Unterklasse sein muss. Daher kann in der Summationsfunktion in Zeile 28 vorausgesetzt werden, dass die Objektvariable
value
auf jeden Fall die Methode
doubleValue()
implementiert, und diese direkt aufgerufen werden. Die Summation selbst erfolgt rekursiv.
Es ist auch denkbar, dass man einen Typparameter mehrfach einschränken möchte. So könnte die "Nummernliste" dahingehend erweitert werden, dass sie ihr maximales Element bestimmen kann. Hierfür kann man das Interface
Comparable
nutzen, dass von den konkreten Zahlenklassen wie
Integer
usw. implementiert wird, jedoch nicht von der abstrakten Basisklasse
Number
.
Comparable
selbst ist ebenfalls ein generisches Interface. Es kann nur Objekte gleichen Typs vergleichen:
So implementiert die Klasse
Integer
das Interface
Comparable<Integer>
, kann also nur gegen andere
Integer
-Objekte verglichen werden. Allgemein heißt dies, dass der Typ E, über den die Liste parametrisiert wird, einmal den Typen
Number
"implementieren" und einmal die Schnittstelle
Comparable<E>
zur Verfügung stellen muss. Diese Mehrfachvererbung spezifiziert man mit einem &:
Durch die Einschränkung kann nun in Zeile 32 vorausgesetzt werden, dass die Objektvariable
value
die Methode
compareTo()
zur Verfügung stellt.
Es ist leicht ersichtlich, dass
Number
nun nicht mehr die Bedingungen für den Typparameter erfüllt; mit diese Klasse können also nur noch "homogene" Listen von
Integer
-,
Long
- oder anderen konkreten Zahlenklassen gebildet werden.
Generische Arrays
Arrays selbst haben in Java bereits einige Eigenschaften von Generizität. Man kann es sich vorstellen, als gäbe es eine allgemeine Klasse
Array
, die mit dem jeweils verwendeten Typen des Arrays parametrisiert wird.
Integer[]
wäre also in diesem Gedankenspiel eine andere Schreibweise für
Array<Integer>
.
Bei Arrays wurde bei der Java-Entwicklung jedoch nicht auf Typsicherheit geachtet. So ist z. B.
Integer[]
zuweisungskompatibel zu
Object[]
. Über diesen Weg kann man auch andere Objekte als
Integer
-Objekte in diesem Array ablegen, was jedoch zur Laufzeit zu einer
ArrayStoreException
führt. Schon aus diesem Grunde gibt es einige Reibungspunkte zwischen Arrays und Generics in Java.
Zunächst einmal werden jedoch einige Beispiele für den Einsatz von Arrays bei Generics gezeigt:
Es handelt sich hier um eine generische Klasse X, die mit einem Typparameter T parametrisiert ist. In Zeile 2 wird ein Array von Objekten dieses Typparameters definiert.
In Zeile 4 wird ein Array von X-Objekten definiert, die mit dem Typparameter T parametrisiert wurden. Zeile 6 definiert einen Array von X-Objekten, deren Typparameter
Number
oder eine Unterklasse ist.
In Zeile 8 zeigt sich ein weiteres Einsatzgebiet: Hier wird für den Parameter der Methode
makeSomething
festgelegt, dass er ein X-Objekt sein soll, das als Typparameter einen Array von Number-Objekten oder etwas zuweisungskompatibles (also z. B. auch einen Array von Integer-Objekten) verwenden soll. Man sieht also, dass man als Typparameter auch Array-Typen verwenden kann, da sich diese ja in Java wie ein normaler Typ verwenden lassen.
Bei der Verwendung von Arrays gelten jedoch starke Einschränkungen. So dürfen Arrays von generischen Typen oder Typparametern nicht erzeugt werden:
Dies hat nicht nur mit den Unzulänglichkeiten der Java-Arrays zu tun, sondern auch mit der internen Funktionsweise der Generics. Diese wird im nächsten Kapitel eingehend erläutert.