![]() |
|
|||||
Die Schnittstelle MapEine 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 SortedMapEine 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 SetEin 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 SortedSetSortedSet 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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Beispiel Eine synchronisierte Liste
List list = Collections.synchronizedList( new LinkedList(...) ); |
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();
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.
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.
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. |
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() ); }
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.
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.
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()]).
| << zurück |
Copyright © Galileo Press GmbH 2003
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.