|
|||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||
|
Java Generics - Generic Creation
|
||||||||||||||||||||||||||||
In der letzten Ausgabe dieser Kolumne haben wir uns angesehen, welche
Überraschungen sich bei der Benutzung von generischen Typen durch
die Type Erasure Technik ergeben können. Diesmal wollen wir uns ein
Problem ansehen, dass sich häufig ergibt, wenn man eigene generische
Typen bzw. Methoden implementieren möchte. Auch dieses Problem wird
durch die Type Erasure verursacht. Es geht um das Erzeugen von Objekten
und Arrays, in deren Typ ein Typparameter vorkommt, also um Objekte vom
Typ T und Arrays vom Typ T[], wobei T ein Typparameter ist.
Wiederholung Type ErasureFangen wir noch einmal mit einer kurzen Wiederholung der Type Erasure an. Als Type Erasure wird die Technik bezeichnet, mit der generische Typen in Java übersetzt werden. Die Regeln für die Übersetzung sind dabei:
Das ProblemBei der Implementierung eines generischen Typs oder einer generischen Methode kann es vorkommen kann, dass man ein neues Objekt vom Typ des Typparameters erzeugen möchte. Hier ein Beispiel dazu:class Utilities {Eigentlich sieht der Beispielcode ganz gut aus. Trotzdem lässt sich die generische Methode createBuffer() nicht übersetzen. Die Fehlermeldung ist wenig hilfreich: „Cannot create a generic array of T.“. Um sicher zu gehen, dass es nicht daran liegt, dass wir ein Array erzeugen wollen, sondern dass der Typparameter hier die entscheidende Rolle spielt, versuchen wir es mit einer zweiten generischen Methode createObject() in der Klasse Utilities: public static <T> T createObject() {Auch diese sehr reduzierte Methode lässt sich nicht übersetzen. Die Fehlermeldung ist wieder recht knapp: „Cannot instantiate the type T.“. Ganz offensichtlich geht das so nicht. Es scheint am Typparameter T zu liegen, aber die Fehlermeldung ist nicht so aussagekräftig, dass man den eigentlichen Grund versteht. Der ist aber vielleicht entscheidend, deshalb kommen wir noch mal zur Type Erasure Technik. Wenn man createObject() übersetzen könnte, so entspräche der erzeugte Bytecode in etwa dem folgenden Javacode: public static Object createObject() {
Ein weiterer Grund für die Weigerung des Compilers, Objekte oder Arrays von unbekanntem Typ zu erzeugen, liegt darin, dass Ausdrücke wie new T oder new T[SIZE] unterstellen, dass der unbekannte Typ T eine Klasse mit einem Default-Konstruktor ohne Argumente ist. Das ist aber keineswegs gewiß; für den Typparameter T könnte später ein Interface-Typ eingesetzt werden oder eine Klasse, die keinen Default-Konstruktor hat. Der Compiler weiss also gar nicht, ob new T oder new T[SIZE] korrekte Ausdrücke sind und weist diese Konstrukte deshalb mit einer Fehlermeldung ab. Verschiedene LösungenWenn man eigene generische Typen oder Methoden implementieren will, stellt man aber fest, dass man relativ häufig in eine Situation kommt, in der man Objekte oder Arrays vom Typ des Typparameters erzeugen möchte. Selbst wenn man sich dann noch an die Diskussion oben erinnert und versteht, warum das nicht möglich ist, hilft es einem bei dem Problem erst einmal nicht weiter. Leider gibt es auch nicht das Standardidiom, das man in solchen Situationen immer zur Lösung einsetzten kann. Der Ansatz ist vielmehr, das Problem geschickt zu umgehen. Und dazu gibt es verschiedene Techniken, mit spezifischen Vor- und Nachteilen, die wir uns im Folgenden ansehen wollen.Das Beispiel, das wir dazu diskutieren wollen, ist folgendes: public class CyclicBuffer<T> {Die Klasse CyclicBuffer implementiert einen Ringpuffer. Dazu hat sie ein ein privates Feld myBuffer, in dem sie Objekte des Typparameters T speichert. Die Initialisierung dieses Feldes ist das Problem. Sie lässt sich so wie oben implementiert nicht übersetzen. Es hilft auch nichts, die Zuweisung an myBuffer in einen expliziten Konstruktor zu schreiben. Es wird vielmehr darum gehen, den Ausdruck new T[1024] zu vermeiden. Die Funktionalität von CyclicBuffer wird durch zwei Methoden bereitgestellt:
1. Lösungsansatz: ArrayList<T> statt T[]Immer wenn es darum geht, ein Array vom Typparameter erzeugen zu wollen, kann man als Alternative zum Array die java.util.ArrayList<T> erwägen. Das geänderte Beispiel sieht so aus:public class CyclicBuffer<T> {Diese Implementierung lässt sich problemlos übersetzen. Der Nachteil ist natürlich, dass die Performance etwas schlechter wird, weil put() und get() jetzt ArrayList<T>.add() und ArrayList<T>.remove() nutzen müssen, statt direkt auf das Array zugreifen zu können. Um den Overhead abzuschätzen, kann man sich die Implementierung dieser Methoden im JDK Source Code ansehen und dann im jeweiligen Fall entscheiden, ob man damit leben will oder einem der anderen Ansätze den Vorzug gibt. 2. Lösungsansatz: Expliziter Parameter (Objekt)Ein alternativer Ansatz besteht darin, das Array vom Typparamter erst gar nicht innerhalb des generischen Typs erzeugen zu wollen, sondern es zu einem Konstruktorparameter zu machen. Für unser Beispiel bedeutet das folgende Implementierung:public class CyclicBuffer<T> {Der Nachteil ist hier, dass der Erzeuger des CyclicBuffers relativ viel Einfluss auf das erzeugte Objekt hat. So legt er die Länge des internen Puffers fest. Und er kann - wenn auch unabsichtlich - direkt auf den internen Puffer zugreifen, wenn er sich die Referenz auf den Konstruktorparameter speichert. Dieser Lösungsansatz wird sich auch nicht immer verallgemeinern lassen. So mag es zum Beispiel unintuitiv sein, wenn eine Methode, die ein temporäres Ergebnis erzeugt und im Returnwert zurückgibt, das eigentliche Rückgabeobjekt als Parameter übergeben bekommt. Aber auch dafür gibt es Beispiele im JDK, wie wir weiter unten sehen werden. Einen entscheidenden Vorteil hat diese Lösung aber gegenüber der ersten: sie kann auch angewandt werden, wenn ein Objekt und kein Array vom Typ des Typparameters erzeugt werden soll. 3. Lösungsansatz: Expliziter Parameter (Klasse)Erinnern wir uns noch mal an die eigentlich Ursache des Problems: nach der Type Erasure bleibt von der Typinformation des Typparameter nur noch der erste Boundstyp (falls es einen gibt) oder Object übrig. Das heißt, wenn man zusätzlich in einem Konstruktorparameter den wirklichen Typ des Typparameters übergibt, kann das Array (oder im verallgemeinerten Fall auch das Objekt) über Reflection erzeugt werden und so das Problem gelöst werden.Übertragen auf unser Beispiel sieht dieser Lösungsansatz dann so aus: public class CyclicBuffer<T> {Der Nachteil dieses Ansatzes ist, dass man den Typ des Typparameters bei der Konstruktion eines CylicBuffers redundant angeben muss, einmal als Typparameter und einmal als Konstruktorparameter: CyclicBuffer<String> myCb = new CyclicBuffer<String>(java.lang.String.class);Das ist zwar unschön, aber immerhin prüft schon der Compiler, ob die Typen gleich sind. Bei der folgenden Zeile: CyclicBuffer<String> myCb = new CyclicBuffer<String>(java.lang.Integer.class);erhält man die Fehlermeldung: „The constructor CyclicBuffer<String>(Class<Interger>) is undefined“. Da macht sich doch die frühe Typprüfung parametrisierter Typen mal bezahlt. Das übergebene Class-Objekt bezeichnet man übrigens als Type Tag. AnmerkungenWir haben die Lösungsansätze oben immer am Beispiel einer generischen Klasse diskutiert. Wie man die Lösungsansätze bei generischen Methoden verwendet, dürfte bei Lösungsansatz 1 offensichtlich sein und bei 2 und 3 müssen das Objekt bzw. die Klasse Parameter der generischen Methode sein. Die Methode toArray(), deren Implementierung wir weiter unten diskutieren, ist ein Beispiel dafür.Wenn man sich den CyclicBuffer<T> noch einmal genauer ansieht, kann man feststellen, dass man nicht unbedingt einen Puffer vom Typ T[] braucht. Mit der entsprechenden Implementierung von get() und put() funktioniert das Ganze auch mit einem Buffer vom Typ Object[]: public class CyclicBuffer<T> {Die Details der Konsistenzprüfung von Lese- und Schreibzeiger (readPtr, writePtr) haben wir bei der Implementierung wieder weggelassen. Entscheidend für unsere Lösung ist der Cast in Zeile 14. Er führt dazu, dass die get() Methode immer ein Objekt vom Typ T zurückgibt. Dass der Cast immer funktioniert, ist gesichert, weil in der put() Methode nur Objekte vom Typ T im myBuffer abgelegt werden können. Weil der Compiler aber die Semantik unserer Klasse nicht versteht, meckert er trotzdem über den Cast, weil er grundsätzlich immer eine "unchecked cast"-Warnung ausgibt, wenn der Zieltyp eines Casts ein Typparameter ist. Die Warnung könnten wir aber getrost mit einem @SuppressWarnings("unchecked") Annotation unterdrücken. Damit ist die Verwendung eines Object[] anstelle eines T[] ein weiterer Lösungsansatz für unser Beispiel, der aber leider nicht so universell anwendbar ist. Deshalb haben wir ihn auch nicht oben schon diskutiert. Wie schnell diese Lösung an ihre Grenzen stößt, kann man sehen, wenn CyclicBuffer<T> eine Methode dumpBuffer() erhalten soll, bei der man den internen Puffer als T[] zurück gibt: public class CyclicBuffer<T> { Und im JDK ?Im Verlauf dieses Artikels haben wir verschiedene Techniken dafür diskutiert, wie man es umgehen kann, in einem generischen Typ oder in einer generischen Methode ein Objekt oder ein Array vom Typparameter zu erzeugen. In der Praxis können aber auch Varianten und Kombinationen dieser Techniken eine noch treffendere Lösung liefern.Die Methode java.util.AbstractCollection<E>.toArray() aus dem JDK ist ein gutes Beispiel für die Kombination der diskutierten Lösungsansätze. Die Methode gibt es in zwei überladenen Varianten, die an alle Collections (aus dem mit JDK 1.2 eingeführten Collection Framework) weitervererbt werden: public abstract class AbstractCollection<E> implements Collection<E> {Die Idee bei beiden Methoden ist, dass sie einen Dump der Collection als Array erzeugen. Die erste Variante, die ein Objetct[] zurückgibt, ist für unsere Diskussion weniger interessant. Aber die zweite wollen wir uns genauer ansehen. Auffallend ist, dass es sich nicht um eine einfache Methode handelt, sondern um eine generische Methode mit Typparameter T eingebettet in die generische Klasse AbstractCollection mit Typparameter E. Damit ist es nicht nur möglich, den Inhalt der Collection in ein Array vom Elementtyp der Collection zu dumpen, sondern auch in ein Array, dessen Elementtyp ein Supertyp des Elementtyps der Collection ist. Schauen wir uns dazu ein Beispiel an: LinkedList<Integer> lli = new LinkedList<Integer>();Wir haben eine LinkedList<Integer>, deren Elemente wir in der Zeile 3 in ein Integer[] dumpen. Da die zweite Variante der toArray() Methode aber eine generische Methode ist, kann ich den Inhalt mit dieser Methode auch in ein Number[] bzw. ein Object[] dumpen (Zeile 4 bzw. 5). Den Dump in das Object[] hätten wir aber auch mit der ersten, nicht-generischen Variante bekommen (Zeile 6).
Wie man sehen kann, basiert die Lösung hier auf einer Kombination des Lösungsansatzes 2 mit einer Variante des Lösungsansatzes 3. Wenn das als Parameter übergebene Array groß genug ist, wird der Inhalt der Collection in das Array kopiert und dieses wieder zurückgegeben (Lösungsansatz 2). Wenn das als Parameter übergebene Array nicht groß genug ist, wird über Reflection ein neues konstruiert, wobei der Elementtyp derselbe ist, wie der des übergebenen Arrays. Dies ist eine Variante des Lösungsansatzes 3, denn der Typ des Arrays wird nicht direkt als Parameter übergeben, sondern erst an Hand des übergebenen Array-Objekts ermittelt. Dieses Bespiel aus dem JDK zeigt sehr schön, wie die oben diskutierten Lösungsansätze in der Praxis verschmelzen können. ZusammenfassungWer selbst generische Typen oder generische Methoden implementiert, wird schnell mit dem Problem konfrontiert, ein Objekt oder ein Array des Typparameters erzeugen zu wollen. Beim ersten Mal will man häufig gar nicht glauben, dass dies nicht möglich ist. In diesem Artikel haben wir verschiedene mögliche Lösungen dieses Problems sowie ihre Vor- und Nachteile diskutiert. Wir hoffen, dass diese Lösungen Ihnen als Hilfe bei der Implementierung eigener generischer Typen / generischer Methoden dienen können.Literaturverweise und weitere Informationsquellen
Die gesamte Serie über Java Generics:
|
|||||||||||||||||||||||||||||
© Copyright 1995-2012 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/34.GenericCreation/34.GenericCreation.html> last update: 4 Nov 2012 |