Java Stack
LIFO-Stack-Datenstrukturen in Java — die veraltete Stack-Klasse und die empfohlene ArrayDeque-basierte Alternative.
Ein Stack ist eine Last-in-first-out-Collection: Man pusht ein Element hinein, und das nächste pop gibt das zuletzt hinzugefügte zurück. Java bietet zwei Möglichkeiten, einen Stack zu erstellen — die veraltete Klasse java.util.Stack aus dem Jahr 1995 und die moderne Empfehlung, Deque<E> (typischerweise ArrayDeque) zu verwenden. Beide funktionieren und haben dieselben konzeptuellen Operationen, aber die offizielle Javadoc von Stack selbst empfiehlt, Deque zu bevorzugen. Dieses Kapitel zeigt, wie Stack aussieht, warum das JDK heute auf eine andere Lösung verweist und wie man den empfohlenen Ersatz verwendet.
Wofür ein Stack verwendet wird
Stacks tauchen in allen Algorithmen auf, die von Natur aus Last-in-first-out funktionieren:
- Rückgängig machen / Wiederherstellen von Historien — jede neue Aktion wird gepusht; Rückgängig macht die letzte pop.
- Parser- und Interpreter-Zustand — Ausdrucksauswertung, Prüfung auf ausgewogene Klammern, Aufrufrahmen.
- Tiefensuche — Nachfolger pushen, nächsten zu besuchenden Knoten poppen.
- Dinge umkehren — jedes Element einer Sequenz pushen, dann wieder poppen.
Die Form ist immer dieselbe: push, pop, peek, isEmpty, size. Fünf Operationen.
Die veraltete Klasse Stack
java.util.Stack<E> erweitert Vector<E>, was bedeutet, dass sie alles aus dem vorherigen Kapitel erbt — einschließlich des per-Methode synchronized, der veralteten Methodennamen und der ungewöhnlichen Tatsache, dass es eine List ist. Ein Stack ist durch Vererbung eine indizierbare Liste — man kann stack.get(3) darauf aufrufen, was genau die Art von API-Fehler ist, die die Empfehlung motiviert, stattdessen Deque zu verwenden.
Stack<String> s = new Stack<>();
s.push("a");
s.push("b");
s.push("c");
System.out.println(s.peek()); // c
System.out.println(s.pop()); // c
System.out.println(s.size()); // 2
System.out.println(s.empty()); // false (note: empty(), not isEmpty())Sechs Methoden spezifisch für Stack:
E push(E item)— fügt oben hinzu. Gibt das Element zurück.E pop()— entfernt und gibt das oberste Element zurück. WirftEmptyStackExceptionwenn leer.E peek()— oberstes Element ohne Entfernen. Gleiche Ausnahme.boolean empty()— beachte die Kleinschreibung, unterscheidet sich vonCollection.isEmpty().int search(Object o)— 1-basierter Abstand von der Spitze oder-1wenn nicht vorhanden. Seltsame Wahl — das Ergebnis ist "wie vielepops bisoerreicht ist", was selten nützlich ist.
Alles andere wird von Vector und List geerbt.
Warum die Javadoc auf Deque verweist
Öffnet man die tatsächliche Javadoc für Stack in einem modernen JDK, findet man folgende Zeile:
A more complete and consistent set of LIFO stack operations is provided by the
Dequeinterface and its implementations, which should be used in preference to this class.
Die Gründe decken sich mit dem vorherigen Kapitel über Vector:
Stackist bei jeder Methode synchronisiert. Man zahlt die Sperrkosten auch in einThreadigem Code, und die Synchronisierung hat die falsche Granularität für zusammengesetzte Operationen.Stackist eineList. Indizierten Zugriff auf einem Stack zu haben ist ein kategorischer Fehler — man kann in die Mitte schauen, was die LIFO-Disziplin verletzt, die die Klasse eigentlich durchsetzen soll.- Die fünf Stack-Operationen, die man tatsächlich braucht, sind bereits in
Deque(push,pop,peek,isEmpty,size) vorhanden, undArrayDequeist die schnellste Implementierung im JDK.
Kurz gesagt: Stack funktioniert, ist aber ein Design von 1995, das in ein Framework von 1998 gepresst wurde. Neuer Code sollte Deque verwenden.
Der empfohlene Ersatz
Das Idiom ist eine Zeile:
Deque<String> stack = new ArrayDeque<>();Dann ruft man push, pop, peek auf. Das Deque-Interface definiert sie intern als addFirst, removeFirst, peekFirst, sodass die Spitze des Stacks der Kopf der Deque ist.
Deque<String> calls = new ArrayDeque<>();
calls.push("main");
calls.push("parseArgs");
calls.push("split");
System.out.println(calls.peek()); // split
System.out.println(calls.pop()); // split
System.out.println(calls); // [parseArgs, main]Drei Details zu beachten:
peek()auf einerDequegibtnullzurück wenn leer;peek()aufStackwirft eine Ausnahme.peekFirst()hat ebenfalls die null-zurückgebende Form. Wenn man stattdessen eine Ausnahme möchte, ruft mangetFirst()(oderelement()) auf.- Die gleiche Unterscheidung zwischen
pop()/removeFirst()gilt:pop()wirft eine Ausnahme wenn leer. ArrayDequelehntnull-Elemente ab. Man sollte es nur für Nicht-null-Werte verwenden — was der übliche Fall bei Stacks ist.
Wann Stack in neuem Code akzeptabel ist
Fast nie, und die Hürde ist niedrig:
- Pflege von altem Code, der ihn bereits verwendet — nicht nur wegen des Klassen-Austauschs umbauen.
- Arbeit mit einer API, die explizit
Stackerfordert — äußerst selten. Die Swing-Klassen, dieVectorund ähnliche verwendeten, tendieren nicht dazu,Stackzu verlangen.
Für alles andere: Deque<E> deklarieren und ArrayDeque<> instanziieren.
Ein ausgearbeitetes Beispiel: Klammerprüfer auf zwei Arten
Das folgende Programm verwendet sowohl Stack als auch Deque<Character>, um das klassische Problem der ausgewogenen Klammern zu lösen. Gleicher Algorithmus, zwei Implementierungen — nebeneinander lesen, dann die Deque-Form betrachten und entscheiden, welche man in drei Monaten lieber lesen würde.
Was zu beachten ist:
- Die beiden Funktionen sind bis auf den Typnamen und
empty()vsisEmpty()identisch. Es gibt keinen algorithmischen Grund,Stackzu bevorzugen — der Code liest sich genauso. - Der Block "Stack-specific API" am Ende ist das Argument gegen die veraltete Klasse:
empty()ist ein anderer Name als der Rest des Frameworks,search()gibt einen 1-basierten Abstand zurück (im restlichen Java selten), undget(0)erlaubt es, in die Mitte eines Stacks zu greifen — was den gesamten Zweck der Abstraktion untergräbt.
Was kommt als Nächstes
Sie haben nun die drei Hauptimplementierungen von List (ArrayList, LinkedList, Vector) und die LIFO-Spezialisierung gesehen, die auf Vector aufgebaut ist. Die nächsten vier Kapitel behandeln die parallele Geschichte für Dinge-die-man-in-Reihenfolge-hinzufügt-und-entnimmt: das Queue-Interface, PriorityQueue, ArrayDeque und den allgemeineren Deque-Vertrag.