Java Generics vs. Java Listen
Beim Thema Generics in Verbindung mit Listen geht vieles drunter und drüber. Dabei ist die Sache gar nicht so schwer, wenn man das Prinzip einmal verstanden hat…
Zunächst einmal: Generics ermöglichen mittels formaler Typparamter, dass Klassen (und Funktionen), die bestimmte Dienste zur Verfügung stellen, mit mehreren Datentypen umgehen können.
Wenn wir zum Beispiel zwei beliebige Objekte als “Paar” verwalten wollen, dann benötigen wir eine entsprechende Klasse mit zwei Attributen, einer Möglichkeit die Objekte darin zu speichern und wieder auszulesen.
An sich nichts schweres:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Pair { Object obj1, obj2; // Das Einspeichern von Objekten ist der Einfachheit halber nur beim Erzeugen des Paares möglich... Pair(Object obj1, Object obj2) { this.obj1 = obj1; this.obj2 = obj2; } public Object getFirst() { return this.obj1; } public Object getSecond() { return this.obj2; } } |
Diese Klasse hat aber einen Haken. Beim Einspeichern gehen die Informationen über den Typ der eingespeicherten Objekte verloren. Wenn wir den Typ wiederherstellen wollen müssen wir einen expliziten Cast durchführen. Andernfalls erhalten wir immer nur Objects.
Pair paar = new Pair(new MeinApfel(), new MeinApfel()); MeinApfel apfel = (MeinApfel)paar.getFirst();
Bei Listen ist das Problem noch dramatischer. Ohne Generics muss man bei jeder Leseoperation auf einer Liste explizit casten:
Vector liste = new Vector(); for(int i=0; i<10; i++) liste.add(new MeinApfel()); // ... for(int i=0; i<liste.size(); i++) esseApfel((MeinApfel) liste.get(i));
Bei so vielen expliziten Casts riskiert man außerdem ClassCastExceptions, falls das referenzierte Objekt nicht zu dem entsprechenden Typ konvertiert werden kann.
Abhilfe schaffen hier die Generics. Das soll erstmal am Beispiel der obigen Pair-Klasse demonstriert werden, die folgendermaßen generisch gemacht werden kann:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class Pair<A,B> { A obj1; B obj2; Pair(A obj1, B obj2) { this.obj1 = obj1; this.obj2 = obj2; } public A getFirst() { return this.obj1; } public B getSecond() { return this.obj2; } } |
A und B sind sogenannte formale Typparameter, die in diesem Fall für beliebige Typen stehen.
Folgendermaßen erzeugen wir jetzt Paare von Integer-Objekten:
// anschaulich wird hier A = Integer und B = Integer gesetzt Pair<Integer, Integer> paar = new Pair<Integer, Integer>(new Integer(2), new Integer(3)); // kein Cast mehr nötig, weil auch die Signatur von getFirst() quasi dynamisch an Integer angepasst wurde Integer zahl = paar.getFirst();
Dasselbe ist natürlich auch bei Listen möglich, die bei der Einführung der Generics auch gleich generisch gemacht wurden. Bei Listen gibt es im Gegensatz zu dem Pair-Beispiel nur einen formalen Typparameter, der den Typ der gespeicherten Elemente festlegt.
// Eine Liste von Elementen des Typs "MeinApfel" anlegen... Vector<MeinApfel> liste = new Vector<MeinApfel>(); liste.add(new MeinApfel()); // ... // funktioniert problemlos, da die Signatur von get() genau wie getFirst() aus dem ersten Beispiel an den ggb. Typ angepasst wurde MeinApfel apfel = liste.get(0);
Richtig trickreich wird es, wenn Funktionen, die Listen entgegennehmen, generisch gemacht werden sollen. Angenommen wir wollen eine Funktion schreiben, die eine Liste von Nahrungsmitteln entgegennimmt und ihr Gewicht bestimmt.
In diesem Fall werden sogenannte Wildcards benötigt.
Intuitiv müsste die Funktion folgendermaßen aussehen:
public double wiege(List<Nahrungsmittel> liste) { // ... }
Diese Funktion würde aber entgegen unserer Intuition bspw. keine Liste mit Äpfeln annehmen, obwohl Äpfel in der Klassenhierarchie ganz klar ein Subtyp von Nahrungsmittel darstellen würden (public class Apfel extends Nahrungsmittel).
Es funktioniert deshalb nicht, weil List<Apfel> einfach nicht kompatibel ist zu List<Nahrungsmittel>. Beispiel:
1 2 3 | Vector<Nahrungsmittel> liste1 = new Vector<Nahrungsmittel>(); // OK! Vector<Apfel> liste2 = new Vector<Apfel>(); // OK! liste1 = liste2; // FEHLER: cannot convert from Vector<Apfel> to Vector<Nahrungsmittel> |
Dieser Code wird vom Compiler deshalb nicht akzeptiert, weil man in eine Liste von Äpfeln keine Nahrungsmittel einfügen kann. Wenn liste1 vom Typ List<Nahrungsmittel> auf liste2 vom Typ List<Apfel> verweisen würde, dann würde das bspw. die Signatur von liste1.add(…) verletzen, denn liste1.add(new Nahrungsmittel()) wäre nicht möglich, weil liste1 auf eine Liste von Äpfeln (und nur Äpfel!) zeigen würde. Das meine ich, wenn ich schreibe, dass List<Apfel> nicht kompatibel ist zu List<Nahrungsmittel>.
Das Problem löst man mit besagten Wildcards:
public double wiege(List<? extends Nahrungsmittel> liste) { // akzeptiert alle Listen, deren Elemente Nahrungsmittel oder Subtypen davon sind }
? steht für einen unbekannten Typ. Bekannt ist bei einer List<?> nur, dass – wie bei jeder typ-parametrisierten Liste – die Elemente, die in der Liste gespeichert sind, alle vom selben (unbekannten) Typ sind.
Das “extends Nahrungsmittel” schränkt die möglichen Typen hier aber insofern ein, als dass Nahrungsmittel die “obere Schranke” darstellt. ? darf hier also vom Typ Nahrungsmittel oder ein Subtyp davon sein.
Es gibt auch die Möglichkeit eine untere Schranke zu definieren: List<? super Nahrungsmittel> bzw. allgemein List<? super X>. Erlaubt sind dann Elemente vom Typ X oder Supertypen davon.
Bleiben wir zunächst bei unserer List<? extends Nahrungsmittel>. Wir können zumindest eingeschränkt mit einer solchen Liste arbeiten: Lesen, aber nicht Einfügen. Das Einfügen klappt deshalb nicht, weil der genaue Typ der Liste nicht bekannt ist. Zwar kann man einen Apfel zu einem Nahrungsmittel konvertieren. Aber die Liste enthält ja nicht notwendigerweise Nahrungsmittel. Sie kann genausogut Birnen enthalten – daher darf man in eine solche Liste keine neuen Objekte einfügen.
Im Folgenden eine Tabelle, die die erlaubten Operationen auf einer Liste dokumentiert:
| Typ | Lesen | Typ der gelesenen Objekte | Einfügen | Einfügbare Typen |
|---|---|---|---|---|
| List | Ja | Object | Ja | Beliebig |
| List<?> | Ja | Object | Nein | - |
| List<X> | Ja | X | Ja | X oder Subtypen von X |
| List<? extends X> | Ja | X | Nein | - |
| List<? super X> | Ja | Object | Ja | X oder Subtypen von X |
Bleibt nur noch die Frage zu klären, warum man in eine List<? super X> Elemente vom Typ X und Subtypen von X einfügen darf. Bei List<? extends X> ist das schließlich auch nicht möglich. Nunja, bei extends X sind die Elemente der Liste vom Typ X oder Subtypen davon, d.h. spezieller. Bei super X sind die Elemente vom Typ X oder Supertypen von X, d.h. allgemeiner. Daher kann man Objekte vom Typ X oder von einem Subtyp von X nach Belieben in eine solche Liste einfügen, weil man Objekte von einem bestimmten Typ immer zu einem allgemeineren Typ konvertieren kann.
Faustregel: Jeder Apfel ist ein Obst, aber nicht jedes Obst ein Apfel!

Danke erstmal für den doch informativen Artikel, studiere selber im Moment noch TI mussten aber selber noch nich mit Generics arbeiten,
mit Listen allerdings schon. Vielleicht kommt das ja noch dann bin ich gewappnet
mfg!
Kommentar by Flo — 18.06.2009 @ 10:06:08