Java Tips | コレクション:PriorityQueue利用

Java Java
スポンサーリンク

PriorityQueue は「優先度の高いものから順に取り出せるキュー」

PriorityQueue は、普通のキューと違って
「入れた順」ではなく「優先度の高い順(または小さい順)」で取り出せるキュー です。

イメージとしては、
受付で番号札を配るのではなく、
「重症度が高い人から診る救急外来」のようなものです。

業務では、
「優先度付きのタスク処理」
「スコアの高いものから順に処理」
「最小値・最大値を効率よく取り出したい」
といった場面で使えます。


基本:PriorityQueue は「最小値(または最大値)を効率よく取り出す箱」

最も小さい値から取り出す例(デフォルト)

PriorityQueue は、デフォルトでは「自然順序(昇順)」で並びます。
つまり、一番小さい値が先に出てくる キューです。

import java.util.PriorityQueue;
import java.util.Queue;

public class BasicPriorityQueue {

    public static void main(String[] args) {
        Queue<Integer> queue = new PriorityQueue<>();

        queue.offer(30);
        queue.offer(10);
        queue.offer(20);

        System.out.println(queue.poll()); // 10
        System.out.println(queue.poll()); // 20
        System.out.println(queue.poll()); // 30
        System.out.println(queue.poll()); // null(空)
    }
}
Java

ここでの重要ポイントは二つです。

一つ目は、「入れた順番(30, 10, 20)とは関係なく、10 → 20 → 30 の順で出てくる」ことです。
PriorityQueue は内部でヒープ構造を持っていて、
「最小値(または最大値)を素早く取り出す」ことに特化 しています。

二つ目は、「poll() は空のときに null を返す」ことです。
remove() だと空のときに例外になるので、
「空かもしれない」前提なら poll() を使う方が安全です。


重要ポイント:PriorityQueue は「全体がソートされているわけではない」

イテレーションすると「バラバラ」に見える理由

PriorityQueue の中身を for-each で回すと、
「ソートされていないように見える」ことがあります。

PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.offer(30);
queue.offer(10);
queue.offer(20);

System.out.println(queue); // [10, 30, 20] など、順番がバラバラに見えることがある
Java

ここで大事なのは、
「PriorityQueue が保証するのは“先頭(取り出し対象)が最小(または最大)であること”だけ
という点です。

内部構造はヒープなので、
「全体がきれいにソートされている」わけではありません。

「小さい順に全部取り出したい」なら、
poll() を繰り返すのが正しい使い方です。

while (!queue.isEmpty()) {
    System.out.println(queue.poll());
}
Java

Comparator を使って「優先度のルール」を自分で決める

大きい値から取り出したい(最大値優先)

デフォルトは「最小値優先」ですが、
「大きい値から取り出したい」 場面もよくあります。

その場合は、Comparator を渡してあげます。

import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

public class MaxPriorityQueue {

    public static void main(String[] args) {
        Queue<Integer> queue = new PriorityQueue<>(Comparator.reverseOrder());

        queue.offer(30);
        queue.offer(10);
        queue.offer(20);

        System.out.println(queue.poll()); // 30
        System.out.println(queue.poll()); // 20
        System.out.println(queue.poll()); // 10
    }
}
Java

ここでの重要ポイントは、
「PriorityQueue は“優先度のルール”を Comparator で自由に変えられる」
ということです。

昇順・降順だけでなく、
「スコアの高いタスク」「締切が早いタスク」など、
業務ロジックに合わせた優先度を定義できます。


業務っぽい例:優先度付きタスクキュー

「優先度の高いタスクから処理する」キューを作る

タスクに「優先度」を持たせて、
優先度の高いものから処理する例を考えます。

import java.util.PriorityQueue;
import java.util.Queue;

public class TaskQueue {

    static class Task {
        final int priority;   // 数字が大きいほど優先度高いとする
        final String name;

        Task(int priority, String name) {
            this.priority = priority;
            this.name = name;
        }

        @Override
        public String toString() {
            return "Task{" + priority + ", " + name + "}";
        }
    }

    public static void main(String[] args) {
        Queue<Task> queue = new PriorityQueue<>(
                (a, b) -> Integer.compare(b.priority, a.priority) // priority の大きい順
        );

        queue.offer(new Task(1, "低優先度"));
        queue.offer(new Task(3, "高優先度"));
        queue.offer(new Task(2, "中優先度"));

        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
        // Task{3, 高優先度}
        // Task{2, 中優先度}
        // Task{1, 低優先度}
    }
}
Java

ここで深掘りしたいポイントは三つです。

一つ目は、「PriorityQueue<Task> に対して、Comparator で“優先度の比較ルール”を渡している」ことです。
(a, b) -> Integer.compare(b.priority, a.priority) で「priority の大きい順」を定義しています。

二つ目は、「タスクの追加順ではなく、優先度で処理順が決まる」ことです。
業務では、「緊急度の高いジョブから処理したい」場面でそのまま使えます。

三つ目は、「Task 自体を Comparable にしてもよい」が、
業務では「比較ルールを外から差し替えたい」ことも多いので、
Comparator を渡すスタイルを覚えておくと応用が効く、という点です。


締切(deadline)で優先度を決める例

「締切が早いタスクから処理する」

今度は、「締切が早いタスクほど優先度が高い」キューを考えます。

import java.time.Instant;
import java.util.PriorityQueue;
import java.util.Queue;

public class DeadlineTaskQueue {

    static class Task {
        final Instant deadline;
        final String name;

        Task(Instant deadline, String name) {
            this.deadline = deadline;
            this.name = name;
        }

        @Override
        public String toString() {
            return "Task{" + deadline + ", " + name + "}";
        }
    }

    public static void main(String[] args) {
        Queue<Task> queue = new PriorityQueue<>(
                (a, b) -> a.deadline.compareTo(b.deadline) // 早い締切が先
        );

        queue.offer(new Task(Instant.parse("2025-03-01T00:00:00Z"), "3月1日まで"));
        queue.offer(new Task(Instant.parse("2025-02-01T00:00:00Z"), "2月1日まで"));
        queue.offer(new Task(Instant.parse("2025-04-01T00:00:00Z"), "4月1日まで"));

        while (!queue.isEmpty()) {
            System.out.println(queue.poll());
        }
    }
}
Java

ここでのポイントは、
「PriorityQueue は“何を優先するか”を自由に設計できる」
という感覚を持つことです。

数値でも日付でも、複数フィールドの組み合わせでも、
Comparator で比較ルールを書ければ、そのまま優先度付きキューになります。


PriorityQueue を使うときの注意点

スレッドセーフではない、イテレーション順は信用しない

PriorityQueue を使うときに、特に意識してほしい点が二つあります。

一つ目は、「PriorityQueue はスレッドセーフではない」ことです。
複数スレッドから同時に offer / poll するなら、
外側で同期を取るか、PriorityBlockingQueue などのスレッドセーフ版を検討します。

二つ目は、「イテレーション順は“優先度順”とは限らない」ことです。
for-each で回したときの順番は内部構造次第で、
「最小(最大)から順に並んでいる」とは限りません。

「優先度順に処理したいなら、必ず poll() を繰り返す」
というルールを覚えておいてください。


まとめ:PriorityQueue利用で身につけてほしい感覚

PriorityQueue は、
単なる「ちょっと変わったキュー」ではなく、
「優先度付きで要素を取り出すための、業務向きの道具」 です。

デフォルトでは「最小値優先」だが、Comparator で自由に優先度ルールを決められる。
「全体がソートされている」のではなく、「先頭(取り出し対象)だけが最小/最大」であることを保証する。
優先度付きタスクキューや、締切順処理など、「何を先に処理すべきか」が明確な場面で真価を発揮する。
スレッドセーフではないので、マルチスレッドなら PriorityBlockingQueue なども視野に入れる。

あなたのコードのどこかに、
「List を sort してから先頭を取り出す」処理を何度も繰り返している箇所があれば、
そこを一度「PriorityQueue に置き換えられないか?」という目で眺めてみてください。

それが、「優先度」という軸でコレクションを選べるようになる、いい一歩になります。

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