- JAVA
Java Queue・Deque・Stackの使い方|ArrayDeque早見表
Javaでキュー(先入れ先出し)もスタック(後入れ先出し)も、まず ArrayDeque を選べば用が足ります。 キューなら offer・poll・peek、スタックなら push・pop・peek の3つを覚えるのが近道です。優先度順に取り出したいときは PriorityQueue、スレッド間で受け渡すときは BlockingQueue を使います。
この記事では、名前が似ていて迷いやすい 全メソッドを「例外を投げるか、null/false を返すか」で一覧表に整理し、OpenJDK 21.0.10 で実行した出力を載せています。ArrayDeque に null を入れたときの例外、PriorityQueue を for で回したときの並び、LinkedList との速度計測、BFS・DFS・括弧の対応チェックのコピペ用コードまで、1本で確認できます。
Queue・Deque・Stackの早見表
どのクラスを使うか
| やりたいこと | 宣言の書き方 | 入れる/取り出す/のぞく |
|---|---|---|
| キュー(先に入れたものから出す) | Queue<T> q = new ArrayDeque<>(); |
offer(e)/poll()/peek() |
| スタック(後に入れたものから出す) | Deque<T> s = new ArrayDeque<>(); |
push(e)/pop()/peek() |
| 両端キュー(両側から出し入れ) | Deque<T> d = new ArrayDeque<>(); |
offerFirst・offerLast/pollFirst・pollLast/peekFirst・peekLast |
| 優先度の小さい順に出す | Queue<T> pq = new PriorityQueue<>(); |
offer(e)/poll()/peek() |
| 大きい順に出す(最大ヒープ) | new PriorityQueue<>(Comparator.reverseOrder()) |
同上 |
| スレッド間で受け渡す | BlockingQueue<T> bq = new ArrayBlockingQueue<>(容量); |
put(e)/take()(待つ) |
全メソッド:例外か、null/false か
| 操作 | 対象の端 | 例外を投げる | 特別な値を返す |
|---|---|---|---|
| 入れる(Queue) | 末尾 | add(e) → 満杯で IllegalStateException |
offer(e) → 満杯で false |
| 取り出す(Queue) | 先頭 | remove() → 空で NoSuchElementException |
poll() → 空で null |
| のぞく(Queue) | 先頭 | element() → 空で NoSuchElementException |
peek() → 空で null |
| 入れる(Deque) | 先頭 | addFirst(e) |
offerFirst(e) |
| 入れる(Deque) | 末尾 | addLast(e) |
offerLast(e) |
| 取り出す(Deque) | 先頭 | removeFirst() |
pollFirst() |
| 取り出す(Deque) | 末尾 | removeLast() |
pollLast() |
| のぞく(Deque) | 先頭 | getFirst() |
peekFirst() |
| のぞく(Deque) | 末尾 | getLast() |
peekLast() |
| 積む(スタック) | 先頭 | push(e)(=addFirst) |
(offerFirst(e) を使う) |
| 取る(スタック) | 先頭 | pop()(=removeFirst) |
(pollFirst() を使う) |
| のぞく(スタック) | 先頭 | (getFirst() を使う) |
peek()(=peekFirst) |
覚え方:add・remove・element・get・push・pop は例外、offer・poll・peek は null/false です。容量に上限のない ArrayDeque では add と offer はどちらも常に成功するので、違いが出るのは「空のときの取り出し」と「容量つきキューへの追加」です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Queue・Deque・Stackの使い方|ArrayDeque早見表」
キュー・スタック・両端キューの基本(コピペ用)
環境は OpenJDK 21.0.10 です。この記事のコードはすべて1ファイルで完結し、java ファイル名.java で実行できます(日本語の表示が ? になる環境では java -Dstdout.encoding=UTF-8 ファイル名.java)。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;
public class DequeBasics {
public static void main(String[] args) {
// キュー(先入れ先出し):末尾に入れて先頭から出す
Queue<String> queue = new ArrayDeque<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println("queue = " + queue + " / peek = " + queue.peek());
System.out.println("poll = " + queue.poll() + " → " + queue);
// スタック(後入れ先出し):先頭に積んで先頭から取る
Deque<String> stack = new ArrayDeque<>();
stack.push("A");
stack.push("B");
stack.push("C");
System.out.println("stack = " + stack + " / peek = " + stack.peek());
System.out.println("pop = " + stack.pop() + " → " + stack);
// 両端キュー:両側から出し入れできる
Deque<String> deque = new ArrayDeque<>();
deque.offerLast("B");
deque.offerFirst("A");
deque.offerLast("C");
System.out.println("deque = " + deque);
System.out.println("pollFirst = " + deque.pollFirst() + " / pollLast = " + deque.pollLast()
+ " → " + deque);
}
}
実行結果:
queue = [A, B, C] / peek = A
poll = A → [B, C]
stack = [C, B, A] / peek = C
pop = C → [B, A]
deque = [A, B, C]
pollFirst = A / pollLast = C → [B]
変数の型はインターフェイス(Queue/Deque)で宣言すると、「この変数はキューとして使う」という意図がコードから読み取れ、使えるメソッドもその用途に絞られます。ArrayDeque をスタックとして表示すると [C, B, A] と、次に取り出す要素が左に来る点も覚えておくと出力が読みやすくなります。
全メソッドの動きを実行して確かめる
早見表の「例外か、null/false か」を、空のキュー・満杯のキューで実際に呼び出して確認します。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.function.Supplier;
public class QueueMethods {
static void show(String label, Supplier<Object> action) {
try {
System.out.printf("%-28s -> %s%n", label, action.get());
} catch (RuntimeException e) {
System.out.printf("%-28s -> 例外 %s%s%n", label, e.getClass().getSimpleName(),
e.getMessage() == null ? "" : ": " + e.getMessage());
}
}
public static void main(String[] args) {
System.out.println("== 空のキューから取り出す・のぞく ==");
Queue<String> q = new ArrayDeque<>();
show("remove()", q::remove);
show("poll()", q::poll);
show("element()", q::element);
show("peek()", q::peek);
System.out.println("== 空の Deque(先頭側・末尾側・スタック操作) ==");
Deque<String> d = new ArrayDeque<>();
show("removeFirst()", d::removeFirst);
show("pollFirst()", d::pollFirst);
show("getFirst()", d::getFirst);
show("peekFirst()", d::peekFirst);
show("removeLast()", d::removeLast);
show("pollLast()", d::pollLast);
show("getLast()", d::getLast);
show("peekLast()", d::peekLast);
show("pop()", d::pop);
System.out.println("== 容量2のキューが満杯のときに入れる ==");
Queue<String> full = new ArrayBlockingQueue<>(2);
show("add(\"A\")", () -> full.add("A"));
show("offer(\"B\")", () -> full.offer("B"));
show("add(\"C\")", () -> full.add("C"));
show("offer(\"C\")", () -> full.offer("C"));
System.out.println("中身 = " + full);
}
}
実行結果:
== 空のキューから取り出す・のぞく ==
remove() -> 例外 NoSuchElementException
poll() -> null
element() -> 例外 NoSuchElementException
peek() -> null
== 空の Deque(先頭側・末尾側・スタック操作) ==
removeFirst() -> 例外 NoSuchElementException
pollFirst() -> null
getFirst() -> 例外 NoSuchElementException
peekFirst() -> null
removeLast() -> 例外 NoSuchElementException
pollLast() -> null
getLast() -> 例外 NoSuchElementException
peekLast() -> null
pop() -> 例外 NoSuchElementException
== 容量2のキューが満杯のときに入れる ==
add("A") -> true
offer("B") -> true
add("C") -> 例外 IllegalStateException: Queue full
offer("C") -> false
中身 = [A, B]
どちらの系統を使うかの判断表
| 場面 | 使う系統 | 理由 |
|---|---|---|
| 空のことが普通にある(ループで取り出す) | poll/peek + null 判定、または isEmpty() で確認 |
例外処理なしで書ける |
| 空なら不具合(必ず入っているはず) | remove/element/pop |
想定外にすぐ気づける |
| 容量つきキューに入れる | offer の戻り値を確認 |
入らなかったときの処理を書ける |
1つの変数に対して系統を混ぜないのがコツです。たとえばスタックとして push で積んだものを pollLast() で取ると、いちばん最初に積んだ要素が出てきます。「先頭側で出し入れするのがスタック、末尾に入れて先頭から出すのがキュー」と決めておくと迷いません。
StackクラスよりArrayDequeを使う理由
java.util.Stack も今のJavaで使えます。そのうえで、公式の Javadoc 自身が Deque の利用をすすめています。Java 25 の Stack クラスの説明には次の一文があります(公式ドキュメントで確認)。
A more complete and consistent set of LIFO stack operations is provided by the Deque interface and its implementations, which should be used in preference to this class.
(より完全で一貫した LIFO スタック操作は Deque インターフェイスとその実装が提供しており、このクラスより優先して使うべきです、という内容です。)続けて Deque<Integer> stack = new ArrayDeque<Integer>(); という例も示されています。ArrayDeque の説明にも「スタックとして使うと Stack より、キューとして使うと LinkedList より速い見込み」と書かれています。
両者の動きの違いを実行して比べます。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Stack;
public class StackVsDeque {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 1; i <= 3; i++) {
stack.push(i);
deque.push(i);
}
System.out.println("Stack の表示 = " + stack);
System.out.println("ArrayDeque の表示 = " + deque);
System.out.print("Stack を for で : ");
for (int x : stack) System.out.print(x + " ");
System.out.print("\nArrayDeque を for で: ");
for (int x : deque) System.out.print(x + " ");
System.out.println();
// Stack は Vector を継承しているので、途中への挿入や位置指定の取得もできる
stack.add(0, 99);
System.out.println("stack.add(0, 99) 後 = " + stack + " / get(1) = " + stack.get(1));
System.out.println("pop の順: " + stack.pop() + " " + stack.pop() + " " + stack.pop() + " " + stack.pop());
try {
stack.pop();
} catch (RuntimeException e) {
System.out.println("空の Stack.pop() -> " + e.getClass().getName());
}
try {
new ArrayDeque<Integer>().pop();
} catch (RuntimeException e) {
System.out.println("空の ArrayDeque.pop() -> " + e.getClass().getName());
}
}
}
実行結果:
Stack の表示 = [1, 2, 3]
ArrayDeque の表示 = [3, 2, 1]
Stack を for で : 1 2 3
ArrayDeque を for で: 3 2 1
stack.add(0, 99) 後 = [99, 1, 2, 3] / get(1) = 1
pop の順: 3 2 1 99
空の Stack.pop() -> java.util.EmptyStackException
空の ArrayDeque.pop() -> java.util.NoSuchElementException
| 比べる点 | Stack |
ArrayDeque |
|---|---|---|
| 登場した版 | JDK 1.0 | Java 6 |
| 継承・実装 | Vector を継承(List でもある) |
Deque を実装 |
| for での走査順 | 積んだ順(底から) | 取り出す順(上から) |
| 途中への挿入 | add(0, x)・get(i) ができる |
できない(両端だけ) |
| 同期 | 各メソッドが synchronized |
同期なし(1スレッドで使う前提) |
空で pop() |
EmptyStackException |
NoSuchElementException |
| null | 入れられる | 入れると NullPointerException |
Stack から ArrayDeque に置き換えるときは、走査順と例外の型が変わる点を確認しましょう。for で中身を表示している箇所や、catch (EmptyStackException e) と書いている箇所が対象です。
ArrayDequeにnullを入れるとNullPointerException
ArrayDeque と PriorityQueue は null を要素にできません。poll() や peek() が「空である」ことを null で伝えるため、要素としての null と区別がつかなくなるからです(Deque の Javadoc にも、null を入れない実装が強く推奨されると書かれています)。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.PriorityQueue;
public class NullInDeque {
public static void main(String[] args) {
Deque<String> ad = new ArrayDeque<>();
try { ad.offer(null); } catch (RuntimeException e) { System.out.println("ArrayDeque.offer(null) -> " + e); }
try { ad.push(null); } catch (RuntimeException e) { System.out.println("ArrayDeque.push(null) -> " + e); }
try { new PriorityQueue<String>().add(null); } catch (RuntimeException e) { System.out.println("PriorityQueue.add(null) -> " + e); }
// よくある形:Map から取った値をそのままキューへ
Map<String, String> next = new HashMap<>(Map.of("A", "B"));
try {
ad.offer(next.get("A"));
ad.offer(next.get("B")); // キーがないので null
} catch (NullPointerException e) {
System.out.println("offer(map.get(\"B\")) -> " + e);
}
// LinkedList は null を入れられるが、poll の null と区別できなくなる
Deque<String> ll = new LinkedList<>();
ll.offer(null);
System.out.println("LinkedList: size=" + ll.size() + " poll=" + ll.poll() + " size=" + ll.size()
+ " もう一度poll=" + ll.poll());
}
}
実行結果:
ArrayDeque.offer(null) -> java.lang.NullPointerException
ArrayDeque.push(null) -> java.lang.NullPointerException
PriorityQueue.add(null) -> java.lang.NullPointerException
offer(map.get("B")) -> java.lang.NullPointerException
LinkedList: size=1 poll=null size=0 もう一度poll=null
LinkedList は null を入れられますが、最後の行のとおり 「null という要素を取り出した」のか「空だった」のかを poll() の戻り値で見分けられません。
| 状況 | 対処 |
|---|---|
map.get(key) の結果をそのまま入れている |
null かどうかを確認してから offer する |
| 「値なし」を要素として表したい | Optional<T> や専用の定数(番兵)を要素にする |
LinkedList から ArrayDeque に置き換える |
null を入れている箇所がないか先に確認する |
PriorityQueueの使い方(走査順・Comparator・最大ヒープ)
PriorityQueue は、poll() するたびに「いちばん小さい要素」が出てくるキューです。内部はヒープ(親が子より小さい、という条件だけを保った配列)で、全体が整列されているわけではありません。そのため for や toString() で見える順番は整列順になりません。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
public class PriorityQueueDemo {
record Task(String name, int priority) {}
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>(List.of(5, 1, 4, 2, 3, 9, 7));
System.out.println("toString = " + pq);
System.out.print("for で走査 = ");
for (int x : pq) System.out.print(x + " ");
System.out.println();
// 整列順で見たいとき (1):コピーを並べ替える(元のキューは減らない)
List<Integer> sorted = new ArrayList<>(pq);
sorted.sort(null);
System.out.println("コピーして sort = " + sorted + " / 元の size = " + pq.size());
// 整列順で見たいとき (2):poll で1件ずつ取り出す(キューは空になる)
System.out.print("poll で取り出す = ");
while (!pq.isEmpty()) System.out.print(pq.poll() + " ");
System.out.println();
// 最大ヒープ:大きい順に取り出す
PriorityQueue<Integer> max = new PriorityQueue<>(Comparator.reverseOrder());
max.addAll(List.of(5, 1, 4, 2, 3, 9, 7));
System.out.print("最大ヒープ = ");
while (!max.isEmpty()) System.out.print(max.poll() + " ");
System.out.println();
// record を優先度の小さい順 → 同じなら名前順で取り出す
PriorityQueue<Task> tasks = new PriorityQueue<>(
Comparator.comparingInt(Task::priority).thenComparing(Task::name));
tasks.add(new Task("資料作成", 2));
tasks.add(new Task("障害対応", 1));
tasks.add(new Task("備品発注", 3));
tasks.add(new Task("問い合わせ返信", 1));
while (!tasks.isEmpty()) System.out.println(" " + tasks.poll());
// Comparable でも Comparator でもない要素は入れた時点で例外
try {
new PriorityQueue<Task>().add(new Task("x", 1));
} catch (ClassCastException e) {
System.out.println("Comparator なし -> ClassCastException: " + e.getMessage().split(" \\(")[0]);
}
}
}
実行結果:
toString = [1, 2, 4, 5, 3, 9, 7]
for で走査 = 1 2 4 5 3 9 7
コピーして sort = [1, 2, 3, 4, 5, 7, 9] / 元の size = 7
poll で取り出す = 1 2 3 4 5 7 9
最大ヒープ = 9 7 5 4 3 2 1
Task[name=問い合わせ返信, priority=1]
Task[name=障害対応, priority=1]
Task[name=資料作成, priority=2]
Task[name=備品発注, priority=3]
Comparator なし -> ClassCastException: class PriorityQueueDemo$Task cannot be cast to class java.lang.Comparable
for の結果は 1 2 4 5 3 9 7 で、先頭の 1 だけが最小と決まっています。Javadoc にも、iterator() は特定の順序で走査することを保証しないと明記されています。
| やりたいこと | 書き方 | 計算量の目安 |
|---|---|---|
| 最小を取り出す | poll() |
O(log n) |
| 最小をのぞく | peek() |
O(1) |
| 入れる | offer(e) |
O(log n) |
| 整列順で全部見る(中身は残す) | new ArrayList<>(pq) を sort |
O(n log n) |
| 整列順で全部取り出す | while (!pq.isEmpty()) pq.poll() |
O(n log n) |
| 大きい順にする | new PriorityQueue<>(Comparator.reverseOrder()) |
同じ |
| 項目で並べる | Comparator.comparingInt(Task::priority).thenComparing(Task::name) |
同じ |
| 特定の要素を消す・含むか調べる | remove(obj)/contains(obj) |
O(n) |
PriorityQueueでつまずきやすい点
| つまずき | 起きること | 対処 |
|---|---|---|
| for で回して整列順だと思う | ヒープの並びで出る | poll() で取り出すか、コピーして sort |
| record や自作クラスをそのまま入れる | ClassCastException(Comparable に変換できない) |
コンストラクタに Comparator を渡すか、Comparable を実装 |
(a, b) -> b - a で降順にする |
値が大きいと桁あふれで順序が乱れる | Comparator.reverseOrder() や Integer.compare(b, a) |
| 入れたあとで要素の優先度を書き換える | 位置は変わらない | いったん remove して、変更後に offer し直す |
ArrayDequeとLinkedListの速度を計測する
Deque の実装には LinkedList もあります。どちらを選ぶかの参考に、3つの使い方で時間を計りました。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Stack;
import java.util.function.Supplier;
public class DequeBench {
static final int N = 1_000_000;
static final Integer V = 1; // 同じ要素を使い回し、入れ物そのものの差を見る
// キューとして:N 件入れてから N 件出す
static long asQueue(Deque<Integer> d) {
long sum = 0;
for (int i = 0; i < N; i++) d.offerLast(V);
while (!d.isEmpty()) sum += d.pollFirst();
return sum;
}
// スタックとして:N 件積んでから N 件取る
static long asStack(Deque<Integer> d) {
long sum = 0;
for (int i = 0; i < N; i++) d.push(V);
while (!d.isEmpty()) sum += d.pop();
return sum;
}
// 流しながら:常に1000件ほど保ったまま、入れる・出すを N 回くり返す
static long asFlow(Deque<Integer> d) {
long sum = 0;
for (int i = 0; i < 1000; i++) d.offerLast(V);
for (int i = 0; i < N; i++) {
d.offerLast(V);
sum += d.pollFirst();
}
return sum;
}
static long legacyStack() {
Stack<Integer> s = new Stack<>();
long sum = 0;
for (int i = 0; i < N; i++) s.push(V);
while (!s.isEmpty()) sum += s.pop();
return sum;
}
static void measure(String label, Supplier<Long> body) {
for (int i = 0; i < 20; i++) body.get(); // ウォームアップ
long[] times = new long[21];
for (int i = 0; i < times.length; i++) {
long t0 = System.nanoTime();
body.get();
times[i] = System.nanoTime() - t0;
}
java.util.Arrays.sort(times);
System.out.printf("%-20s 中央値 %5.1f ms / 最短 %5.1f ms%n", label,
times[times.length / 2] / 1e6, times[0] / 1e6);
}
public static void main(String[] args) {
System.out.println("N = " + N + "(21回計測)");
measure("ArrayDeque キュー", () -> asQueue(new ArrayDeque<>()));
measure("ArrayDeque(N) キュー", () -> asQueue(new ArrayDeque<>(N)));
measure("LinkedList キュー", () -> asQueue(new LinkedList<>()));
measure("ArrayDeque 流しながら", () -> asFlow(new ArrayDeque<>()));
measure("LinkedList 流しながら", () -> asFlow(new LinkedList<>()));
measure("ArrayDeque スタック", () -> asStack(new ArrayDeque<>()));
measure("ArrayDeque(N) スタック", () -> asStack(new ArrayDeque<>(N)));
measure("LinkedList スタック", () -> asStack(new LinkedList<>()));
measure("Stack スタック", DequeBench::legacyStack);
}
}
実行結果(java -Xms1g -Xmx1g DequeBench.java、OpenJDK 21.0.10、2コアのLinuxコンテナ):
N = 1000000(21回計測)
ArrayDeque キュー 中央値 23.5 ms / 最短 12.2 ms
ArrayDeque(N) キュー 中央値 11.4 ms / 最短 10.6 ms
LinkedList キュー 中央値 10.1 ms / 最短 9.7 ms
ArrayDeque 流しながら 中央値 2.7 ms / 最短 2.5 ms
LinkedList 流しながら 中央値 6.2 ms / 最短 5.6 ms
ArrayDeque スタック 中央値 21.3 ms / 最短 20.1 ms
ArrayDeque(N) スタック 中央値 11.6 ms / 最短 10.9 ms
LinkedList スタック 中央値 10.6 ms / 最短 10.2 ms
Stack スタック 中央値 16.5 ms / 最短 10.3 ms
| 使い方 | この計測での結果 | 読み取れること |
|---|---|---|
| 流しながら(入れる・出すを交互に) | ArrayDeque が約2.3倍速い(2.7 ms 対 6.2 ms) |
BFS やタスク処理など、よくある使い方では ArrayDeque が有利 |
| 100万件を一気にためてから出す(容量指定なし) | LinkedList のほうが速かった(10.1 ms 対 23.5 ms) |
ArrayDeque は内部配列を広げ直す時間がかかる |
同じく一気にためる(new ArrayDeque<>(N) で容量指定) |
ほぼ同じ(11.4 ms 対 10.1 ms) | 件数の見当がつくなら初期容量を渡すと差がなくなる |
計測は簡易的なもので、数回実行すると数 ms から十数 ms の幅で揺れました。「常に何倍速い」とは言えず、使い方で変わるというのが手元の結果です。メモリの面では、LinkedList は要素ごとに前後の参照を持つノードを作るのに対し、ArrayDeque は配列の1マスで済むため、件数が多いほど ArrayDeque が軽くなります。
どの実装を選ぶかの判断表
flowchart TD
A[何をしたい?] --> B{優先度順に取り出す}
A --> C{複数スレッドで受け渡す}
A --> D{それ以外のキュー・スタック}
B --> E[PriorityQueue]
C --> F[BlockingQueue の実装]
D --> G{null を入れる/位置指定で読む必要がある}
G -->|ない| H[ArrayDeque]
G -->|ある| I[LinkedList]
| 実装 | 向いている場面 | null | スレッド |
|---|---|---|---|
ArrayDeque |
キュー・スタック全般(第一候補) | 不可 | 1スレッド用 |
LinkedList |
null を扱う、List としても使う、走査中の削除が多い |
可 | 1スレッド用 |
PriorityQueue |
優先度順の取り出し、上位 N 件 | 不可 | 1スレッド用 |
ArrayBlockingQueue など |
スレッド間の受け渡し | 不可 | 複数スレッド対応 |
Stack |
既存コードとの互換 | 可 | 各メソッドが同期 |
コピペで使える例(BFS・DFS・括弧の対応チェック)
キューとスタックの定番の使いどころを3つまとめました。BFS(幅優先探索)はキュー、DFS(深さ優先探索)と括弧チェックはスタックを使います。
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
public class Algo {
static final Map<String, List<String>> GRAPH = Map.of(
"A", List.of("B", "C"),
"B", List.of("D", "E"),
"C", List.of("F"),
"D", List.of(),
"E", List.of("F"),
"F", List.of());
/** 幅優先探索(BFS):キューを使い、近い順にたどる */
static List<String> bfs(String start) {
List<String> order = new ArrayList<>();
Set<String> seen = new HashSet<>(Set.of(start));
Queue<String> queue = new ArrayDeque<>();
queue.offer(start);
while (!queue.isEmpty()) {
String cur = queue.poll();
order.add(cur);
for (String nx : GRAPH.get(cur)) {
if (seen.add(nx)) queue.offer(nx); // 初めて見た頂点だけ入れる
}
}
return order;
}
/** 深さ優先探索(DFS):スタックを使い、行けるところまで進む */
static List<String> dfs(String start) {
List<String> order = new ArrayList<>();
Set<String> seen = new HashSet<>();
Deque<String> stack = new ArrayDeque<>();
stack.push(start);
while (!stack.isEmpty()) {
String cur = stack.pop();
if (!seen.add(cur)) continue; // 訪問済みは飛ばす
order.add(cur);
List<String> nexts = GRAPH.get(cur);
for (int i = nexts.size() - 1; i >= 0; i--) { // 逆順に積むと定義順にたどれる
stack.push(nexts.get(i));
}
}
return order;
}
/** 括弧の対応チェック:開きを積み、閉じが来たら取り出して照合する */
static boolean isBalanced(String s) {
Map<Character, Character> pair = Map.of(')', '(', ']', '[', '}', '{');
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else if (pair.containsKey(c)) {
Character open = stack.poll(); // 空なら null(例外にならない)
if (open == null || !open.equals(pair.get(c))) return false;
}
}
return stack.isEmpty(); // 開きが残っていないこと
}
public static void main(String[] args) {
System.out.println("BFS = " + bfs("A"));
System.out.println("DFS = " + dfs("A"));
for (String s : List.of("{[()]}", "a(b[c]d)e", "(]", "(()", "())", "")) {
System.out.println("isBalanced(\"" + s + "\") = " + isBalanced(s));
}
}
}
実行結果:
BFS = [A, B, C, D, E, F]
DFS = [A, B, D, E, F, C]
isBalanced("{[()]}") = true
isBalanced("a(b[c]d)e") = true
isBalanced("(]") = false
isBalanced("(()") = false
isBalanced("())") = false
isBalanced("") = true
| 例 | 使う構造 | ポイント |
|---|---|---|
| BFS | キュー(offer/poll) |
入れるときに訪問済みにすると、同じ頂点が二重に入らない |
| DFS | スタック(push/pop) |
取り出したときに訪問済みを確認。逆順に積むと定義順にたどる |
| 括弧チェック | スタック(push/poll) |
閉じ括弧のとき空なら poll() が null を返すので、例外処理なしで判定できる |
BFS は出発点から近い順(A → B, C → D, E, F)に、DFS は行けるところまで進んでから戻る順(A → B → D → E → F → C)になっています。括弧チェックで pop() ではなく poll() を使っているのは、"())" のように閉じ括弧が多い入力でも NoSuchElementException にならないようにするためです。
BlockingQueueでスレッド間の受け渡しをする
ArrayDeque や PriorityQueue は1つのスレッドで使う前提です。「作る側」と「処理する側」を別スレッドに分けるときは BlockingQueue を使います。空のときに取り出そうとすると入るまで待ち、満杯のときに入れようとすると空くまで待ってくれます。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingDemo {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2); // 容量2
final String END = "END"; // 終了の合図
Thread consumer = new Thread(() -> {
try {
while (true) {
String job = queue.take(); // 空なら入るまで待つ
if (job.equals(END)) break;
System.out.println(" 処理: " + job);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "consumer");
consumer.start();
for (int i = 1; i <= 5; i++) {
queue.put("job-" + i); // 満杯なら空くまで待つ
System.out.println("投入: job-" + i + " (待ち " + queue.size() + " 件)");
}
queue.put(END);
consumer.join();
// 待ち時間つき:空のまま 200ms たつと null
long t0 = System.nanoTime();
String r = queue.poll(200, TimeUnit.MILLISECONDS);
System.out.println("poll(200ms) = " + r + " / " + (System.nanoTime() - t0) / 1_000_000 + " ms");
// 待たない版:満杯なら false
queue.offer("x"); queue.offer("y");
System.out.println("満杯で offer = " + queue.offer("z")
+ " / offer(100ms) = " + queue.offer("z", 100, TimeUnit.MILLISECONDS));
}
}
実行結果:
投入: job-1 (待ち 1 件)
投入: job-2 (待ち 2 件)
処理: job-1
投入: job-3 (待ち 2 件)
処理: job-2
投入: job-4 (待ち 2 件)
処理: job-3
投入: job-5 (待ち 2 件)
処理: job-4
処理: job-5
poll(200ms) = null / 200 ms
満杯で offer = false / offer(100ms) = false
容量2のキューが満杯になると put が待ち、処理側が1件取り出すたびに次の投入が進んでいます(表示の前後は実行のたびに少し変わります)。
| 操作 | 例外を投げる | 特別な値 | 待つ | 時間を決めて待つ |
|---|---|---|---|---|
| 入れる | add(e) |
offer(e) |
put(e) |
offer(e, 時間, 単位) |
| 取り出す | remove() |
poll() |
take() |
poll(時間, 単位) |
| のぞく | element() |
peek() |
なし | なし |
| 実装 | 特徴 |
|---|---|
ArrayBlockingQueue |
容量固定。配列ベース |
LinkedBlockingQueue |
容量は任意(省略すると上限なしに近い)。件数の上限を渡すとためすぎを防げる |
ConcurrentLinkedQueue |
待たない(ブロックしない)スレッド対応キュー |
Java 21で増えたメソッドとJava 25での違い
Java 21 で SequencedCollection というインターフェイスが加わり、Deque もその仲間になりました。ArrayDeque では reversed() で逆向きのビューを取れます。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
public class Seq21 {
public static void main(String[] args) {
Deque<Integer> d = new ArrayDeque<>(List.of(1, 2, 3));
System.out.println("reversed() = " + d.reversed());
d.reversed().addFirst(4); // 逆向きビューの先頭 = 元の末尾
System.out.println("d = " + d);
}
}
実行結果:
reversed() = [3, 2, 1]
d = [1, 2, 3, 4]
reversed() はコピーではなく元の Deque を逆から見るビューなので、ビューの先頭に足した 4 は元の末尾に入っています。
Java 25 での違い(公式の Javadoc で確認、手元では未実行):Java 25 の ArrayDeque・Stack・PriorityQueue・Deque の Javadoc を確認したところ、この記事で扱ったメソッドと注意書き(null の禁止、Stack より Deque を優先、PriorityQueue の走査順)は Java 21 と同じ内容でした。この記事のコードは Java 21 で実行したものです。
よくある質問
Q. Queue<Integer> q = new LinkedList<>(); と書いている例をよく見ます。ArrayDeque に変えたほうがよいですか?
A. どちらでも正しく動きます。入れる・出すを交互に行う一般的な使い方では、手元の計測で ArrayDeque が約2.3倍速く、メモリも少なく済みます。null を要素として入れていないことを確認できれば、new ArrayDeque<>() に置き換えるだけで移行できます。
Q. peek() と element()、poll() と remove() はどちらを使えばよいですか?
A. 空の可能性があるなら peek()/poll()(null が返る)、空であってはならない場面なら element()/remove()(例外で気づける)です。ループで空になるまで取り出すときは while (!q.isEmpty()) と poll() の組み合わせが定番です。
Q. 上位3件だけ取り出したいときはどう書きますか?
A. 件数が少なければ list.stream().sorted().limit(3) が簡潔です。件数が多いときは、大きさ3の PriorityQueue(最小ヒープ)を用意し、要素を入れて3件を超えたら poll() で最小を捨てていくと、最後に上位3件が残ります。
まとめ
- キューもスタックも第一候補は
ArrayDeque。型はQueue/Dequeで宣言する add・remove・element・get・push・popは例外、offer・poll・peekは null/false を返すStackクラスの Javadoc 自身がDequeの利用をすすめている。置き換えるときは走査順と例外の型を確認するArrayDequeとPriorityQueueに null を入れるとNullPointerException。poll()の null は「空」の合図PriorityQueueを for で回しても整列順にならない。整列順はpoll()で取り出すか、コピーしてsort- 速度は使い方で変わる。入れる・出すを交互に行う使い方では
ArrayDequeが約2.3倍速く、一気にためるなら初期容量を渡す - スレッド間の受け渡しは
BlockingQueue(put/takeで待つ)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java Queue・Deque・Stackの使い方|ArrayDeque早見表」
株式会社RJCでは、JavaのQueue・Deque・Stackといったコレクションの使い分けを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。