「Stack代替」は“古い Stack を捨てて、Deque に乗り換える”という発想
Java には java.util.Stack というクラスがありますが、
今の実務では 「Stack は使わず、Deque(特に ArrayDeque)を使う」 のが定石です。
理由はシンプルで、Stack は古い設計(Vector 継承・全部同期)で、
性能も使い勝手も微妙だからです。
代わりに、Deque を「スタックとして使う」ことで、
よりシンプルで高速なコードが書けます。
ここでは、
「なぜ Stack をやめるのか」
「Deque でどう書き換えるのか」
を、初心者向けにかみ砕いて説明します。
なぜ java.util.Stack を使わない方がいいのか
Stack の正体は「同期付き Vector」
Stack の宣言を辿ると、こうなっています。
public class Stack<E> extends Vector<E> {
...
}
JavaVector は「全部のメソッドが synchronized な List」です。
つまり Stack は、「全部の操作がロック付きの List」でもあります。
ここで問題になるのは二つです。
一つ目は、不要な同期で遅くなりやすい こと。
単一スレッドでしか使わないのに、毎回ロックを取るのは無駄です。
二つ目は、API が古くて、今のコレクション設計と馴染まない こと。push / pop / peek などは便利ですが、List としての顔も持っていて、意図がぼやけやすいです。
「新しく書くコードで Stack を選ぶ理由はほぼない」
というのが、今の Java の世界観です。
代替の主役:Deque(特に ArrayDeque)でスタックを書く
Deque を「スタックモード」で使う
Deque は「両端キュー」ですが、
「片側だけを使う」と、きれいにスタックになります。
import java.util.ArrayDeque;
import java.util.Deque;
public class DequeStackExample {
public static void main(String[] args) {
Deque<String> stack = new ArrayDeque<>();
// push 相当
stack.addLast("A");
stack.addLast("B");
stack.addLast("C");
// pop 相当
System.out.println(stack.pollLast()); // C
System.out.println(stack.pollLast()); // B
System.out.println(stack.pollLast()); // A
System.out.println(stack.pollLast()); // null(空)
}
}
Javaここでの重要ポイントは三つです。
一つ目は、「末尾を“上”とみなしている」ことです。addLast で積み、pollLast で取り出す——これがスタックの動きです。
二つ目は、「pollLast は空のときに null を返す」ことです。Stack#pop は空だと例外を投げるので、
「空かもしれない」前提なら Deque の方が扱いやすいです。
三つ目は、「ArrayDeque は同期なしで軽量」なことです。
単一スレッド、または外側で必要なところだけ同期を取る設計にしやすくなります。
Stack から Deque への書き換え例
典型的な Stack コードを Deque に置き換える
よくある Stack のコードを一つ挙げます。
import java.util.Stack;
Stack<String> stack = new Stack<>();
stack.push("A");
stack.push("B");
stack.push("C");
while (!stack.isEmpty()) {
String s = stack.pop();
System.out.println(s);
}
Javaこれを Deque に書き換えると、こうなります。
import java.util.ArrayDeque;
import java.util.Deque;
Deque<String> stack = new ArrayDeque<>();
stack.addLast("A");
stack.addLast("B");
stack.addLast("C");
while (!stack.isEmpty()) {
String s = stack.pollLast();
System.out.println(s);
}
Javaここで深掘りしたいポイントは二つです。
一つ目は、「メソッド名が変わっただけで、ロジックの構造はまったく同じ」ことです。push → addLast、pop → pollLast に置き換えるだけで、
スタックとしての意味はそのまま保てます。
二つ目は、「Deque を使うことで、“スタック専用クラス”に縛られなくなる」ことです。
同じ Deque を、状況によってキューとしてもスタックとしても使えます。
設計の自由度が上がります。
実務での具体例:括弧の対応チェックを Deque で書く
「LIFO の典型問題」を Stack ではなく Deque で解く
括弧の対応チェックは、スタックの典型例です。
public class ParenthesesChecker {
public static boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.addLast(c); // push
} else if (c == ')') {
if (stack.isEmpty()) {
return false;
}
stack.pollLast(); // pop
}
}
return stack.isEmpty();
}
public static void main(String[] args) {
System.out.println(isValid("()()")); // true
System.out.println(isValid("(()")); // false
System.out.println(isValid("())(")); // false
}
}
Javaここでの重要ポイントは三つです。
一つ目は、「“最後に開いた括弧から閉じていく”という LIFO の性質を、Deque で素直に表現している」ことです。
二つ目は、「ArrayDeque を使うことで、Stack より軽量で今風のコードになっている」ことです。
三つ目は、「初心者にも“スタック=Deque の末尾操作”というイメージを持ってもらえる」ことです。
これを一度掴むと、Stack に戻る理由がなくなります。
スレッドセーフなスタックが欲しいときは?
Stack ではなく、ConcurrentLinkedDeque などを検討する
「複数スレッドから同時に push/pop したいスタック」が必要な場合、Stack を選ぶのはおすすめしません。
代わりに、ConcurrentLinkedDeque を「スタックモード」で使う方が今風です。
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
Deque<String> stack = new ConcurrentLinkedDeque<>();
stack.addLast("A"); // push
String v = stack.pollLast(); // pop
Javaここでのポイントは、
「“スタックかどうか”より、“スレッドセーフかどうか”で実装を選ぶ」
という発想です。
単一スレッドなら ArrayDeque、
マルチスレッドなら ConcurrentLinkedDeque、
というように、「Deque + 実装クラス」で考える癖をつけると、設計がきれいになります。
まとめ:Stack代替で身につけてほしい感覚
Stack代替の本質は、
「古い Stack クラスにしがみつかず、Deque を軸にスタックを設計する」
という発想の転換です。
Stack は古い・重い・今の設計と馴染まない。
代わりに Deque(特に ArrayDeque)で「末尾をスタックの上」と決めて、addLast / pollLast で書く。
スレッドセーフが必要なら、ConcurrentLinkedDeque などの Deque 実装を選ぶ。
「スタック専用クラス」ではなく、「Deque の使い方」として LIFO を捉える。
あなたのコードのどこかに、new Stack<>() がまだ生き残っていたら、
それを一度 Deque ベースに書き換えられないか眺めてみてください。
その小さな置き換えが、
「古い API に引きずられず、今の Java の流儀でコレクションを選べるエンジニア」への、
確かな一歩になります。
