Java Tips | コレクション:固定長キュー

Java Java
スポンサーリンク

固定長キューは「常に“最後のN件だけ”を覚えておく箱」

固定長キューは、
「新しいものをどんどん入れるけれど、サイズは最大N件まで」
「あふれたら“一番古いもの”から自動的に捨てる」

というルールを持ったキューです。

イメージとしては、
「椅子が5脚しかない待合室。6人目が来たら、一番最初から座っていた人が出ていく」
そんな感じです。

業務だと、
「直近100件のログだけ覚えておきたい」
「最後にアクセスしたユーザーIDを50件だけ保持したい」
「センサー値の最新100サンプルだけで統計を取りたい」
といった場面でよく使います。


基本設計:「入れるときに、あふれていたら先頭を捨てる」

ルールを日本語で整理する

固定長キューのルールは、とてもシンプルです。

  1. 新しい要素は「末尾」に追加する(普通のキューと同じ)。
  2. 要素数が上限 maxSize を超えたら、「先頭」を1つ捨てる。
  3. 取り出すときは、先頭から取り出す(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 している」ような箇所があれば、
それを一度「固定長キュー」という形に切り出せないか眺めてみてください。

その小さな抽象化が、
「データの“寿命”まで含めて設計できるエンジニア」への、
いいステップになります。

タイトルとURLをコピーしました