Titel | Inhalt | Suchen | Index | API | Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung |
<< | < | > | >> | Kapitel 15 - Collections II |
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:
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. |
|
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 | API | Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0 |
<< | < | > | >> | © 2000 Guido Krüger, http://www.gkrueger.com |