Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage
<< | < | > | >> | API | Kapitel 15 - Collections II

## 15.1 Grundlagen und Konzepte

- 15.1 Grundlagen und Konzepte

Wie in Kapitel 14 erläutert, gibt es seit dem JDK 1.0 die "traditionellen" Collections mit den Klassen `Vector`, `Stack`, `Dictionary`, `Hashtable` und `BitSet`. Obwohl sie ihren Zweck durchaus erfüllen, gab es einige Kritik am Collections-Konzept des JDK 1.0. Zunächst wurde die geringe Vielseitigkeit kritisiert, etwa im Vergleich zum mächtigen Collection-Konzept der Sprache SmallTalk. Zudem galten die JDK-1.0-Collections als nicht sehr performant. Fast alle wichtigen Methoden sind `synchronized`, und als generische Speicher von Elementen des Typs `Object` ist bei jedem Zugriff ein Typecast bzw. eine Typüberprüfung notwendig (wir kommen auf diese Begriffe in Kapitel 22 zurück). Zudem gab es Detailschwächen, die immer wieder kritisiert wurden. Als Beispiel kann die Wahl der Methodennamen des `Enumeration`-Interfaces genannt werden. Oder die Implementierung der Klasse `Stack`, die als Ableitung von `Vector` weit über ihr eigentliches Ziel hinausschießt.

Diese Kritik wurde von den Java-Designern zum Anlaß genommen, das Collection-Konzept im Rahmen der Version 1.2 neu zu überdenken. Herausgekommen ist dabei eine Sammlung von gut 20 Klassen und Interfaces im Paket `java.util`, die das Collections-Framework des JDK 1.2 bilden. Wer bei dieser Zahl erschreckt, sei getröstet. Letztlich werden im wesentlichen die drei Grundformen `Set`, `List` und `Map` realisiert. Die große Anzahl ergibt sich aus der Bereitstellung verschiedender Implementierungsvarianten, Interfaces und einiger abstrakter Basisklassen, mit denen die Implementierung eigener Collections vereinfacht werden kann. Wir wollen uns zunächst mit der grundlegenden Arbeitsweise der Collection-Typen vertraut machen:

- Eine `List` ist eine beliebig große Liste von Elementen beliebigen Typs, auf die sowohl wahlfrei als auch sequentiell zugegriffen werden kann.
- Ein `Set` ist eine (doublettenlose) Menge von Elementen, auf die mit typischen Mengenoperationen zugegriffen werden kann.
- Eine `Map` ist eine Abbildung von Elementen eines Typs auf Elemente eines anderen Typs, also eine Menge zusammengehöriger Paare von Objekten.

Jede dieser Grundformen ist als Interface unter dem oben angegebenen Namen implementiert. Zudem gibt es jeweils eine oder mehrere konkrete Implementierungen. Sie unterscheiden sich in den verwendeten Datenstrukturen und Algorithmen und damit in ihrer Eignung für verschiedene Anwendungsfälle. Weiterhin gibt es eine abstrakte Implementierung des Interfaces, mit dessen Hilfe das Erstellen eigener Collections erleichtert wird.

|  |  |  | Im Gegensatz zu den 1.1-Klassen sind die Collections des JDK 1.2 aus Performancegründen durchgängig unsynchronisiert. Soll also von mehr als einem Thread gleichzeitig auf eine Collection zugegriffen werden (Collections sind häufig Kommunikationsmittel zwischen gekoppelten Threads), so ist unbedingt darauf zu achten, die Zugriffe selbst zu synchronisieren. Andernfalls können leicht Programmfehler und Dateninkonsistenzen entstehen. |  |
JDK1.1-1.4

Die Namensgebung der Interfaces und Klassen folgt einem einfachen Schema. Das Interface hat immer den allgemein verwendeten Namen der zu implementierenden Collection, also beispielsweise `List`, `Set` oder `Map`. Jede Implementierungsvariante stellt vor den Namen dann eine spezifische Bezeichnung, die einen Hinweis auf die Art der verwendeten Datenstrukturen und Algorithmen geben soll. So gibt es für das Interface `List` beispielsweise die Implementierungsvarianten `LinkedList` und `ArrayList` sowie die abstrakte Implementierung `AbstractList`. Wenn man dieses Schema einmal begriffen hat, verliert der »Collection-Zoo« viel von seinem Schrecken.

Die Interfaces spielen eine wichtige Rolle, denn sie beschreiben bereits recht detailliert die Eigenschaften der folgenden Implementierungen. Dabei wurde im JDK 1.2 eine weitreichende Design-Entscheidung getroffen. Um nicht für jede denkbare Collection-Klasse ein eigenes Interface definieren zu müssen, wurde ein Basisinterface `Collection` geschaffen, aus dem die meisten Interfaces abgeleitet wurden. Es faßt die wesentlichen Eigenschaften einer großen Menge unterschiedlicher Collections zusammen:

    int size()
    boolean isEmpty()
    boolean contains(Object o)
    Iterator iterator()
    Object[] toArray()
    Object[] toArray(Object[] a)
    boolean add(Object o)
    boolean remove(Object o)
    boolean containsAll(Collection c)
    boolean addAll(Collection c)
    boolean removeAll(Collection c)
    boolean retainAll(Collection c)
    void clear()
    boolean equals(Object o)
    int hashCode()
| java.util.Collection

Zusätzlich fordert die JDK-1.2-Spezifikation für jede Collection-Klasse zwei Konstruktoren (was ja leider nicht im Rahmen der Interface-Definition sichergestellt werden kann). Ein parameterloser Konstruktor wird verwendet, um eine leere Collection anzulegen. Ein weiterer, der ein einzelnes `Collection`-Argument besitzt, dient dazu, eine neue Collection anzulegen und mit allen Elementen aus der als Argument übergebenen Collection zu füllen. Die Interfaces `List` und `Set` sind direkt aus `Collection` abgeleitet, `SortedSet` entstammt ihr (als Nachfolger von `Set`) mittelbar. Lediglich die Interfaces `Map` und das daraus abgeleitete Interface `SortedMap` wurden nicht aus `Collection` abgeleitet.

Der Vorteil dieses Designs ist natürlich, daß eine flexible Schnittstelle für den Umgang mit Mengen von Objekten zur Verfügung steht. Wenn eine Methode einen Rückgabewert vom Typ `Collection` besitzt, können die Aufrufer dieser Methode auf die zurückgegebenen Elemente - unabhängig vom Typ der tatsächlich zurückgegebenen Collection-Klasse - einheitlich zugreifen. Selbst wenn eine andere Implementierung gewählt wird, ändert sich für den Aufrufer nichts, solange das zurückgegebene Objekt das `Collection`-Interface implementiert.

Soweit zur Theorie. Der designerische Nachteil dieses Ansatzes besteht darin, daß längst nicht alle tatsächlichen Collections eine Schnittstelle besitzen, wie sie in `Collection` definiert wurde. Während diese für eine Liste noch passen mag, besitzt eine Queue oder ein Stack gänzlich anders arbeitende Zugriffsroutinen. Dieser Konflikt wurde dadurch zu lösen versucht, daß die Methoden zum Ändern der Collection optional sind. Während also `contains`, `containsAll`, `equals`, `hashCode`, `isEmpty`, `size` und `toArray` obligatorisch sind, müssen die übrigen Methoden in einer konkreten Implementierung nicht unbedingt zur Verfügung gestellt werden, sondern können weggelassen, ersetzt oder ergänzt werden.

Aus Kapitel 9 wissen wir allerdings, daß alle definierten Methoden eines Interfaces in einer konkreten Implementierung zur Verfügung gestellt werden müssen. Davon sind natürlich auch die Collection-Klassen nicht ausgenommen. Wenn sich diese entschließen, eine optionale Methode nicht zu realisieren, so muß diese zwar implementiert werden, löst aber beim Aufruf eine Exception des Typs `UnsupportedOperationException` aus. Die tatsächliche Schnittstelle einer konkreten Collection-Klasse kann also nicht dem Interface entnommen werden, das sie implementiert, sondern erfordert zusätzlich die schriftliche Dokumentation der Klasse.

Über diesen Designansatz kann man sich streiten. Da sowieso nur die nicht-verändernden Methoden nicht-optional sind, hätte man ebenso gut diese allein in ein generisches Collection-Interface stecken können und von allen Collection-Klassen implementieren lassen können. Zusätzliche Methoden wären dann in abgeleiteten Interfaces passend zur jeweiligen Collection-Klasse zu definieren.

Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0
<< | < | > | >> | API | © 1998-2002 Guido Krüger, http://www.javabuch.de