11.10 Die Collection-API
 
Eine der größten Neuerungen, die die Java 2-Plattform eingeführt hat, war die Collection-API. Wir werden die Begriffe »Container« und »Collection« synonym verwenden. Ein Container ist ein Objekt, welches wiederum Objekte aufnimmt und die Verantwortung für die Elemente übernimmt. Im util-Paket befinden sich sechs Schnittstellen, die grundlegende Eigenschaften der Container-Klassen definieren. Daher sehen wir uns die Schnittstellen zuerst an, da die wichtigen Methoden genau dort festgelegt sind.
11.10.1 Die Schnittstelle Collection
 
Collection ist die Basis der Hierarchie. Alle Container-Klassen bis auf die Assoziativspeicher implementieren das Collection-Interface und erhalten damit einen gemeinsamen, äußeren Rahmen. Mit den dort definierten Operationen lassen sich Elemente hinzufügen, löschen, selektieren und finden.
Die Collection-Schnittstelle wird von mehreren Schnittstellen erweitert, die sich darin unterscheiden, ob der Container etwa Werte doppelt beinhalten darf oder ob der Container die Werte sortiert hält. Das heißt, die Unterschnittstellen werden in der Anwendung konkreter. BeanContext, BeanContextServices, List, Set und SortedSet sind diese Schnittstellen.
Im Folgenden sind nur die drei letzten Klassen von besonderem Interesse. Die einzige Klasse, die direkt das Interface Collection implementiert, heißt AbstractCollection und beinhaltet die Basisfunktionalität für die meisten Klassen.
|
boolean add( Object o )
Optional. Fügt ein Element dem Container hinzu und gibt true zurück, falls sich das Element einfügen lässt. Gibt false zurück, falls schon ein Objekt gleichen Werts vorhanden ist und doppelte Werte nicht erlaubt sind. Erlaubt der Container das Hinzufügen nicht, muss eine UnsupportedOperationException ausgeworfen werden. |
|
boolean addAll( Collection c )
Fügt alle Elemente der Collection c dem Container hinzu. |
|
void clear()
Optional. Löscht alle Elemente im Container. Wird dies vom Container nicht unterstützt, wird eine UnsupportedOperationException ausgeworfen. |
|
boolean contains( Object o )
Liefert true, falls der Container ein inhaltlich gleiches Element enthält. |
|
boolean containsAll( Collection c )
Liefert true, falls der Container alle Elemente der Collection c enthält. |
|
boolean equals( Object o )
Prüft, ob das angegebene Objekt ebenfalls ein Container ist und die gleichen Elemente enthält wie dieser Container. |
|
int hashCode()
Liefert den Hashwert des Containers. Dies ist nur intressant, wenn der Container als Schlüssel in Hashtabellen verwendet wird. Dann darf der Inhalt aber nicht mehr geändert werden, da der Hashwert von allen Elementen des Containers abhängt. |
|
boolean isEmpty()
Liefert true, falls der Container keine Elemente enthält. |
|
Iterator iterator()
Liefert ein Iterator-Objekt über alle Elemente des Containers. |
|
boolean remove( Object o )
Optional. Entfernt das angegebene Objekt aus dem Container, falls es vorhanden ist. |
|
boolean removeAll( Collection c )
Optional. Entfernt alle Objekte der Collection c aus dem Container. |
|
boolean retainAll( Collection c )
Optional. Entfernt alle Objekte, die nicht in der Collection c vorkommen. |
|
int size()
Gibt die Anzahl der Elemente im Container zurück. |
|
Object[] toArray()
Gibt ein Array mit allen Elementen des Containers zurück. |
|
Object[] toArray( Object a[] )
Gibt ein Array mit allen Elementen des Containers zurück. Verwendet das als Parameter übergebene Array, wenn es groß genug ist. Sonst wird ein Array passender Größe angelegt, dessen Laufzeittyp a entspricht. |
11.10.2 Schnittstellen, die Collection erweitern, und Map
 
Es gibt einige elementare Schnittstellen, die einen Container weiter untergliedern. Etwa in der Art, wie Elemente gespeichert werden.
Die Schnittstelle List
Die Schnittstelle List, die die Collection-Schnittstelle erweitert, enthält zusätzliche Operationen für eine geordnete Liste (auch Sequenz genannt) von Elementen. Seit Java 1.2 implementiert auch die Klasse Vector die Schnittstelle List. Auf die Elemente einer Liste lässt sich über einen ganzzahligen Index zugreifen und es kann linear nach Elementen gesucht werden. Doppelte Elemente sind erlaubt. Da das AWT-Paket ebenfalls eine Klasse mit dem Namen »List« definiert, sollte bei Namenskonflikten der voll qualifizierte Name, also java.util.List oder java.awt.List verwendet werden. Neben Vector implementieren die Klassen AbstractList, LinkedList sowie ArrayList die List-Schnittstelle.
Die Schnittstelle Map
Eine Klasse, die Map implementiert, realisiert einen assoziativen Speicher. Dieser verbindet einen Schlüssel mit einem Wert. Ebenso wie Vector nun eine Implementierung von List ist, implementiert die Klasse Hashtable seit Java 1.2 die Schnittstelle Map. Im Gegensatz zu List ist eine Map unsortiert, und die Reihenfolge, in der die Elemente eingefügt werden, spielt keine Rolle.
Die Schnittstelle Map implementiert Collection nicht. Das liegt daran, dass nicht nur ein Element mit add() dem Container hinzugeführt wird, sondern zwei, Schlüssel und Wert. Darauf ist die allgemeine Collection nicht vorbereitet.
Die Schnittstelle SortedMap
Eine Map kann mit Hilfe eines Kriteriums sortiert werden und nennt sich dann SortedMap, es ist also eine Schnittstelle, die Map direkt erweitert. Das Sortierkriterium wird mittels eines Comparator-Objekts festgelegt. Damit kann auf einen assoziativen Speicher über einen Iterator in einer definierten Reihenfolge iteriert werden. Bisher implementiert nur die konkrete Klasse TreeMap die Schnittstelle SortedMap.
Die Schnittstelle Set
Ein Set ist eine im mathematischen Sinne definierte Menge von Objekten. Wie von mathematischen Mengen bekannt, darf ein Set keine doppelten Elemente enthalten. Für zwei nicht identische Elemente e1 und e2 eines Set-Objekts liefert der Vergleich e1.equals(e2) also immer false. Genauer gesagt: Aus e1.equals(e2) folgt, dass e1 und e2 identische Objektreferenzen sind, sich also auf dasselbe Mengenelement beziehen.
Besondere Beachtung muss Objekten geschenkt werden, die ihren Wert nachträglich ändern, da so zunächst ungleiche Mengenelemente inhaltlich gleich werden können. Dies kann ein Set nicht kontrollieren. Als weitere Einschränkung gilt, dass eine Menge sich selbst nicht als Element enthalten darf.
Zwei Klassen implementieren die Schnittstelle Set: die abstrakte Klasse AbstractSet und die konkrete Klasse HashSet.
Die Schnittstelle SortedSet
SortedSet erweitert Set um die Eigenschaft, Elemente sortiert auslesen zu können. Das Sortierkriterium wird durch ein Exemplar der Hilfsklasse Comparator bestimmt. TreeMap ist die einzige Klasse, die SortedMap implementiert.
11.10.3 Abstrakte Basisklassen für Container
 
Das Designprinzip der Collection-Klassen folgt drei Stufen: Schnittstellen legen Gruppen von Operationen für die verschiedenen Behältertypen fest; abstrakte Basisklassen führen die Operationen der Schnittstellen auf eine minimale Zahl von als abstrakt definierten Grundoperationen zurück, etwa addAll() auf add() oder isEmpty() auf getSize(); konkrete Klassen für bestimmte Behältertypen beerben die entsprechende abstrakte Basisklasse und ergänzen die unbedingt erforderlichen Grundoperationen (und einige die Performance steigernde Abkürzungen gegenüber der allgemeinen Lösung in der Oberklasse).
Es gibt eine Reihe von abstrakten Basisklassen, die den Containern eine Basisfunktionalität geben. Unter diesen Klassen sind:
AbstractCollection
Implementiert die Methoden der Schnittstelle Collection ohne iterator() und size(). AbstractCollection ist Basisklasse von AbstractList und AbstractSet.
AbstractList
Erweitert AbstractCollection und implementiert die Schnittstelle List. Für eine konkrete Klasse müssen lediglich Methoden für get(int index) und size() implementiert werden. Soll die Liste auch Elemente aufnehmen, sollte sie auch set(int index, Object element) implementieren. Andernfalls bewirkt das Einfügen von Elementen nur eine Unsupported OperationException. Die direkten Unterklassen sind AbstractSequentialList, ArrayList und Vector.
AbstractSequentialList
AbstractSequentialList erweitert AbstractList (und damit auch AbstractCollection) und bildet die Grundlage für die Klasse LinkedList. Im Gegensatz zur konkreten Klasse ArrayList bereitet Abstract SequentialList die Klasse LinkedList darauf vor, die Elemente in einer Liste zu verwalten und nicht wie ArrayList in einem internen Array.
AbstractSet
Erweitert AbstractCollection und implementiert die Schnittstelle Set. AbstractSet dient als Basis für die beiden Klassen HashSet und TreeSet. Es überschreibt auch keine Methoden der Oberklasse AbstractCollection, sondern fügt nur equals(Object)- und hashCode()-Methoden hinzu.
AbstractMap
Implementiert die Schnittstelle Map. Um eine konkrete Unterklasse zu erstellen, muss put() sinnvoll implementiert werden; überschreiben wir put() nicht, erhalten wir die bekannte UnsupportedOperationException. Für get(Object) gibt es eine Implementierung.
11.10.4 Konkrete Container-Klassen
 
Werden wir nun ein wenig konkreter. Alle bisher vorgestellten Schnittstellen und Klassen dienen der Modellierung und dem Programmierer nur als Basis. Folgende Klassen sind konkrete Klassen und können von uns benutzt werden:
ArrayList
Implementiert Listen-Funktionalität wie ein Vector. Sie erweitert dabei die Klasse AbstractList, sodass ArrayList natürlich auch die Schnittstelle List implementiert.
LinkedList
LinkedList ist eine doppelt verkettete Liste, also eine Liste von Einträgen mit einer Referenz auf den jeweiligen Nachfolger und Vorgänger. Das ist nützlich beim Einfügen und Löschen von Elementen an beliebigen Stellen innerhalb der Liste. Diese Klasse erweitert AbstractSequentialList.
HashMap
Implementiert einen assoziativen Speicher wie die Hashtable. Sie erweitert die Klasse AbstractMap und ist damit auch eine Map.
TreeMap
Exemplare dieser Klasse halten ihre Elemente in einem Binärbaum sortiert. TreeMap erweitert AbstractMap und implementiert SortedMap.
11.10.5 Unterschiede zu den älteren Datenstrukturen und die Synchronisation
 
Die Implementierungen der oben genannten Klassen ArrayList und HashMap unterscheiden sich von den unter 1.0 existierenden Klassen Vector und Hashtable in der Form, dass Einfüge- und Löschoperationen nicht mehr automatisch synchronized sind. Wenn also die Möglichkeit besteht, dass mehrere Threads konkurrierend auf die Elemente eines Containers zugreifen, müssen die Exemplare der neuen Container-Klassen extern synchronisiert werden. In der Regel wird dazu ein spezielles synchronisierendes Container-Objekt mit einer statischen Methode synchronizedXXX() angefordert.
Beispiel Eine synchronisierte Liste
List list = Collections.synchronizedList( new LinkedList(...) );
|
11.10.6 Das erste Programm mit Container-Klassen
 
Wie wir schon gesehen haben, implementieren alle Container-Klassen das Interface Collection und haben dadurch schon wichtige Funktionen, um Daten aufzunehmen, zu manipulieren und auszulesen. Das folgende Programm erzeugt die Datenstruktur verkettete Liste und fügt zehn String-Elemente ein. Diese werden mit einem Iterator wieder ausgelesen. Mit Iteratoren lassen sich ähnlich wie mit Enumeratoren Daten der Reihe nach auslesen. Um Iteratoren kümmern wir uns im nächsten Abschnitt genauer.
Listing 11.9
ErsteSammlung.java
import java.util.*;
class ErsteSammlung
{
public static void main( String args[] )
{
Collection c = new LinkedList();
for ( int i = 0; i < 10; i++ )
c.add( "" + i );
Iterator it = c.iterator();
while ( it.hasNext() )
System.out.println( it.next() );
}
}
Besonders leicht – unter softwaretechnischen Gesichtspunkten – lässt sich die Datenstruktur ändern. Wir müssen nur
Collection c = new LinkedList();
etwa in
Collection c = new ArrayList();
ändern, und schon ist die Liste intern nicht mehr mit verketteten Elementen implementiert, sondern als Array. Es ist immer schön, wenn wir, etwa aus Gründen der Geschwindigkeit, so leicht die Datenstruktur ändern können. Der Rest des Programms bleibt unverändert.
Sich selbst in einer Liste haben
Die Implementierung der Listen-Klasse hat ein Problem, wenn ein Listen-Objekt sich selbst als Element enthält.
Die folgenden Zeilen provozieren einen StackOverflowError:
List l = new ArrayList();
l.add( "Hübsch" );
l.add( l );
System.out.println( l ); // hier ist das Problem
Das Phänomen tritt erst bei println() auf. Denn die Methode toString() auf l ruft wiederum toString() auf l auf, was wiederum toString() auf l aufruft und so weiter.
11.10.7 Iteratoren
 
Ein Iterator ist für die neuen Collection-Klassen das, was Enumeration für die herkömmlichen Datenstruktur-Klassen ist. Die Schnittstelle Iterator besitzt kürzere Methodennamen als Enumeration. Nun heißt es hasNext() an Stelle von hasMoreElements() und next() an Stelle von nextElement(). Übergeben wir ein false von hasNext(), so erhalten wir eine NoSuchElementException. Zudem besitzt ein Iterator auch die Möglichkeit, das zuletzt aufgezählte Element aus dem zugrunde liegenden Contrainer-Objekt zu löschen. Dazu dient die Methode remove(); sie lässt sich allerdings nur unmittelbar aufrufen, nachdem next() das zu löschende Element als Ergebnis geliefert hat. Eine Enumeration kann die aufgezählte Datenstruktur grundsätzlich nicht verändern.
|
boolean hasNext()
Liefert true, falls die Iteration weitere Elemente bietet. |
|
Object next()
Liefert das nächste Element in der Aufzählung oder NoSuchElementException, wenn keine weiteren Elemente mehr vorhanden sind. |
|
void remove()
Entfernt das Element, das der Iterator zuletzt bei next() geliefert hat. |
Hinweis Es ist eine interessante Frage, warum es die Methode remove() im Iterator gibt. Die Erklärung dafür ist, dass der Iterator die Stelle kennt, an der sich die Daten befinden (eine Art Cursor). Darum können die Daten auch effizient direkt dort gelöscht werden. Das erklärt jedoch nicht unbedingt, warum es keine Einfüge-Methode gibt. Ein allgemeiner Grund mag sein, dass bei vielen Containertypen das Einfügen an einer bestimmten Stelle keinen Sinn ergibt, etwa bei SortedSet, SortedMap, Set und Map. Dort ist die Einfügeposition durch die Sortierung vorgegeben oder belanglos (beziehungsweise bei HashSet durch die interne Realisierung bestimmt), also kein Fall für einen Iterator. Dazu wirft Einfügen weitere Fragen auf: Vor oder nach dem zuletzt per next() gelieferten Element? Soll das neue Element mit aufgezählt werden, oder nicht? Auch dann nicht, wenn es in der Sortierung erst später an die Reihe käme? Eine Löschen-Methode ist problemloser und universell anwendbar.
|
Arrays mit Iteratoren durchlaufen
Die Konzepte Array und Container-Objekte passen oft nicht genau zusammen, da zwischen ihnen ein Bruch in der Programmierung liegt. Beide werden unterschiedlich angesprochen: ein Container häufig mittels Iteratoren, ein Array direkt über die ganzzahligen Indexwerte. Wenn es nicht auf Geschwindigkeit ankommt, sollten wir als Container besser eine Datenstruktur verwenden und kein »rohes« Array. Bei einem Array müssen wir uns immer selbst um Strategien zum Durchlaufen der Array-Elemente kümmern, bei Datenstrukturen haben wir das Konzept der Enumeratoren und Iteratoren. Gut ist es, ein Array nachträglich mit denselben Abstraktionen auszustatten wie eine Datenstruktur, also mit einem Iterator. Folgende Implementierung soll uns dabei helfen, von den Vorteilen eines Iterators zu profitieren. Dadurch kann zum Beispiel ein Array leichter gegen eine mächtigere Datenstruktur ausgetauscht werden. Wir müssen dazu nur für drei Methoden Programmcode bereitstellen: hasNext(), next() und remove(). Für Letztere wollen wir keine Implementierung bieten und eine UnsupportedOperationException auslösen. Damit sieht die Klasse ArrayIterator wie folgt aus:
Listing 11.10
ArrayIterator.java
import java.util.*;
public class ArrayIterator implements Iterator
{
private Object array[];
private int pos = 0;
public ArrayIterator( Object anArray[] )
{
array = anArray;
}
public boolean hasNext()
{
return pos < array.length;
}
public Object next() throws NoSuchElementException
{
if ( hasNext() )
return array[pos++];
else
throw new NoSuchElementException();
}
public void remove() throws UnsupportedOperationException
{
throw new UnsupportedOperationException();
}
}
Ein ArrayIterator wird über einen parametrisierten Konstruktor für ein bestimmtes Array-Objekt erzeugt. Das Array kann parallel im Hintergrund noch verändert werden. Da es sich in der Größe allerdings nicht mehr ändern kann, müssen wir die ersten beiden kritischen Zeilen in next() nicht synchronisieren.
Praktisch bei dem ArrayIterator ist nun, dass er an alle Funktionen weitergegeben werden kann, die einen Iterator als Parameter erwarten und kein remove() verwenden. Sonst hätten wir eine andere Datenstruktur wählen müssen.
Folgendes Beispiel zeigt unseren neuen Iterator im Einsatz beim Aufzählen der Kommandozeilen-Argumente:
static public void main( String arg[] )
{
Iterator i = new ArrayIterator( arg );
while ( i.hasNext() )
System.out.println( "Eintrag: " + i.next() );
}
11.10.8 Der Comparator
 
Das Angenehme an der Collection-Schnittstelle ist, dass sie zu universellen Routinen führt. Vergleiche zwischen den Elementen einer Datenstruktur werden von speziellen Hilfsobjekten durchgeführt, den Comparatoren. Eine Klasse für konkrete Comparator-Objekte implementiert eine Schnittstelle, die zwei Methoden vorgibt.
|
int compare( Object o1, Object o2 )
Vergleicht zwei Argumente auf ihre Ordnung. |
|
boolean equals( Object obj )
Testet, ob zwei Objekte aus Sicht des Comparator-Objekts gleich sind. |
Wir definieren uns nun über die Comparator-Schnittstelle eine Klasse EvenComparator, die equals() so implementiert, dass gerade Zahlen angenommen und ungerade Zahlen abgelehnt werden. Die compare()-Methode, die wir ebenfalls implementieren müssen, da sie uns die Schnittstelle vorschreibt, lassen wir leer. Es verfehlt zwar etwas die Aufgabe der Comparator-Schnittstelle, aber dann müssen wir uns keine neue Klasse ausdenken. Wir nutzen jetzt die spezielle Klasse EvenComparator dafür, dass wir einen Vector so über eine filter()-Methode modifizieren können, dass alle ungeraden Zahlen herausgenommen werden. Die Filter-Methode funktioniert dank Collection und Comparator auf allen erdenklichen Collection-basierenden Datenstrukturen, die Integer-Objekte beinhalten.
Listing 11.11
FilterCollection.java
import java.util.*;
class FilterCollection
{
static class EvenComparator implements Comparator
{
public boolean equals( Object o )
{
return ((Integer)o).intValue() %2 == 0;
}
public int compare( Object o, Object p )
{
return 0;
}
}
static void filter( Iterator i, Comparator comp )
{
while ( i.hasNext() )
if ( !comp.equals(i.next()) )
i.remove();
}
public static void main( String args[] )
{
List l = new ArrayList( Arrays.asList( new Integer[] {
new Integer(9), new Integer(10), new Integer(20), new Integer(31) } ) );
filter( l.iterator(), new EvenComparator() );
System.out.println( l ); // [10, 20]
}
}
Einen Iterator bekommen wir mit der iterator()-Methode der Collection-Schnittstelle. Da alle Container-Klassen diese implementieren, können wir also immer mit der gleichen Technik auf die Elemente zurückgreifen. Ein Rückwärtslaufen ist, ebenso wie beim Enumeration-Interface, nicht möglich.
ListIterator ist eine Erweiterung von Iterator. Diese Schnittstelle fügt noch Methoden hinzu, damit an der aktuellen Stelle auch Elemente eingefügt werden können. Mit einem ListIterator lässt sich rückwärts laufen und auf das vorhergehende Element zugreifen.
11.10.9 toArray() von Collection verstehen – Chance für eine Falle erkennen
 
Die toArray()-Methode aus der Schnittstelle Collection gibt laut Definition ein Array von Objekten zurück. Es ist wichtig, zu verstehen, welchen Typ die Einträge und das Array selbst haben. Eine Implementierung der Collection-Schnittstelle ist ArrayList.
Beispiel Eine Anwendung von toArray(), die Punkte in ein Feld kopiert
ArrayList l = new ArrayList();
l.add( new Point(13,43) );
l.add( new Point(9,4) );
Object points[] = l.toArray();
|
Wir erhalten nun ein Feld mit Referenzen auf Point-Objekte. Doch wir können zum Beispiel nicht einfach points[1].x schreiben, um auf das Attribut des Point-Exemplars zuzugreifen, denn das Array points hat den deklarierten Elementtyp Object. Es fehlt die explizite Typumwandlung und erst ((Point)points[1]).x ist korrekt. Doch spontan kommen wir sicherlich auf die Idee, einfach den Typ des Arrays auf Point zu ändern. In dem Array befinden sich ja nur Referenzen auf Point-Exemplare.
Point points[] = l.toArray(); // Vorsicht!
Jetzt wird der Compiler einen Fehler melden, da der Rückgabewert von toArray() Object[] ist. Spontan reparieren wir dies, indem wir eine Typumwandlung auf ein Point-Array an die rechte Seite setzen.
Point points[] = (Point[])l.toArray(); // Gefährlich!
Jetzt haben wir zur Übersetzungszeit kein Problem mehr, aber zur Laufzeit wird es immer knallen; auch wenn sich im Array tatsächlich nur Point-Objekte befinden.
Diesen Programmierfehler müssen wir verstehen. Was wir falsch gemacht haben, ist einfach: Wir haben den Typ des Arrays mit den Typen der Array-Elemente durcheinander gebracht. Einem Array von Objekt-Referenzen können wir alles zuweisen.
Object os[] = new Object[3];
os[0] = new Point();
os[1] = "Trecker fahr'n";
os[2] = new Date();
Wir merken, dass der Typ des Arrays Object[] ist, und die Array-Elemente sind ebenfalls vom Typ Object. Hinter dem new-Operator, der das Array-Objekt erzeugt, steht der gemeinsame Obertyp für zulässige Array-Elemente. Bei Object[]-Arrays dürfen die Elemente Referenzen für beliebige Objekte sein. Klar ist, dass ein Array nur Objektreferenzen aufnehmen kann, die mit dem Typ für das Array selbst kompatibel sind, also sich auf Exemplare der angegebenen Klasse beziehen oder auf Exemplare von Unterklassen dieser Klasse.
Object os[] = new Point[2];
os[0] = new Point();
// os[1] = new Date(); das geht nicht.
os = new Object[];
os[0] = "Trecker fahr'n"; // das ist erlaubt.
Kommen wir wieder zurück zur Methode toArray(). Da die auszulesende Datenstruktur alles Mögliche enthalten kann, muss also der Typ der Elemente Object sein. Wir haben gerade festgestellt, dass der Elementtyp des Array-Objekts, das die Methode toArray() als Ergebnis liefert, mindestens so umfassend sein muss. Da es einen allgemeineren (umfassenderen) Typ als Object nicht gibt, ist auch der Typ des Arrays Object[]. Dies muss so sein, auch wenn die Elemente einer Datenstruktur im Einzelfall einen spezielleren Typ haben. Einer allgemein gültigen Implementierung von toArray() bleibt gar nichts anderes übrig, als das Array vom Typ Object[] und die Elemente vom Typ Object zu erzeugen.
public Object[] toArray() {
Object[] objs = new Object[size()];
Iterator it = iterator();
for (int i = 0; i < objs.length; i++) {
objs[i] = it.next();
}
return objs;
}
Wenn sich auch die Elemente wieder auf einen spezielleren Typ konvertieren lassen, ist das jedoch bei dem Array-Objekt selbst nicht der Fall. Ein Array-Objekt mit Elementen vom Typ X ist nicht automatisch auch selbst vom Typ X[], sondern von einem Typ Y[], wobei Y eine (echte) Oberklasse von X ist.
Die Lösung für das Problem
Bevor wir nun eine Schleife mit einer Typumwandlung für jedes einzelne Array-Element schreiben oder eine Typumwandlung bei jedem Zugriff auf die Elemente vornehmen, sollten wir einen Blick auf die zweite toArray()-Funktion werfen. Sie akzeptiert als Parameter ein vorgefertigtes Array für das Ergebnis. Mit dieser Funktion lässt sich erreichen, dass das Ergebnis-Array von einem spezielleren Typ als Object[] ist.
Beispiel Wir fordern von der toArray()-Funktion ein Feld vom Typ Point.
ArrayList l = new ArrayList();
l.add( new Point(13,43) );
l.add( new Point(9,4) );
Point points[] = (Point[])l.toArray( new Point[0]);
|
Jetzt bekommen wir die Listenelemente in ein Array kopiert und der Typ des Arrays ist, passend zu den aktuell vorhandenen Listenelementen, Point[].
Spannend ist die Frage, wie so etwas funktionieren kann. Dazu verwendet die Methode toArray(Object[]) die Technik Reflection, um dynamisch ein Array vom gleichen Typ wie das Parameter-Array zu erzeugen. Wollten wir ein Array b vom Typ des Arrays a mit Platz für len Elemente anlegen, so schreiben wir
Object b[] = (Object[])Array.newInstance(
a.getClass().getComponentType(), len );
Mit a.getClass().getComponentType() erhalten wir ein Class-Objekt für den Elementtyp des Arrays, zum Beispiel das Class-Objekt Point.class für die Klasse Point. a.getClass()allein liefert ein Class-Objekt für das Array a, etwa ein Objekt, das den Typ Point[] repräsentiert. Array.newInstance(), eine statische Methode von java.lang.reflect.Array, konstruiert ein neues Array mit dem Elementtyp aus dem Class-Objekt und der angegebenen Länge. Nichts anderes macht auch ein new X[len], nur dass hier der Elementtyp zur Übersetzungszeit festgelegt werden muss. Da der Rückgabewert von newInstance() Object ist, muss letztendlich noch die Konvertierung in ein passendes Array stattfinden.
Passt der Inhalt der Collection in das als Parameter übergebene Array, so wird er dort hineinkopiert. Oft wird aber dort ein new X[0] anzeigen, dass wir ein neu erzeugtes Array-Objekt wünschen. Im Übrigen entspricht natürlich toArray(new Object[0]) dem Aufruf von toArray(). Die Java-Bibliothek gibt aber zwei völlig getrennte Implementierungen an, da ja die parametrisierte Methode auch in das Parameter-Array kopieren kann. Das ist komisch, denn toArray() könnte toArray(new Object[0]) aufrufen oder effizienter auch toArray(new Object[size()]).
|