Generics in Java 1.5

 


... [ Seminar BS, WWW und Java ] ... [ Thema Generics in Java ] ... [ Interne Funktionsweise ] ...

Detaillierte Betrachtung


Gründe für Invarianz

Wie im letzten Beispiel des letzten Kapitels festgestellt, scheint folgende Zuweisung nicht erlaubt zu sein:
1: LinkedList<Integer> intList = new LinkedList<Integer>();
2: LinkedList<Number> numList = intList; // error
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:
1: LinkedList<Integer> intList = new LinkedList<Integer>();
2: LinkedList<Number> numList = intList; // angenommen erlaubt
3: numList.add(new Double(3.14)); // 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:
1: List<Integer> intList = new Vector<Integer>();
2: List<?> list = intList;
3: list.add(new Double(3.14)); // nicht erlaubt
4: list.add(new Integer(1)); // nicht erlaubt
5: list.add(null); // erlaubt
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:
1: List<Integer> intList = new Vector<Integer>();
2: intList.add(new Integer(42));
3: LinkedList<?> list = intList;
4: Integer i = list.get(0); // nicht erlaubt
5: Object o = list.get(0); // erlaubt
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:
1: List<Integer> intList = new Vector<Integer>();
2: intList.add(new Integer(42));
3: List<? extends Number> list = intList;
4:
5: Integer i = list.get(0); // nicht erlaubt
6: Number n = list.get(0); // erlaubt
7: list.add(n); // nicht erlaubt
8: list.add(i); // nicht erlaubt
9: list.add(null); // erlaubt
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:
1: List<Number> numList = new Vector<Number>();
2: numList.add(new Integer(42));
3: List<? super Integer> list = numList;
4:
5: Integer i = list.get(0); // nicht erlaubt
6: Number num = list.get(0); // nicht erlaubt
7: Object obj = list.get(0); // erlaubt
8:
9: list.add(obj); // nicht erlaubt
10: list.add(num); // nicht erlaubt
11: list.add(i); // erlaubt
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:
1: public double sum(List<? extends Number> numList) {
2: double result = 0;
3: for(int i=0;i<numList.size();i++)
4: result += numList.get(i).doubleValue();
5: return result;
6: }


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:
1: public class NumberList<E extends Number> {
2:
3: private E value;
4: private NumberList<E> next;
5:
6: public NumberList() {
7: }
8:
9: public boolean isEmpty() {
10: return value == null;
11: }
12:
13: public void add(E number) {
14: // letztes Element?
15: if(isEmpty()) {
16: value = number;
17:
18: // neues leeres Element anhängen
19: next = new NumberList<E>();
20: }
21: else
22: next.add(number);
23: }
24:
25: public double sum() {
26: if(isEmpty())
27: return 0;
28: return value.doubleValue() + next.sum();
29: }
30: }
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:
1: public interface Comparable<T> {
2:
3: int compareTo(T o);
4: }
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 &:
1: public class NumberList<E extends Number & Comparable<E>> {
2:
3: private E value;
4: private NumberList<E> next;
5:
6: public NumberList() {
7: }
8:
9: public boolean isEmpty() {
10: return value == null;
11: }
12:
13: public void add(E number) {
14: if(isEmpty()) {
15: value = number;
16:
17: next = new NumberList<E>();
18: }
19: else
20: next.add(number);
21: }
22:
23: public E max() {
24: if(isEmpty())
25: return null;
26:
27: E nextmax = next.max();
28:
29: if(nextmax == null)
30: return value;
31:
32: return value.compareTo(nextmax) < 0 ? nextmax : value;
33: }
34: }
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:
1: class X<T> {
2: private T[] elements;
3:
4: private X<T>[] children;
5:
6: private X<? extends Number>[] names;
7:
8: private void makeSomething(X<? extends Number[]>) { }
9: }
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:
1: class X {
2: T[] tArray = new T[4]; // illegal: generic array creation
3:
4: X<T>[] xArray = new X<T>[4]; // illegal: generic array creation
5: }
6:
7:
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.

... [ Seminar BS, WWW und Java ] ... [ Thema Generics in Java ] ... [ Detaillierte Betrachtung ] ... [ Interne Funktionsweise ] ...

Valid XHTML 1.0 Strict