Galileo Computing < openbook >
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel (2. Aufl.) von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Java ist auch eine Insel (2. Auflage)
gp Kapitel 11 Datenstrukturen und Algorithmen
  gp 11.1 Mit einem Iterator durch die Daten wandern
  gp 11.2 Dynamische Datenstrukturen
  gp 11.3 Die Klasse Vector
    gp 11.3.1 Vektoren erzeugen
    gp 11.3.2 Funktionen
    gp 11.3.3 Arbeitsweise des internen Arrays
    gp 11.3.4 Die Größe eines Felds
    gp 11.3.5 Eine Aufzählung und gleichzeitiges Verändern
  gp 11.4 Stack, der Stapel
    gp 11.4.1 Die Methoden von Stack
    gp 11.4.2 Ein Stack ist ein Vektor – aha!
  gp 11.5 Die Klasse Hashtable und assoziative Speicher
    gp 11.5.1 Ein Objekt der Klasse Hashtable erzeugen
    gp 11.5.2 Einfügen und Abfragen der Datenstruktur
    gp 11.5.3 Die Arbeitsweise einer Hashtabelle
    gp 11.5.4 Aufzählen der Elemente
    gp 11.5.5 Ausgabe der Hashtabelle und Gleichheitstest
    gp 11.5.6 Klonen
  gp 11.6 Die abstrakte Klasse Dictionary
    gp 11.6.1 Zugriff und Abfrage
    gp 11.6.2 Metainformationen
    gp 11.6.3 Iterationen über die Elemente
  gp 11.7 Die Properties-Klasse
    gp 11.7.1 Über die Klasse Properties
    gp 11.7.2 put(), get() und getProperties()
    gp 11.7.3 Eigenschaften ausgeben
    gp 11.7.4 Systemeigenschaften der Java-Umgebung
    gp 11.7.5 Browser-Version abfragen
    gp 11.7.6 Properties von der Konsole aus setzen
  gp 11.8 Windows-typische INI-Dateien
  gp 11.9 Queue, die Schlange
  gp 11.10 Die Collection-API
    gp 11.10.1 Die Schnittstelle Collection
    gp 11.10.2 Schnittstellen, die Collection erweitern, und Map
    gp 11.10.3 Abstrakte Basisklassen für Container
    gp 11.10.4 Konkrete Container-Klassen
    gp 11.10.5 Unterschiede zu den älteren Datenstrukturen und die Synchronisation
    gp 11.10.6 Das erste Programm mit Container-Klassen
    gp 11.10.7 Iteratoren
    gp 11.10.8 Der Comparator
    gp 11.10.9 toArray() von Collection verstehen – Chance für eine Falle erkennen
  gp 11.11 Listen
    gp 11.11.1 AbstractList
    gp 11.11.2 Optionale Methoden
    gp 11.11.3 ArrayList
    gp 11.11.4 LinkedList
  gp 11.12 Algorithmen
    gp 11.12.1 Datenmanipulation
    gp 11.12.2 Größten und kleinsten Wert einer Collection finden
    gp 11.12.3 Sortieren
    gp 11.12.4 Elemente in der Collection suchen
  gp 11.13 Typsichere Datenstrukturen
  gp 11.14 Die Klasse BitSet für Bitmengen
    gp 11.14.1 Ein BitSet anlegen und füllen
    gp 11.14.2 Mengenorientierte Operationen
    gp 11.14.3 Funktionsübersicht
    gp 11.14.4 Primzahlen in einem BitSet verwalten
  gp 11.15 Ein Design-Pattern durch Beobachten von Änderungen
    gp 11.15.1 Design-Pattern
    gp 11.15.2 Das Beobachter-Pattern (Observer/Observable)


Galileo Computing

11.10 Die Collection-API  downtop

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.


Galileo Computing

11.10.1 Die Schnittstelle Collection  downtop

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.

Abbildung


gp  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.
gp  boolean addAll( Collection c )
Fügt alle Elemente der Collection c dem Container hinzu.
gp  void clear()
Optional. Löscht alle Elemente im Container. Wird dies vom Container nicht unterstützt, wird eine UnsupportedOperationException ausgeworfen.
gp  boolean contains( Object o )
Liefert true, falls der Container ein inhaltlich gleiches Element enthält.
gp  boolean containsAll( Collection c )
Liefert true, falls der Container alle Elemente der Collection c enthält.
gp  boolean equals( Object o )
Prüft, ob das angegebene Objekt ebenfalls ein Container ist und die gleichen Elemente enthält wie dieser Container.
gp  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.
gp  boolean isEmpty()
Liefert true, falls der Container keine Elemente enthält.
gp  Iterator iterator()
Liefert ein Iterator-Objekt über alle Elemente des Containers.
gp  boolean remove( Object o )
Optional. Entfernt das angegebene Objekt aus dem Container, falls es vorhanden ist.
gp  boolean removeAll( Collection c )
Optional. Entfernt alle Objekte der Collection c aus dem Container.
gp  boolean retainAll( Collection c )
Optional. Entfernt alle Objekte, die nicht in der Collection c vorkommen.
gp  int size()
Gibt die Anzahl der Elemente im Container zurück.
gp  Object[] toArray()
Gibt ein Array mit allen Elementen des Containers zurück.
gp  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.

Galileo Computing

11.10.2 Schnittstellen, die Collection erweitern, und Map  downtop

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.

Abbildung

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.


Galileo Computing

11.10.3 Abstrakte Basisklassen für Container  downtop

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.


Galileo Computing

11.10.4 Konkrete Container-Klassen  downtop

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.


Galileo Computing

11.10.5 Unterschiede zu den älteren Datenstrukturen und die Synchronisation  downtop

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(...) );


Galileo Computing

11.10.6 Das erste Programm mit Container-Klassen  downtop

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.


Galileo Computing

11.10.7 Iteratoren  downtop

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.

Abbildung


gp  boolean hasNext()
Liefert true, falls die Iteration weitere Elemente bietet.
gp  Object next()
Liefert das nächste Element in der Aufzählung oder NoSuchElementException, wenn keine weiteren Elemente mehr vorhanden sind.
gp  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() );
}

Galileo Computing

11.10.8 Der Comparator  downtop

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.


gp  int compare( Object o1, Object o2 )
Vergleicht zwei Argumente auf ihre Ordnung.
gp  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]
  }
}
Abbildung

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.


Galileo Computing

11.10.9 toArray() von Collection verstehen – Chance für eine Falle erkennen  toptop

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()]).





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.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de