固定長キューは「常に“最後のN件だけ”を覚えておく箱」
固定長キューは、
「新しいものをどんどん入れるけれど、サイズは最大N件まで」
「あふれたら“一番古いもの”から自動的に捨てる」
というルールを持ったキューです。
イメージとしては、
「椅子が5脚しかない待合室。6人目が来たら、一番最初から座っていた人が出ていく」
そんな感じです。
業務だと、
「直近100件のログだけ覚えておきたい」
「最後にアクセスしたユーザーIDを50件だけ保持したい」
「センサー値の最新100サンプルだけで統計を取りたい」
といった場面でよく使います。
基本設計:「入れるときに、あふれていたら先頭を捨てる」
ルールを日本語で整理する
固定長キューのルールは、とてもシンプルです。
- 新しい要素は「末尾」に追加する(普通のキューと同じ)。
- 要素数が上限
maxSizeを超えたら、「先頭」を1つ捨てる。 - 取り出すときは、先頭から取り出す(FIFO)。
つまり、「入れるときに“あふれていないか”をチェックして、あふれていたら古い方を捨てる」だけです。
この「入れる瞬間に調整する」という感覚を持っておくと、実装が理解しやすくなります。
ArrayDeque を使ったシンプルな固定長キュー実装
コード全体をまず見てみる
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
public class FixedQueue<T> implements Iterable<T> {
private final int maxSize;
private final Deque<T> deque;
public FixedQueue(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize must be > 0");
}
this.maxSize = maxSize;
this.deque = new ArrayDeque<>(maxSize);
}
public void add(T value) {
if (deque.size() == maxSize) {
deque.removeFirst(); // 一番古い要素を捨てる
}
deque.addLast(value); // 新しい要素を末尾に追加
}
public T poll() {
return deque.pollFirst(); // 先頭から取り出す(なければ null)
}
public int size() {
return deque.size();
}
public boolean isEmpty() {
return deque.isEmpty();
}
@Override
public Iterator<T> iterator() {
return deque.iterator();
}
}
Java使い方の例です。
FixedQueue<String> q = new FixedQueue<>(3);
q.add("A");
q.add("B");
q.add("C");
System.out.println(q); // [A, B, C] だとイメージしてください
q.add("D"); // ここで A が捨てられ、[B, C, D] になる
Java(toString をオーバーライドしていないので実際の表示は違いますが、イメージとして。)
重要ポイント1:「サイズが上限に達していたら、先に removeFirst」
add の中身を丁寧に読む
public void add(T value) {
if (deque.size() == maxSize) {
deque.removeFirst();
}
deque.addLast(value);
}
Javaここが固定長キューの心臓部です。
size() == maxSize のとき、
つまり「すでに満杯」のときに新しい要素を入れようとしたら、
先に removeFirst() で一番古い要素を捨てています。
そのあとで addLast(value) で新しい要素を末尾に追加するので、
結果として「常に最大 maxSize 件まで」に保たれます。
ここで大事なのは、
「あふれたら例外にする」のではなく、「あふれたら古いものを自動で捨てる」
という挙動にしていることです。
「常に最後のN件だけ欲しい」という用途では、この動きがとても自然です。
重要ポイント2:Deque を使う理由
先頭と末尾の両方を効率よく触りたい
Deque(両端キュー)は、
「先頭」「末尾」の両方に対して、追加・削除が効率よくできるコレクションです。
固定長キューでは、
先頭から捨てる(removeFirst)
末尾に追加する(addLast)
という操作を頻繁に行うので、Deque がぴったりです。
ArrayDeque は、配列ベースで実装された Deque で、
一般的な用途では LinkedList より高速なことが多いので、
「固定長キューの中身」としてよく使われます。
具体例1:直近N件のログを覚えておく
「最後の100件だけ見られればいい」ログバッファ
例えば、「画面上で“直近100件のエラーだけ”を確認できればよい」という要件があるとします。
public class ErrorLogBuffer {
private final FixedQueue<String> buffer = new FixedQueue<>(100);
public void logError(String message) {
buffer.add(message);
}
public Iterable<String> recentErrors() {
return buffer;
}
}
Java使い方のイメージです。
ErrorLogBuffer log = new ErrorLogBuffer();
log.logError("DB接続エラー");
log.logError("タイムアウト");
...
for (String msg : log.recentErrors()) {
System.out.println(msg);
}
Javaここでのポイントは、
「古いログは自動的に消えていくが、“直近100件”は必ず残る」
という性質です。
メモリを無限に食い続けることなく、
「最近の状況だけをざっと振り返る」用途に向いています。
具体例2:直近N件の数値で平均を取る
「最新50サンプルの移動平均」を計算する
センサー値やレスポンスタイムなど、
「最新のN件だけで平均を取りたい」というケースもよくあります。
public class MovingAverage {
private final FixedQueue<Double> window;
private double sum = 0.0;
public MovingAverage(int size) {
this.window = new FixedQueue<>(size);
}
public void add(double value) {
if (window.size() == getMaxSize()) {
Double oldest = window.poll();
if (oldest != null) {
sum -= oldest;
}
}
window.add(value);
sum += value;
}
public double getAverage() {
if (window.isEmpty()) {
return 0.0;
}
return sum / window.size();
}
private int getMaxSize() {
// FixedQueue に maxSize を返すメソッドを足してもよい
return 50; // 仮に固定値としておく例
}
}
Javaここでは少しアレンジして、
「捨てた値を sum から引く」ことで、
毎回全件を足し直さなくても平均が計算できるようにしています。
固定長キューは、
「常に“最近のN件”だけを対象にした計算」
と相性がとても良いです。
スレッドセーフにしたいときの考え方
単純に synchronized をかけるパターン
複数スレッドから同時に add / poll されるなら、FixedQueue に同期を入れる必要があります。
一番簡単なのは、メソッドを synchronized にすることです。
public class FixedQueue<T> implements Iterable<T> {
// フィールドは同じ
public synchronized void add(T value) {
if (deque.size() == maxSize) {
deque.removeFirst();
}
deque.addLast(value);
}
public synchronized T poll() {
return deque.pollFirst();
}
public synchronized int size() {
return deque.size();
}
public synchronized boolean isEmpty() {
return deque.isEmpty();
}
@Override
public synchronized Iterator<T> iterator() {
return new ArrayDeque<>(deque).iterator(); // スナップショットを返す
}
}
Javaここでのポイントは、
「iterator も同期付きで“スナップショット”を返す」ようにしていることです。
生の deque.iterator() を返すと、
イテレーション中に別スレッドが変更して ConcurrentModificationException が出る可能性があります。
スレッドセーフにするかどうかは用途次第ですが、
「ログバッファを複数スレッドから書き込む」ような場面では意識しておきたいポイントです。
まとめ:固定長キューで身につけてほしい感覚
固定長キューは、
単に「サイズ制限付きのキュー」ではなく、
「常に“最新N件だけ”を対象にした処理を、シンプルに書くための箱」です。
入れるときに「満杯なら先頭を捨ててから末尾に追加する」というルールを、クラスの中に閉じ込める。Deque(特に ArrayDeque)を使って、先頭・末尾の操作を効率よく行う。
ログバッファや移動平均など、「最近のデータだけ見たい」処理と組み合わせる。
必要なら同期を加えて、マルチスレッド環境でも安全に使えるようにする。
あなたのコードのどこかに、
「List にどんどん add して、古いものは手動で remove している」ような箇所があれば、
それを一度「固定長キュー」という形に切り出せないか眺めてみてください。
その小さな抽象化が、
「データの“寿命”まで含めて設計できるエンジニア」への、
いいステップになります。
