Java ArrayDeque
ArrayDeque für schnelle, dynamisch wachsende doppelseitige Warteschlangen und Stacks in Java nutzen.
ArrayDeque<E> ist ein zirkuläres Array, das bei Bedarf wächst. Es implementiert sowohl Queue<E> als auch Deque<E> und kann daher ohne Codeänderungen als FIFO-Warteschlange, LIFO-Stack oder doppelseitige Warteschlange eingesetzt werden. Das Javadoc zu Stack empfiehlt Deque gegenüber der Legacy-Klasse; das Javadoc zu Deque ergänzt die praktische Empfehlung, dass ArrayDeque Ihre Standardimplementierung sein sollte. Es ist fast immer schneller als LinkedList, verbraucht weniger Speicher und ist die bevorzugte Wahl der Standardbibliothek, sobald man das List-Interface nicht mehr benötigt.
Warum ein zirkuläres Array
Eine klassische „Warteschlange aus einem Array" hat ein Problem: Nach wenigen Zyklen von Hinzufügen am Ende und Entfernen am Anfang füllt sich das Array am Ende, während am Anfang leere Slots entstehen. Eine naive Lösung wäre, bei jedem Entfernen alles nach links zu verschieben — O(n) pro Entfernung, was die Performance ruiniert.
Der zirkuläre-Array-Trick löst das Problem. Die Klasse hält zwei Indizes, head und tail, und lässt sie über das Ende hinauslaufen:
slots: [ . . . D E F G H A B C ]
^head ^... wraps to slot 0 ... ^tailaddFirst dekrementiert head, addLast inkrementiert tail, beides modulo der Array-Länge. removeFirst und removeLast machen das Umgekehrte. Jede Operation ist O(1); die einzigen Kosten entstehen beim Verdoppeln des Backing-Arrays, wenn head mit tail kollidieren würde — dies wird amortisiert.
Das Ergebnis: keine Knoten-Allokation pro Element, keine Zeiger-Verfolgung, cache-freundlicher Zugriff. Das ist der technische Grund, warum ArrayDeque für Warteschlangen- und Stack-Workloads meist die schnellste Wahl ist.
Das Deque-Interface im Überblick
Ein Deque hat zwei Enden. Jede Operation gibt es in drei Varianten:
| Operation | Erstes (head) — wirft Exception | Erstes (head) — Sonderwert | Letztes (tail) — wirft Exception | Letztes (tail) — Sonderwert |
|---|---|---|---|---|
| Einfügen | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
| Entfernen | removeFirst() | pollFirst() | removeLast() | pollLast() |
| Betrachten | getFirst() | peekFirst() | getLast() | peekLast() |
Darüber hinaus werden die Queue- und Stack-Synonyme auf das head-Ende abgebildet:
| Queue-Methode | Deque-Äquivalent |
|---|---|
add(e) / offer(e) | addLast(e) / offerLast(e) |
remove() / poll() | removeFirst() / pollFirst() |
element() / peek() | getFirst() / peekFirst() |
| Stack-Methode | Deque-Äquivalent |
|---|---|
push(e) | addFirst(e) |
pop() | removeFirst() |
peek() | peekFirst() |
Deque ist also ein Interface, das gleichzeitig eine Warteschlange und einen Stack bereitstellt — je nachdem, welchen Namen man für das head-Ende verwendet. Diese Abbildungen wurden in den Kapiteln zu Queue und Stack behandelt; Deque ist der Punkt, an dem sie zusammenlaufen.
Was ArrayDeque zusätzlich bietet
In der Praxis ist es API-seitig kaum mehr — das ist der Sinn. Die Klasse stellt den Deque-Vertrag zuverlässig bereit: die zwei Enden, die drei Fehlerbehandlungsstile pro Operation sowie clear(), iterator(), descendingIterator(), contains, size, isEmpty. Die Iteration mit dem Standarditerator erfolgt von head nach tail; descendingIterator ist der günstige Weg, von tail nach head zu traversieren, ohne umzukehren.
Die Konstruktoren entsprechen denen von ArrayList:
Deque<String> a = new ArrayDeque<>(); // initial capacity 16
Deque<String> b = new ArrayDeque<>(1_000); // pre-sized
Deque<String> c = new ArrayDeque<>(other); // from any Collection (head = first iter element)Vorbelegen, wenn die Arbeitslast bekannt ist. Der Standardwert 16 wird auf die nächste Zweierpotenz aufgerundet, und das Wachstum verdoppelt das Array — eine Million offerLast-Aufrufe ausgehend von einem Standard-Deque lösen etwa 16 Vergrößerungs- und Kopiervorgänge aus.
Die Regeln: keine null-Werte, nicht thread-sicher, fail-fast
Drei Regeln, die man verinnerlichen sollte:
ArrayDequelässt keinenull-Elemente zu. Jeder Einfügevorgang wirft eineNullPointerException. So bleibt eindeutig, dasspollFirst()bei einer leeren Dequenullzurückgibt.- Nicht thread-sicher. Zwei Threads, die dieselbe
ArrayDequeverändern, korrumpieren sie. Für parallelen Zugriff sollte manConcurrentLinkedDeque(sperrfrei, unbegrenzt) oderLinkedBlockingDeque(begrenzt, blockierend) verwenden. - Fail-fast-Iteration. Eine Änderung der Deque außerhalb des Iterators während der Iteration wirft eine
ConcurrentModificationException. VerwendeIterator.remove()oderremoveIffür sichere Mutation.
Wann ArrayDeque gegenüber Alternativen bevorzugt werden sollte
Der Entscheidungsfluss:
- Brauche ich eine
Queue? →ArrayDeque. Standardwahl. - Brauche ich einen Stack? →
ArrayDeque. Standardwahl.push/pop/peekverwenden. - Brauche ich eine
Deque? →ArrayDeque. Standardwahl. - Brauche ich sowohl
Deque- als auchList-Semantik am selben Objekt? →LinkedList. Selten. - Brauche ich eine Prioritätsreihenfolge? →
PriorityQueue. Andere Abstraktion. - Brauche ich gleichzeitigen Zugriff? →
ConcurrentLinkedDeque(unbegrenzt) oderLinkedBlockingDeque(begrenzt). Die richtige Wahl hängt davon ab, ob Gegendruck benötigt wird.
Das Javadoc formuliert die Empfehlung im Klartext: „This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue." Das stammt direkt von den JDK-Autoren und ist wörtlich zu nehmen.
Ein durchgearbeitetes Beispiel: Warteschlange, Stack, Deque, gleitendes Fenster
Das folgende Programm verwendet eine ArrayDeque-Instanz als FIFO-Warteschlange, eine weitere als LIFO-Stack und eine dritte als Speicher hinter einem gleitenden Fenster-Maximum — ein klassisches Problem, bei dem ArrayDeque die Lehrbuchantwort ist, weil man günstige Mutation an beiden Enden benötigt.
Was man aus diesem Ablauf mitnehmen kann:
- Abschnitte 1 und 2 zeigen dieselbe Klasse, die zwei Aufgaben übernimmt, indem verschiedene Methoden aufgerufen werden. FIFO-Warteschlange:
offerLast/pollFirst. LIFO-Stack:push/pop. Es gibt keine separate Klasse zu erlernen. descendingIterator()ist der günstige Rückwärtsdurchlauf — nützlich, wenn man eine Deque als Stack aufgebaut hat und sie von unten nach oben ausgeben möchte.- Die Sliding-Window-Funktion verwendet beide Enden in derselben Schleife —
pollFirst, um Indizes zu entfernen, die aus dem Fenster gefallen sind,pollLast, um einen monoton absteigenden Stack zu erhalten,peekFirst, um das aktuelle Maximum zu lesen. Dieser beidseitige Zugriff ist der Grund, warumArrayDequeexistiert; der Versuch, dies mit einerArrayListzu tun, wäre quadratisch.
Was kommt als Nächstes
Du hast gesehen, wie ArrayDeque beide Enden der Warteschlangen-/Stack-Geschichte implementiert. Das Interface, das es die ganze Zeit implementiert, verdient ein eigenes Kapitel — Deque ist der Vertrag, der alles zusammenführt, was wir über warteschlangenartige Collections behandelt haben. Das nehmen wir in der nächsten Sitzung auf.