Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
NEWSLETTER 
CONTACT 
Java Generics - Generic Creation

Java Generics - Generic Creation
Java Generics: Generic Creation
Das Erzeugen von Objekten und Arrays eines unbekannten Typs
(Die Verwendung von Typparametern in new-Expressions)

JavaSPEKTRUM, September 2007
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

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 Erasure

Fangen 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:
  • Aus einem generischen Typ (z.B. interface Callable<V>) wird ein nicht-generischer Typ, indem die Typparameter entfernt und durch den ersten Typ in ihrer Bounds-Klausel ersetzt werden. Wenn es keine Bounds-Klausel gibt, wird der Typparameter durch Object ersetzt.
  • Aus einem parametrisierten Typ (z.B. Callable<Long>) wird ein regulärer Typ, indem die Typargumente entfernt werden.
Auch wenn wir das in den vorangegangenen Artikeln nicht explizit erwähnt haben: derselbe Ansatz wird natürlich auch für die Übersetzung generischer Methoden verwendet. Wer unsere vorhergehenden Artikel über Type Erasure (siehe / GEN3 / und / GEN4 /) verpasst hat und detailliertere Information zur Type Erasure sucht, kann auch in unser FAQ zu Java Generics schauen (siehe / ERASURE /).

Das Problem

Bei 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 {
  private static final int SIZE = 1024;
  public static <T> T[] createBuffer() {
    return new T[SIZE] ; // Fehler beim Kompilieren
  }
}
public static void main(String[] args) {
  String[] buffer = Utilities. <String> createBuffer();
}
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() {
  return new T(); // Fehler beim Kompilieren
}
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() {
  return new Object();
}
Aus der generischen Methode wird eine nicht-generische Methode, indem der Typparameter T entfernt und durch Object ersetzt wird, da es keine Bounds-Klausel für T gibt. Bei den beiden folgenden Aufrufen 1) :
String  s = Utilities. <String> createObject ();
Integer i = Utilities. <Integer> createObject () ;
Würde also immer ein neues Objekt vom Typ Object erzeugt und zurückgegeben.  Die Zuweisung des Returnwerts zu s bzw. i würde zur Laufzeit mit einer ClassCastException scheitern. Damit dieses Problem nicht erst zur Laufzeit auftritt, lässt der Compiler die Übersetzung von createObject() erst gar nicht zu. Die gleiche Argumentation lässt sich auch auf dem Fall new T[] anwenden, womit klar wird, warum sich createBuffer() nicht übersetzten lässt.
1) Eine Anmerkung zur Syntax: ein Methodenaufruf wie Utilities.<String>createObject() benutzt die sogenannte explizite Typargumentspezifikation.  Normalerweise können generische Methoden aufgerufen werden, ohne dass man angibt, durch welchen Typ der Typparameter T ersetzt werden soll.  Der Compiler überlegt sich das selber, indem er sich den Typ des übergebenen Arguments ansieht.  Die Methode createObject() hat aber kein Argument.  Also kann sich der Compiler nichts überlegen und man muss ihm sagen, dass er den Typparameter T durch den Typ String ersetzen soll.  Das ist jetzt ein wenig vereinfacht; der Compiler zieht in bestimmten Situation auch Schlüsse aus der Verwendung des Returnwerts einer Methode.  Wer sich für die Details interessiert kann diese im Java Generics FAQ nachlesen (siehe / EXCPL /).

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ösungen

Wenn 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> {
  private T[] myBuffer = new T[1024]; // Fehler beim Compilieren
   ...

  public void put(T t) { ... }
  public T get() { ... }

}

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:

  • put() tut ein Objekt vom Typ T in den Ringpuffer und
  • get() holt eines heraus.
Die konkrete Implementierung der Methoden sowie weitere Felder, die man für die Implementierung braucht (z.B. Schreib- und Lesezeiger), haben wir zur Vereinfachung weggelassen. Sie tragen nichts zu dem oben beschriebenen Problem bei, für das wir nun Lösungsalternativen diskutieren wollen.
 

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> {
  private ArrayList<T> myBuffer = new ArrayList<T>(1024);
   ...
}
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> {
  private T[] myBuffer;
  private int length;
   ...

  public CyclicBuffer (T[] buf) {
    myBuffer = buf;
    length = buf.length;
  }
   ...
}

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> {
  private T[] myBuffer;
   ...

  public CyclicBuffer (Class<T> bufType) {
    myBuffer = (T[]) Array.newInstance(bufType, 1024);
  }
   ...
}

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.

Anmerkungen

Wir 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> {
  private Object[] myBuffer = new Object[1024];
  private int readPtr;
  private int writePtr;

  public void put(T t) {
     ...
    myBuffer[writePtr] = t ;
     ...
  }

  public T get() {
     ...
    return ((T) myBuffer[readPtr]); // Zeile 14
  }

}

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> {
  private Object[] myBuffer = new Object[1024];

  public void put(T t) { ... }
  public T get() { ... }
  public T[] dumpBuffer { return myBuffer; } // Compiler Fehler: Type mismatch ...
}

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> {
   …
  public Object[] toArray() { … }
  public <T> T[] toArray(T[] a) { … }
}
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>();
// … lli füllen
Integer[] ia = lli.toArray(new Integer[0]);         // Zeile 3
Number[]  na = lli.toArray(new Number[lli.size()]); // Zeile 4
Object[] oa1 = lli.toArray(new Object[1]);          // Zeile 5
Object[] oa2 = lli.toArray();                       // Zeile 6
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).
 
Das waren die guten Nachrichten zu dem Thema; die schlechte ist, dass sich auch die folgende Zeile übersetzten lässt:
String[] sa = lli.toArray(new String[0]);
Da der Typparameter E des generischen Typs unabhängig vom Typparamter T der generischen Methode ist, kann der Compiler in dieser Zeile keinen Fehler finden. Sie entspricht der Definition von:
T[] java.util.AbstractCollection<E>.<T> toArray(T[] a)
Erst zur Laufzeit wird es hier zum Fehler kommen, wenn ein Integer in ein Element eines String[] geschrieben werden soll. 2)

Kommen wir auf unser ursprüngliches Problem zurück. Da die zweite Variante von toArray() eine generische Methode mit Typparameter T ist, kann das Array, das zurückgegeben wird, nicht einfach mit new T[] innerhalb der Methode erzeugt werden. Der Aspekt, dass die generische Methode einen Parameter braucht, den die nicht-generische nicht benötigt, können wir als Indiz nehmen, dass dort einer oder mehrere der oben diskutieren Lösungsansätze zum Zuge kommt. Schauen wir uns also gleich die Implementierung an:

public <T> T[] toArray(T[] a) {
  int size = size();
  if (a.length < size)
    a = (T[]) Array .newInstance(a.getClass().getComponentType(), size);

    Iterator<E> it=iterator();
  for (int i=0; i<size; i++) a[i] = it.next();

  if (a.length > size) a[size] = null;
  return a;

2) Nun kann man sich fragen, warum der Typparameter T der generischen toArray()-Methode völlig unabhängig vom Typparameter E der Collection ist.  Naheliegend wäre eine Bounds-Klausel für den Typparameter T, der nur Supertypen von E als Typargumente zulässt.  Dann wäre die Übergabe eines String[] an die toArray()-Methode einer LinkedList<Integer> gar nicht möglich.  Dafür bräuchte man eine super-Bounds-Klausel, die so aussähe:
T[] java.util.AbstractCollection<E>.<T super E> toArray(T[] a)  // nicht zulässig
Der Haken ist nur: super-Bounds gibt es zwar für Wildcards, aber nicht für Typparameter.  Die obige Deklaration ist daher in Java gar nicht möglich.
Außerdem wäre eine solche Deklation zwar sinnvoll, aber nicht kompatibel zur alten nicht-generischen toArray()-Methode aus Java 1.4.  Denn früher war jede Collection inherent eine Collection<Object> und man konnte jeden beliebigen Typ von Array an die toArray()-Methode übergeben.  Das wäre nicht mehr möglich, wenn der Typparameter T der generischen toArray()-Methode nur Supertypen von E zuliesse.  Und es wäre auch im JDK 5.0 durchaus denkbar, dass man ein Long[] an die toArray()-Methode einer LinkedList<Number> oder einer LinkedList<Number> übergeben möchte, weil man weiss, dass alle Elemente in der Liste vom Typ Long sind.

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.

Zusammenfassung

Wer 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

/FAQ/ Java Generics FAQ
Angelika Langer
URL: http://www.AngelikaLanger.com/GenericsFAQ/JavaGenericsFAQ.html
/ERASURE/ Java Generics FAQ: What is type erasure?
URL: http://www.AngelikaLanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ101
/EXCPL/  Java Generics FAQ: What happens if a type parameter does not appear in the method parameter list?
URL: http://www.AngelikaLanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ403

Die gesamte Serie über Java Generics:

/GEN1/  Java Generics - Einführung
Klaus Kreft & Angelika Langer
Java Spektrum, März 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/30.GenericsIntro/30.GenericsIntro.html
/GEN2/  Java Generics - Wildcards
Klaus Kreft & Angelika Langer
Java Spektrum, Mai 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/31.Wildcards/31.Wildcards.html
/GEN3/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, Juli 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/32.TypeErasure/32.TypeErasure.html
/GEN4/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, September 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/33.TypeErasurePitfall/33.TypeErasurePitfall.html
/GEN5/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, November 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/34.GenericCreation/34.GenericCreation.html

 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Effective Java - Java best practice programming techniques, common pitfalls, and off-the-beaten-path language features
4 day seminar ( open enrollment and on-site)
 

 
  © 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