Java Deque Interface
Operationen an doppelseitigen Warteschlangen in Java mit dem Deque-Interface — addFirst, addLast, pollFirst, pollLast.
Deque<E> — ausgesprochen „Deck", kurz für double-ended queue (doppelseitige Warteschlange) — ist Queue<E> mit einem zweiten Ende. Während eine Queue das Entfernen nur am Kopf erlaubt, erlaubt eine Deque das Einfügen, Entfernen und Untersuchen an beiden Enden — am Kopf und am Ende. Diese eine zusätzliche Fähigkeit reicht aus, um Deque zum Vertrag hinter zwei völlig unterschiedlichen Abstraktionen zu machen: Es ist eine Warteschlange, es ist ein Stack, und das JDK empfiehlt es standardmäßig für beide.
Zwei Enden × drei Operationen × zwei Fehlerstile
Das Interface wirkt auf den ersten Blick einschüchternd, weil jede Operation in zwölf Varianten erscheint — Kopf vs. Ende, Einfügen vs. Entfernen vs. Untersuchen, Ausnahme vs. Sonderwert. Es ist aber lediglich das Queue-3×2-Raster auf zwei Enden ausgedehnt:
| Operation | Erstes Element (Kopf) — wirft | Erstes Element (Kopf) — Sonderwert | Letztes Element (Ende) — wirft | Letztes Element (Ende) — Sonderwert |
|---|---|---|---|---|
| Einfügen | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
| Entfernen | removeFirst() | pollFirst() | removeLast() | pollLast() |
| Untersuchen | getFirst() | peekFirst() | getLast() | peekLast() |
Die Spalte „wirft" ist diejenige, die man aufruft, wenn eine leere oder volle Deque an dieser Stelle ein Fehler wäre. Die Spalte „Sonderwert" ist diejenige, die man aufruft, wenn eine leere Deque ein erwarteter Zustand ist und man darauf in einer Schleifenbedingung prüfen möchte.
Die Queue-Zuordnung
Deque erweitert Queue, sodass eine Deque auch eine Queue ist. Die geerbten Methoden sind Aliase für die kopfseitigen Varianten:
Queue-Methode | Deque-Äquivalent |
|---|---|
add(e) / offer(e) | addLast(e) / offerLast(e) |
remove() / poll() | removeFirst() / pollFirst() |
element() / peek() | getFirst() / peekFirst() |
Am Ende einfügen, am Kopf entfernen — das ist die FIFO-Disziplin, die eine Queue bereits bietet, nur mit den endseitig expliziten Namen geschrieben.
Die Stack-Zuordnung
Deque definiert außerdem drei „Stack"-Methoden, die alle am Kopf wirken:
| Stack-Methode | Deque-Äquivalent |
|---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
Obenauf schieben, oben abnehmen — LIFO-Disziplin, dasselbe Kopfende, entgegengesetztes Ende im Vergleich zur Queue-Zuordnung. Deshalb hat das Stack-Kapitel auf dieses hier verwiesen: Der moderne Weg, in Java einen Stack zu schreiben, ist Deque<E> stack = new ArrayDeque<>(); und dann push / pop / peek aufzurufen.
Die Regel: null ist nicht erlaubt
Wie bei Queue reserviert der Deque-Vertrag null als „leeres"-Sentinel für pollFirst, pollLast, peekFirst, peekLast. Daher lehnen alle universellen Deque-Implementierungen im JDK null-Elemente beim Einfügen mit einer NullPointerException ab. Die einzige Ausnahme ist LinkedList, die aus historischen Gründen null erlaubt — und alle damit verbundenen Mehrdeutigkeiten erbt. Das sollte man vermeiden.
Iteration und Rückwärtsiteration
Eine Deque besitzt per Konvention zwei Iteratoren:
iterator()läuft von Kopf zu Ende — in der Reihenfolge, in der man wiederholtpollFirstaufrufen würde.descendingIterator()läuft von Ende zu Kopf — in der Reihenfolge, in der man wiederholtpollLastaufrufen würde.
Beide sind typischerweise fail-fast: Das Verändern der Deque von außerhalb des Iterators während der Iteration wirft eine ConcurrentModificationException. Verwende Iterator.remove() oder removeIf, wenn du während des Durchlaufens mutieren musst.
Eine Implementierung wählen
Im single-threaded Code gibt es im Wesentlichen zwei Möglichkeiten:
ArrayDeque— die Standardwahl. Zirkuläres Array, O(1) an beiden Enden, keine Knotenallokation pro Element, cache-freundlich. Lehntnullab. ImplementiertDequeundQueue.LinkedList— doppelt verkettete Knoten. Implementiert auchList, sodass jedes Element einen Knoten mitprev/next-Zeigern erhält. An beiden Enden langsamer alsArrayDequeund speicherintensiver. Nur wählen, wenn man tatsächlichDeque- undList-Semantik am selben Objekt benötigt.
Für nebenläufigen Code (später im Nebenläufigkeitsteil des Buches behandelt):
ConcurrentLinkedDeque— sperrfrei, unbegrenzt.LinkedBlockingDeque— optional begrenzt, blockierend — die Variante, die man zum Aufbau einer Work-Stealing-Queue oder eines Producer/Consumer mit Gegendruck an beiden Enden verwenden würde.
Ein durchgearbeitetes Beispiel: Queue, Stack und Palindromprüfung in einer Deque
Das folgende Programm verwendet eine ArrayDeque als FIFO-Warteschlange, eine weitere als LIFO-Stack und eine dritte, um zu prüfen, ob ein Satz ein Palindrom ist, indem beide Enden befüttert und verglichen werden. Der Punkt ist, dass dasselbe Interface, sogar dieselbe Klasse, alle drei Rollen übernimmt.
Was dieser Durchlauf zeigt:
- Dieselbe Klasse füllt sowohl Queue- als auch Stack-Rollen aus, ohne eine einzige Methode umzubenennen — außer an welchem Ende man angreift.
- Die „Sonderwert"-Methoden liefern bei leerem Zustand still
nullzurück; die „wirft"-Methoden lösenNoSuchElementExceptionaus. Man wählt nach dem Kriterium, ob Leerheit erwartet oder ein Fehler ist. - Die Palindromprüfung verwendet beide Enden in derselben Schleife —
pollFirstundpollLastzusammen. Das ist das Zugriffsmuster, das nur eineDequegünstig ermöglicht.
Was kommt als Nächstes
Das Deque-Kapitel schließt die warteschlangenseitige Betrachtung des Frameworks ab. Die andere große Abstraktion in Collection ist diejenige, die Duplikate ablehnt: Ein Set ist eine Collection mit eingebautem Eindeutigkeitsvertrag. Das schauen wir uns als Nächstes an.