Überblick über das Java Collections Framework
Ein Überblick über das Java Collections Framework — Interfaces, Implementierungen und Algorithmen in java.util.
Fast jedes Programm muss irgendwann eine Gruppe von Dingen verwalten — ausstehende Bestellungen, Wörter in einem Dokument, aktuell angemeldete Benutzer oder die Warteschlange von Aufgaben, die ein Worker-Thread abarbeiten soll. Javas Antwort auf die Frage „Wo soll ich diese Gruppe ablegen?" ist das Collections Framework: eine koordinierte Menge von Interfaces in java.util, mehr als ein Dutzend Implementierungen dahinter und eine einzige Hilfsklasse mit Algorithmen (Collections), die gegen die Interfaces und nicht gegen eine bestimmte Implementierung arbeitet. Dieses Kapitel ist die Karte — was im Framework steckt, warum es so aufgebaut ist und welche Familie in welcher Situation die richtige Wahl ist. Jedes Kapitel in diesem Teil widmet sich einer der Boxen auf dieser Karte.
Warum ein Framework und nicht einfach Klassen
Vor Java 1.2 gab es zwar Klassen für Gruppen (Vector, Hashtable, Stack), aber kein gemeinsames Interface — man konnte keine Methode schreiben, die „irgendeine Liste" entgegennimmt und alle drei akzeptiert. Das Collections Framework hat das behoben, indem es was eine Gruppe ist (das Interface) von wie sie gespeichert wird (die Implementierung) trennt. Der Nutzen zeigt sich täglich:
// You program against the interface, not the class.
List<String> names = new ArrayList<>();
// Later, swap in a different implementation without touching the rest of the code:
List<String> names = new LinkedList<>();Jede Methode, die auf names aufgerufen wird, ist auf List definiert. Die Wahl zwischen ArrayList und LinkedList ist eine Leistungsentscheidung, keine API-Entscheidung. Man kann auch Methoden wie void print(List<String> xs) einmal schreiben und beide Implementierungen übergeben.
Die vier Wurzel-Interfaces
Das Framework ist um vier Wurzel-Interfaces herum organisiert. Man wählt dasjenige, dessen Vertrag zu den eigenen Daten passt:
Collection<E>— die Wurzel. Eine Gruppe vonE-Elementen. Keine Aussage über Reihenfolge, Duplikate oder Indizierung.List<E>— eine geordnete Collection, zugänglich per Index. Duplikate erlaubt. Denk daran als „Array, das wächst" oder „verkettete Sequenz."ArrayList,LinkedList,Vector.Set<E>— eine Collection, die Duplikate verbietet. Mathematische Menge. Kann geordnet sein oder nicht.HashSet,LinkedHashSet,TreeSet.Map<K, V>— Schlüssel→Wert-Nachschlagen. KeineCollection(seine Elemente sind Einträge, keine Einzelwerte), gehört aber zum Framework.HashMap,LinkedHashMap,TreeMap.
Zwei weitere stehen neben List und Set als Spezialisierungen von Collection:
Queue<E>— eine „nächster in der Reihe"-Collection. Standardmäßig FIFO.LinkedList,ArrayDeque,PriorityQueue.Deque<E>— eine doppelseitige Queue. Hinzufügen und Entfernen an beiden Enden.ArrayDeque,LinkedList.
Jede Collection-Klasse in der Standardbibliothek implementiert mindestens eines dieser Interfaces.
Familie nach Verhalten wählen, dann Klasse nach Kosten
Die Entscheidung erfolgt in zwei Schritten:
- Was brauchen die Daten? Geordnet mit Duplikaten →
List. Keine Duplikate →Set. Nachschlagen per Schlüssel →Map. Produzent/Konsument →QueueoderDeque. - Welche Zugriffsmuster gibt es innerhalb der Familie? Wahlfreier Zugriff per Index →
ArrayList. Häufiges Hinzufügen/Entfernen am Anfang →LinkedListoderArrayDeque. Sortierte Iteration →TreeSet/TreeMap. Einfach „schnelle Menge gesucht" →HashSet/HashMap.
Eine grobe Übersicht für die häufigsten Fälle:
| Bedarf | Verwende | Hinweise |
|---|---|---|
| Veränderliche Liste, schneller wahlfreier Zugriff | ArrayList | Die Standard-List. |
| Liste mit sehr häufigem Einfügen/Entfernen an den Enden | ArrayDeque (als listenähnliche Queue) | Schlägt LinkedList in der Praxis. |
| Menge, schnelles contains/add/remove | HashSet | Keine Reihenfolgegarantie. |
| Menge mit vorhersehbarer Iteration | LinkedHashSet | Iteration in Einfügereihenfolge. |
| Sortierte Menge | TreeSet | O(log n) Operationen. |
| Map, schnelles Nachschlagen | HashMap | Die Standard-Map. |
| Map mit vorhersehbarer Iteration | LinkedHashMap | Nützlich für Caches (LRU). |
| Sortierte Map | TreeMap | Schlüssel in sortierter Reihenfolge. |
| FIFO-Queue | ArrayDeque | Schneller als LinkedList. |
| Immer-das-Kleinste-Entnehmen | PriorityQueue | Heap-basiert. |
Vector, Stack und Hashtable sind die Klassen aus der Zeit vor 1.2 — noch im JDK für Abwärtskompatibilität, aber keine gute Wahl für neuen Code. Ihre modernen Ersatzklassen werden in eigenen Kapiteln behandelt.
Generics machen Collections typsicher
Jedes Collection-Interface und jede Klasse ist generisch. Man parametrisiert es mit dem Elementtyp bei der Deklaration, und der Compiler erzwingt dies ab dann:
List<String> names = new ArrayList<>();
names.add("Ada");
names.add(42); // ❌ compile error — Integer is not a String
String first = names.get(0); // no cast neededDas Diamond-Symbol <> auf der rechten Seite weist den Compiler an, den Typ von der linken Seite abzuleiten — das spart Tipparbeit ohne den Sicherheitsvorteil zu verlieren. Der vorherige Teil des Buchs (Generics) ist das Regelwerk hinter all dem; der Rest dieses Teils setzt voraus, dass dieser gelesen wurde.
Algorithmen leben auf der Collections-Klasse
Operationen, die nicht an eine bestimmte Implementierung gebunden sind — sortieren, mischen, umkehren, Binärsuche, min, max, Häufigkeit, unveränderliche Wrapper — befinden sich auf der statischen Hilfsklasse java.util.Collections. Achtung auf das s am Ende: Collection (das Interface) ist ein Element; Collections (die Klasse) ist das Werkzeugkasten:
List<Integer> xs = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));
Collections.sort(xs); // mutates xs
int idx = Collections.binarySearch(xs, 5);
List<Integer> ro = Collections.unmodifiableList(xs);Gegen Ende dieses Teils widmen wir drei Kapitel Collections — Suchen, Sortieren und unveränderliche Wrapper — da die Hilfsklasse der Ort ist, an dem viel praktische Arbeit stattfindet.
Eine erste vollständige Tour
Das folgende Programm wählt einen Vertreter aus jeder Familie und zeigt die Operationen, die immer wieder verwendet werden. Keine Sorge, wenn noch nicht jede Zeile verständlich ist — jede Klasse hier bekommt ihr eigenes Kapitel. Ziel ist es, die Form des Frameworks zu sehen: gleiche Methodennamen, gleiche Muster, gleiche Iteration.
In der Ausgabe fallen drei Dinge auf:
- Das
Sethat das Duplikat"Effective Java"stillschweigend entfernt. Das ist der Vertrag — kein zusätzlicher Code auf der eigenen Seite nötig. - Die
Queuegab Elemente in der Reihenfolge zurück, in der sie hinzugefügt wurden.offerfügt ein,peekschaut auf den Kopf ohne zu entfernen,pollentfernt und gibt zurück. - Die
for-each-Schleife funktioniert auf jederCollection. Maps iterieren überentrySet(),keySet()odervalues(), je nachdem was benötigt wird.
Das ist das Framework in der Kurzfassung.
HashSet und HashMap machen keine Aussage über die Iterationsreihenfolge — die oben in der Ausgabe gezeigte Reihenfolge ist ein Implementierungsdetail und kann zwischen JVM-Versionen oder Läufen variieren. Wenn eine stabile Reihenfolge benötigt wird, greife auf LinkedHashSet/LinkedHashMap (Einfügereihenfolge) oder TreeSet/TreeMap (sortierte Reihenfolge) zurück. Schreibe niemals Code, der von der Iterationsreihenfolge eines einfachen HashSet oder HashMap abhängt.
Was kommt als Nächstes
Die Karte ist nun bekannt. Der Rest von Teil 11 erkundet jede Region. Der natürliche Ausgangspunkt ist die Wurzel, von der jede Collection erbt — das Collection-Interface selbst, wo Methoden wie add, remove, contains, size und iterator() ein für alle Mal definiert sind.