こんにちは、かつコーチです。
「スタックとキューって結局何が違うの?」「Stackクラスは使わない方がいいって聞いたけど本当?」という質問をよくもらいます。
今回はJava 21時点でのDeque・Stack・Queueの使い分けを、実際に手を動かしながら整理していきます。
Deque・Stack・Queueとは?
それぞれの役割の違い
Queue(キュー、先に入れたものを先に取り出すFIFO構造を扱うインターフェース)とStack(スタック、後に入れたものを先に取り出すLIFO構造)は、どちらもデータの出し入れの順番を管理する仕組みです。
Deque(デック、両端キューと訳され、先頭と末尾の両方から要素の追加・削除ができるインターフェース)は、このQueueとStackの両方の役割を1つでこなせる万能型です。
| 用語 | 読み方 | 出し入れの順番 | イメージ |
|---|---|---|---|
| Queue | キュー | FIFO(先入れ先出し) | 行列に並ぶ |
| Stack | スタック | LIFO(後入れ先出し) | 本を積み重ねる |
| Deque | デック | 両端から自由に | 両側が開いた筒 |
なぜArrayDequeを使うべきなのか
Javaには古くからjava.util.Stackクラスが存在しますが、現在はArrayDequeを使うのが推奨です。Stackは内部でVector(同期処理が入る古いクラス)を継承しており、シングルスレッドで使う分にも同期のオーバーヘッドがかかってしまうためです。
公式のJavadocにも「Dequeインターフェースの方がStackクラスより高速で好ましい」という趣旨の記載があります。
基本の書き方
ArrayDequeをStack(後入れ先出し)として使う
スタックとして使う場合は、push・pop・peekの3つのメソッドを使います。
import java.util.ArrayDeque;
import java.util.Deque;
Deque<String> stack = new ArrayDeque<>();
stack.push("1件目");
stack.push("2件目");
stack.push("3件目");
System.out.println(stack.pop()); // 3件目(最後に入れたものが先に出る)
System.out.println(stack.peek()); // 2件目(先頭を覗くだけで取り出さない)
System.out.println(stack); // [2件目, 1件目]
「元に戻す(Undo)」機能や、再帰処理を自前でスタック管理する場面などでよく使われる形です。
ArrayDequeをQueue(先入れ先出し)として使う
キューとして使う場合は、offer・poll・peekを使います。
Deque<String> queue = new ArrayDeque<>();
queue.offer("1番目の客");
queue.offer("2番目の客");
queue.offer("3番目の客");
System.out.println(queue.poll()); // 1番目の客(先に入れたものが先に出る)
System.out.println(queue.peek()); // 2番目の客
System.out.println(queue); // [2番目の客, 3番目の客]
同じArrayDequeのインスタンスでも、呼び出すメソッドを変えるだけでスタックにもキューにもなる点がポイントです。
両端から操作するDeque本来の使い方
Dequeという名前の通り、先頭(First)と末尾(Last)を明示して操作するメソッドも用意されています。
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1); // 先頭に追加
deque.addLast(2); // 末尾に追加
deque.addFirst(0); // 先頭に追加
System.out.println(deque); // [0, 1, 2]
System.out.println(deque.pollFirst()); // 0(先頭を取り出す)
System.out.println(deque.pollLast()); // 2(末尾を取り出す)
System.out.println(deque); // [1]
「直近3件の履歴だけ保持したい」といった、先頭と末尾の両方を扱う用途に向いています。
よくあるつまずきポイント・エラー対処
空のDequeでpop・removeを呼んで例外が出た
私が実際にハマった一次情報として、空になったスタックに気づかずpop()を呼んで実行時エラーになったことがあります。
// ❌Before:空チェックをせずにpopを呼ぶ
Deque<String> stack = new ArrayDeque<>();
String top = stack.pop(); // 実行時エラー
このコードを実行すると、次の例外が発生します。
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.ArrayDeque.pop(ArrayDeque.java:551)
pop()やremove()、element()系のメソッドは、要素が無いと例外を投げる仕様になっています。
一方でpoll()やpeek()系は、要素が無い場合に例外ではなくnullを返すという違いがあり、この使い分けを知らないと予期しない場所で落ちてしまいます。
// ✅After:isEmptyで確認するか、poll系のnull許容メソッドを使う
Deque<String> stack = new ArrayDeque<>();
if (!stack.isEmpty()) {
String top = stack.pop();
} else {
System.out.println("スタックは空です");
}
// もしくはpollを使ってnullで判定する
String top2 = stack.poll();
if (top2 == null) {
System.out.println("スタックは空です");
}
「例外を投げる系」と「nullを返す系」のメソッドがペアで用意されているのはQueue・Dequeインターフェース共通の設計なので、覚えておくと他のメソッドでも迷わなくなります。
応用・一歩先の使い方
LinkedListもDequeを実装している
実はLinkedListもDequeインターフェースを実装しているため、Dequeとして扱うことができます。
Deque<String> linkedDeque = new java.util.LinkedList<>();
linkedDeque.addFirst("A");
linkedDeque.addLast("B");
System.out.println(linkedDeque); // [A, B]
ただし、要素数が事前にある程度想定できるならArrayDequeの方が一般的に高速です。
LinkedListは各要素をノードとして管理するためメモリのオーバーヘッドが大きく、公式Javadocでも単一スレッド用途ではArrayDequeが推奨されています。
優先度付きで処理したいならPriorityQueue
「先入れ先出し」ではなく「優先度の高いものから処理したい」場合は、Queueの実装であるPriorityQueueを使います。
import java.util.PriorityQueue;
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5);
pq.offer(1);
pq.offer(3);
System.out.println(pq.poll()); // 1(最小値から取り出される)
System.out.println(pq.poll()); // 3
タスクの優先度管理やダイクストラ法などのアルゴリズムでよく登場するので、Dequeと合わせて覚えておくと応用範囲が広がります。
まとめ
この記事のポイント
- QueueはFIFO(先入れ先出し)、StackはLIFO(後入れ先出し)の構造
- Dequeは両端から操作できるインターフェースで、Stack・Queueどちらの役割もこなせる
- 新規実装では古い
StackクラスではなくArrayDequeを使うのが推奨 pop・remove系は例外を投げ、poll・peek系はnullを返すという使い分けがある- 優先度順に処理したい場合は
PriorityQueueを使う
次に読むべき記事
- Iteratorの仕組みと拡張for文との関係
- Collectionsユーティリティクラスの活用
タグ: Java, 中級者向け, コレクション
